From 72baa93dcc53a2670dd7beb59357de2ebeb92adb Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 14 Dec 2022 15:59:38 +0100 Subject: [PATCH 001/225] Add GET endpoint for subconversations (#2869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add tge GET subconverstion endpoint * Add golden tests for subconversations * Test injectivity of `initialGroupId` Co-authored-by: Marko Dimjašević Co-authored-by: Stefan Matting --- cassandra-schema.cql | 27 +- changelog.d/1-api-changes/get-subconversation | 1 + changelog.d/5-internal/galley-db-subconv | 1 + changelog.d/5-internal/group-id-subconv | 1 + changelog.d/5-internal/subconv-store | 1 + libs/wire-api/src/Wire/API/Error/Galley.hs | 4 + .../src/Wire/API/MLS/SubConversation.hs | 45 +- .../API/Routes/Public/Galley/Conversation.hs | 21 + .../golden/Test/Wire/API/Golden/Manual.hs | 6 + .../Wire/API/Golden/Manual/SubConversation.hs | 77 ++++ .../testObject_PublicSubConversation_1.json | 11 + .../testObject_PublicSubConversation_2.json | 17 + libs/wire-api/test/unit/Main.hs | 4 +- .../unit/Test/Wire/API/MLS/SubConversation.hs | 46 +++ libs/wire-api/wire-api.cabal | 2 + services/galley/default.nix | 2 + services/galley/galley.cabal | 5 + services/galley/schema/src/Main.hs | 4 +- .../schema/src/V78_MLSSubconversation.hs | 42 ++ services/galley/src/Galley/API/Federation.hs | 54 +-- services/galley/src/Galley/API/MLS/Message.hs | 385 +++++++++++------- .../galley/src/Galley/API/MLS/Propagate.hs | 78 ++-- services/galley/src/Galley/API/MLS/Removal.hs | 82 ++-- .../src/Galley/API/MLS/SubConversation.hs | 111 +++++ services/galley/src/Galley/API/MLS/Types.hs | 59 ++- .../src/Galley/API/Public/Conversation.hs | 2 + services/galley/src/Galley/API/Util.hs | 2 +- services/galley/src/Galley/App.hs | 2 + services/galley/src/Galley/Cassandra.hs | 2 +- .../src/Galley/Cassandra/Conversation.hs | 28 +- .../src/Galley/Cassandra/Conversation/MLS.hs | 15 +- .../Galley/Cassandra/Conversation/Members.hs | 9 +- .../galley/src/Galley/Cassandra/Instances.hs | 7 + .../galley/src/Galley/Cassandra/Queries.hs | 29 +- .../src/Galley/Cassandra/SubConversation.hs | 80 ++++ services/galley/src/Galley/Effects.hs | 2 + .../src/Galley/Effects/ConversationStore.hs | 9 +- .../Galley/Effects/SubConversationStore.hs | 40 ++ services/galley/test/integration/API/MLS.hs | 24 ++ .../galley/test/integration/API/MLS/Util.hs | 21 + 40 files changed, 1050 insertions(+), 308 deletions(-) create mode 100644 changelog.d/1-api-changes/get-subconversation create mode 100644 changelog.d/5-internal/galley-db-subconv create mode 100644 changelog.d/5-internal/group-id-subconv create mode 100644 changelog.d/5-internal/subconv-store create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs create mode 100644 libs/wire-api/test/golden/testObject_PublicSubConversation_1.json create mode 100644 libs/wire-api/test/golden/testObject_PublicSubConversation_2.json create mode 100644 libs/wire-api/test/unit/Test/Wire/API/MLS/SubConversation.hs create mode 100644 services/galley/schema/src/V78_MLSSubconversation.hs create mode 100644 services/galley/src/Galley/API/MLS/SubConversation.hs create mode 100644 services/galley/src/Galley/Cassandra/SubConversation.hs create mode 100644 services/galley/src/Galley/Effects/SubConversationStore.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index e9e0b6c8b96..0cccd7f5208 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -348,7 +348,8 @@ CREATE TABLE galley_test.legalhold_pending_prekeys ( CREATE TABLE galley_test.group_id_conv_id ( group_id blob PRIMARY KEY, conv_id uuid, - domain text + domain text, + subconv_id text ) WITH bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' @@ -524,6 +525,30 @@ CREATE TABLE galley_test.mls_commit_locks ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; +CREATE TABLE galley_test.subconversation ( + conv_id uuid, + subconv_id text, + cipher_suite int, + epoch bigint, + group_id blob, + public_group_state blob, + PRIMARY KEY (conv_id, subconv_id) +) WITH CLUSTERING ORDER BY (subconv_id ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + CREATE TABLE galley_test.team ( team uuid PRIMARY KEY, binding boolean, diff --git a/changelog.d/1-api-changes/get-subconversation b/changelog.d/1-api-changes/get-subconversation new file mode 100644 index 00000000000..175ddb4b909 --- /dev/null +++ b/changelog.d/1-api-changes/get-subconversation @@ -0,0 +1 @@ +Introduce a subconversation GET endpoint diff --git a/changelog.d/5-internal/galley-db-subconv b/changelog.d/5-internal/galley-db-subconv new file mode 100644 index 00000000000..d57f71df5a6 --- /dev/null +++ b/changelog.d/5-internal/galley-db-subconv @@ -0,0 +1 @@ +Introduce a Galley DB table for subconversations diff --git a/changelog.d/5-internal/group-id-subconv b/changelog.d/5-internal/group-id-subconv new file mode 100644 index 00000000000..2706db951bf --- /dev/null +++ b/changelog.d/5-internal/group-id-subconv @@ -0,0 +1 @@ +Support mapping MLS group IDs to subconversations diff --git a/changelog.d/5-internal/subconv-store b/changelog.d/5-internal/subconv-store new file mode 100644 index 00000000000..ef3798fdc6c --- /dev/null +++ b/changelog.d/5-internal/subconv-store @@ -0,0 +1 @@ +Introduce an effect for subconversations diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index 65596d70fac..d61666ec6ef 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -85,6 +85,8 @@ data GalleyError | MLSWelcomeMismatch | MLSMissingGroupInfo | MLSMissingSenderClient + | MLSUnexpectedSenderClient + | MLSSubConvUnsupportedConvType | -- NoBindingTeamMembers | NoBindingTeam @@ -217,6 +219,8 @@ type instance MapError 'MLSMissingGroupInfo = 'StaticError 404 "mls-missing-grou type instance MapError 'MLSMissingSenderClient = 'StaticError 403 "mls-missing-sender-client" "The client has to refresh their access token and provide their client ID" +type instance MapError 'MLSSubConvUnsupportedConvType = 'StaticError 403 "mls-subconv-unsupported-convtype" "MLS subconversations are only supported for regular conversations" + type instance MapError 'NoBindingTeamMembers = 'StaticError 403 "non-binding-team-members" "Both users must be members of the same binding team" type instance MapError 'NoBindingTeam = 'StaticError 403 "no-binding-team" "Operation allowed only on binding teams" diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index e867b555e08..a0d3ccf386c 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -20,17 +20,26 @@ module Wire.API.MLS.SubConversation where -import Control.Lens (makePrisms) +import Control.Lens (makePrisms, (?~)) import Control.Lens.Tuple (_1) import Control.Monad.Except +import qualified Crypto.Hash as Crypto import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.Aeson as A +import Data.ByteArray +import Data.ByteString.Conversion import Data.Id +import Data.Qualified import Data.Schema import qualified Data.Swagger as S import qualified Data.Text as T import Imports import Servant (FromHttpApiData (..), ToHttpApiData (toQueryParam)) import Test.QuickCheck +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Credential +import Wire.API.MLS.Epoch +import Wire.API.MLS.Group import Wire.Arbitrary -- | An MLS subconversation ID, which identifies a subconversation within a @@ -54,6 +63,40 @@ instance FromHttpApiData SubConvId where instance ToHttpApiData SubConvId where toQueryParam = unSubConvId +-- | Compute the inital group ID for a subconversation +initialGroupId :: Local ConvId -> SubConvId -> GroupId +initialGroupId lcnv sconv = + GroupId + . convert + . Crypto.hash @ByteString @Crypto.SHA256 + $ toByteString' (tUnqualified lcnv) + <> toByteString' (tDomain lcnv) + <> toByteString' (unSubConvId sconv) + +data PublicSubConversation = PublicSubConversation + { pscParentConvId :: Qualified ConvId, + pscSubConvId :: SubConvId, + pscGroupId :: GroupId, + pscEpoch :: Epoch, + pscCipherSuite :: CipherSuiteTag, + pscMembers :: [ClientIdentity] + } + deriving (Eq, Show) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema PublicSubConversation) + +instance ToSchema PublicSubConversation where + schema = + objectWithDocModifier + "PublicSubConversation" + (description ?~ "A MLS subconversation") + $ PublicSubConversation + <$> pscParentConvId .= field "parent_qualified_id" schema + <*> pscSubConvId .= field "subconv_id" schema + <*> pscGroupId .= field "group_id" schema + <*> pscEpoch .= field "epoch" schema + <*> pscCipherSuite .= field "cipher_suite" schema + <*> pscMembers .= field "members" (array schema) + data ConvOrSubTag = ConvTag | SubConvTag deriving (Eq, Enum, Bounded) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 023963c96c6..1fcc514df16 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -32,6 +32,7 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Servant +import Wire.API.MLS.SubConversation import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -375,6 +376,26 @@ type ConversationAPI = Conversation ) ) + :<|> Named + "get-subconversation" + ( Summary "Get information about an MLS subconversation" + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvAccessDenied + :> CanThrow 'MLSSubConvUnsupportedConvType + :> ZLocalUser + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> "subconversations" + :> Capture "subconv" SubConvId + :> MultiVerb1 + 'GET + '[JSON] + ( Respond + 200 + "Subconversation" + PublicSubConversation + ) + ) -- This endpoint can lead to the following events being sent: -- - ConvCreate event to members -- TODO: add note: "On 201, the conversation ID is the `Location` header" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs index eddafd7649e..a7f7474143b 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs @@ -33,6 +33,7 @@ import Test.Wire.API.Golden.Manual.GroupId import Test.Wire.API.Golden.Manual.ListConversations import Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap import Test.Wire.API.Golden.Manual.SearchResultContact +import Test.Wire.API.Golden.Manual.SubConversation import Test.Wire.API.Golden.Manual.TeamSize import Test.Wire.API.Golden.Manual.Token import Test.Wire.API.Golden.Manual.UserClientPrekeyMap @@ -139,5 +140,10 @@ tests = [ (testObject_TeamSize_1, "testObject_TeamSize_1.json"), (testObject_TeamSize_2, "testObject_TeamSize_2.json"), (testObject_TeamSize_3, "testObject_TeamSize_3.json") + ], + testGroup "PublicSubConversation" $ + testObjects + [ (testObject_PublicSubConversation_1, "testObject_PublicSubConversation_1.json"), + (testObject_PublicSubConversation_2, "testObject_PublicSubConversation_2.json") ] ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs new file mode 100644 index 00000000000..640dde1c778 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs @@ -0,0 +1,77 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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.Wire.API.Golden.Manual.SubConversation + ( testObject_PublicSubConversation_1, + testObject_PublicSubConversation_2, + ) +where + +import Data.Domain +import Data.Id +import Data.Qualified +import qualified Data.UUID as UUID +import Imports +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Credential +import Wire.API.MLS.Epoch +import Wire.API.MLS.Group +import Wire.API.MLS.SubConversation + +subConvId1 :: SubConvId +subConvId1 = SubConvId "test_group" + +subConvId2 :: SubConvId +subConvId2 = SubConvId "call" + +domain :: Domain +domain = Domain "golden.example.com" + +convId :: Qualified ConvId +convId = + Qualified + ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")) + ) + domain + +testObject_PublicSubConversation_1 :: PublicSubConversation +testObject_PublicSubConversation_1 = + PublicSubConversation + convId + subConvId1 + (GroupId "test_group") + (Epoch 5) + MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + [] + +testObject_PublicSubConversation_2 :: PublicSubConversation +testObject_PublicSubConversation_2 = + PublicSubConversation + convId + subConvId2 + (GroupId "test_group_2") + (Epoch 0) + MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + [mkClientIdentity user cid] + where + user :: Qualified UserId + user = + Qualified + ( Id (fromJust (UUID.fromString "00000000-0000-0007-0000-000a00000002")) + ) + domain + cid = ClientId "deadbeef" diff --git a/libs/wire-api/test/golden/testObject_PublicSubConversation_1.json b/libs/wire-api/test/golden/testObject_PublicSubConversation_1.json new file mode 100644 index 00000000000..d81e3853f4e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_PublicSubConversation_1.json @@ -0,0 +1,11 @@ +{ + "cipher_suite": 1, + "epoch": 5, + "group_id": "dGVzdF9ncm91cA==", + "members": [], + "parent_qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0001-0000-000100000001" + }, + "subconv_id": "test_group" +} diff --git a/libs/wire-api/test/golden/testObject_PublicSubConversation_2.json b/libs/wire-api/test/golden/testObject_PublicSubConversation_2.json new file mode 100644 index 00000000000..ac57e7e8e1b --- /dev/null +++ b/libs/wire-api/test/golden/testObject_PublicSubConversation_2.json @@ -0,0 +1,17 @@ +{ + "cipher_suite": 1, + "epoch": 0, + "group_id": "dGVzdF9ncm91cF8y", + "members": [ + { + "client_id": "deadbeef", + "domain": "golden.example.com", + "user_id": "00000000-0000-0007-0000-000a00000002" + } + ], + "parent_qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0001-0000-000100000001" + }, + "subconv_id": "call" +} diff --git a/libs/wire-api/test/unit/Main.hs b/libs/wire-api/test/unit/Main.hs index 46178a1d7a5..e90bd6b3fe1 100644 --- a/libs/wire-api/test/unit/Main.hs +++ b/libs/wire-api/test/unit/Main.hs @@ -25,6 +25,7 @@ import Test.Tasty import qualified Test.Wire.API.Call.Config as Call.Config import qualified Test.Wire.API.Conversation as Conversation import qualified Test.Wire.API.MLS as MLS +import qualified Test.Wire.API.MLS.SubConversation as SubConversation import qualified Test.Wire.API.Roundtrip.Aeson as Roundtrip.Aeson import qualified Test.Wire.API.Roundtrip.ByteString as Roundtrip.ByteString import qualified Test.Wire.API.Roundtrip.CSV as Roundtrip.CSV @@ -59,5 +60,6 @@ main = Roundtrip.CSV.tests, Routes.tests, Conversation.tests, - MLS.tests + MLS.tests, + SubConversation.tests ] diff --git a/libs/wire-api/test/unit/Test/Wire/API/MLS/SubConversation.hs b/libs/wire-api/test/unit/Test/Wire/API/MLS/SubConversation.hs new file mode 100644 index 00000000000..6783346756f --- /dev/null +++ b/libs/wire-api/test/unit/Test/Wire/API/MLS/SubConversation.hs @@ -0,0 +1,46 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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.Wire.API.MLS.SubConversation where + +import Data.Domain +import Data.Id +import Data.Qualified +import Imports +import Test.QuickCheck +import Test.Tasty +import Test.Tasty.QuickCheck +import Wire.API.MLS.SubConversation + +tests :: TestTree +tests = + testGroup + "Subconversation" + [ testProperty "injectivity of the initial group ID mapping" $ + forAll genIds injectiveInitialGroupId + ] + where + genIds :: Gen (ConvId, SubConvId, SubConvId) + genIds = do + s1 <- arbitrary + (,,) <$> arbitrary <*> pure s1 <*> arbitrary `suchThat` (/= s1) + +injectiveInitialGroupId :: (ConvId, SubConvId, SubConvId) -> Property +injectiveInitialGroupId (cnv, scnv1, scnv2) = do + let domain = Domain "group.example.com" + lcnv = toLocalUnsafe domain cnv + initialGroupId lcnv scnv1 =/= initialGroupId lcnv scnv2 diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 0ec71019ec7..9cc7cafecdf 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -530,6 +530,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.ListConversations Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap Test.Wire.API.Golden.Manual.SearchResultContact + Test.Wire.API.Golden.Manual.SubConversation Test.Wire.API.Golden.Manual.TeamSize Test.Wire.API.Golden.Manual.Token Test.Wire.API.Golden.Manual.UserClientPrekeyMap @@ -640,6 +641,7 @@ test-suite wire-api-tests Test.Wire.API.Call.Config Test.Wire.API.Conversation Test.Wire.API.MLS + Test.Wire.API.MLS.SubConversation Test.Wire.API.Roundtrip.Aeson Test.Wire.API.Roundtrip.ByteString Test.Wire.API.Roundtrip.CSV diff --git a/services/galley/default.nix b/services/galley/default.nix index cf8abcd2063..f2a502882fd 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -46,6 +46,7 @@ , HsOpenSSL , HsOpenSSL-x509-system , hspec +, http-api-data , http-client , http-client-openssl , http-client-tls @@ -276,6 +277,7 @@ mkDerivation { HsOpenSSL HsOpenSSL-x509-system hspec + http-api-data http-client http-client-openssl http-client-tls diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 69cb4423385..346c984e7d8 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -38,6 +38,7 @@ library Galley.API.MLS.Message Galley.API.MLS.Propagate Galley.API.MLS.Removal + Galley.API.MLS.SubConversation Galley.API.MLS.Types Galley.API.MLS.Util Galley.API.MLS.Welcome @@ -79,6 +80,7 @@ library Galley.Cassandra.SearchVisibility Galley.Cassandra.Services Galley.Cassandra.Store + Galley.Cassandra.SubConversation Galley.Cassandra.Team Galley.Cassandra.TeamFeatures Galley.Cassandra.TeamNotifications @@ -108,6 +110,7 @@ library Galley.Effects.SearchVisibilityStore Galley.Effects.ServiceStore Galley.Effects.SparAccess + Galley.Effects.SubConversationStore Galley.Effects.TeamFeatureStore Galley.Effects.TeamMemberStore Galley.Effects.TeamNotificationStore @@ -473,6 +476,7 @@ executable galley-integration , HsOpenSSL , HsOpenSSL-x509-system , hspec + , http-api-data , http-client , http-client-openssl , http-client-tls @@ -692,6 +696,7 @@ executable galley-schema V75_MLSGroupInfo V76_ProposalOrigin V77_MLSGroupMemberClient + V78_MLSSubconversation hs-source-dirs: schema/src default-extensions: diff --git a/services/galley/schema/src/Main.hs b/services/galley/schema/src/Main.hs index bb3642744b7..33f3f719a1c 100644 --- a/services/galley/schema/src/Main.hs +++ b/services/galley/schema/src/Main.hs @@ -80,6 +80,7 @@ import qualified V74_ExposeInvitationsToTeamAdmin import qualified V75_MLSGroupInfo import qualified V76_ProposalOrigin import qualified V77_MLSGroupMemberClient +import qualified V78_MLSSubconversation main :: IO () main = do @@ -145,7 +146,8 @@ main = do V74_ExposeInvitationsToTeamAdmin.migration, V75_MLSGroupInfo.migration, V76_ProposalOrigin.migration, - V77_MLSGroupMemberClient.migration + V77_MLSGroupMemberClient.migration, + V78_MLSSubconversation.migration -- When adding migrations here, don't forget to update -- 'schemaVersion' in Galley.Cassandra -- (see also docs/developer/cassandra-interaction.md) diff --git a/services/galley/schema/src/V78_MLSSubconversation.hs b/services/galley/schema/src/V78_MLSSubconversation.hs new file mode 100644 index 00000000000..f83ec0bc1e2 --- /dev/null +++ b/services/galley/schema/src/V78_MLSSubconversation.hs @@ -0,0 +1,42 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 V78_MLSSubconversation (migration) where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 78 "Add the MLS subconversation tables" $ do + schema' + [r| CREATE TABLE subconversation ( + conv_id uuid, + subconv_id text, + group_id blob, + cipher_suite int, + public_group_state blob, + epoch bigint, + PRIMARY KEY (conv_id, subconv_id) + ); + |] + schema' + [r| ALTER TABLE group_id_conv_id ADD ( + subconv_id text + ); + |] diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 870c51b658e..dbf85df9149 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -57,6 +57,7 @@ import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E import Galley.Effects.ProposalStore (ProposalStore) +import Galley.Effects.SubConversationStore import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.UserList (UserList (UserList)) @@ -90,7 +91,6 @@ import Wire.API.MLS.Credential import Wire.API.MLS.Message import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation -import Wire.API.MLS.SubConversation import Wire.API.MLS.Welcome import Wire.API.Message import Wire.API.Routes.Internal.Brig.Connection @@ -202,7 +202,7 @@ onNewRemoteConversation :: onNewRemoteConversation domain nrc = do -- update group_id -> conv_id mapping for_ (preview (to F.nrcProtocol . _ProtocolMLS) nrc) $ \mls -> - E.setGroupId (cnvmlsGroupId mls) (Qualified (F.nrcConvId nrc) domain) + E.setGroupIdForConversation (cnvmlsGroupId mls) (Qualified (F.nrcConvId nrc) domain) pure EmptyResponse @@ -601,24 +601,25 @@ updateConversation origDomain updateRequest = do sendMLSCommitBundle :: ( Members - [ BrigAccess, - ConversationStore, - ExternalAccess, - Error FederationError, - Error InternalError, - FederatorAccess, - GundeckAccess, - Input (Local ()), - Input Env, - Input Opts, - Input UTCTime, - LegalHoldStore, - MemberStore, - Resource, - TeamStore, - P.TinyLog, - ProposalStore - ] + '[ BrigAccess, + ConversationStore, + Error FederationError, + Error InternalError, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Input Env, + Input (Local ()), + Input Opts, + Input UTCTime, + LegalHoldStore, + MemberStore, + ProposalStore, + P.TinyLog, + Resource, + SubConversationStore, + TeamStore + ] r ) => Domain -> @@ -638,10 +639,10 @@ sendMLSCommitBundle remoteDomain msr = let sender = toRemoteUnsafe remoteDomain (F.mmsrSender msr) bundle <- either (throw . mlsProtocolError) pure $ deserializeCommitBundle (fromBase64ByteString (F.mmsrRawMessage msr)) let msg = rmValue (cbCommitMsg bundle) - qcnv <- E.getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound - when (Conv (qUnqualified qcnv) /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch + qConvOrSub <- E.lookupConvByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound + when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch F.MLSMessageResponseUpdates . map lcuUpdate - <$> postMLSCommitBundle loc (tUntagged sender) Nothing qcnv Nothing bundle + <$> postMLSCommitBundle loc (tUntagged sender) Nothing qConvOrSub Nothing bundle sendMLSMessage :: ( Members @@ -659,6 +660,7 @@ sendMLSMessage :: LegalHoldStore, MemberStore, Resource, + SubConversationStore, TeamStore, P.TinyLog, ProposalStore @@ -683,10 +685,10 @@ sendMLSMessage remoteDomain msr = raw <- either (throw . mlsProtocolError) pure $ decodeMLS' (fromBase64ByteString (F.mmsrRawMessage msr)) case rmValue raw of SomeMessage _ msg -> do - qcnv <- E.getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound - when (Conv (qUnqualified qcnv) /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch + qConvOrSub <- E.lookupConvByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound + when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch F.MLSMessageResponseUpdates . map lcuUpdate - <$> postMLSMessage loc (tUntagged sender) Nothing qcnv Nothing raw + <$> postMLSMessage loc (tUntagged sender) Nothing qConvOrSub Nothing raw class ToGalleyRuntimeError (effs :: EffectRow) r where mapToGalleyError :: diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 16fb3e71a4c..4e908005475 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -56,6 +56,7 @@ import Galley.Effects.ConversationStore import Galley.Effects.FederatorAccess import Galley.Effects.MemberStore import Galley.Effects.ProposalStore +import Galley.Effects.SubConversationStore import Galley.Env import Galley.Options import Galley.Types.Conversations.Members @@ -138,6 +139,7 @@ postMLSMessageFromLocalUserV1 :: Input (Local ()), ProposalStore, Resource, + SubConversationStore, TinyLog ] r @@ -151,9 +153,9 @@ postMLSMessageFromLocalUserV1 lusr mc conn smsg = do assertMLSEnabled case rmValue smsg of SomeMessage _ msg -> do - qcnv <- getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound + cnvOrSub <- lookupConvByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound map lcuEvent - <$> postMLSMessage lusr (tUntagged lusr) mc qcnv (Just conn) smsg + <$> postMLSMessage lusr (tUntagged lusr) mc cnvOrSub (Just conn) smsg postMLSMessageFromLocalUser :: ( HasProposalEffects r, @@ -176,6 +178,7 @@ postMLSMessageFromLocalUser :: Input (Local ()), ProposalStore, Resource, + SubConversationStore, TinyLog ] r @@ -207,6 +210,7 @@ postMLSCommitBundle :: MemberStore, ProposalStore, Resource, + SubConversationStore, TinyLog ] r @@ -214,16 +218,16 @@ postMLSCommitBundle :: Local x -> Qualified UserId -> Maybe ClientId -> - Qualified ConvId -> + Qualified ConvOrSubConvId -> Maybe ConnId -> CommitBundle -> Sem r [LocalConversationUpdate] -postMLSCommitBundle loc qusr mc qcnv conn rawBundle = +postMLSCommitBundle loc qusr mc qConvOrSub conn rawBundle = foldQualified loc (postMLSCommitBundleToLocalConv qusr mc conn rawBundle) (postMLSCommitBundleToRemoteConv loc qusr conn rawBundle) - qcnv + qConvOrSub postMLSCommitBundleFromLocalUser :: ( HasProposalEffects r, @@ -239,6 +243,7 @@ postMLSCommitBundleFromLocalUser :: MemberStore, ProposalStore, Resource, + SubConversationStore, TinyLog ] r @@ -251,10 +256,10 @@ postMLSCommitBundleFromLocalUser :: postMLSCommitBundleFromLocalUser lusr mc conn bundle = do assertMLSEnabled let msg = rmValue (cbCommitMsg bundle) - qcnv <- getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound + qConvOrSub <- lookupConvByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound events <- map lcuEvent - <$> postMLSCommitBundle lusr (tUntagged lusr) mc qcnv (Just conn) bundle + <$> postMLSCommitBundle lusr (tUntagged lusr) mc qConvOrSub (Just conn) bundle t <- toUTCTimeMillis <$> input pure $ MLSMessageSendingStatus events t @@ -268,8 +273,10 @@ postMLSCommitBundleToLocalConv :: Error MLSProtocolError, Input Opts, Input UTCTime, + MemberStore, ProposalStore, Resource, + SubConversationStore, TinyLog ] r @@ -278,22 +285,19 @@ postMLSCommitBundleToLocalConv :: Maybe ClientId -> Maybe ConnId -> CommitBundle -> - Local ConvId -> + Local ConvOrSubConvId -> Sem r [LocalConversationUpdate] -postMLSCommitBundleToLocalConv qusr mc conn bundle lcnv = do - let msg = rmValue (cbCommitMsg bundle) - conv <- getLocalConvForUser qusr lcnv - mlsMeta <- Data.mlsMetadata conv & noteS @'ConvNotFound +postMLSCommitBundleToLocalConv qusr mc conn bundle lConvOrSubId = do + lConvOrSub <- fetchConvOrSub qusr lConvOrSubId - let lconv = qualifyAs lcnv conv - cm <- lookupMLSClients (cnvmlsGroupId mlsMeta) + let msg = rmValue (cbCommitMsg bundle) senderClient <- fmap ciClient <$> getSenderIdentity qusr mc SMLSPlainText msg events <- case msgPayload msg of CommitMessage commit -> do - action <- getCommitData lconv mlsMeta (msgEpoch msg) commit + action <- getCommitData lConvOrSub (msgEpoch msg) commit -- check that the welcome message matches the action for_ (cbWelcome bundle) $ \welcome -> when @@ -306,22 +310,20 @@ postMLSCommitBundleToLocalConv qusr mc conn bundle lcnv = do qusr senderClient conn - lconv - mlsMeta - cm + lConvOrSub (msgEpoch msg) action (msgSender msg) commit - storeGroupInfoBundle lconv (cbGroupInfoBundle bundle) + storeGroupInfoBundle (idForConvOrSub . tUnqualified $ lConvOrSub) (cbGroupInfoBundle bundle) pure updates ApplicationMessage _ -> throwS @'MLSUnsupportedMessage ProposalMessage _ -> throwS @'MLSUnsupportedMessage - propagateMessage qusr (qualifyAs lcnv conv) cm conn (rmRaw (cbCommitMsg bundle)) + propagateMessage qusr lConvOrSub conn (rmRaw (cbCommitMsg bundle)) for_ (cbWelcome bundle) $ - postMLSWelcome lcnv conn + postMLSWelcome lConvOrSub conn pure events @@ -343,19 +345,20 @@ postMLSCommitBundleToRemoteConv :: Qualified UserId -> Maybe ConnId -> CommitBundle -> - Remote ConvId -> + Remote ConvOrSubConvId -> Sem r [LocalConversationUpdate] -postMLSCommitBundleToRemoteConv loc qusr con bundle rcnv = do +postMLSCommitBundleToRemoteConv loc qusr con bundle rConvOrSubId = do -- only local users can send messages to remote conversations lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr -- only members may send commit bundles to a remote conversation - flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) rcnv + + flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) (convOfConvOrSub <$> rConvOrSubId) resp <- - runFederated rcnv $ + runFederated rConvOrSubId $ fedClient @'Galley @"send-mls-commit-bundle" $ MLSMessageSendRequest - { mmsrConvOrSubId = Conv $ tUnqualified rcnv, + { mmsrConvOrSubId = tUnqualified rConvOrSubId, mmsrSender = tUnqualified lusr, mmsrRawMessage = Base64ByteString (serializeCommitBundle bundle) } @@ -366,7 +369,7 @@ postMLSCommitBundleToRemoteConv loc qusr con bundle rcnv = do MLSMessageResponseUpdates updates -> pure updates for updates $ \update -> do - e <- notifyRemoteConversationAction loc (qualifyAs rcnv update) con + e <- notifyRemoteConversationAction loc (qualifyAs rConvOrSubId update) con pure (LocalConversationUpdate e update) postMLSMessage :: @@ -390,6 +393,7 @@ postMLSMessage :: Input (Local ()), ProposalStore, Resource, + SubConversationStore, TinyLog ] r @@ -397,18 +401,18 @@ postMLSMessage :: Local x -> Qualified UserId -> Maybe ClientId -> - Qualified ConvId -> + Qualified ConvOrSubConvId -> Maybe ConnId -> RawMLS SomeMessage -> Sem r [LocalConversationUpdate] -postMLSMessage loc qusr mc qcnv con smsg = case rmValue smsg of +postMLSMessage loc qusr mc qconvOrSub con smsg = case rmValue smsg of SomeMessage tag msg -> do mSender <- fmap ciClient <$> getSenderIdentity qusr mc tag msg foldQualified loc (postMLSMessageToLocalConv qusr mSender con smsg) (postMLSMessageToRemoteConv loc qusr mSender con smsg) - qcnv + qconvOrSub -- Check that the MLS client who created the message belongs to the user who -- is the sender of the REST request, identified by HTTP header. @@ -474,8 +478,12 @@ postMLSMessageToLocalConv :: ErrorS 'MLSSelfRemovalNotAllowed, ErrorS 'MLSStaleMessage, ErrorS 'MLSUnsupportedMessage, + ErrorS 'MissingLegalholdConsent, + ErrorS 'ConvNotFound, + MemberStore, ProposalStore, Resource, + SubConversationStore, TinyLog ] r @@ -484,39 +492,37 @@ postMLSMessageToLocalConv :: Maybe ClientId -> Maybe ConnId -> RawMLS SomeMessage -> - Local ConvId -> + Local ConvOrSubConvId -> Sem r [LocalConversationUpdate] -postMLSMessageToLocalConv qusr senderClient con smsg lcnv = case rmValue smsg of +postMLSMessageToLocalConv qusr senderClient con smsg convOrSubId = case rmValue smsg of SomeMessage tag msg -> do - conv <- getLocalConvForUser qusr lcnv - mlsMeta <- Data.mlsMetadata conv & noteS @'ConvNotFound - - -- construct client map - cm <- lookupMLSClients (cnvmlsGroupId mlsMeta) - let lconv = qualifyAs lcnv conv + lConvOrSub <- fetchConvOrSub qusr convOrSubId -- validate message events <- case tag of SMLSPlainText -> case msgPayload msg of CommitMessage c -> - processCommit qusr senderClient con lconv mlsMeta cm (msgEpoch msg) (msgSender msg) c + processCommit qusr senderClient con lConvOrSub (msgEpoch msg) (msgSender msg) c ApplicationMessage _ -> throwS @'MLSUnsupportedMessage ProposalMessage prop -> - processProposal qusr conv mlsMeta msg prop $> mempty + processProposal qusr lConvOrSub msg prop $> mempty SMLSCipherText -> case toMLSEnum' (msgContentType (msgPayload msg)) of Right CommitMessageTag -> throwS @'MLSUnsupportedMessage Right ProposalMessageTag -> throwS @'MLSUnsupportedMessage Right ApplicationMessageTag -> pure mempty Left _ -> throwS @'MLSUnsupportedMessage - -- forward message - propagateMessage qusr lconv cm con (rmRaw smsg) + propagateMessage qusr lConvOrSub con (rmRaw smsg) pure events postMLSMessageToRemoteConv :: ( Members MLSMessageStaticErrors r, - Members '[Error FederationError, TinyLog] r, + Members + '[ Error FederationError, + TinyLog + ] + r, HasProposalEffects r ) => Local x -> @@ -524,19 +530,19 @@ postMLSMessageToRemoteConv :: Maybe ClientId -> Maybe ConnId -> RawMLS SomeMessage -> - Remote ConvId -> + Remote ConvOrSubConvId -> Sem r [LocalConversationUpdate] -postMLSMessageToRemoteConv loc qusr _senderClient con smsg rcnv = do +postMLSMessageToRemoteConv loc qusr _senderClient con smsg rConvOrSubId = do -- only local users can send messages to remote conversations lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr -- only members may send messages to the remote conversation - flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) rcnv + flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) (convOfConvOrSub <$> rConvOrSubId) resp <- - runFederated rcnv $ + runFederated rConvOrSubId $ fedClient @'Galley @"send-mls-message" $ MLSMessageSendRequest - { mmsrConvOrSubId = Conv $ tUnqualified rcnv, + { mmsrConvOrSubId = tUnqualified rConvOrSubId, mmsrSender = tUnqualified lusr, mmsrRawMessage = Base64ByteString (rmRaw smsg) } @@ -547,7 +553,7 @@ postMLSMessageToRemoteConv loc qusr _senderClient con smsg rcnv = do MLSMessageResponseUpdates updates -> pure updates for updates $ \update -> do - e <- notifyRemoteConversationAction loc (qualifyAs rcnv update) con + e <- notifyRemoteConversationAction loc (qualifyAs rConvOrSubId update) con pure (LocalConversationUpdate e update) type HasProposalEffects r = @@ -613,19 +619,20 @@ getCommitData :: Member (Input UTCTime) r, Member TinyLog r ) => - Local Data.Conversation -> - ConversationMLSData -> + Local ConvOrSubConv -> Epoch -> Commit -> Sem r ProposalAction -getCommitData lconv mlsMeta epoch commit = do - let curEpoch = cnvmlsEpoch mlsMeta +getCommitData lConvOrSub epoch commit = do + let convOrSub = tUnqualified lConvOrSub + mlsMeta = mlsMetaConvOrSub convOrSub + curEpoch = cnvmlsEpoch mlsMeta groupId = cnvmlsGroupId mlsMeta suite = cnvmlsCipherSuite mlsMeta -- check epoch number when (epoch /= curEpoch) $ throwS @'MLSStaleMessage - foldMap (applyProposalRef (tUnqualified lconv) mlsMeta groupId epoch suite) (cProposals commit) + foldMap (applyProposalRef (idForConvOrSub convOrSub) mlsMeta groupId epoch suite) (cProposals commit) processCommit :: ( HasProposalEffects r, @@ -642,21 +649,20 @@ processCommit :: Member (Input (Local ())) r, Member ProposalStore r, Member BrigAccess r, - Member Resource r + Member Resource r, + Member SubConversationStore r ) => Qualified UserId -> Maybe ClientId -> Maybe ConnId -> - Local Data.Conversation -> - ConversationMLSData -> - ClientMap -> + Local ConvOrSubConv -> Epoch -> Sender 'MLSPlainText -> Commit -> Sem r [LocalConversationUpdate] -processCommit qusr senderClient con lconv mlsMeta cm epoch sender commit = do - action <- getCommitData lconv mlsMeta epoch commit - processCommitWithAction qusr senderClient con lconv mlsMeta cm epoch action sender commit +processCommit qusr senderClient con lConvOrSub epoch sender commit = do + action <- getCommitData lConvOrSub epoch commit + processCommitWithAction qusr senderClient con lConvOrSub epoch action sender commit processExternalCommit :: forall r. @@ -669,6 +675,7 @@ processExternalCommit :: ErrorS 'MLSKeyPackageRefNotFound, ErrorS 'MLSStaleMessage, ErrorS 'MLSMissingSenderClient, + Error InternalError, ExternalAccess, FederatorAccess, GundeckAccess, @@ -677,19 +684,19 @@ processExternalCommit :: MemberStore, ProposalStore, Resource, + SubConversationStore, TinyLog ] r => Qualified UserId -> Maybe ClientId -> - Local Data.Conversation -> - ConversationMLSData -> - ClientMap -> + Local ConvOrSubConv -> Epoch -> ProposalAction -> Maybe UpdatePath -> Sem r () -processExternalCommit qusr mSenderClient lconv mlsMeta cm epoch action updatePath = withCommitLock (cnvmlsGroupId mlsMeta) epoch $ do +processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = withCommitLock (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub) epoch $ do + let convOrSub = tUnqualified lConvOrSub newKeyPackage <- upLeaf <$> note @@ -711,7 +718,7 @@ processExternalCommit qusr mSenderClient lconv mlsMeta cm epoch action updatePat nkpresClientIdentity <$$> validateAndAddKeyPackageRef NewKeyPackage - { nkpConversation = Data.convId <$> tUntagged lconv, + { nkpConversation = tUntagged (convOfConvOrSub . idForConvOrSub <$> lConvOrSub), nkpKeyPackage = KeyPackageData (rmRaw newKeyPackage) } cid <- either (\errMsg -> throw (mlsProtocolError ("Tried to add invalid KeyPackage: " <> errMsg))) pure eithCid @@ -738,16 +745,18 @@ processExternalCommit qusr mSenderClient lconv mlsMeta cm epoch action updatePat $ "The external commit attempts to remove a client from a user other than themselves" pure (Just r) - updateKeyPackageMapping lconv (cnvmlsGroupId mlsMeta) qusr (ciClient cid) remRef newRef + updateKeyPackageMapping lConvOrSub qusr (ciClient cid) remRef newRef -- increment epoch number - setConversationEpoch (Data.convId (tUnqualified lconv)) (succ epoch) - -- fetch local conversation with new epoch - lc <- qualifyAs lconv <$> getLocalConvForUser qusr (convId <$> lconv) + setConvOrSubEpoch (idForConvOrSub convOrSub) (succ epoch) + -- fetch conversation or sub with new epoch + lConvOrSub' <- fetchConvOrSub qusr (idForConvOrSub <$> lConvOrSub) + let convOrSub' = tUnqualified lConvOrSub + -- fetch backend remove proposals of the previous epoch - kpRefs <- getPendingBackendRemoveProposals (cnvmlsGroupId mlsMeta) epoch + kpRefs <- getPendingBackendRemoveProposals (cnvmlsGroupId . mlsMetaConvOrSub $ convOrSub') epoch -- requeue backend remove proposals for the current epoch - removeClientsWithClientMap lc kpRefs cm qusr + removeClientsWithClientMap lConvOrSub' kpRefs qusr where derefUser :: ClientMap -> Qualified UserId -> Sem r (ClientIdentity, KeyPackageRef) derefUser (Map.toList -> l) user = case l of @@ -784,23 +793,22 @@ processCommitWithAction :: Member (Input (Local ())) r, Member ProposalStore r, Member BrigAccess r, - Member Resource r + Member Resource r, + Member SubConversationStore r ) => Qualified UserId -> Maybe ClientId -> Maybe ConnId -> - Local Data.Conversation -> - ConversationMLSData -> - ClientMap -> + Local ConvOrSubConv -> Epoch -> ProposalAction -> Sender 'MLSPlainText -> Commit -> Sem r [LocalConversationUpdate] -processCommitWithAction qusr senderClient con lconv mlsMeta cm epoch action sender commit = +processCommitWithAction qusr senderClient con lConvOrSub epoch action sender commit = case sender of - MemberSender ref -> processInternalCommit qusr senderClient con lconv mlsMeta cm epoch action ref commit - NewMemberSender -> processExternalCommit qusr senderClient lconv mlsMeta cm epoch action (cPath commit) $> [] + MemberSender ref -> processInternalCommit qusr senderClient con lConvOrSub epoch action ref commit + NewMemberSender -> processExternalCommit qusr senderClient lConvOrSub epoch action (cPath commit) $> [] _ -> throw (mlsProtocolError "Unexpected sender") processInternalCommit :: @@ -824,24 +832,29 @@ processInternalCommit :: Qualified UserId -> Maybe ClientId -> Maybe ConnId -> - Local Data.Conversation -> - ConversationMLSData -> - ClientMap -> + Local ConvOrSubConv -> Epoch -> ProposalAction -> KeyPackageRef -> Commit -> Sem r [LocalConversationUpdate] -processInternalCommit qusr senderClient con lconv mlsMeta cm epoch action senderRef commit = do - self <- noteS @'ConvNotFound $ getConvMember lconv (tUnqualified lconv) qusr - - withCommitLock (cnvmlsGroupId mlsMeta) epoch $ do +processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef commit = do + let convOrSub = tUnqualified lConvOrSub + mlsMeta = mlsMetaConvOrSub convOrSub + self <- + noteS @'ConvNotFound $ + getConvMember + lConvOrSub + (mcConv . convOfConvOrSub $ convOrSub) + qusr + + withCommitLock (cnvmlsGroupId . mlsMetaConvOrSub $ convOrSub) epoch $ do postponedKeyPackageRefUpdate <- if epoch == Epoch 0 then do - let cType = cnvmType . convMetadata . tUnqualified $ lconv - case (self, cType, cmAssocs cm) of - (Left _, SelfConv, []) -> do + let cType = cnvmType . convMetadata . mcConv . convOfConvOrSub $ convOrSub + case (self, cType, cmAssocs . membersConvOrSub $ convOrSub, convOrSub) of + (Left _, SelfConv, [], Conv _) -> do creatorClient <- noteS @'MLSMissingSenderClient senderClient creatorRef <- maybe @@ -855,13 +868,12 @@ processInternalCommit qusr senderClient con lconv mlsMeta cm epoch action sender (cnvmlsGroupId mlsMeta) qusr (Set.singleton (creatorClient, creatorRef)) - (Left _, SelfConv, _) -> - throw . InternalErrorWithDescription $ - "Unexpected creator client set in a self-conversation" - -- this is a newly created conversation, and it should contain exactly one - -- client (the creator) - (Left lm, _, [(qu, (creatorClient, _))]) - | qu == tUntagged (qualifyAs lconv (lmId lm)) -> do + (Left _, SelfConv, _, _) -> + -- this is a newly created (sub)conversation, and it should + -- contain exactly one client (the creator) + throw (InternalErrorWithDescription "Unexpected creator client set") + (Left lm, _, [(qu, (creatorClient, _))], Conv _) + | qu == tUntagged (qualifyAs lConvOrSub (lmId lm)) -> do -- use update path as sender reference and if not existing fall back to sender senderRef' <- maybe @@ -872,11 +884,17 @@ processInternalCommit qusr senderClient con lconv mlsMeta cm epoch action sender ) $ cPath commit -- register the creator client - updateKeyPackageMapping lconv (cnvmlsGroupId mlsMeta) qusr creatorClient Nothing senderRef' + updateKeyPackageMapping + lConvOrSub + qusr + creatorClient + Nothing + senderRef' -- remote clients cannot send the first commit - (Right _, _, _) -> throwS @'MLSStaleMessage + (Right _, _, _, _) -> throwS @'MLSStaleMessage + (_, _, _, SubConv _ _) -> pure () -- uninitialised conversations should contain exactly one client - (_, _, _) -> + (_, _, _, Conv _) -> throw (InternalErrorWithDescription "Unexpected creator client set") pure $ pure () -- no key package ref update necessary else case upLeaf <$> cPath commit of @@ -884,7 +902,15 @@ processInternalCommit qusr senderClient con lconv mlsMeta cm epoch action sender updatedRef <- kpRef' updatedKeyPackage & note (mlsProtocolError "Could not compute key package ref") -- postpone key package ref update until other checks/processing passed case senderClient of - Just cli -> pure (updateKeyPackageMapping lconv (cnvmlsGroupId mlsMeta) qusr cli (Just senderRef) updatedRef) + Just cli -> + pure + ( updateKeyPackageMapping + lConvOrSub + qusr + cli + (Just senderRef) + updatedRef + ) Nothing -> pure (pure ()) Nothing -> pure (pure ()) -- ignore commits without update path @@ -895,37 +921,37 @@ processInternalCommit qusr senderClient con lconv mlsMeta cm epoch action sender throwS @'MLSCommitMissingReferences -- process and execute proposals - updates <- executeProposalAction qusr con lconv mlsMeta cm action + updates <- executeProposalAction lConvOrSub qusr con convOrSub action -- update key package ref if necessary postponedKeyPackageRefUpdate -- increment epoch number - setConversationEpoch (Data.convId (tUnqualified lconv)) (succ epoch) + setConvOrSubEpoch (idForConvOrSub convOrSub) (succ epoch) pure updates -- | Note: Use this only for KeyPackage that are already validated updateKeyPackageMapping :: Members '[BrigAccess, MemberStore] r => - Local Data.Conversation -> - GroupId -> + Local ConvOrSubConv -> Qualified UserId -> ClientId -> Maybe KeyPackageRef -> KeyPackageRef -> Sem r () -updateKeyPackageMapping lconv groupId qusr cid mOld new = do - let lcnv = fmap Data.convId lconv +updateKeyPackageMapping lConvOrSub qusr cid mOld new = do + let qconv = tUntagged (convOfConvOrSub . idForConvOrSub <$> lConvOrSub) -- update actual mapping in brig case mOld of Nothing -> - addKeyPackageRef new qusr cid (tUntagged lcnv) + addKeyPackageRef new qusr cid qconv Just old -> updateKeyPackageRef KeyPackageUpdate { kpupPrevious = old, kpupNext = new } + let groupId = cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub -- remove old (client, key package) pair removeMLSClients groupId qusr (Set.singleton cid) @@ -942,50 +968,50 @@ applyProposalRef :: ] r ) => - Data.Conversation -> + ConvOrSubConvId -> ConversationMLSData -> GroupId -> Epoch -> CipherSuiteTag -> ProposalOrRef -> Sem r ProposalAction -applyProposalRef conv mlsMeta groupId epoch _suite (Ref ref) = do +applyProposalRef convOrSubConvId mlsMeta groupId epoch _suite (Ref ref) = do p <- getProposal groupId epoch ref >>= noteS @'MLSProposalNotFound checkEpoch epoch mlsMeta checkGroup groupId mlsMeta - applyProposal (convId conv) groupId (rmValue p) -applyProposalRef conv _mlsMeta groupId _epoch suite (Inline p) = do + applyProposal convOrSubConvId groupId (rmValue p) +applyProposalRef convOrSubConvId _mlsMeta groupId _epoch suite (Inline p) = do checkProposalCipherSuite suite p - applyProposal (convId conv) groupId p + applyProposal convOrSubConvId groupId p applyProposal :: forall r. HasProposalEffects r => - ConvId -> + ConvOrSubConvId -> GroupId -> Proposal -> Sem r ProposalAction -applyProposal convId groupId (AddProposal kp) = do +applyProposal convOrSubConvId groupId (AddProposal kp) = do ref <- kpRef' kp & note (mlsProtocolError "Could not compute ref of a key package in an Add proposal") mbClientIdentity <- getClientByKeyPackageRef ref clientIdentity <- case mbClientIdentity of Nothing -> do -- external add proposal for a new key package unknown to the backend - lconvId <- qualifyLocal convId - addKeyPackageMapping lconvId ref (KeyPackageData (rmRaw kp)) + lConvOrSubConvId <- qualifyLocal convOrSubConvId + addKeyPackageMapping lConvOrSubConvId ref (KeyPackageData (rmRaw kp)) Just ci -> -- ad-hoc add proposal in commit, the key package has been claimed before pure ci pure (paAddClient . (<$$>) (,ref) . cidQualifiedClient $ clientIdentity) where - addKeyPackageMapping :: Local ConvId -> KeyPackageRef -> KeyPackageData -> Sem r ClientIdentity - addKeyPackageMapping lconv ref kpdata = do + addKeyPackageMapping :: Local ConvOrSubConvId -> KeyPackageRef -> KeyPackageData -> Sem r ClientIdentity + addKeyPackageMapping lConvOrSubConvId ref kpdata = do -- validate and update mapping in brig eithCid <- nkpresClientIdentity <$$> validateAndAddKeyPackageRef NewKeyPackage - { nkpConversation = tUntagged lconv, + { nkpConversation = tUntagged (convOfConvOrSub <$> lConvOrSubConvId), nkpKeyPackage = kpdata } cid <- either (\errMsg -> throw (mlsProtocolError ("Tried to add invalid KeyPackage: " <> errMsg))) pure eithCid @@ -994,14 +1020,14 @@ applyProposal convId groupId (AddProposal kp) = do -- update mapping in galley addMLSClients groupId qusr (Set.singleton (ciClient cid, ref)) pure cid -applyProposal _conv _groupId (RemoveProposal ref) = do +applyProposal _convOrSubConvId _groupId (RemoveProposal ref) = do qclient <- cidQualifiedClient <$> derefKeyPackage ref pure (paRemoveClient ((,ref) <$$> qclient)) -applyProposal _conv _groupId (ExternalInitProposal _) = +applyProposal _convOrSubConvId _groupId (ExternalInitProposal _) = -- only record the fact there was an external init proposal, but do not -- process it in any way. pure paExternalInitPresent -applyProposal _conv _groupId _ = pure mempty +applyProposal _convOrSubConvId _groupId _ = pure mempty checkProposalCipherSuite :: Members @@ -1036,15 +1062,16 @@ processProposal :: ] r => Qualified UserId -> - Data.Conversation -> - ConversationMLSData -> + Local ConvOrSubConv -> Message 'MLSPlainText -> RawMLS Proposal -> Sem r () -processProposal qusr conv mlsMeta msg prop = do +processProposal qusr lConvOrSub msg prop = do + let mlsMeta = mlsMetaConvOrSub (tUnqualified lConvOrSub) checkEpoch (msgEpoch msg) mlsMeta checkGroup (msgGroupId msg) mlsMeta let suiteTag = cnvmlsCipherSuite mlsMeta + let cid = convId . mcConv . convOfConvOrSub . tUnqualified $ lConvOrSub -- validate the proposal -- @@ -1054,11 +1081,11 @@ processProposal qusr conv mlsMeta msg prop = do foldQualified loc ( fmap isJust - . getLocalMember (convId conv) + . getLocalMember cid . tUnqualified ) ( fmap isJust - . getRemoteMember (convId conv) + . getRemoteMember cid ) qusr unless isMember' $ throwS @'ConvNotFound @@ -1131,7 +1158,7 @@ checkExternalProposalUser qusr prop = do qusr executeProposalAction :: - forall r. + forall r x. ( Member BrigAccess r, Member ConversationStore r, Member (Error InternalError) r, @@ -1155,15 +1182,18 @@ executeProposalAction :: Member TeamStore r, Member TinyLog r ) => + Local x -> Qualified UserId -> Maybe ConnId -> - Local Data.Conversation -> - ConversationMLSData -> - ClientMap -> + ConvOrSubConv -> ProposalAction -> Sem r [LocalConversationUpdate] -executeProposalAction qusr con lconv mlsMeta cm action = do - let ss = csSignatureScheme (cnvmlsCipherSuite mlsMeta) +executeProposalAction _loc _qusr _con (SubConv _ _) _action = pure [] +executeProposalAction loc qusr con (Conv mlsConv) action = do + let lconv = qualifyAs loc . mcConv $ mlsConv + mlsMeta = mcMLSData mlsConv + cm = mcMembers mlsConv + ss = csSignatureScheme (cnvmlsCipherSuite mlsMeta) newUserClients = Map.assocs (paAdd action) -- Note [client removal] @@ -1218,17 +1248,17 @@ executeProposalAction qusr con lconv mlsMeta cm action = do -- FUTUREWORK: turn this error into a proper response throwS @'MLSClientMismatch - membersToRemove <- catMaybes <$> for removedUsers (uncurry checkRemoval) + membersToRemove <- catMaybes <$> for removedUsers (uncurry (checkRemoval cm)) -- add users to the conversation and send events - addEvents <- foldMap addMembers . nonEmpty . map fst $ newUserClients + addEvents <- foldMap (addMembers lconv) . nonEmpty . map fst $ newUserClients -- add clients in the conversation state for_ newUserClients $ \(qtarget, newClients) -> do addMLSClients (cnvmlsGroupId mlsMeta) qtarget newClients -- remove users from the conversation and send events - removeEvents <- foldMap removeMembers (nonEmpty membersToRemove) + removeEvents <- foldMap (removeMembers lconv) (nonEmpty membersToRemove) -- Remove clients from the conversation state. This includes client removals -- of all types (see Note [client removal]). @@ -1238,10 +1268,11 @@ executeProposalAction qusr con lconv mlsMeta cm action = do pure (addEvents <> removeEvents) where checkRemoval :: + ClientMap -> Qualified UserId -> Set ClientId -> Sem r (Maybe (Qualified UserId)) - checkRemoval qtarget clients = do + checkRemoval cm qtarget clients = do let clientsInConv = Set.map fst (Map.findWithDefault mempty qtarget cm) when (clients /= clientsInConv) $ do -- FUTUREWORK: turn this error into a proper response @@ -1250,20 +1281,20 @@ executeProposalAction qusr con lconv mlsMeta cm action = do throwS @'MLSSelfRemovalNotAllowed pure (Just qtarget) - existingLocalMembers :: Set (Qualified UserId) - existingLocalMembers = + existingLocalMembers :: Local Data.Conversation -> Set (Qualified UserId) + existingLocalMembers lconv = (Set.fromList . map (fmap lmId . tUntagged)) (traverse convLocalMembers lconv) - existingRemoteMembers :: Set (Qualified UserId) - existingRemoteMembers = + existingRemoteMembers :: Local Data.Conversation -> Set (Qualified UserId) + existingRemoteMembers lconv = Set.fromList . map (tUntagged . rmId) . convRemoteMembers . tUnqualified $ lconv - existingMembers :: Set (Qualified UserId) - existingMembers = existingLocalMembers <> existingRemoteMembers + existingMembers :: Local Data.Conversation -> Set (Qualified UserId) + existingMembers lconv = existingLocalMembers lconv <> existingRemoteMembers lconv - addMembers :: NonEmpty (Qualified UserId) -> Sem r [LocalConversationUpdate] - addMembers = + addMembers :: Local Data.Conversation -> NonEmpty (Qualified UserId) -> Sem r [LocalConversationUpdate] + addMembers lconv = -- FUTUREWORK: update key package ref mapping to reflect conversation membership foldMap ( handleNoChanges @@ -1273,11 +1304,11 @@ executeProposalAction qusr con lconv mlsMeta cm action = do . flip ConversationJoin roleNameWireMember ) . nonEmpty - . filter (flip Set.notMember existingMembers) + . filter (flip Set.notMember (existingMembers lconv)) . toList - removeMembers :: NonEmpty (Qualified UserId) -> Sem r [LocalConversationUpdate] - removeMembers = + removeMembers :: Local Data.Conversation -> NonEmpty (Qualified UserId) -> Sem r [LocalConversationUpdate] + removeMembers lconv = foldMap ( handleNoChanges . handleMLSProposalFailures @ProposalErrors @@ -1285,7 +1316,7 @@ executeProposalAction qusr con lconv mlsMeta cm action = do . updateLocalConversationUnchecked @'ConversationRemoveMembersTag lconv qusr con ) . nonEmpty - . filter (flip Set.member existingMembers) + . filter (flip Set.member (existingMembers lconv)) . toList handleNoChanges :: Monoid a => Sem (Error NoChanges ': r) a -> Sem r a @@ -1402,11 +1433,55 @@ withCommitLock gid epoch action = ttl = fromIntegral (600 :: Int) -- 10 minutes storeGroupInfoBundle :: - Member ConversationStore r => - Local Data.Conversation -> + Members + '[ ConversationStore, + SubConversationStore + ] + r => + ConvOrSubConvId -> GroupInfoBundle -> Sem r () -storeGroupInfoBundle lconv = - setPublicGroupState (Data.convId (tUnqualified lconv)) - . toOpaquePublicGroupState - . gipGroupState +storeGroupInfoBundle convOrSub bundle = case convOrSub of + Conv cid -> do + setPublicGroupState cid + . toOpaquePublicGroupState + . gipGroupState + $ bundle + SubConv _cid _subconvid -> do + -- FUTUREWORK: Write to subconversation + pure () + +fetchConvOrSub :: + forall r. + Members + '[ ConversationStore, + Error InternalError, + ErrorS 'ConvNotFound, + MemberStore, + SubConversationStore + ] + r => + Qualified UserId -> + Local ConvOrSubConvId -> + Sem r (Local ConvOrSubConv) +fetchConvOrSub qusr convOrSubId = for convOrSubId $ \case + Conv convId -> Conv <$> getMLSConv qusr (qualifyAs convOrSubId convId) + SubConv convId sconvId -> do + let lconv = qualifyAs convOrSubId convId + c <- getMLSConv qusr lconv + subconv <- getSubConversation lconv sconvId >>= noteS @'ConvNotFound + pure (SubConv c subconv) + where + getMLSConv :: Qualified UserId -> Local ConvId -> Sem r MLSConversation + getMLSConv u lconv = do + c <- getLocalConvForUser u lconv + meta <- mlsMetadata c & noteS @'ConvNotFound + cm <- lookupMLSClients (cnvmlsGroupId meta) + pure $ MLSConversation c meta cm + +setConvOrSubEpoch :: Members '[ConversationStore] r => ConvOrSubConvId -> Epoch -> Sem r () +setConvOrSubEpoch (Conv cid) epoch = + setConversationEpoch cid epoch +setConvOrSubEpoch (SubConv _ _) _epoch = + -- FUTUREWORK: Write to subconversation + pure () diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index ab6e3876611..c7cbb2a3efb 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -44,6 +44,7 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.MLS.SubConversation import Wire.API.Message -- | Propagate a message. @@ -55,52 +56,59 @@ propagateMessage :: Member TinyLog r ) => Qualified UserId -> - Local Data.Conversation -> - ClientMap -> + Local ConvOrSubConv -> Maybe ConnId -> ByteString -> Sem r () -propagateMessage qusr lconv cm con raw = do - -- FUTUREWORK: check the epoch - let lmems = Data.convLocalMembers . tUnqualified $ lconv - botMap = Map.fromList $ do - m <- lmems - b <- maybeToList $ newBotMember m - pure (lmId m, b) - mm = defMessageMetadata - now <- input @UTCTime - let lcnv = fmap Data.convId lconv - qcnv = tUntagged lcnv - e = Event qcnv qusr now $ EdMLSMessage raw - mkPush :: UserId -> ClientId -> MessagePush 'NormalMessage - mkPush u c = newMessagePush lcnv botMap con mm (u, c) e - runMessagePush lconv (Just qcnv) $ - foldMap (uncurry mkPush) (lmems >>= localMemberMLSClients lcnv) +propagateMessage qusr lConvOrSub con raw = do + case tUnqualified lConvOrSub of + (SubConv _ _) -> do + -- FUTUREWORK: Implement propagating the message to the subconversation + pure () + (Conv mlsMessage) -> do + let lMlsMessage = qualifyAs lConvOrSub mlsMessage + let cm = mcMembers mlsMessage + lconv = mcConv <$> lMlsMessage + -- FUTUREWORK: check the epoch + let lmems = Data.convLocalMembers . tUnqualified $ lconv + botMap = Map.fromList $ do + m <- lmems + b <- maybeToList $ newBotMember m + pure (lmId m, b) + mm = defMessageMetadata + now <- input @UTCTime + let lcnv = fmap Data.convId lconv + qcnv = tUntagged lcnv + e = Event qcnv qusr now $ EdMLSMessage raw + mkPush :: UserId -> ClientId -> MessagePush 'NormalMessage + mkPush u c = newMessagePush lcnv botMap con mm (u, c) e + runMessagePush lconv (Just qcnv) $ + foldMap (uncurry mkPush) (lmems >>= localMemberMLSClients lcnv cm) - -- send to remotes - traverse_ handleError - <=< runFederatedConcurrentlyEither (map remoteMemberQualify (Data.convRemoteMembers . tUnqualified $ lconv)) - $ \(tUnqualified -> rs) -> - fedClient @'Galley @"on-mls-message-sent" $ - RemoteMLSMessage - { rmmTime = now, - rmmSender = qusr, - rmmMetadata = mm, - rmmConversation = tUnqualified lcnv, - rmmRecipients = rs >>= remoteMemberMLSClients, - rmmMessage = Base64ByteString raw - } + -- send to remotes + traverse_ handleError + <=< runFederatedConcurrentlyEither (map remoteMemberQualify (Data.convRemoteMembers . tUnqualified $ lconv)) + $ \(tUnqualified -> rs) -> + fedClient @'Galley @"on-mls-message-sent" $ + RemoteMLSMessage + { rmmTime = now, + rmmSender = qusr, + rmmMetadata = mm, + rmmConversation = tUnqualified lcnv, + rmmRecipients = rs >>= remoteMemberMLSClients cm, + rmmMessage = Base64ByteString raw + } where - localMemberMLSClients :: Local x -> LocalMember -> [(UserId, ClientId)] - localMemberMLSClients loc lm = + localMemberMLSClients :: Local x -> ClientMap -> LocalMember -> [(UserId, ClientId)] + localMemberMLSClients loc cm lm = let localUserQId = tUntagged (qualifyAs loc localUserId) localUserId = lmId lm in map (\(c, _) -> (localUserId, c)) (toList (Map.findWithDefault mempty localUserQId cm)) - remoteMemberMLSClients :: RemoteMember -> [(UserId, ClientId)] - remoteMemberMLSClients rm = + remoteMemberMLSClients :: ClientMap -> RemoteMember -> [(UserId, ClientId)] + remoteMemberMLSClients cm rm = let remoteUserQId = tUntagged (rmId rm) remoteUserId = qUnqualified remoteUserQId in map diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index f16edf2bd26..6f96c071b49 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -18,7 +18,6 @@ module Galley.API.MLS.Removal ( removeClientsWithClientMap, removeClient, - removeUserWithClientMap, removeUser, ) where @@ -48,6 +47,7 @@ import Wire.API.MLS.KeyPackage import Wire.API.MLS.Message import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation +import Wire.API.MLS.SubConversation -- | Send remove proposals for a set of clients to clients in the ClientMap. removeClientsWithClientMap :: @@ -63,31 +63,28 @@ removeClientsWithClientMap :: r, Traversable t ) => - Local Data.Conversation -> + Local ConvOrSubConv -> t KeyPackageRef -> - ClientMap -> Qualified UserId -> Sem r () -removeClientsWithClientMap lc cs cm qusr = do - case Data.convProtocol (tUnqualified lc) of - ProtocolProteus -> pure () - ProtocolMLS meta -> do - mKeyPair <- getMLSRemovalKey - case mKeyPair of - Nothing -> do - warn $ Log.msg ("No backend removal key is configured (See 'mlsPrivateKeyPaths' in galley's config). Not able to remove client from MLS conversation." :: Text) - Just (secKey, pubKey) -> do - for_ cs $ \kpref -> do - let proposal = mkRemoveProposal kpref - msg = mkSignedMessage secKey pubKey (cnvmlsGroupId meta) (cnvmlsEpoch meta) (ProposalMessage proposal) - msgEncoded = encodeMLS' msg - storeProposal - (cnvmlsGroupId meta) - (cnvmlsEpoch meta) - (proposalRef (cnvmlsCipherSuite meta) proposal) - ProposalOriginBackend - proposal - propagateMessage qusr lc cm Nothing msgEncoded +removeClientsWithClientMap lConvOrSubConv cs qusr = do + let meta = mlsMetaConvOrSub (tUnqualified lConvOrSubConv) + mKeyPair <- getMLSRemovalKey + case mKeyPair of + Nothing -> do + warn $ Log.msg ("No backend removal key is configured (See 'mlsPrivateKeyPaths' in galley's config). Not able to remove client from MLS conversation." :: Text) + Just (secKey, pubKey) -> do + for_ cs $ \kpref -> do + let proposal = mkRemoveProposal kpref + msg = mkSignedMessage secKey pubKey (cnvmlsGroupId meta) (cnvmlsEpoch meta) (ProposalMessage proposal) + msgEncoded = encodeMLS' msg + storeProposal + (cnvmlsGroupId meta) + (cnvmlsEpoch meta) + (proposalRef (cnvmlsCipherSuite meta) proposal) + ProposalOriginBackend + proposal + propagateMessage qusr lConvOrSubConv Nothing msgEncoded -- | Send remove proposals for a single client of a user to the local conversation. removeClient :: @@ -109,33 +106,12 @@ removeClient :: ClientId -> Sem r () removeClient lc qusr cid = do - for_ (cnvmlsGroupId <$> Data.mlsMetadata (tUnqualified lc)) $ \groupId -> do - cm <- lookupMLSClients groupId + for_ (Data.mlsMetadata (tUnqualified lc)) $ \mlsMeta -> do + -- FUTUREWORK: also remove the client from from subconversations of lc + cm <- lookupMLSClients (cnvmlsGroupId mlsMeta) + let mlsConv = MLSConversation (tUnqualified lc) mlsMeta cm let cidAndKP = Set.toList . Set.map snd . Set.filter ((==) cid . fst) $ Map.findWithDefault mempty qusr cm - removeClientsWithClientMap lc cidAndKP cm qusr - --- | Send remove proposals for all clients of the user to clients in the ClientMap. --- --- All clients of the user have to be contained in the ClientMap. -removeUserWithClientMap :: - ( Members - '[ Input UTCTime, - TinyLog, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Error InternalError, - ProposalStore, - Input Env - ] - r - ) => - Local Data.Conversation -> - ClientMap -> - Qualified UserId -> - Sem r () -removeUserWithClientMap lc cm qusr = - removeClientsWithClientMap lc (Set.toList . Set.map snd $ Map.findWithDefault mempty qusr cm) cm qusr + removeClientsWithClientMap (qualifyAs lc (Conv mlsConv)) cidAndKP qusr -- | Send remove proposals for all clients of the user to the local conversation. removeUser :: @@ -156,6 +132,8 @@ removeUser :: Qualified UserId -> Sem r () removeUser lc qusr = do - for_ (Data.mlsMetadata (tUnqualified lc)) $ \meta -> do - cm <- lookupMLSClients (cnvmlsGroupId meta) - removeUserWithClientMap lc cm qusr + for_ (Data.mlsMetadata (tUnqualified lc)) $ \mlsMeta -> do + -- FUTUREWORK: also remove the client from from subconversations of lc + cm <- lookupMLSClients (cnvmlsGroupId mlsMeta) + let mlsConv = MLSConversation (tUnqualified lc) mlsMeta cm + removeClientsWithClientMap (qualifyAs lc (Conv mlsConv)) (Set.toList . Set.map snd $ Map.findWithDefault mempty qusr cm) qusr diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs new file mode 100644 index 00000000000..7446376511b --- /dev/null +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -0,0 +1,111 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.API.MLS.SubConversation where + +import Data.Id +import Data.Qualified +import Galley.API.Error +import Galley.API.MLS.Types +import Galley.API.Util (getConversationAndCheckMembership) +import qualified Galley.Data.Conversation as Data +import Galley.Data.Conversation.Types +import Galley.Effects.ConversationStore (ConversationStore) +import Galley.Effects.SubConversationStore +import qualified Galley.Effects.SubConversationStore as Eff +import Imports +import qualified Network.Wai.Utilities.Error as Wai +import Polysemy +import Polysemy.Error +import qualified Polysemy.TinyLog as P +import Wire.API.Conversation +import Wire.API.Conversation.Protocol +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Federation.Error (federationNotImplemented) +import Wire.API.MLS.SubConversation + +getSubConversation :: + Members + '[ SubConversationStore, + ConversationStore, + ErrorS 'ConvNotFound, + ErrorS 'ConvAccessDenied, + ErrorS 'MLSSubConvUnsupportedConvType, + Error InternalError, + Error Wai.Error, + P.TinyLog + ] + r => + Local UserId -> + Qualified ConvId -> + SubConvId -> + Sem r PublicSubConversation +getSubConversation lusr qconv sconv = do + foldQualified + lusr + (\lcnv -> getLocalSubConversation lusr lcnv sconv) + (\_rcnv -> throw federationNotImplemented) + qconv + +getLocalSubConversation :: + Members + '[ SubConversationStore, + ConversationStore, + ErrorS 'ConvNotFound, + ErrorS 'ConvAccessDenied, + ErrorS 'MLSSubConvUnsupportedConvType, + Error InternalError, + P.TinyLog + ] + r => + Local UserId -> + Local ConvId -> + SubConvId -> + Sem r PublicSubConversation +getLocalSubConversation lusr lconv sconv = do + c <- getConversationAndCheckMembership (tUnqualified lusr) lconv + + unless (Data.convType c == RegularConv) $ + throwS @'MLSSubConvUnsupportedConvType + + msub <- Eff.getSubConversation lconv sconv + sub <- case msub of + Nothing -> do + mlsMeta <- noteS @'ConvNotFound (mlsMetadata c) + -- deriving this detemernistically to prevent race condition between + -- multiple threads creating the subconversation + let groupId = initialGroupId lconv sconv + epoch = Epoch 0 + suite = cnvmlsCipherSuite mlsMeta + createSubConversation (tUnqualified lconv) sconv suite epoch groupId Nothing + setGroupIdForSubConversation groupId (tUntagged lconv) sconv + let sub = + SubConversation + { scParentConvId = lconv, + scSubConvId = sconv, + scMLSData = + ConversationMLSData + { cnvmlsGroupId = groupId, + cnvmlsEpoch = epoch, + cnvmlsCipherSuite = suite + }, + scMembers = mkClientMap [] + } + pure sub + Just sub -> pure sub + pure (toPublicSubConv sub) diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index 935702c3a55..aeee9fe2bc7 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -14,22 +14,22 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# LANGUAGE RecordWildCards #-} -module Galley.API.MLS.Types - ( ClientMap, - mkClientMap, - cmAssocs, - ListGlobalSelfConvs (..), - ) -where +module Galley.API.MLS.Types where import Data.Domain import Data.Id import qualified Data.Map as Map import Data.Qualified import qualified Data.Set as Set +import Galley.Data.Conversation +import qualified Galley.Data.Conversation as Data import Imports +import Wire.API.Conversation.Protocol +import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage +import Wire.API.MLS.SubConversation type ClientMap = Map (Qualified UserId) (Set (ClientId, KeyPackageRef)) @@ -48,3 +48,48 @@ cmAssocs cm = Map.assocs cm >>= traverse toList -- response. data ListGlobalSelfConvs = ListGlobalSelf | DoNotListGlobalSelf deriving (Eq) + +data MLSConversation = MLSConversation + { mcConv :: Conversation, + mcMLSData :: ConversationMLSData, + mcMembers :: ClientMap + } + deriving (Show) + +data SubConversation = SubConversation + { scParentConvId :: Local ConvId, + scSubConvId :: SubConvId, + scMLSData :: ConversationMLSData, + scMembers :: ClientMap + } + deriving (Eq, Show) + +toPublicSubConv :: SubConversation -> PublicSubConversation +toPublicSubConv SubConversation {..} = + let members = fmap (\(quid, (cid, _kp)) -> mkClientIdentity quid cid) (cmAssocs scMembers) + in PublicSubConversation + { pscParentConvId = tUntagged scParentConvId, + pscSubConvId = scSubConvId, + pscGroupId = cnvmlsGroupId scMLSData, + pscEpoch = cnvmlsEpoch scMLSData, + pscCipherSuite = cnvmlsCipherSuite scMLSData, + pscMembers = members + } + +type ConvOrSubConv = ConvOrSubChoice MLSConversation SubConversation + +mlsMetaConvOrSub :: ConvOrSubConv -> ConversationMLSData +mlsMetaConvOrSub (Conv c) = mcMLSData c +mlsMetaConvOrSub (SubConv _ s) = scMLSData s + +membersConvOrSub :: ConvOrSubConv -> ClientMap +membersConvOrSub (Conv c) = mcMembers c +membersConvOrSub (SubConv _ s) = scMembers s + +convOfConvOrSub :: ConvOrSubChoice c s -> c +convOfConvOrSub (Conv c) = c +convOfConvOrSub (SubConv c _) = c + +idForConvOrSub :: ConvOrSubConv -> ConvOrSubConvId +idForConvOrSub (Conv c) = Conv (Data.convId . mcConv $ c) +idForConvOrSub (SubConv c s) = SubConv (Data.convId . mcConv $ c) (scSubConvId s) diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index f14ee733977..8d9437f8afc 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -19,6 +19,7 @@ module Galley.API.Public.Conversation where import Galley.API.Create import Galley.API.MLS.GroupInfo +import Galley.API.MLS.SubConversation import Galley.API.MLS.Types import Galley.API.Query import Galley.API.Update @@ -47,6 +48,7 @@ conversationAPI = <@> mkNamedAPI @"create-self-conversation@v2" createProteusSelfConversation <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError + <@> mkNamedAPI @"get-subconversation" getSubConversation <@> mkNamedAPI @"create-one-to-one-conversation@v2" createOne2OneConversation <@> mkNamedAPI @"create-one-to-one-conversation" createOne2OneConversation <@> mkNamedAPI @"add-members-to-conversation-unqualified" addMembersUnqualified diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 1110ba9a14a..ef544fe54cc 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -22,7 +22,7 @@ module Galley.API.Util where import Control.Lens (set, view, (.~), (^.)) import Control.Monad.Extra (allM, anyM) import Data.Bifunctor -import Data.ByteString.Conversion +import Data.ByteString.Conversion (ToByteString, toByteString') import qualified Data.Code as Code import Data.Domain (Domain) import Data.Id as Id diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 4e18718f229..5f15bf3be8f 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -70,6 +70,7 @@ import Galley.Cassandra.LegalHold import Galley.Cassandra.Proposal import Galley.Cassandra.SearchVisibility import Galley.Cassandra.Services +import Galley.Cassandra.SubConversation (interpretSubConversationStoreToCassandra) import Galley.Cassandra.Team import Galley.Cassandra.TeamFeatures import Galley.Cassandra.TeamNotifications @@ -257,6 +258,7 @@ evalGalley e = . interpretMemberStoreToCassandra . interpretLegalHoldStoreToCassandra lh . interpretCustomBackendStoreToCassandra + . interpretSubConversationStoreToCassandra . interpretConversationStoreToCassandra . interpretProposalStoreToCassandra . interpretCodeStoreToCassandra diff --git a/services/galley/src/Galley/Cassandra.hs b/services/galley/src/Galley/Cassandra.hs index ea4d501ef69..2056cfc2c25 100644 --- a/services/galley/src/Galley/Cassandra.hs +++ b/services/galley/src/Galley/Cassandra.hs @@ -20,4 +20,4 @@ module Galley.Cassandra (schemaVersion) where import Imports schemaVersion :: Int32 -schemaVersion = 77 +schemaVersion = 78 diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 1b26c2207fd..a6ce01494d6 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -27,6 +27,7 @@ import qualified Cassandra as Cql import Control.Error.Util import Control.Monad.Trans.Maybe import Data.ByteString.Conversion +import Data.Domain import Data.Id import qualified Data.Map as Map import Data.Misc @@ -56,6 +57,7 @@ import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.SubConversation createMLSSelfConversation :: Local UserId -> @@ -101,7 +103,7 @@ createMLSSelfConversation lusr = do Just gid, Just cs ) - addPrepQuery Cql.insertGroupId (gid, cnv, tDomain lusr) + addPrepQuery Cql.insertGroupIdForConversation (gid, cnv, tDomain lusr) (lmems, rmems) <- addMembers cnv (ncUsers nc) pure @@ -158,7 +160,7 @@ createConversation lcnv nc = do mcs ) for_ (cnvmTeam meta) $ \tid -> addPrepQuery Cql.insertTeamConv (tid, tUnqualified lcnv) - for_ mgid $ \gid -> addPrepQuery Cql.insertGroupId (gid, tUnqualified lcnv, tDomain lcnv) + for_ mgid $ \gid -> addPrepQuery Cql.insertGroupIdForConversation (gid, tUnqualified lcnv, tDomain lcnv) (lmems, rmems) <- addMembers (tUnqualified lcnv) (ncUsers nc) pure Conversation @@ -365,13 +367,19 @@ toConv cid ms remoteMems mconv = do } } -mapGroupId :: GroupId -> Qualified ConvId -> Client () -mapGroupId gId conv = - write Cql.insertGroupId (params LocalQuorum (gId, qUnqualified conv, qDomain conv)) +setGroupIdForConversation :: GroupId -> Qualified ConvId -> Client () +setGroupIdForConversation gId conv = + write Cql.insertGroupIdForConversation (params LocalQuorum (gId, qUnqualified conv, qDomain conv)) -lookupGroupId :: GroupId -> Client (Maybe (Qualified ConvId)) -lookupGroupId gId = - uncurry Qualified <$$> retry x1 (query1 Cql.lookupGroupId (params LocalQuorum (Identity gId))) +lookupConvByGroupId :: GroupId -> Client (Maybe (Qualified ConvOrSubConvId)) +lookupConvByGroupId gId = + toConvOrSubConv <$$> retry x1 (query1 Cql.lookupGroupId (params LocalQuorum (Identity gId))) + where + toConvOrSubConv :: (ConvId, Domain, Maybe SubConvId) -> Qualified ConvOrSubConvId + toConvOrSubConv (convId, domain, mbSubConvId) = + case mbSubConvId of + Nothing -> Qualified (Conv convId) domain + Just subConvId -> Qualified (SubConv convId subConvId) domain interpretConversationStoreToCassandra :: Members '[Embed IO, Input ClientState, TinyLog] r => @@ -382,7 +390,7 @@ interpretConversationStoreToCassandra = interpret $ \case CreateConversation loc nc -> embedClient $ createConversation loc nc CreateMLSSelfConversation lusr -> embedClient $ createMLSSelfConversation lusr GetConversation cid -> embedClient $ getConversation cid - GetConversationIdByGroupId gId -> embedClient $ lookupGroupId gId + LookupConvByGroupId gId -> embedClient $ lookupConvByGroupId gId GetConversations cids -> localConversations cids GetConversationMetadata cid -> embedClient $ conversationMeta cid GetPublicGroupState cid -> embedClient $ getPublicGroupState cid @@ -396,7 +404,7 @@ interpretConversationStoreToCassandra = interpret $ \case SetConversationMessageTimer cid value -> embedClient $ updateConvMessageTimer cid value SetConversationEpoch cid epoch -> embedClient $ updateConvEpoch cid epoch DeleteConversation cid -> embedClient $ deleteConversation cid - SetGroupId gId cid -> embedClient $ mapGroupId gId cid + SetGroupIdForConversation gId cid -> embedClient $ setGroupIdForConversation gId cid SetPublicGroupState cid gib -> embedClient $ setPublicGroupState cid gib AcquireCommitLock gId epoch ttl -> embedClient $ acquireCommitLock gId epoch ttl ReleaseCommitLock gId epoch -> embedClient $ releaseCommitLock gId epoch diff --git a/services/galley/src/Galley/Cassandra/Conversation/MLS.hs b/services/galley/src/Galley/Cassandra/Conversation/MLS.hs index 7fda9519689..7ca5f89d358 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/MLS.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/MLS.hs @@ -15,11 +15,17 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Cassandra.Conversation.MLS where +module Galley.Cassandra.Conversation.MLS + ( acquireCommitLock, + releaseCommitLock, + lookupMLSClients, + ) +where import Cassandra import Cassandra.Settings (fromRow) import Data.Time +import Galley.API.MLS.Types import qualified Galley.Cassandra.Queries as Cql import Galley.Data.Types import Imports @@ -54,3 +60,10 @@ releaseCommitLock groupId epoch = checkTransSuccess :: [Row] -> Bool checkTransSuccess [] = False checkTransSuccess (row : _) = either (const False) (fromMaybe False) $ fromRow 0 row + +lookupMLSClients :: GroupId -> Client ClientMap +lookupMLSClients groupId = + mkClientMap + <$> retry + x5 + (query Cql.lookupMLSClients (params LocalQuorum (Identity groupId))) diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index 7f79df3042d..147d76c6e79 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -32,7 +32,7 @@ import qualified Data.List.Extra as List import Data.Monoid import Data.Qualified import qualified Data.Set as Set -import Galley.API.MLS.Types +import Galley.Cassandra.Conversation.MLS (lookupMLSClients) import Galley.Cassandra.Instances () import qualified Galley.Cassandra.Queries as Cql import Galley.Cassandra.Services @@ -356,13 +356,6 @@ removeMLSClients groupId (Qualified usr domain) cs = retry x5 . batch $ do for_ cs $ \c -> addPrepQuery Cql.removeMLSClient (groupId, domain, usr, c) -lookupMLSClients :: GroupId -> Client ClientMap -lookupMLSClients groupId = - mkClientMap - <$> retry - x5 - (query Cql.lookupMLSClients (params LocalQuorum (Identity groupId))) - interpretMemberStoreToCassandra :: Members '[Embed IO, Input ClientState] r => Sem (MemberStore ': r) a -> diff --git a/services/galley/src/Galley/Cassandra/Instances.hs b/services/galley/src/Galley/Cassandra/Instances.hs index 90b648e8ff3..8f9deba5eae 100644 --- a/services/galley/src/Galley/Cassandra/Instances.hs +++ b/services/galley/src/Galley/Cassandra/Instances.hs @@ -41,6 +41,7 @@ import Wire.API.MLS.CipherSuite import Wire.API.MLS.Proposal import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation +import Wire.API.MLS.SubConversation import Wire.API.Team import qualified Wire.API.Team.Feature as Public import Wire.API.Team.SearchVisibility @@ -255,3 +256,9 @@ instance Cql CipherSuite where then Right . CipherSuite . fromIntegral $ i else Left "CipherSuite: an out of bounds value for Word16" fromCql _ = Left "CipherSuite: int expected" + +instance Cql SubConvId where + ctype = Tagged TextColumn + toCql = CqlText . unSubConvId + fromCql (CqlText txt) = Right (SubConvId txt) + fromCql _ = Left "SubConvId: Text expected" diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 5cdac44f74d..6b9379ad85b 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -37,6 +37,7 @@ import Wire.API.Conversation.Role import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.SubConversation import Wire.API.Provider import Wire.API.Provider.Service import Wire.API.Team @@ -313,11 +314,31 @@ deleteUserConv = "delete from user where user = ? and conv = ?" -- MLS Conversations -------------------------------------------------------- -insertGroupId :: PrepQuery W (GroupId, ConvId, Domain) () -insertGroupId = "INSERT INTO group_id_conv_id (group_id, conv_id, domain) VALUES (?, ?, ?)" +insertGroupIdForConversation :: PrepQuery W (GroupId, ConvId, Domain) () +insertGroupIdForConversation = "INSERT INTO group_id_conv_id (group_id, conv_id, domain) VALUES (?, ?, ?)" -lookupGroupId :: PrepQuery R (Identity GroupId) (ConvId, Domain) -lookupGroupId = "SELECT conv_id, domain from group_id_conv_id where group_id = ?" +lookupGroupId :: PrepQuery R (Identity GroupId) (ConvId, Domain, Maybe SubConvId) +lookupGroupId = "SELECT conv_id, domain, subconv_id from group_id_conv_id where group_id = ?" + +-- MLS SubConversations ----------------------------------------------------- + +selectSubConversation :: PrepQuery R (ConvId, SubConvId) (CipherSuiteTag, Epoch, GroupId) +selectSubConversation = "SELECT cipher_suite, epoch, group_id FROM subconversation WHERE conv_id = ? and subconv_id = ?" + +insertSubConversation :: PrepQuery W (ConvId, SubConvId, CipherSuiteTag, Epoch, GroupId, Maybe OpaquePublicGroupState) () +insertSubConversation = "INSERT INTO subconversation (conv_id, subconv_id, cipher_suite, epoch, group_id, public_group_state) VALUES (?, ?, ?, ?, ?, ?)" + +updateSubConvPublicGroupState :: PrepQuery W (ConvId, SubConvId, Maybe OpaquePublicGroupState) () +updateSubConvPublicGroupState = "INSERT INTO subconversation (conv_id, subconv_id, public_group_state) VALUES (?, ?, ?)" + +selectSubConvPublicGroupState :: PrepQuery R (ConvId, SubConvId) (Identity (Maybe OpaquePublicGroupState)) +selectSubConvPublicGroupState = "SELECT public_group_state FROM subconversation WHERE conv_id = ? AND subconv_id = ?" + +insertGroupIdForSubConversation :: PrepQuery W (GroupId, ConvId, Domain, SubConvId) () +insertGroupIdForSubConversation = "INSERT INTO group_id_conv_id (group_id, conv_id, domain, subconv_id) VALUES (?, ?, ?, ?)" + +lookupGroupIdForSubConversation :: PrepQuery R (Identity GroupId) (ConvId, Domain, SubConvId) +lookupGroupIdForSubConversation = "SELECT conv_id, domain, subconv_id from group_id_conv_id where group_id = ?" -- Members ------------------------------------------------------------------ diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs new file mode 100644 index 00000000000..0216f84ff7a --- /dev/null +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -0,0 +1,80 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.Cassandra.SubConversation where + +import Cassandra +import Data.Id +import Data.Qualified +import Galley.API.MLS.Types (SubConversation (..)) +import Galley.Cassandra.Conversation.MLS (lookupMLSClients) +import qualified Galley.Cassandra.Queries as Cql +import Galley.Cassandra.Store (embedClient) +import Galley.Effects.SubConversationStore (SubConversationStore (..)) +import Imports +import Polysemy +import Polysemy.Input +import Wire.API.Conversation.Protocol +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Group +import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.SubConversation + +selectSubConversation :: Local ConvId -> SubConvId -> Client (Maybe SubConversation) +selectSubConversation convId subConvId = do + m <- retry x5 (query1 Cql.selectSubConversation (params LocalQuorum (tUnqualified convId, subConvId))) + for m $ \(suite, epoch, groupId) -> do + cm <- lookupMLSClients groupId + pure $ + SubConversation + { scParentConvId = convId, + scSubConvId = subConvId, + scMLSData = + ConversationMLSData + { cnvmlsGroupId = groupId, + cnvmlsEpoch = epoch, + cnvmlsCipherSuite = suite + }, + scMembers = cm + } + +insertSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> Epoch -> GroupId -> Maybe OpaquePublicGroupState -> Client () +insertSubConversation convId subConvId suite epoch groupId mPgs = + retry x5 (write Cql.insertSubConversation (params LocalQuorum (convId, subConvId, suite, epoch, groupId, mPgs))) + +updateSubConvPublicGroupState :: ConvId -> SubConvId -> Maybe OpaquePublicGroupState -> Client () +updateSubConvPublicGroupState convId subConvId mPgs = + retry x5 (write Cql.updateSubConvPublicGroupState (params LocalQuorum (convId, subConvId, mPgs))) + +selectSubConvPublicGroupState :: ConvId -> SubConvId -> Client (Maybe OpaquePublicGroupState) +selectSubConvPublicGroupState convId subConvId = + (runIdentity =<<) <$> retry x5 (query1 Cql.selectSubConvPublicGroupState (params LocalQuorum (convId, subConvId))) + +setGroupIdForSubConversation :: GroupId -> Qualified ConvId -> SubConvId -> Client () +setGroupIdForSubConversation groupId qconv sconv = + retry x5 (write Cql.insertGroupIdForSubConversation (params LocalQuorum (groupId, qUnqualified qconv, qDomain qconv, sconv))) + +interpretSubConversationStoreToCassandra :: + Members '[Embed IO, Input ClientState] r => + Sem (SubConversationStore ': r) a -> + Sem r a +interpretSubConversationStoreToCassandra = interpret $ \case + GetSubConversation convId subConvId -> embedClient (selectSubConversation convId subConvId) + CreateSubConversation convId subConvId suite epoch groupId mPgs -> embedClient (insertSubConversation convId subConvId suite epoch groupId mPgs) + SetSubConversationPublicGroupState convId subConvId mPgs -> embedClient (updateSubConvPublicGroupState convId subConvId mPgs) + GetSubConversationPublicGroupState convId subConvId -> embedClient (selectSubConvPublicGroupState convId subConvId) + SetGroupIdForSubConversation gId cid sconv -> embedClient $ setGroupIdForSubConversation gId cid sconv diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index 05e4997e3c6..dd8195e22d3 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -81,6 +81,7 @@ import Galley.Effects.Queue import Galley.Effects.SearchVisibilityStore import Galley.Effects.ServiceStore import Galley.Effects.SparAccess +import Galley.Effects.SubConversationStore import Galley.Effects.TeamFeatureStore import Galley.Effects.TeamMemberStore import Galley.Effects.TeamNotificationStore @@ -108,6 +109,7 @@ type GalleyEffects1 = CodeStore, ProposalStore, ConversationStore, + SubConversationStore, CustomBackendStore, LegalHoldStore, MemberStore, diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index 1660c2f6893..f8e5336e3a6 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -28,7 +28,7 @@ module Galley.Effects.ConversationStore -- * Read conversation getConversation, - getConversationIdByGroupId, + lookupConvByGroupId, getConversations, getConversationMetadata, getPublicGroupState, @@ -44,7 +44,7 @@ module Galley.Effects.ConversationStore setConversationMessageTimer, setConversationEpoch, acceptConnectConversation, - setGroupId, + setGroupIdForConversation, setPublicGroupState, -- * Delete conversation @@ -69,6 +69,7 @@ import Polysemy import Wire.API.Conversation hiding (Conversation, Member) import Wire.API.MLS.Epoch import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.SubConversation data ConversationStore m a where CreateConversationId :: ConversationStore m ConvId @@ -78,7 +79,7 @@ data ConversationStore m a where ConversationStore m Conversation DeleteConversation :: ConvId -> ConversationStore m () GetConversation :: ConvId -> ConversationStore m (Maybe Conversation) - GetConversationIdByGroupId :: GroupId -> ConversationStore m (Maybe (Qualified ConvId)) + LookupConvByGroupId :: GroupId -> ConversationStore m (Maybe (Qualified ConvOrSubConvId)) GetConversations :: [ConvId] -> ConversationStore m [Conversation] GetConversationMetadata :: ConvId -> ConversationStore m (Maybe ConversationMetadata) GetPublicGroupState :: @@ -96,7 +97,7 @@ data ConversationStore m a where SetConversationReceiptMode :: ConvId -> ReceiptMode -> ConversationStore m () SetConversationMessageTimer :: ConvId -> Maybe Milliseconds -> ConversationStore m () SetConversationEpoch :: ConvId -> Epoch -> ConversationStore m () - SetGroupId :: GroupId -> Qualified ConvId -> ConversationStore m () + SetGroupIdForConversation :: GroupId -> Qualified ConvId -> ConversationStore m () SetPublicGroupState :: ConvId -> OpaquePublicGroupState -> diff --git a/services/galley/src/Galley/Effects/SubConversationStore.hs b/services/galley/src/Galley/Effects/SubConversationStore.hs new file mode 100644 index 00000000000..46d90b34287 --- /dev/null +++ b/services/galley/src/Galley/Effects/SubConversationStore.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.Effects.SubConversationStore where + +import Data.Id +import Data.Qualified +import Galley.API.MLS.Types +import Imports +import Polysemy +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Epoch +import Wire.API.MLS.Group +import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.SubConversation + +data SubConversationStore m a where + GetSubConversation :: Local ConvId -> SubConvId -> SubConversationStore m (Maybe SubConversation) + CreateSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> Epoch -> GroupId -> Maybe OpaquePublicGroupState -> SubConversationStore m () + SetSubConversationPublicGroupState :: ConvId -> SubConvId -> Maybe OpaquePublicGroupState -> SubConversationStore m () + GetSubConversationPublicGroupState :: ConvId -> SubConvId -> SubConversationStore m (Maybe OpaquePublicGroupState) + SetGroupIdForSubConversation :: GroupId -> Qualified ConvId -> SubConvId -> SubConversationStore m () + +makeSem ''SubConversationStore diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 5379b701805..3f11bad6d6d 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -204,6 +204,11 @@ tests s = test s "cannot send an MLS message" postMLSMessageDisabled, test s "cannot send a commit bundle" postMLSBundleDisabled, test s "cannot get group info" getGroupInfoDisabled + ], + testGroup + "SubConversation" + [ test s "get subconversation of MLS conv - 200" (testCreateSubConv True), + test s "get subconversation of Proteus conv - 404" (testCreateSubConv False) ] ] @@ -2276,3 +2281,22 @@ getGroupInfoDisabled = do withMLSDisabled $ getGroupInfo (qUnqualified alice) qcnv !!! assertMLSNotEnabled + +testCreateSubConv :: Bool -> TestM () +testCreateSubConv parentIsMLSConv = do + alice <- randomQualifiedUser + runMLSTest $ do + qcnv <- + if parentIsMLSConv + then do + creator <- createMLSClient alice + (_, qcnv) <- setupMLSGroup creator + pure qcnv + else + cnvQualifiedId + <$> liftTest (postConvQualified (qUnqualified alice) defNewProteusConv >>= responseJsonError) + let sconv = SubConvId "call" + liftTest $ + getSubConv (qUnqualified alice) qcnv sconv + !!! do + const (if parentIsMLSConv then 200 else 404) === statusCode diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 132843a5243..6174b09abe4 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -31,6 +31,7 @@ import qualified Control.Monad.State as State import Control.Monad.Trans.Maybe import Crypto.PubKey.Ed25519 import Data.Aeson.Lens +import Data.Binary.Builder (toLazyByteString) import qualified Data.ByteArray as BA import qualified Data.ByteString as BS import qualified Data.ByteString.Base64.URL as B64U @@ -60,6 +61,7 @@ import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import TestHelpers import TestSetup +import Web.HttpApiData import Wire.API.Conversation import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol @@ -75,6 +77,7 @@ import Wire.API.MLS.Keys import Wire.API.MLS.Message import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation +import Wire.API.MLS.SubConversation import Wire.API.User.Client import Wire.API.User.Client.Prekey @@ -1040,3 +1043,21 @@ withMLSDisabled :: HasSettingsOverrides m => m a -> m a withMLSDisabled = withSettingsOverrides noMLS where noMLS = Opts.optSettings . Opts.setMlsPrivateKeyPaths .~ Nothing + +getSubConv :: + UserId -> + Qualified ConvId -> + SubConvId -> + TestM ResponseLBS +getSubConv u qcnv sconv = do + g <- viewGalley + get $ + g + . paths + [ "conversations", + toByteString' (qDomain qcnv), + toByteString' (qUnqualified qcnv), + "subconversations", + LBS.toStrict (toLazyByteString (toEncodedUrlPiece sconv)) + ] + . zUser u From aaa6452aafd80903cc38d2ecd31ad932e3e41ef7 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 19 Dec 2022 10:18:34 +0100 Subject: [PATCH 002/225] Commit bundles for subconversations (#2932) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add subconversation group info endpoint (wip, untested) * Fill implemention holes * refactor clientmap and add TODO for check * Throw when a client is not in the parent conv * Remove duplication in MLSConversation type * Refactor: Introduce incrementEpoch * Refactor: unqualify parent in SubConversation type * Bump mls-test-cli * Store group info bundle for subconvs * Fix epoch increment query for subconvs * Add join subconversation test * Turn TODO into FUTUREWORK * Add stubs of more subconversation tests * Add CHANGELOG entries * Deduplicate function to fetch remote group info Co-authored-by: Stefan Matting Co-authored-by: Marko Dimjašević --- .../get-subconversation-groupinfo | 1 + changelog.d/2-features/subconv-commit-bundles | 1 + .../src/Wire/API/Federation/API/Galley.hs | 6 +- .../src/Wire/API/MLS/SubConversation.hs | 3 +- .../API/Routes/Public/Galley/Conversation.hs | 21 ++++ nix/pkgs/mls-test-cli/default.nix | 4 +- services/galley/galley.cabal | 1 + services/galley/src/Galley/API/Action.hs | 4 +- services/galley/src/Galley/API/Federation.hs | 14 ++- .../galley/src/Galley/API/MLS/Conversation.hs | 57 +++++++++ .../galley/src/Galley/API/MLS/GroupInfo.hs | 5 +- services/galley/src/Galley/API/MLS/Message.hs | 115 ++++++++++-------- .../galley/src/Galley/API/MLS/Propagate.hs | 71 +++++------ services/galley/src/Galley/API/MLS/Removal.hs | 21 ++-- .../src/Galley/API/MLS/SubConversation.hs | 58 ++++++++- services/galley/src/Galley/API/MLS/Types.hs | 39 ++++-- .../src/Galley/API/Public/Conversation.hs | 1 + .../galley/src/Galley/Cassandra/Queries.hs | 3 + .../src/Galley/Cassandra/SubConversation.hs | 9 +- .../galley/src/Galley/Effects/MemberStore.hs | 5 +- .../Galley/Effects/SubConversationStore.hs | 3 +- services/galley/test/integration/API/MLS.hs | 84 ++++++++++--- .../galley/test/integration/API/MLS/Util.hs | 57 ++++++--- 23 files changed, 408 insertions(+), 175 deletions(-) create mode 100644 changelog.d/1-api-changes/get-subconversation-groupinfo create mode 100644 changelog.d/2-features/subconv-commit-bundles create mode 100644 services/galley/src/Galley/API/MLS/Conversation.hs diff --git a/changelog.d/1-api-changes/get-subconversation-groupinfo b/changelog.d/1-api-changes/get-subconversation-groupinfo new file mode 100644 index 00000000000..32845ff8279 --- /dev/null +++ b/changelog.d/1-api-changes/get-subconversation-groupinfo @@ -0,0 +1 @@ +Add `GET /conversations/:domain/:cid/subconversations/:id/groupinfo` endpoint to fetch the group info object for a subconversation diff --git a/changelog.d/2-features/subconv-commit-bundles b/changelog.d/2-features/subconv-commit-bundles new file mode 100644 index 00000000000..a6db49b6183 --- /dev/null +++ b/changelog.d/2-features/subconv-commit-bundles @@ -0,0 +1 @@ +Add support for subconversations in `POST /mls/commit-bundles` 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 90fc7de3e37..2166f915c5a 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 @@ -351,9 +351,9 @@ data MLSMessageResponse deriving (ToJSON, FromJSON) via (CustomEncoded MLSMessageResponse) data GetGroupInfoRequest = GetGroupInfoRequest - { -- | Conversation is assumed to be owned by the target domain, this allows - -- us to protect against relay attacks - ggireqConv :: ConvId, + { -- | Conversation (or subconversation) is assumed to be owned by the target + -- domain, this allows us to protect against relay attacks + ggireqConv :: ConvOrSubConvId, -- | Sender is assumed to be owned by the origin domain, this allows us to -- protect against spoofing attacks ggireqSender :: UserId diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index a0d3ccf386c..10e79f727b7 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -46,10 +46,9 @@ import Wire.Arbitrary -- conversation. The pair of a qualified conversation ID and a subconversation -- ID identifies globally. newtype SubConvId = SubConvId {unSubConvId :: Text} - deriving newtype (Eq, ToSchema, Ord) + deriving newtype (Eq, ToSchema, Ord, S.ToParamSchema, ToByteString) deriving stock (Generic) deriving (Arbitrary) via (GenericUniform SubConvId) - deriving newtype (S.ToParamSchema) deriving stock (Show) instance FromHttpApiData SubConvId where diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 1fcc514df16..ddf0e9e7079 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -396,6 +396,27 @@ type ConversationAPI = PublicSubConversation ) ) + :<|> Named + "get-subconversation-group-info" + ( Summary "Get MLS group information of subconversation" + :> CanThrow 'ConvNotFound + :> CanThrow 'MLSMissingGroupInfo + :> CanThrow 'MLSNotEnabled + :> ZLocalUser + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> "subconversations" + :> Capture "subconv" SubConvId + :> "groupinfo" + :> MultiVerb1 + 'GET + '[MLS] + ( Respond + 200 + "The group information" + OpaquePublicGroupState + ) + ) -- This endpoint can lead to the following events being sent: -- - ConvCreate event to members -- TODO: add note: "On 201, the conversation ID is the `Location` header" diff --git a/nix/pkgs/mls-test-cli/default.nix b/nix/pkgs/mls-test-cli/default.nix index 7d7d6961133..b49f61ceaa1 100644 --- a/nix/pkgs/mls-test-cli/default.nix +++ b/nix/pkgs/mls-test-cli/default.nix @@ -15,8 +15,8 @@ rustPlatform.buildRustPackage rec { src = fetchFromGitHub { owner = "wireapp"; repo = "mls-test-cli"; - sha256 = "sha256-/XQ/9oQTPkRqgMzDGRm+Oh9jgkdeDM1vRJ6/wEf2+bY="; - rev = "c6f80be2839ac1ed2894e96044541d1c3cf6ecdf"; + sha256 = "sha256-FjgAcYdUr/ZWdQxbck2UEG6NEEQLuz0S4a55hrAxUs4="; + rev = "82fc148964ef5baa92a90d086fdc61adaa2b5dbf"; }; doCheck = false; cargoSha256 = "sha256-AlZrxa7f5JwxxrzFBgeFSaYU6QttsUpfLYfq1HzsdbE="; diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 346c984e7d8..fd5b9db4bad 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -31,6 +31,7 @@ library Galley.API.Mapping Galley.API.Message Galley.API.MLS + Galley.API.MLS.Conversation Galley.API.MLS.Enabled Galley.API.MLS.GroupInfo Galley.API.MLS.KeyPackage diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 1fd27effc6e..dfa6fc67362 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -635,8 +635,8 @@ updateLocalConversationUnchecked lconv qusr con action = do (convBotsAndMembers conv <> extraTargets) action' --- -------------------------------------------------------------------------------- --- -- Utilities +-------------------------------------------------------------------------------- +-- Utilities ensureConversationActionAllowed :: forall tag mem x r. diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index dbf85df9149..ef1f4851677 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -44,6 +44,7 @@ import Galley.API.MLS.GroupInfo import Galley.API.MLS.KeyPackage import Galley.API.MLS.Message import Galley.API.MLS.Removal +import Galley.API.MLS.SubConversation import Galley.API.MLS.Welcome import qualified Galley.API.Mapping as Mapping import Galley.API.Message @@ -91,6 +92,7 @@ import Wire.API.MLS.Credential import Wire.API.MLS.Message import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation +import Wire.API.MLS.SubConversation import Wire.API.MLS.Welcome import Wire.API.Message import Wire.API.Routes.Internal.Brig.Connection @@ -795,7 +797,8 @@ queryGroupInfo :: ( Members '[ ConversationStore, Input (Local ()), - Input Env + Input Env, + SubConversationStore ] r, Member MemberStore r @@ -809,9 +812,14 @@ queryGroupInfo origDomain req = . mapToGalleyError @MLSGroupInfoStaticErrors $ do assertMLSEnabled - lconvId <- qualifyLocal . ggireqConv $ req let sender = toRemoteUnsafe origDomain . ggireqSender $ req - state <- getGroupInfoFromLocalConv (tUntagged sender) lconvId + state <- case ggireqConv req of + Conv convId -> do + lconvId <- qualifyLocal convId + getGroupInfoFromLocalConv (tUntagged sender) lconvId + SubConv convId subConvId -> do + lconvId <- qualifyLocal convId + getSubConversationGroupInfoFromLocalConv (tUntagged sender) subConvId lconvId pure . Base64ByteString . unOpaquePublicGroupState diff --git a/services/galley/src/Galley/API/MLS/Conversation.hs b/services/galley/src/Galley/API/MLS/Conversation.hs new file mode 100644 index 00000000000..fb2396d9c83 --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Conversation.hs @@ -0,0 +1,57 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.API.MLS.Conversation + ( mkMLSConversation, + mcConv, + ) +where + +import Galley.API.MLS.Types +import Galley.Data.Conversation.Types as Data +import Galley.Effects.MemberStore +import Imports +import Polysemy +import Wire.API.Conversation.Protocol + +mkMLSConversation :: + Member MemberStore r => + Data.Conversation -> + Sem r (Maybe MLSConversation) +mkMLSConversation conv = + for (Data.mlsMetadata conv) $ \mlsData -> do + cm <- lookupMLSClients (cnvmlsGroupId mlsData) + pure + MLSConversation + { mcId = Data.convId conv, + mcMetadata = Data.convMetadata conv, + mcLocalMembers = Data.convLocalMembers conv, + mcRemoteMembers = Data.convRemoteMembers conv, + mcMLSData = mlsData, + mcMembers = cm + } + +mcConv :: MLSConversation -> Data.Conversation +mcConv mlsConv = + Data.Conversation + { convId = mcId mlsConv, + convLocalMembers = mcLocalMembers mlsConv, + convRemoteMembers = mcRemoteMembers mlsConv, + convDeleted = False, + convMetadata = mcMetadata mlsConv, + convProtocol = ProtocolMLS (mcMLSData mlsConv) + } diff --git a/services/galley/src/Galley/API/MLS/GroupInfo.hs b/services/galley/src/Galley/API/MLS/GroupInfo.hs index ea2b16c78d8..3512c5d85c7 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfo.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfo.hs @@ -37,6 +37,7 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.SubConversation type MLSGroupInfoStaticErrors = '[ ErrorS 'ConvNotFound, @@ -62,7 +63,7 @@ getGroupInfo lusr qcnvId = do foldQualified lusr (getGroupInfoFromLocalConv . tUntagged $ lusr) - (getGroupInfoFromRemoteConv lusr) + (getGroupInfoFromRemoteConv lusr . fmap Conv) qcnvId getGroupInfoFromLocalConv :: @@ -84,7 +85,7 @@ getGroupInfoFromRemoteConv :: Members '[Error FederationError, FederatorAccess] r => Members MLSGroupInfoStaticErrors r => Local UserId -> - Remote ConvId -> + Remote ConvOrSubConvId -> Sem r OpaquePublicGroupState getGroupInfoFromRemoteConv lusr rcnv = do let getRequest = diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 4e908005475..5242d900f4b 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -39,6 +39,7 @@ import qualified Data.Text as T import Data.Time import Galley.API.Action import Galley.API.Error +import Galley.API.MLS.Conversation import Galley.API.MLS.Enabled import Galley.API.MLS.KeyPackage import Galley.API.MLS.Propagate @@ -599,10 +600,10 @@ instance Monoid ProposalAction where mempty = ProposalAction mempty mempty mempty paAddClient :: Qualified (UserId, (ClientId, KeyPackageRef)) -> ProposalAction -paAddClient quc = mempty {paAdd = Map.singleton (fmap fst quc) (Set.singleton (snd (qUnqualified quc)))} +paAddClient quc = mempty {paAdd = Map.singleton (fmap fst quc) (uncurry Map.singleton (snd (qUnqualified quc)))} paRemoveClient :: Qualified (UserId, (ClientId, KeyPackageRef)) -> ProposalAction -paRemoveClient quc = mempty {paRemove = Map.singleton (fmap fst quc) (Set.singleton (snd (qUnqualified quc)))} +paRemoveClient quc = mempty {paRemove = Map.singleton (fmap fst quc) (uncurry Map.singleton (snd (qUnqualified quc)))} paExternalInitPresent :: ProposalAction paExternalInitPresent = mempty {paExternalInit = Any True} @@ -733,6 +734,13 @@ processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = wi throw . mlsProtocolError $ "The external commit attempts to add another client of the user, it must only add itself" + case convOrSub of + Conv _ -> pure () + SubConv mlsConv _ -> + unless (isJust (cmLookupRef cid (mcMembers mlsConv))) $ + throw . mlsProtocolError $ + "Cannot join a subconversation before joining the parent conversation" + -- check if there is a key package ref in the remove proposal remRef <- if Map.null (paRemove action) @@ -748,29 +756,26 @@ processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = wi updateKeyPackageMapping lConvOrSub qusr (ciClient cid) remRef newRef -- increment epoch number - setConvOrSubEpoch (idForConvOrSub convOrSub) (succ epoch) - -- fetch conversation or sub with new epoch - lConvOrSub' <- fetchConvOrSub qusr (idForConvOrSub <$> lConvOrSub) - let convOrSub' = tUnqualified lConvOrSub + lConvOrSub' <- for lConvOrSub incrementEpoch -- fetch backend remove proposals of the previous epoch - kpRefs <- getPendingBackendRemoveProposals (cnvmlsGroupId . mlsMetaConvOrSub $ convOrSub') epoch + kpRefs <- getPendingBackendRemoveProposals (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub') epoch -- requeue backend remove proposals for the current epoch removeClientsWithClientMap lConvOrSub' kpRefs qusr where derefUser :: ClientMap -> Qualified UserId -> Sem r (ClientIdentity, KeyPackageRef) - derefUser (Map.toList -> l) user = case l of - [(u, s)] -> do + derefUser cm user = case Map.assocs cm of + [(u, clients)] -> do unless (user == u) $ throwS @'MLSClientSenderUserMismatch - ref <- snd <$> ensureSingleton s + ref <- ensureSingleton clients ci <- derefKeyPackage ref unless (cidQualifiedUser ci == user) $ throwS @'MLSClientSenderUserMismatch pure (ci, ref) _ -> throwRemProposal - ensureSingleton :: Set a -> Sem r a - ensureSingleton (Set.toList -> l) = case l of + ensureSingleton :: Map k a -> Sem r a + ensureSingleton m = case Map.elems m of [e] -> pure e _ -> throwRemProposal throwRemProposal = @@ -826,6 +831,7 @@ processInternalCommit :: Member (ErrorS 'MissingLegalholdConsent) r, Member (Input (Local ())) r, Member ProposalStore r, + Member SubConversationStore r, Member BrigAccess r, Member Resource r ) => @@ -841,20 +847,15 @@ processInternalCommit :: processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef commit = do let convOrSub = tUnqualified lConvOrSub mlsMeta = mlsMetaConvOrSub convOrSub - self <- - noteS @'ConvNotFound $ - getConvMember - lConvOrSub - (mcConv . convOfConvOrSub $ convOrSub) - qusr + localSelf = isLocal lConvOrSub qusr withCommitLock (cnvmlsGroupId . mlsMetaConvOrSub $ convOrSub) epoch $ do postponedKeyPackageRefUpdate <- if epoch == Epoch 0 then do - let cType = cnvmType . convMetadata . mcConv . convOfConvOrSub $ convOrSub - case (self, cType, cmAssocs . membersConvOrSub $ convOrSub, convOrSub) of - (Left _, SelfConv, [], Conv _) -> do + let cType = cnvmType . mcMetadata . convOfConvOrSub $ convOrSub + case (localSelf, cType, cmAssocs . membersConvOrSub $ convOrSub, convOrSub) of + (True, SelfConv, [], Conv _) -> do creatorClient <- noteS @'MLSMissingSenderClient senderClient creatorRef <- maybe @@ -868,12 +869,12 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co (cnvmlsGroupId mlsMeta) qusr (Set.singleton (creatorClient, creatorRef)) - (Left _, SelfConv, _, _) -> + (True, SelfConv, _, _) -> -- this is a newly created (sub)conversation, and it should -- contain exactly one client (the creator) throw (InternalErrorWithDescription "Unexpected creator client set") - (Left lm, _, [(qu, (creatorClient, _))], Conv _) - | qu == tUntagged (qualifyAs lConvOrSub (lmId lm)) -> do + (True, _, [(qu, (creatorClient, _))], Conv _) + | qu == qusr -> do -- use update path as sender reference and if not existing fall back to sender senderRef' <- maybe @@ -891,7 +892,7 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co Nothing senderRef' -- remote clients cannot send the first commit - (Right _, _, _, _) -> throwS @'MLSStaleMessage + (False, _, _, _) -> throwS @'MLSStaleMessage (_, _, _, SubConv _ _) -> pure () -- uninitialised conversations should contain exactly one client (_, _, _, Conv _) -> @@ -926,7 +927,7 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co -- update key package ref if necessary postponedKeyPackageRefUpdate -- increment epoch number - setConvOrSubEpoch (idForConvOrSub convOrSub) (succ epoch) + for_ lConvOrSub incrementEpoch pure updates @@ -1071,7 +1072,7 @@ processProposal qusr lConvOrSub msg prop = do checkEpoch (msgEpoch msg) mlsMeta checkGroup (msgGroupId msg) mlsMeta let suiteTag = cnvmlsCipherSuite mlsMeta - let cid = convId . mcConv . convOfConvOrSub . tUnqualified $ lConvOrSub + let cid = mcId . convOfConvOrSub . tUnqualified $ lConvOrSub -- validate the proposal -- @@ -1206,7 +1207,7 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do -- out all removals of that type, so that further checks and processing can -- be applied only to type 1 removals. removedUsers <- mapMaybe hush <$$> for (Map.assocs (paRemove action)) $ - \(qtarget, Set.map fst -> clients) -> runError @() $ do + \(qtarget, Map.keysSet -> clients) -> runError @() $ do -- fetch clients from brig clientInfo <- Set.map ciId <$> getClientInfo lconv qtarget ss -- if the clients being removed don't exist, consider this as a removal of @@ -1225,7 +1226,7 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do -- new user Nothing -> do -- final set of clients in the conversation - let clients = Set.map fst (newclients <> Map.findWithDefault mempty qtarget cm) + let clients = Map.keysSet (newclients <> Map.findWithDefault mempty qtarget cm) -- get list of mls clients from brig clientInfo <- getClientInfo lconv qtarget ss let allClients = Set.map ciId clientInfo @@ -1255,7 +1256,7 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do -- add clients in the conversation state for_ newUserClients $ \(qtarget, newClients) -> do - addMLSClients (cnvmlsGroupId mlsMeta) qtarget newClients + addMLSClients (cnvmlsGroupId mlsMeta) qtarget (Set.fromList (Map.assocs newClients)) -- remove users from the conversation and send events removeEvents <- foldMap (removeMembers lconv) (nonEmpty membersToRemove) @@ -1263,7 +1264,7 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do -- Remove clients from the conversation state. This includes client removals -- of all types (see Note [client removal]). for_ (Map.assocs (paRemove action)) $ \(qtarget, clients) -> do - removeMLSClients (cnvmlsGroupId mlsMeta) qtarget (Set.map fst clients) + removeMLSClients (cnvmlsGroupId mlsMeta) qtarget (Map.keysSet clients) pure (addEvents <> removeEvents) where @@ -1273,7 +1274,7 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do Set ClientId -> Sem r (Maybe (Qualified UserId)) checkRemoval cm qtarget clients = do - let clientsInConv = Set.map fst (Map.findWithDefault mempty qtarget cm) + let clientsInConv = Map.keysSet (Map.findWithDefault mempty qtarget cm) when (clients /= clientsInConv) $ do -- FUTUREWORK: turn this error into a proper response throwS @'MLSClientMismatch @@ -1441,15 +1442,11 @@ storeGroupInfoBundle :: ConvOrSubConvId -> GroupInfoBundle -> Sem r () -storeGroupInfoBundle convOrSub bundle = case convOrSub of - Conv cid -> do - setPublicGroupState cid - . toOpaquePublicGroupState - . gipGroupState - $ bundle - SubConv _cid _subconvid -> do - -- FUTUREWORK: Write to subconversation - pure () +storeGroupInfoBundle convOrSub bundle = do + let gs = toOpaquePublicGroupState (gipGroupState bundle) + case convOrSub of + Conv cid -> setPublicGroupState cid gs + SubConv cid subconvid -> setSubConversationPublicGroupState cid subconvid (Just gs) fetchConvOrSub :: forall r. @@ -1469,19 +1466,29 @@ fetchConvOrSub qusr convOrSubId = for convOrSubId $ \case SubConv convId sconvId -> do let lconv = qualifyAs convOrSubId convId c <- getMLSConv qusr lconv - subconv <- getSubConversation lconv sconvId >>= noteS @'ConvNotFound + subconv <- getSubConversation convId sconvId >>= noteS @'ConvNotFound pure (SubConv c subconv) where getMLSConv :: Qualified UserId -> Local ConvId -> Sem r MLSConversation - getMLSConv u lconv = do - c <- getLocalConvForUser u lconv - meta <- mlsMetadata c & noteS @'ConvNotFound - cm <- lookupMLSClients (cnvmlsGroupId meta) - pure $ MLSConversation c meta cm - -setConvOrSubEpoch :: Members '[ConversationStore] r => ConvOrSubConvId -> Epoch -> Sem r () -setConvOrSubEpoch (Conv cid) epoch = - setConversationEpoch cid epoch -setConvOrSubEpoch (SubConv _ _) _epoch = - -- FUTUREWORK: Write to subconversation - pure () + getMLSConv u = + getLocalConvForUser u + >=> mkMLSConversation + >=> noteS @'ConvNotFound + +incrementEpoch :: + Members + '[ ConversationStore, + SubConversationStore + ] + r => + ConvOrSubConv -> + Sem r ConvOrSubConv +incrementEpoch (Conv c) = do + let epoch' = succ (cnvmlsEpoch (mcMLSData c)) + setConversationEpoch (mcId c) epoch' + pure $ Conv c {mcMLSData = (mcMLSData c) {cnvmlsEpoch = epoch'}} +incrementEpoch (SubConv c s) = do + let epoch' = succ (cnvmlsEpoch (scMLSData s)) + setSubConversationEpoch (scParentConvId s) (scSubConvId s) epoch' + let s' = s {scMLSData = (scMLSData s) {cnvmlsEpoch = epoch'}} + pure (SubConv c s') diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index c7cbb2a3efb..9809fda6fb5 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -26,7 +26,6 @@ import Data.Qualified import Data.Time import Galley.API.MLS.Types import Galley.API.Push -import qualified Galley.Data.Conversation.Types as Data import Galley.Data.Services import Galley.Effects import Galley.Effects.FederatorAccess @@ -44,7 +43,6 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error -import Wire.API.MLS.SubConversation import Wire.API.Message -- | Propagate a message. @@ -61,43 +59,36 @@ propagateMessage :: ByteString -> Sem r () propagateMessage qusr lConvOrSub con raw = do - case tUnqualified lConvOrSub of - (SubConv _ _) -> do - -- FUTUREWORK: Implement propagating the message to the subconversation - pure () - (Conv mlsMessage) -> do - let lMlsMessage = qualifyAs lConvOrSub mlsMessage - let cm = mcMembers mlsMessage - lconv = mcConv <$> lMlsMessage - -- FUTUREWORK: check the epoch - let lmems = Data.convLocalMembers . tUnqualified $ lconv - botMap = Map.fromList $ do - m <- lmems - b <- maybeToList $ newBotMember m - pure (lmId m, b) - mm = defMessageMetadata - now <- input @UTCTime - let lcnv = fmap Data.convId lconv - qcnv = tUntagged lcnv - e = Event qcnv qusr now $ EdMLSMessage raw - mkPush :: UserId -> ClientId -> MessagePush 'NormalMessage - mkPush u c = newMessagePush lcnv botMap con mm (u, c) e - runMessagePush lconv (Just qcnv) $ - foldMap (uncurry mkPush) (lmems >>= localMemberMLSClients lcnv cm) + now <- input @UTCTime + let cm = membersConvOrSub (tUnqualified lConvOrSub) + mlsConv = convOfConvOrSub <$> lConvOrSub + lmems = mcLocalMembers . tUnqualified $ mlsConv + botMap = Map.fromList $ do + m <- lmems + b <- maybeToList $ newBotMember m + pure (lmId m, b) + mm = defMessageMetadata + qcnv = tUntagged (fmap mcId mlsConv) + -- FUTUREWORK: Add subconv field + e = Event qcnv qusr now $ EdMLSMessage raw + mkPush :: UserId -> ClientId -> MessagePush 'NormalMessage + mkPush u c = newMessagePush mlsConv botMap con mm (u, c) e + runMessagePush mlsConv (Just qcnv) $ + foldMap (uncurry mkPush) (lmems >>= localMemberMLSClients mlsConv cm) - -- send to remotes - traverse_ handleError - <=< runFederatedConcurrentlyEither (map remoteMemberQualify (Data.convRemoteMembers . tUnqualified $ lconv)) - $ \(tUnqualified -> rs) -> - fedClient @'Galley @"on-mls-message-sent" $ - RemoteMLSMessage - { rmmTime = now, - rmmSender = qusr, - rmmMetadata = mm, - rmmConversation = tUnqualified lcnv, - rmmRecipients = rs >>= remoteMemberMLSClients cm, - rmmMessage = Base64ByteString raw - } + -- send to remotes + traverse_ handleError + <=< runFederatedConcurrentlyEither (map remoteMemberQualify (mcRemoteMembers . tUnqualified $ mlsConv)) + $ \(tUnqualified -> rs) -> + fedClient @'Galley @"on-mls-message-sent" $ + RemoteMLSMessage + { rmmTime = now, + rmmSender = qusr, + rmmMetadata = mm, + rmmConversation = qUnqualified qcnv, + rmmRecipients = rs >>= remoteMemberMLSClients cm, + rmmMessage = Base64ByteString raw + } where localMemberMLSClients :: Local x -> ClientMap -> LocalMember -> [(UserId, ClientId)] localMemberMLSClients loc cm lm = @@ -105,7 +96,7 @@ propagateMessage qusr lConvOrSub con raw = do localUserId = lmId lm in map (\(c, _) -> (localUserId, c)) - (toList (Map.findWithDefault mempty localUserQId cm)) + (Map.assocs (Map.findWithDefault mempty localUserQId cm)) remoteMemberMLSClients :: ClientMap -> RemoteMember -> [(UserId, ClientId)] remoteMemberMLSClients cm rm = @@ -113,7 +104,7 @@ propagateMessage qusr lConvOrSub con raw = do remoteUserId = qUnqualified remoteUserQId in map (\(c, _) -> (remoteUserId, c)) - (toList (Map.findWithDefault mempty remoteUserQId cm)) + (Map.assocs (Map.findWithDefault mempty remoteUserQId cm)) handleError :: Member TinyLog r => diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 6f96c071b49..af8cfe9a750 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -25,15 +25,14 @@ where import Data.Id import qualified Data.Map as Map import Data.Qualified -import qualified Data.Set as Set import Data.Time import Galley.API.Error +import Galley.API.MLS.Conversation import Galley.API.MLS.Keys (getMLSRemovalKey) import Galley.API.MLS.Propagate import Galley.API.MLS.Types import qualified Galley.Data.Conversation.Types as Data import Galley.Effects -import Galley.Effects.MemberStore import Galley.Effects.ProposalStore import Galley.Env import Imports @@ -43,6 +42,7 @@ import Polysemy.Input import Polysemy.TinyLog import qualified System.Logger as Log import Wire.API.Conversation.Protocol +import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage import Wire.API.MLS.Message import Wire.API.MLS.Proposal @@ -106,12 +106,11 @@ removeClient :: ClientId -> Sem r () removeClient lc qusr cid = do - for_ (Data.mlsMetadata (tUnqualified lc)) $ \mlsMeta -> do + mMlsConv <- mkMLSConversation (tUnqualified lc) + for_ mMlsConv $ \mlsConv -> do -- FUTUREWORK: also remove the client from from subconversations of lc - cm <- lookupMLSClients (cnvmlsGroupId mlsMeta) - let mlsConv = MLSConversation (tUnqualified lc) mlsMeta cm - let cidAndKP = Set.toList . Set.map snd . Set.filter ((==) cid . fst) $ Map.findWithDefault mempty qusr cm - removeClientsWithClientMap (qualifyAs lc (Conv mlsConv)) cidAndKP qusr + let cidAndKPs = maybeToList (cmLookupRef (mkClientIdentity qusr cid) (mcMembers mlsConv)) + removeClientsWithClientMap (qualifyAs lc (Conv mlsConv)) cidAndKPs qusr -- | Send remove proposals for all clients of the user to the local conversation. removeUser :: @@ -132,8 +131,8 @@ removeUser :: Qualified UserId -> Sem r () removeUser lc qusr = do - for_ (Data.mlsMetadata (tUnqualified lc)) $ \mlsMeta -> do + mMlsConv <- mkMLSConversation (tUnqualified lc) + for_ mMlsConv $ \mlsConv -> do -- FUTUREWORK: also remove the client from from subconversations of lc - cm <- lookupMLSClients (cnvmlsGroupId mlsMeta) - let mlsConv = MLSConversation (tUnqualified lc) mlsMeta cm - removeClientsWithClientMap (qualifyAs lc (Conv mlsConv)) (Set.toList . Set.map snd $ Map.findWithDefault mempty qusr cm) qusr + let kprefs = toList (Map.findWithDefault mempty qusr (mcMembers mlsConv)) + removeClientsWithClientMap (qualifyAs lc (Conv mlsConv)) kprefs qusr diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 7446376511b..dfc0234b55b 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -20,23 +20,29 @@ module Galley.API.MLS.SubConversation where import Data.Id import Data.Qualified import Galley.API.Error +import Galley.API.MLS +import Galley.API.MLS.GroupInfo import Galley.API.MLS.Types -import Galley.API.Util (getConversationAndCheckMembership) +import Galley.API.MLS.Util +import Galley.API.Util +import Galley.App (Env) import qualified Galley.Data.Conversation as Data import Galley.Data.Conversation.Types -import Galley.Effects.ConversationStore (ConversationStore) +import Galley.Effects import Galley.Effects.SubConversationStore import qualified Galley.Effects.SubConversationStore as Eff import Imports import qualified Network.Wai.Utilities.Error as Wai import Polysemy import Polysemy.Error +import Polysemy.Input import qualified Polysemy.TinyLog as P import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.Federation.Error (federationNotImplemented) +import Wire.API.Federation.Error (FederationError, federationNotImplemented) +import Wire.API.MLS.PublicGroupState import Wire.API.MLS.SubConversation getSubConversation :: @@ -83,7 +89,7 @@ getLocalSubConversation lusr lconv sconv = do unless (Data.convType c == RegularConv) $ throwS @'MLSSubConvUnsupportedConvType - msub <- Eff.getSubConversation lconv sconv + msub <- Eff.getSubConversation (tUnqualified lconv) sconv sub <- case msub of Nothing -> do mlsMeta <- noteS @'ConvNotFound (mlsMetadata c) @@ -96,7 +102,7 @@ getLocalSubConversation lusr lconv sconv = do setGroupIdForSubConversation groupId (tUntagged lconv) sconv let sub = SubConversation - { scParentConvId = lconv, + { scParentConvId = tUnqualified lconv, scSubConvId = sconv, scMLSData = ConversationMLSData @@ -108,4 +114,44 @@ getLocalSubConversation lusr lconv sconv = do } pure sub Just sub -> pure sub - pure (toPublicSubConv sub) + pure (toPublicSubConv (tUntagged (qualifyAs lusr sub))) + +getSubConversationGroupInfo :: + Members + '[ ConversationStore, + Error FederationError, + FederatorAccess, + Input Env, + MemberStore, + SubConversationStore + ] + r => + Members MLSGroupInfoStaticErrors r => + Local UserId -> + Qualified ConvId -> + SubConvId -> + Sem r OpaquePublicGroupState +getSubConversationGroupInfo lusr qcnvId subconv = do + assertMLSEnabled + foldQualified + lusr + (getSubConversationGroupInfoFromLocalConv (tUntagged lusr) subconv) + (getGroupInfoFromRemoteConv lusr . fmap (flip SubConv subconv)) + qcnvId + +getSubConversationGroupInfoFromLocalConv :: + Members + '[ ConversationStore, + SubConversationStore, + MemberStore + ] + r => + Members MLSGroupInfoStaticErrors r => + Qualified UserId -> + SubConvId -> + Local ConvId -> + Sem r OpaquePublicGroupState +getSubConversationGroupInfoFromLocalConv qusr subConvId lcnvId = do + void $ getLocalConvForUser qusr lcnvId + getSubConversationPublicGroupState (tUnqualified lcnvId) subConvId + >>= noteS @'MLSMissingGroupInfo diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index aeee9fe2bc7..ec2643884e3 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -22,26 +22,36 @@ import Data.Domain import Data.Id import qualified Data.Map as Map import Data.Qualified -import qualified Data.Set as Set -import Galley.Data.Conversation -import qualified Galley.Data.Conversation as Data +import Galley.Types.Conversations.Members import Imports +import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage import Wire.API.MLS.SubConversation -type ClientMap = Map (Qualified UserId) (Set (ClientId, KeyPackageRef)) +type ClientMap = Map (Qualified UserId) (Map ClientId KeyPackageRef) mkClientMap :: [(Domain, UserId, ClientId, KeyPackageRef)] -> ClientMap mkClientMap = foldr addEntry mempty where addEntry :: (Domain, UserId, ClientId, KeyPackageRef) -> ClientMap -> ClientMap addEntry (dom, usr, c, kpr) = - Map.insertWith (<>) (Qualified usr dom) (Set.singleton (c, kpr)) + Map.insertWith (<>) (Qualified usr dom) (Map.singleton c kpr) + +cmLookupRef :: ClientIdentity -> ClientMap -> Maybe KeyPackageRef +cmLookupRef cid cm = do + clients <- Map.lookup (cidQualifiedUser cid) cm + Map.lookup (ciClient cid) clients + +isClientMember :: ClientIdentity -> ClientMap -> Bool +isClientMember ci = isJust . cmLookupRef ci cmAssocs :: ClientMap -> [(Qualified UserId, (ClientId, KeyPackageRef))] -cmAssocs cm = Map.assocs cm >>= traverse toList +cmAssocs cm = do + (quid, clients) <- Map.assocs cm + (clientId, ref) <- Map.assocs clients + pure (quid, (clientId, ref)) -- | Inform a handler for 'POST /conversations/list-ids' if the MLS global team -- conversation and the MLS self-conversation should be included in the @@ -50,25 +60,28 @@ data ListGlobalSelfConvs = ListGlobalSelf | DoNotListGlobalSelf deriving (Eq) data MLSConversation = MLSConversation - { mcConv :: Conversation, + { mcId :: ConvId, + mcMetadata :: ConversationMetadata, mcMLSData :: ConversationMLSData, + mcLocalMembers :: [LocalMember], + mcRemoteMembers :: [RemoteMember], mcMembers :: ClientMap } deriving (Show) data SubConversation = SubConversation - { scParentConvId :: Local ConvId, + { scParentConvId :: ConvId, scSubConvId :: SubConvId, scMLSData :: ConversationMLSData, scMembers :: ClientMap } deriving (Eq, Show) -toPublicSubConv :: SubConversation -> PublicSubConversation -toPublicSubConv SubConversation {..} = +toPublicSubConv :: Qualified SubConversation -> PublicSubConversation +toPublicSubConv (Qualified (SubConversation {..}) domain) = let members = fmap (\(quid, (cid, _kp)) -> mkClientIdentity quid cid) (cmAssocs scMembers) in PublicSubConversation - { pscParentConvId = tUntagged scParentConvId, + { pscParentConvId = Qualified scParentConvId domain, pscSubConvId = scSubConvId, pscGroupId = cnvmlsGroupId scMLSData, pscEpoch = cnvmlsEpoch scMLSData, @@ -91,5 +104,5 @@ convOfConvOrSub (Conv c) = c convOfConvOrSub (SubConv c _) = c idForConvOrSub :: ConvOrSubConv -> ConvOrSubConvId -idForConvOrSub (Conv c) = Conv (Data.convId . mcConv $ c) -idForConvOrSub (SubConv c s) = SubConv (Data.convId . mcConv $ c) (scSubConvId s) +idForConvOrSub (Conv c) = Conv (mcId c) +idForConvOrSub (SubConv c s) = SubConv (mcId c) (scSubConvId s) diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 8d9437f8afc..7de59d5d560 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -49,6 +49,7 @@ conversationAPI = <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError <@> mkNamedAPI @"get-subconversation" getSubConversation + <@> mkNamedAPI @"get-subconversation-group-info" getSubConversationGroupInfo <@> mkNamedAPI @"create-one-to-one-conversation@v2" createOne2OneConversation <@> mkNamedAPI @"create-one-to-one-conversation" createOne2OneConversation <@> mkNamedAPI @"add-members-to-conversation-unqualified" addMembersUnqualified diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 6b9379ad85b..9585f9c038e 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -340,6 +340,9 @@ insertGroupIdForSubConversation = "INSERT INTO group_id_conv_id (group_id, conv_ lookupGroupIdForSubConversation :: PrepQuery R (Identity GroupId) (ConvId, Domain, SubConvId) lookupGroupIdForSubConversation = "SELECT conv_id, domain, subconv_id from group_id_conv_id where group_id = ?" +insertEpochForSubConversation :: PrepQuery W (Epoch, ConvId, SubConvId) () +insertEpochForSubConversation = "UPDATE subconversation set epoch = ? WHERE conv_id = ? AND subconv_id = ?" + -- Members ------------------------------------------------------------------ type MemberStatus = Int32 diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index 0216f84ff7a..259255ffe1a 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -34,9 +34,9 @@ import Wire.API.MLS.Group import Wire.API.MLS.PublicGroupState import Wire.API.MLS.SubConversation -selectSubConversation :: Local ConvId -> SubConvId -> Client (Maybe SubConversation) +selectSubConversation :: ConvId -> SubConvId -> Client (Maybe SubConversation) selectSubConversation convId subConvId = do - m <- retry x5 (query1 Cql.selectSubConversation (params LocalQuorum (tUnqualified convId, subConvId))) + m <- retry x5 (query1 Cql.selectSubConversation (params LocalQuorum (convId, subConvId))) for m $ \(suite, epoch, groupId) -> do cm <- lookupMLSClients groupId pure $ @@ -68,6 +68,10 @@ setGroupIdForSubConversation :: GroupId -> Qualified ConvId -> SubConvId -> Clie setGroupIdForSubConversation groupId qconv sconv = retry x5 (write Cql.insertGroupIdForSubConversation (params LocalQuorum (groupId, qUnqualified qconv, qDomain qconv, sconv))) +setEpochForSubConversation :: ConvId -> SubConvId -> Epoch -> Client () +setEpochForSubConversation cid sconv epoch = + retry x5 (write Cql.insertEpochForSubConversation (params LocalQuorum (epoch, cid, sconv))) + interpretSubConversationStoreToCassandra :: Members '[Embed IO, Input ClientState] r => Sem (SubConversationStore ': r) a -> @@ -78,3 +82,4 @@ interpretSubConversationStoreToCassandra = interpret $ \case SetSubConversationPublicGroupState convId subConvId mPgs -> embedClient (updateSubConvPublicGroupState convId subConvId mPgs) GetSubConversationPublicGroupState convId subConvId -> embedClient (selectSubConvPublicGroupState convId subConvId) SetGroupIdForSubConversation gId cid sconv -> embedClient $ setGroupIdForSubConversation gId cid sconv + SetSubConversationEpoch cid sconv epoch -> embedClient $ setEpochForSubConversation cid sconv epoch diff --git a/services/galley/src/Galley/Effects/MemberStore.hs b/services/galley/src/Galley/Effects/MemberStore.hs index f9f5b57d505..ec865b8e273 100644 --- a/services/galley/src/Galley/Effects/MemberStore.hs +++ b/services/galley/src/Galley/Effects/MemberStore.hs @@ -50,6 +50,7 @@ where import Data.Id import Data.Qualified +import Galley.API.MLS.Types import Galley.Data.Services import Galley.Types.Conversations.Members import Galley.Types.ToUserRole @@ -77,9 +78,7 @@ data MemberStore m a where DeleteMembersInRemoteConversation :: Remote ConvId -> [UserId] -> MemberStore m () AddMLSClients :: GroupId -> Qualified UserId -> Set (ClientId, KeyPackageRef) -> MemberStore m () RemoveMLSClients :: GroupId -> Qualified UserId -> Set ClientId -> MemberStore m () - LookupMLSClients :: - GroupId -> - MemberStore m (Map (Qualified UserId) (Set (ClientId, KeyPackageRef))) + LookupMLSClients :: GroupId -> MemberStore m ClientMap makeSem ''MemberStore diff --git a/services/galley/src/Galley/Effects/SubConversationStore.hs b/services/galley/src/Galley/Effects/SubConversationStore.hs index 46d90b34287..00120d40c38 100644 --- a/services/galley/src/Galley/Effects/SubConversationStore.hs +++ b/services/galley/src/Galley/Effects/SubConversationStore.hs @@ -31,10 +31,11 @@ import Wire.API.MLS.PublicGroupState import Wire.API.MLS.SubConversation data SubConversationStore m a where - GetSubConversation :: Local ConvId -> SubConvId -> SubConversationStore m (Maybe SubConversation) + GetSubConversation :: ConvId -> SubConvId -> SubConversationStore m (Maybe SubConversation) CreateSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> Epoch -> GroupId -> Maybe OpaquePublicGroupState -> SubConversationStore m () SetSubConversationPublicGroupState :: ConvId -> SubConvId -> Maybe OpaquePublicGroupState -> SubConversationStore m () GetSubConversationPublicGroupState :: ConvId -> SubConvId -> SubConversationStore m (Maybe OpaquePublicGroupState) SetGroupIdForSubConversation :: GroupId -> Qualified ConvId -> SubConvId -> SubConversationStore m () + SetSubConversationEpoch :: ConvId -> SubConvId -> Epoch -> SubConversationStore m () makeSem ''SubConversationStore diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 3f11bad6d6d..10df04b4930 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -208,7 +208,13 @@ tests s = testGroup "SubConversation" [ test s "get subconversation of MLS conv - 200" (testCreateSubConv True), - test s "get subconversation of Proteus conv - 404" (testCreateSubConv False) + test s "get subconversation of Proteus conv - 404" (testCreateSubConv False), + test s "join subconversation with an external commit bundle" testJoinSubConv, + test s "join subconversation with a client that is not in the main conv" testJoinSubNonMemberClient, + test s "add another client to a subconversation" testAddClientSubConv, + test s "remove another client from a subconversation" testRemoveClientSubConv, + test s "join remote subconversation" testJoinRemoteSubConv, + test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv ] ] @@ -372,7 +378,7 @@ testAddUserWithBundle = do returnedGS <- fmap responseBody $ - getGroupInfo (qUnqualified alice) qcnv + getGroupInfo (qUnqualified alice) (fmap Conv qcnv) returnedGS @@ -990,8 +996,8 @@ testExternalCommitNotMember = do pgs <- LBS.toStrict . fromJust . responseBody - <$> getGroupInfo (ciUser alice1) qcnv - mp <- createExternalCommit bob1 (Just pgs) qcnv + <$> getGroupInfo (ciUser alice1) (fmap Conv qcnv) + mp <- createExternalCommit bob1 (Just pgs) (fmap Conv qcnv) bundle <- createBundle mp postCommitBundle (mpSender mp) bundle !!! const 404 === statusCode @@ -1007,7 +1013,9 @@ testExternalCommitSameClient = do void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle let rejoiner = alice1 - ecEvents <- createExternalCommit rejoiner Nothing qcnv >>= sendAndConsumeCommitBundle + ecEvents <- + createExternalCommit rejoiner Nothing (fmap Conv qcnv) + >>= sendAndConsumeCommitBundle liftIO $ assertBool "No events after external commit expected" (null ecEvents) @@ -1025,7 +1033,9 @@ testExternalCommitNewClient = do void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle nc <- createMLSClient bob - ecEvents <- createExternalCommit nc Nothing qcnv >>= sendAndConsumeCommitBundle + ecEvents <- + createExternalCommit nc Nothing (fmap Conv qcnv) + >>= sendAndConsumeCommitBundle liftIO $ assertBool "No events after external commit expected" (null ecEvents) @@ -1075,7 +1085,7 @@ testExternalCommitNewClientResendBackendProposal = do WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ void . wsAssertAddProposal bob qcnv - mp <- createExternalCommit bob4 Nothing qcnv + mp <- createExternalCommit bob4 Nothing (fmap Conv qcnv) ecEvents <- sendAndConsumeCommitBundle mp liftIO $ assertBool "No events after external commit expected" (null ecEvents) @@ -1941,7 +1951,7 @@ testGetGroupInfoOfLocalConv = do gs <- assertJust (mpPublicGroupState commit) returnedGS <- fmap responseBody $ - getGroupInfo (qUnqualified alice) qcnv + getGroupInfo (qUnqualified alice) (fmap Conv qcnv) returnedGS @@ -1975,10 +1985,10 @@ testGetGroupInfoOfRemoteConv = do (_, reqs) <- withTempMockFederator' mock $ do res <- fmap responseBody $ - getGroupInfo (qUnqualified bob) qcnv + getGroupInfo (qUnqualified bob) (fmap Conv qcnv) assertFailure ("Unexpected error: " <> show err) @@ -2034,7 +2044,7 @@ testFederatedGetGroupInfo = do @"query-group-info" fedGalleyClient (ciDomain bob1) - (GetGroupInfoRequest (qUnqualified qcnv) (qUnqualified charlie)) + (GetGroupInfoRequest (Conv (qUnqualified qcnv)) (qUnqualified charlie)) liftIO $ case resp of GetGroupInfoResponseError err -> @@ -2279,7 +2289,7 @@ getGroupInfoDisabled = do void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit withMLSDisabled $ - getGroupInfo (qUnqualified alice) qcnv + getGroupInfo (qUnqualified alice) (fmap Conv qcnv) !!! assertMLSNotEnabled testCreateSubConv :: Bool -> TestM () @@ -2295,8 +2305,54 @@ testCreateSubConv parentIsMLSConv = do else cnvQualifiedId <$> liftTest (postConvQualified (qUnqualified alice) defNewProteusConv >>= responseJsonError) - let sconv = SubConvId "call" + let sconv = SubConvId "conference" liftTest $ getSubConv (qUnqualified alice) qcnv sconv !!! do const (if parentIsMLSConv then 200 else 404) === statusCode + +testJoinSubConv :: TestM () +testJoinSubConv = do + [alice, bob] <- createAndConnectUsers [Nothing, Nothing] + + runMLSTest $ + do + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + + let subId = SubConvId "conference" + sub <- + liftTest $ + responseJsonError + =<< getSubConv (qUnqualified bob) qcnv (SubConvId "conference") + >= sendAndConsumeCommitBundle + + -- now alice joins with her own client + void $ + createExternalCommit alice1 Nothing (fmap (flip SubConv subId) qcnv) + >>= sendAndConsumeCommitBundle + +-- FUTUREWORK: implement the following tests + +testJoinSubNonMemberClient :: TestM () +testJoinSubNonMemberClient = pure () + +testAddClientSubConv :: TestM () +testAddClientSubConv = pure () + +testRemoveClientSubConv :: TestM () +testRemoveClientSubConv = pure () + +testJoinRemoteSubConv :: TestM () +testJoinRemoteSubConv = pure () + +testRemoteUserJoinSubConv :: TestM () +testRemoteUserJoinSubConv = pure () diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 6174b09abe4..df6839f7f3e 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -444,14 +444,19 @@ createGroup cid gid = do State.gets mlsGroupId >>= \case Just _ -> liftIO $ assertFailure "only one group can be created" Nothing -> pure () + resetGroup cid gid +resetGroup :: ClientIdentity -> GroupId -> MLSTest () +resetGroup cid gid = do groupJSON <- mlscli cid ["group", "create", T.unpack (toBase64Text (unGroupId gid))] Nothing g <- nextGroupFile cid liftIO $ BS.writeFile g groupJSON State.modify $ \s -> s { mlsGroupId = Just gid, - mlsMembers = Set.singleton cid + mlsMembers = Set.singleton cid, + mlsEpoch = 0, + mlsNewMembers = mempty } -- | Create a local group only without a conversation. This simulates creating @@ -546,16 +551,18 @@ createExternalCommit :: HasCallStack => ClientIdentity -> Maybe ByteString -> - Qualified ConvId -> + Qualified ConvOrSubConvId -> MLSTest MessagePackage -createExternalCommit qcid mpgs qcnv = do +createExternalCommit qcid mpgs qcs = do bd <- State.gets mlsBaseDir gNew <- nextGroupFile qcid pgsFile <- liftIO $ emptyTempFile bd "pgs" pgs <- case mpgs of Nothing -> LBS.toStrict . fromJust . responseBody - <$> getGroupInfo (ciUser qcid) qcnv + <$> ( getGroupInfo (ciUser qcid) qcs + pure v commit <- mlscli @@ -1011,21 +1018,37 @@ getGroupInfo :: HasGalley m ) => UserId -> - Qualified ConvId -> + Qualified ConvOrSubConvId -> m ResponseLBS -getGroupInfo sender qcnv = do +getGroupInfo sender qcs = do galley <- viewGalley - get - ( galley - . paths - [ "conversations", - toByteString' (qDomain qcnv), - toByteString' (qUnqualified qcnv), - "groupinfo" - ] - . zUser sender - . zConn "conn" - ) + case qUnqualified qcs of + Conv cnv -> + get + ( galley + . paths + [ "conversations", + toByteString' (qDomain qcs), + toByteString' cnv, + "groupinfo" + ] + . zUser sender + . zConn "conn" + ) + SubConv cnv sub -> + get + ( galley + . paths + [ "conversations", + toByteString' (qDomain qcs), + toByteString' cnv, + "subconversations", + toByteString' sub, + "groupinfo" + ] + . zUser sender + . zConn "conn" + ) getSelfConv :: UserId -> From 586c01a01eb187fe37e6db8183aa7f962b7e2648 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 20 Dec 2022 11:12:29 +0100 Subject: [PATCH 003/225] FS-902: Fix empty client set & set subconv field in message propagation (#2937) --- changelog.d/5-internal/mls-subconv-messages | 1 + charts/redis-cluster/requirements.yaml | 2 +- libs/wire-api/src/Wire/API/Error/Galley.hs | 3 ++ .../src/Wire/API/Routes/Public/Galley/MLS.hs | 3 ++ services/galley/src/Galley/API/MLS/Message.hs | 29 +++++++++++-- .../galley/src/Galley/API/MLS/Propagate.hs | 10 ++++- services/galley/test/integration/API/MLS.hs | 43 +++++++++++++++---- .../galley/test/integration/API/MLS/Util.hs | 15 +++++++ services/galley/test/integration/API/Util.hs | 18 +++++--- 9 files changed, 104 insertions(+), 20 deletions(-) create mode 100644 changelog.d/5-internal/mls-subconv-messages diff --git a/changelog.d/5-internal/mls-subconv-messages b/changelog.d/5-internal/mls-subconv-messages new file mode 100644 index 00000000000..ba9d2579d12 --- /dev/null +++ b/changelog.d/5-internal/mls-subconv-messages @@ -0,0 +1 @@ +Propagate messages in MLS subconversations diff --git a/charts/redis-cluster/requirements.yaml b/charts/redis-cluster/requirements.yaml index f540c858e28..17a9e5fdd21 100644 --- a/charts/redis-cluster/requirements.yaml +++ b/charts/redis-cluster/requirements.yaml @@ -1,4 +1,4 @@ dependencies: - name: redis-cluster - version: 7.4.8 + version: 7.6.4 repository: https://charts.bitnami.com/bitnami diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index d61666ec6ef..4fd742a9327 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -87,6 +87,7 @@ data GalleyError | MLSMissingSenderClient | MLSUnexpectedSenderClient | MLSSubConvUnsupportedConvType + | MLSSubConvClientNotInParent | -- NoBindingTeamMembers | NoBindingTeam @@ -221,6 +222,8 @@ type instance MapError 'MLSMissingSenderClient = 'StaticError 403 "mls-missing-s type instance MapError 'MLSSubConvUnsupportedConvType = 'StaticError 403 "mls-subconv-unsupported-convtype" "MLS subconversations are only supported for regular conversations" +type instance MapError 'MLSSubConvClientNotInParent = 'StaticError 403 "mls-subconv-join-parent-missing" "MLS client cannot join the subconversation because it is not member of the parent conversation" + type instance MapError 'NoBindingTeamMembers = 'StaticError 403 "non-binding-team-members" "Both users must be members of the same binding team" type instance MapError 'NoBindingTeam = 'StaticError 403 "no-binding-team" "Operation allowed only on binding teams" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index 09dbc3c77d9..9fae58df35c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -66,6 +66,7 @@ type MLSMessagingAPI = :> CanThrow 'MLSGroupConversationMismatch :> CanThrow 'MLSMissingSenderClient :> CanThrow 'MissingLegalholdConsent + :> CanThrow 'MLSSubConvClientNotInParent :> CanThrow MLSProposalFailure :> "messages" :> ZOptClient @@ -95,6 +96,7 @@ type MLSMessagingAPI = :> CanThrow 'MLSGroupConversationMismatch :> CanThrow 'MLSMissingSenderClient :> CanThrow 'MissingLegalholdConsent + :> CanThrow 'MLSSubConvClientNotInParent :> CanThrow MLSProposalFailure :> "messages" :> ZOptClient @@ -125,6 +127,7 @@ type MLSMessagingAPI = :> CanThrow 'MLSMissingSenderClient :> CanThrow 'MLSWelcomeMismatch :> CanThrow 'MissingLegalholdConsent + :> CanThrow 'MLSSubConvClientNotInParent :> CanThrow MLSProposalFailure :> "commit-bundles" :> ZOptClient diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 5242d900f4b..486f00964c0 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -111,7 +111,8 @@ type MLSMessageStaticErrors = ErrorS 'MLSSelfRemovalNotAllowed, ErrorS 'MLSClientSenderUserMismatch, ErrorS 'MLSGroupConversationMismatch, - ErrorS 'MLSMissingSenderClient + ErrorS 'MLSMissingSenderClient, + ErrorS 'MLSSubConvClientNotInParent ] type MLSBundleStaticErrors = @@ -137,6 +138,7 @@ postMLSMessageFromLocalUserV1 :: ErrorS 'MLSSelfRemovalNotAllowed, ErrorS 'MLSStaleMessage, ErrorS 'MLSUnsupportedMessage, + ErrorS 'MLSSubConvClientNotInParent, Input (Local ()), ProposalStore, Resource, @@ -176,6 +178,7 @@ postMLSMessageFromLocalUser :: ErrorS 'MLSSelfRemovalNotAllowed, ErrorS 'MLSStaleMessage, ErrorS 'MLSUnsupportedMessage, + ErrorS 'MLSSubConvClientNotInParent, Input (Local ()), ProposalStore, Resource, @@ -391,6 +394,7 @@ postMLSMessage :: ErrorS 'MLSSelfRemovalNotAllowed, ErrorS 'MLSStaleMessage, ErrorS 'MLSUnsupportedMessage, + ErrorS 'MLSSubConvClientNotInParent, Input (Local ()), ProposalStore, Resource, @@ -481,6 +485,7 @@ postMLSMessageToLocalConv :: ErrorS 'MLSUnsupportedMessage, ErrorS 'MissingLegalholdConsent, ErrorS 'ConvNotFound, + ErrorS 'MLSSubConvClientNotInParent, MemberStore, ProposalStore, Resource, @@ -647,6 +652,7 @@ processCommit :: Member (ErrorS 'MLSSelfRemovalNotAllowed) r, Member (ErrorS 'MLSStaleMessage) r, Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'MLSSubConvClientNotInParent) r, Member (Input (Local ())) r, Member ProposalStore r, Member BrigAccess r, @@ -795,6 +801,7 @@ processCommitWithAction :: Member (ErrorS 'MLSSelfRemovalNotAllowed) r, Member (ErrorS 'MLSStaleMessage) r, Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'MLSSubConvClientNotInParent) r, Member (Input (Local ())) r, Member ProposalStore r, Member BrigAccess r, @@ -829,6 +836,7 @@ processInternalCommit :: Member (ErrorS 'MLSSelfRemovalNotAllowed) r, Member (ErrorS 'MLSStaleMessage) r, Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'MLSSubConvClientNotInParent) r, Member (Input (Local ())) r, Member ProposalStore r, Member SubConversationStore r, @@ -893,9 +901,24 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co senderRef' -- remote clients cannot send the first commit (False, _, _, _) -> throwS @'MLSStaleMessage - (_, _, _, SubConv _ _) -> pure () + (True, _, [], SubConv parentConv _) -> do + creatorClient <- noteS @'MLSMissingSenderClient senderClient + unless (isClientMember (mkClientIdentity qusr creatorClient) (mcMembers parentConv)) $ + throwS @'MLSSubConvClientNotInParent + creatorRef <- + maybe + (pure senderRef) + ( note (mlsProtocolError "Could not compute key package ref") + . kpRef' + . upLeaf + ) + $ cPath commit + addMLSClients + (cnvmlsGroupId mlsMeta) + qusr + (Set.singleton (creatorClient, creatorRef)) -- uninitialised conversations should contain exactly one client - (_, _, _, Conv _) -> + (_, _, _, _) -> throw (InternalErrorWithDescription "Unexpected creator client set") pure $ pure () -- no key package ref update necessary else case upLeaf <$> cPath commit of diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 0d7e3f05ce6..e35a23dd0f9 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -43,6 +43,7 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.MLS.SubConversation import Wire.API.Message -- | Propagate a message. @@ -68,8 +69,13 @@ propagateMessage qusr lConvOrSub con raw = do b <- maybeToList $ newBotMember m pure (lmId m, b) mm = defMessageMetadata - let qcnv = mcId . convOfConvOrSub <$> tUntagged lConvOrSub - e = Event qcnv Nothing qusr now $ EdMLSMessage raw + let qt = + tUntagged lConvOrSub <&> \case + Conv c -> (mcId c, Nothing) + SubConv c s -> (mcId c, Just (scSubConvId s)) + qcnv = fst <$> qt + sconv = snd (qUnqualified qt) + e = Event qcnv sconv qusr now $ EdMLSMessage raw mkPush :: UserId -> ClientId -> MessagePush 'NormalMessage mkPush u c = newMessagePush mlsConv botMap con mm (u, c) e runMessagePush mlsConv (Just qcnv) $ diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 10df04b4930..b8758cc984f 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -214,7 +214,8 @@ tests s = test s "add another client to a subconversation" testAddClientSubConv, test s "remove another client from a subconversation" testRemoveClientSubConv, test s "join remote subconversation" testJoinRemoteSubConv, - test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv + test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv, + test s "send an application message in a subconversation" testSendMessageSubConv ] ] @@ -1090,7 +1091,7 @@ testExternalCommitNewClientResendBackendProposal = do liftIO $ assertBool "No events after external commit expected" (null ecEvents) WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ - wsAssertMLSMessage qcnv bob (mpMessage mp) + wsAssertMLSMessage (convsub qcnv Nothing) bob (mpMessage mp) -- The backend proposals for bob2 are replayed, but the external add -- proposal for bob3 has to replayed by the client and is thus not found @@ -1115,7 +1116,7 @@ testAppMessage = do liftIO $ events @?= [] liftIO $ WS.assertMatchN_ (5 # WS.Second) wss $ - wsAssertMLSMessage qcnv alice (mpMessage message) + wsAssertMLSMessage (convsub qcnv Nothing) alice (mpMessage message) testAppMessage2 :: TestM () testAppMessage2 = do @@ -1146,7 +1147,7 @@ testAppMessage2 = do liftIO $ WS.assertMatchN_ (5 # WS.Second) wss $ - wsAssertMLSMessage conversation bob (mpMessage message) + wsAssertMLSMessage (convsub conversation Nothing) bob (mpMessage message) testRemoteToRemote :: TestM () testRemoteToRemote = do @@ -1196,8 +1197,8 @@ testRemoteToRemote = do void $ runFedClient @"on-mls-message-sent" fedGalleyClient bdom rm liftIO $ do -- alice should receive the message on her first client - WS.assertMatch_ (5 # Second) wsA1 $ \n -> wsAssertMLSMessage qconv qbob txt n - WS.assertMatch_ (5 # Second) wsA2 $ \n -> wsAssertMLSMessage qconv qbob txt n + WS.assertMatch_ (5 # Second) wsA1 $ \n -> wsAssertMLSMessage (convsub qconv Nothing) qbob txt n + WS.assertMatch_ (5 # Second) wsA2 $ \n -> wsAssertMLSMessage (convsub qconv Nothing) qbob txt n -- eve should not receive the message WS.assertNoEvent (1 # Second) [wsE] @@ -1259,7 +1260,7 @@ testRemoteToLocal = do liftIO $ do resp @?= MLSMessageResponseUpdates [] WS.assertMatch_ (5 # Second) ws $ - wsAssertMLSMessage qcnv bob (mpMessage message) + wsAssertMLSMessage (convsub qcnv Nothing) bob (mpMessage message) testRemoteToLocalWrongConversation :: TestM () testRemoteToLocalWrongConversation = do @@ -1462,7 +1463,7 @@ testExternalAddProposal = do void $ sendAndConsumeMessage msg liftTest $ WS.assertMatchN_ (5 # Second) wss $ - wsAssertMLSMessage qcnv alice (mpMessage msg) + wsAssertMLSMessage (convsub qcnv Nothing) alice (mpMessage msg) -- bob adds charlie putOtherMemberQualified @@ -2356,3 +2357,29 @@ testJoinRemoteSubConv = pure () testRemoteUserJoinSubConv :: TestM () testRemoteUserJoinSubConv = pure () + +testSendMessageSubConv :: TestM () +testSendMessageSubConv = do + [alice, bob] <- createAndConnectUsers [Nothing, Nothing] + + runMLSTest $ + do + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + + let subname = "conference" + createSubConv qcnv bob1 subname + let qcs = convsub qcnv (Just subname) + + void $ createExternalCommit alice1 Nothing qcs >>= sendAndConsumeCommitBundle + void $ createExternalCommit bob2 Nothing qcs >>= sendAndConsumeCommitBundle + + message <- createApplicationMessage alice1 "some text" + mlsBracket [bob1, bob2] $ \wss -> do + events <- sendAndConsumeMessage message + liftIO $ events @?= [] + liftIO $ + WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do + wsAssertMLSMessage qcs alice (mpMessage message) n diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index df6839f7f3e..6d60f1f04ee 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -459,6 +459,17 @@ resetGroup cid gid = do mlsNewMembers = mempty } +createSubConv :: Qualified ConvId -> ClientIdentity -> Text -> MLSTest () +createSubConv qcnv creator name = do + let subId = SubConvId name + sub <- + liftTest $ + responseJsonError + =<< getSubConv (ciUser creator) qcnv subId + >= sendAndConsumeCommitBundle + -- | Create a local group only without a conversation. This simulates creating -- an MLS conversation on a remote backend. setupFakeMLSGroup :: ClientIdentity -> MLSTest (GroupId, Qualified ConvId) @@ -1084,3 +1095,7 @@ getSubConv u qcnv sconv = do LBS.toStrict (toLazyByteString (toEncodedUrlPiece sconv)) ] . zUser u + +convsub :: Qualified ConvId -> Maybe Text -> Qualified ConvOrSubConvId +convsub qcnv Nothing = Conv <$> qcnv +convsub qcnv (Just subname) = flip SubConv (SubConvId subname) <$> qcnv diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index adfc6d1b290..817a6ca49e8 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -72,6 +72,7 @@ import Data.UUID.V4 import Federator.MockServer (FederatedRequest (..)) import qualified Federator.MockServer as Mock import GHC.TypeLits (KnownSymbol) +import Galley.API.MLS.Types import Galley.Intra.User (chunkify) import qualified Galley.Options as Opts import qualified Galley.Run as Run @@ -119,6 +120,7 @@ import Wire.API.MLS.KeyPackage import Wire.API.MLS.Message import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation +import Wire.API.MLS.SubConversation import Wire.API.Message import qualified Wire.API.Message.Proto as Proto import Wire.API.Routes.Internal.Brig.Connection @@ -1666,15 +1668,15 @@ wsAssertMLSWelcome u welcome n = do wsAssertMLSMessage :: HasCallStack => - Qualified ConvId -> + Qualified ConvOrSubConvId -> Qualified UserId -> ByteString -> Notification -> IO () -wsAssertMLSMessage conv u message n = do +wsAssertMLSMessage qcs u message n = do let e = List1.head (WS.unpackPayload n) ntfTransient n @?= False - assertMLSMessageEvent conv u message e + assertMLSMessageEvent qcs u message e wsAssertClientRemoved :: HasCallStack => @@ -1702,13 +1704,17 @@ wsAssertClientAdded cid n = do assertMLSMessageEvent :: HasCallStack => - Qualified ConvId -> + Qualified ConvOrSubConvId -> Qualified UserId -> ByteString -> Conv.Event -> IO () -assertMLSMessageEvent conv u message e = do - evtConv e @?= conv +assertMLSMessageEvent qcs u message e = do + evtConv e @?= convOfConvOrSub <$> qcs + case qUnqualified qcs of + Conv _ -> pure () + SubConv _ subconvId -> + evtSubConv e @?= Just subconvId evtType e @?= MLSMessageAdd evtFrom e @?= u evtData e @?= EdMLSMessage message From c22a8885fc3168de6a84afeb3e9ef0f63066d6b8 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 20 Dec 2022 12:41:41 +0100 Subject: [PATCH 004/225] Use same validation error in internal & external commit validation (#2944) --- services/galley/src/Galley/API/MLS/Message.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 486f00964c0..2e0d2b19f33 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -682,6 +682,7 @@ processExternalCommit :: ErrorS 'MLSKeyPackageRefNotFound, ErrorS 'MLSStaleMessage, ErrorS 'MLSMissingSenderClient, + ErrorS 'MLSSubConvClientNotInParent, Error InternalError, ExternalAccess, FederatorAccess, @@ -744,8 +745,7 @@ processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = wi Conv _ -> pure () SubConv mlsConv _ -> unless (isJust (cmLookupRef cid (mcMembers mlsConv))) $ - throw . mlsProtocolError $ - "Cannot join a subconversation before joining the parent conversation" + throwS @'MLSSubConvClientNotInParent -- check if there is a key package ref in the remove proposal remRef <- From a572fd2f6a2d5f1e353f365b3d4efc53f11cf1e9 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 20 Dec 2022 15:01:56 +0100 Subject: [PATCH 005/225] Factor out update path handling (#2945) --- services/galley/src/Galley/API/MLS/Message.hs | 42 ++++++------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 2e0d2b19f33..c6e62736730 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -26,6 +26,7 @@ module Galley.API.MLS.Message ) where +import Control.Arrow ((>>>)) import Control.Comonad import Control.Error.Util (hush) import Control.Lens (preview) @@ -744,7 +745,7 @@ processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = wi case convOrSub of Conv _ -> pure () SubConv mlsConv _ -> - unless (isJust (cmLookupRef cid (mcMembers mlsConv))) $ + unless (isClientMember cid (mcMembers mlsConv)) $ throwS @'MLSSubConvClientNotInParent -- check if there is a key package ref in the remove proposal @@ -857,6 +858,11 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co mlsMeta = mlsMetaConvOrSub convOrSub localSelf = isLocal lConvOrSub qusr + updatePathRef <- + for + (cPath commit) + (upLeaf >>> kpRef' >>> note (mlsProtocolError "Could not compute key package ref")) + withCommitLock (cnvmlsGroupId . mlsMetaConvOrSub $ convOrSub) epoch $ do postponedKeyPackageRefUpdate <- if epoch == Epoch 0 @@ -865,14 +871,7 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co case (localSelf, cType, cmAssocs . membersConvOrSub $ convOrSub, convOrSub) of (True, SelfConv, [], Conv _) -> do creatorClient <- noteS @'MLSMissingSenderClient senderClient - creatorRef <- - maybe - (pure senderRef) - ( note (mlsProtocolError "Could not compute key package ref") - . kpRef' - . upLeaf - ) - $ cPath commit + let creatorRef = fromMaybe senderRef updatePathRef addMLSClients (cnvmlsGroupId mlsMeta) qusr @@ -884,35 +883,21 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co (True, _, [(qu, (creatorClient, _))], Conv _) | qu == qusr -> do -- use update path as sender reference and if not existing fall back to sender - senderRef' <- - maybe - (pure senderRef) - ( note (mlsProtocolError "Could not compute key package ref") - . kpRef' - . upLeaf - ) - $ cPath commit + let creatorRef = fromMaybe senderRef updatePathRef -- register the creator client updateKeyPackageMapping lConvOrSub qusr creatorClient Nothing - senderRef' + creatorRef -- remote clients cannot send the first commit (False, _, _, _) -> throwS @'MLSStaleMessage (True, _, [], SubConv parentConv _) -> do creatorClient <- noteS @'MLSMissingSenderClient senderClient unless (isClientMember (mkClientIdentity qusr creatorClient) (mcMembers parentConv)) $ throwS @'MLSSubConvClientNotInParent - creatorRef <- - maybe - (pure senderRef) - ( note (mlsProtocolError "Could not compute key package ref") - . kpRef' - . upLeaf - ) - $ cPath commit + let creatorRef = fromMaybe senderRef updatePathRef addMLSClients (cnvmlsGroupId mlsMeta) qusr @@ -921,9 +906,8 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co (_, _, _, _) -> throw (InternalErrorWithDescription "Unexpected creator client set") pure $ pure () -- no key package ref update necessary - else case upLeaf <$> cPath commit of - Just updatedKeyPackage -> do - updatedRef <- kpRef' updatedKeyPackage & note (mlsProtocolError "Could not compute key package ref") + else case updatePathRef of + Just updatedRef -> do -- postpone key package ref update until other checks/processing passed case senderClient of Just cli -> From afa0fbf4651a2f8011a825b039bae33c80308a4f Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 27 Dec 2022 17:11:47 +0100 Subject: [PATCH 006/225] Add federated endpoints to get subconversations (#2952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add get-sub-conversation endpoint type * Refactor: Generalize getConversationAndCheckMembership * Add federation and generalize client endpoint * Int. test: local member, remote subconversation * Fix bug: conversation qualified wrong * Add tests for federation endpoint * Move test stub into correct group * Add changelog entry * Remove unused constraints Co-authored-by: Marko Dimjašević --- changelog.d/2-features/pr-2952 | 1 + .../src/Wire/API/Federation/API/Galley.hs | 15 ++ .../src/Wire/API/MLS/SubConversation.hs | 2 +- services/galley/src/Galley/API/Federation.hs | 22 +++ .../src/Galley/API/MLS/SubConversation.hs | 65 ++++++--- services/galley/src/Galley/API/Query.hs | 4 +- services/galley/src/Galley/API/Util.hs | 29 ++-- services/galley/test/integration/API/MLS.hs | 130 ++++++++++++++++-- 8 files changed, 232 insertions(+), 36 deletions(-) create mode 100644 changelog.d/2-features/pr-2952 diff --git a/changelog.d/2-features/pr-2952 b/changelog.d/2-features/pr-2952 new file mode 100644 index 00000000000..0bfe30efe71 --- /dev/null +++ b/changelog.d/2-features/pr-2952 @@ -0,0 +1 @@ +Add federated endpoints to get subconversations 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 2166f915c5a..9ef47081b3b 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 @@ -75,6 +75,7 @@ type GalleyApi = :<|> FedEndpoint "query-group-info" GetGroupInfoRequest GetGroupInfoResponse :<|> FedEndpoint "on-client-removed" ClientRemovedRequest EmptyResponse :<|> FedEndpoint "on-typing-indicator-updated" TypingDataUpdateRequest EmptyResponse + :<|> FedEndpoint "get-sub-conversation" GetSubConversationsRequest GetSubConversationsResponse data TypingDataUpdateRequest = TypingDataUpdateRequest { tdurTypingStatus :: TypingStatus, @@ -367,3 +368,17 @@ data GetGroupInfoResponse | GetGroupInfoResponseState Base64ByteString deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded GetGroupInfoResponse) + +data GetSubConversationsRequest = GetSubConversationsRequest + { gsreqUser :: UserId, + gsreqConv :: ConvId, + gsreqSubConv :: SubConvId + } + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded GetSubConversationsRequest) + +data GetSubConversationsResponse + = GetSubConversationsResponseError GalleyError + | GetSubConversationsResponseSuccess PublicSubConversation + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded GetSubConversationsResponse) diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index 10e79f727b7..09a77b89a13 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -46,7 +46,7 @@ import Wire.Arbitrary -- conversation. The pair of a qualified conversation ID and a subconversation -- ID identifies globally. newtype SubConvId = SubConvId {unSubConvId :: Text} - deriving newtype (Eq, ToSchema, Ord, S.ToParamSchema, ToByteString) + deriving newtype (Eq, ToSchema, Ord, S.ToParamSchema, ToByteString, ToJSON, FromJSON) deriving stock (Generic) deriving (Arbitrary) via (GenericUniform SubConvId) deriving stock (Show) diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index e18f660c444..20ab7e727be 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -120,6 +120,7 @@ federationSitemap = :<|> Named @"query-group-info" queryGroupInfo :<|> Named @"on-client-removed" onClientRemoved :<|> Named @"on-typing-indicator-updated" onTypingIndicatorUpdated + :<|> Named @"get-sub-conversation" getSubConversationForRemoteUser onClientRemoved :: ( Members @@ -847,3 +848,24 @@ onTypingIndicatorUpdated origDomain TypingDataUpdateRequest {..} = do runError @(Tagged 'ConvNotFound ()) $ isTyping qusr Nothing lcnv tdurTypingStatus pure EmptyResponse + +getSubConversationForRemoteUser :: + Members + '[ SubConversationStore, + ConversationStore, + Input (Local ()), + Error InternalError, + P.TinyLog + ] + r => + Domain -> + GetSubConversationsRequest -> + Sem r GetSubConversationsResponse +getSubConversationForRemoteUser domain GetSubConversationsRequest {..} = + fmap (either F.GetSubConversationsResponseError F.GetSubConversationsResponseSuccess) + . runError @GalleyError + . mapToGalleyError @MLSGetSubConvStaticErrors + $ do + let qusr = Qualified gsreqUser domain + lconv <- qualifyLocal gsreqConv + getLocalSubConversation qusr lconv gsreqSubConv diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index dfc0234b55b..d9cf14acc04 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -19,7 +19,6 @@ module Galley.API.MLS.SubConversation where import Data.Id import Data.Qualified -import Galley.API.Error import Galley.API.MLS import Galley.API.MLS.GroupInfo import Galley.API.MLS.Types @@ -29,22 +28,29 @@ import Galley.App (Env) import qualified Galley.Data.Conversation as Data import Galley.Data.Conversation.Types import Galley.Effects +import Galley.Effects.FederatorAccess import Galley.Effects.SubConversationStore import qualified Galley.Effects.SubConversationStore as Eff import Imports -import qualified Network.Wai.Utilities.Error as Wai import Polysemy import Polysemy.Error import Polysemy.Input -import qualified Polysemy.TinyLog as P import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.Federation.Error (FederationError, federationNotImplemented) +import Wire.API.Federation.API (Component (Galley), fedClient) +import Wire.API.Federation.API.Galley (GetSubConversationsRequest (..), GetSubConversationsResponse (..)) +import Wire.API.Federation.Error (FederationError) import Wire.API.MLS.PublicGroupState import Wire.API.MLS.SubConversation +type MLSGetSubConvStaticErrors = + '[ ErrorS 'ConvNotFound, + ErrorS 'ConvAccessDenied, + ErrorS 'MLSSubConvUnsupportedConvType + ] + getSubConversation :: Members '[ SubConversationStore, @@ -52,9 +58,8 @@ getSubConversation :: ErrorS 'ConvNotFound, ErrorS 'ConvAccessDenied, ErrorS 'MLSSubConvUnsupportedConvType, - Error InternalError, - Error Wai.Error, - P.TinyLog + Error FederationError, + FederatorAccess ] r => Local UserId -> @@ -64,8 +69,8 @@ getSubConversation :: getSubConversation lusr qconv sconv = do foldQualified lusr - (\lcnv -> getLocalSubConversation lusr lcnv sconv) - (\_rcnv -> throw federationNotImplemented) + (\lcnv -> getLocalSubConversation (tUntagged lusr) lcnv sconv) + (\rcnv -> getRemoteSubConversation lusr rcnv sconv) qconv getLocalSubConversation :: @@ -74,17 +79,15 @@ getLocalSubConversation :: ConversationStore, ErrorS 'ConvNotFound, ErrorS 'ConvAccessDenied, - ErrorS 'MLSSubConvUnsupportedConvType, - Error InternalError, - P.TinyLog + ErrorS 'MLSSubConvUnsupportedConvType ] r => - Local UserId -> + Qualified UserId -> Local ConvId -> SubConvId -> Sem r PublicSubConversation -getLocalSubConversation lusr lconv sconv = do - c <- getConversationAndCheckMembership (tUnqualified lusr) lconv +getLocalSubConversation qusr lconv sconv = do + c <- getConversationAndCheckMembership qusr lconv unless (Data.convType c == RegularConv) $ throwS @'MLSSubConvUnsupportedConvType @@ -114,7 +117,37 @@ getLocalSubConversation lusr lconv sconv = do } pure sub Just sub -> pure sub - pure (toPublicSubConv (tUntagged (qualifyAs lusr sub))) + pure (toPublicSubConv (tUntagged (qualifyAs lconv sub))) + +getRemoteSubConversation :: + forall r. + ( Members + '[ ErrorS 'ConvNotFound, + ErrorS 'ConvAccessDenied, + ErrorS 'MLSSubConvUnsupportedConvType, + FederatorAccess + ] + r, + Members MLSGetSubConvStaticErrors r, + RethrowErrors MLSGetSubConvStaticErrors r + ) => + Local UserId -> + Remote ConvId -> + SubConvId -> + Sem r PublicSubConversation +getRemoteSubConversation lusr rcnv sconv = do + res <- runFederated rcnv $ do + fedClient @'Galley @"get-sub-conversation" $ + GetSubConversationsRequest + { gsreqUser = tUnqualified lusr, + gsreqConv = tUnqualified rcnv, + gsreqSubConv = sconv + } + case res of + GetSubConversationsResponseError err -> + rethrowErrors @MLSGetSubConvStaticErrors @r err + GetSubConversationsResponseSuccess subconv -> + pure subconv getSubConversationGroupInfo :: Members diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 99fdf91f88e..4e93d5839d7 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -136,7 +136,7 @@ getUnqualifiedConversation :: ConvId -> Sem r Public.Conversation getUnqualifiedConversation lusr cnv = do - c <- getConversationAndCheckMembership (tUnqualified lusr) (qualifyAs lusr cnv) + c <- getConversationAndCheckMembership (tUntagged lusr) (qualifyAs lusr cnv) Mapping.conversationView lusr c getConversation :: @@ -272,7 +272,7 @@ getConversationRoles :: ConvId -> Sem r Public.ConversationRolesList getConversationRoles lusr cnv = do - void $ getConversationAndCheckMembership (tUnqualified lusr) (qualifyAs lusr cnv) + void $ getConversationAndCheckMembership (tUntagged lusr) (qualifyAs lusr cnv) -- NOTE: If/when custom roles are added, these roles should -- be merged with the team roles (if they exist) pure $ Public.ConversationRolesList wireConvRoles diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 305226946cc..3e99373a74b 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -502,16 +502,29 @@ getMember p u = noteS @e . find ((u ==) . p) getConversationAndCheckMembership :: Members '[ConversationStore, ErrorS 'ConvNotFound, ErrorS 'ConvAccessDenied] r => - UserId -> + Qualified UserId -> Local ConvId -> Sem r Data.Conversation -getConversationAndCheckMembership uid lcnv = do - (conv, _) <- - getConversationAndMemberWithError - @'ConvAccessDenied - uid - lcnv - pure conv +getConversationAndCheckMembership quid lcnv = do + foldQualified + lcnv + ( \lusr -> do + (conv, _) <- + getConversationAndMemberWithError + @'ConvAccessDenied + (tUnqualified lusr) + lcnv + pure conv + ) + ( \rusr -> do + (conv, _) <- + getConversationAndMemberWithError + @'ConvNotFound + rusr + lcnv + pure conv + ) + quid getConversationWithError :: ( Member ConversationStore r, diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index b8758cc984f..7aaf921179c 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -61,6 +61,7 @@ import Wire.API.Conversation.Role import Wire.API.Error.Galley import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.Keys import Wire.API.MLS.Serialisation @@ -207,15 +208,28 @@ tests s = ], testGroup "SubConversation" - [ test s "get subconversation of MLS conv - 200" (testCreateSubConv True), - test s "get subconversation of Proteus conv - 404" (testCreateSubConv False), - test s "join subconversation with an external commit bundle" testJoinSubConv, - test s "join subconversation with a client that is not in the main conv" testJoinSubNonMemberClient, - test s "add another client to a subconversation" testAddClientSubConv, - test s "remove another client from a subconversation" testRemoveClientSubConv, - test s "join remote subconversation" testJoinRemoteSubConv, - test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv, - test s "send an application message in a subconversation" testSendMessageSubConv + [ testGroup + "Local Sender/Local Subconversation" + [ test s "get subconversation of MLS conv - 200" (testCreateSubConv True), + test s "get subconversation of Proteus conv - 404" (testCreateSubConv False), + test s "join subconversation with an external commit bundle" testJoinSubConv, + test s "join subconversation with a client that is not in the main conv" testJoinSubNonMemberClient, + test s "add another client to a subconversation" testAddClientSubConv, + test s "remove another client from a subconversation" testRemoveClientSubConv, + test s "send an application message in a subconversation" testSendMessageSubConv + ], + testGroup + "Local Sender/Remote Subconversation" + [ test s "get subconversation of remote conversation - member" (testGetRemoteSubConv True), + test s "get subconversation of remote conversation - not member" (testGetRemoteSubConv False), + test s "join remote subconversation" testJoinRemoteSubConv + ], + testGroup + "Remote Sender/Local SubConversation" + [ test s "get subconversation as a remote member" (testRemoteMemberGetSubConv True), + test s "get subconversation as a remote non-member" (testRemoteMemberGetSubConv False), + test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv + ] ] ] @@ -2383,3 +2397,101 @@ testSendMessageSubConv = do liftIO $ WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do wsAssertMLSMessage qcs alice (mpMessage message) n + +testGetRemoteSubConv :: Bool -> TestM () +testGetRemoteSubConv isAMember = do + alice <- randomQualifiedUser + let remoteDomain = Domain "faraway.example.com" + conv <- randomId + let qconv = Qualified conv remoteDomain + sconv = SubConvId "conference" + fakeSubConv = + PublicSubConversation + { pscParentConvId = qconv, + pscSubConvId = sconv, + pscGroupId = GroupId "deadbeef", + pscEpoch = Epoch 0, + pscCipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519, + pscMembers = [] + } + + let mock req = case frRPC req of + "get-sub-conversation" -> + pure $ + if isAMember + then Aeson.encode (GetSubConversationsResponseSuccess fakeSubConv) + else Aeson.encode (GetSubConversationsResponseError ConvNotFound) + rpc -> assertFailure $ "unmocked RPC called: " <> T.unpack rpc + + (_, reqs) <- + withTempMockFederator' mock $ + getSubConv (qUnqualified alice) qconv sconv + TestM () +testRemoteMemberGetSubConv isAMember = do + -- alice is local, bob is remote + -- alice creates a local conversation and invites bob + -- bob gets a subconversation via federated enpdoint + + let bobDomain = Domain "faraway.example.com" + [alice, bob] <- createAndConnectUsers [Nothing, Just (domainText bobDomain)] + + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + + (_groupId, qcnv) <- setupMLSGroup alice1 + kpb <- claimKeyPackages alice1 bob + mp <- createAddCommit alice1 [bob] + + let mockedResponse fedReq = + case frRPC fedReq of + "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) + "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) + "on-conversation-updated" -> pure (Aeson.encode ()) + "get-mls-clients" -> + pure + . Aeson.encode + . Set.singleton + $ ClientInfo (ciClient bob1) True + "claim-key-packages" -> pure . Aeson.encode $ kpb + ms -> assertFailure ("unmocked endpoint called: " <> cs ms) + + void . withTempMockFederator' mockedResponse $ + sendAndConsumeCommit mp + + let subconv = SubConvId "conference" + + randUser <- randomId + let gscr = + GetSubConversationsRequest + { gsreqUser = if isAMember then qUnqualified bob else randUser, + gsreqConv = qUnqualified qcnv, + gsreqSubConv = subconv + } + + fedGalleyClient <- view tsFedGalleyClient + res <- runFedClient @"get-sub-conversation" fedGalleyClient bobDomain gscr + + liftTest $ do + if isAMember + then do + sub <- expectSubConvSuccess res + liftIO $ do + pscParentConvId sub @?= qcnv + pscSubConvId sub @?= subconv + else do + expectSubConvError ConvNotFound res + where + expectSubConvSuccess :: GetSubConversationsResponse -> TestM PublicSubConversation + expectSubConvSuccess (GetSubConversationsResponseSuccess fakeSubConv) = pure fakeSubConv + expectSubConvSuccess (GetSubConversationsResponseError err) = liftIO $ assertFailure ("Unexpected GetSubConversationsResponseError: " <> show err) + + expectSubConvError :: GalleyError -> GetSubConversationsResponse -> TestM () + expectSubConvError _errExpected (GetSubConversationsResponseSuccess _) = liftIO $ assertFailure "Unexpected GetSubConversationsResponseSuccess" + expectSubConvError errExpected (GetSubConversationsResponseError err) = liftIO $ err @?= errExpected From 9947a5f038a733d9de864198757651c048a05d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Mon, 9 Jan 2023 11:13:09 +0100 Subject: [PATCH 007/225] [FS-1334] Reset a Subconversation (#2956) * Define the DELETE subconversation endpoint * Move withCommitLock to a utility module * Generate a fresh group ID upon subconv deletion * Delete a subconversation - Remove all members - Delete group ID mapping - Create a new subconversation with a fresh group ID - Add group ID mapping for the new one * Test: cannot delete when MLS disabled * Test: happy case of deleting and getting * Test: fail to reset with a stale epoch * Add changelogs --- .../1-api-changes/delete-subconversation | 1 + changelog.d/2-features/delete-subconversation | 1 + .../src/Wire/API/MLS/SubConversation.hs | 19 +++- .../API/Routes/Public/Galley/Conversation.hs | 18 ++++ services/galley/galley.cabal | 2 + services/galley/src/Galley/API/MLS/Message.hs | 29 +----- .../src/Galley/API/MLS/SubConversation.hs | 92 +++++++++++++++++-- services/galley/src/Galley/API/MLS/Util.hs | 28 ++++++ .../src/Galley/API/Public/Conversation.hs | 1 + services/galley/src/Galley/App.hs | 4 + .../Galley/Cassandra/Conversation/Members.hs | 5 + .../galley/src/Galley/Cassandra/Queries.hs | 6 ++ .../src/Galley/Cassandra/SubConversation.hs | 5 + services/galley/src/Galley/Effects.hs | 4 + .../galley/src/Galley/Effects/MemberStore.hs | 2 + .../Galley/Effects/SubConversationStore.hs | 1 + .../Galley/Effects/SubConversationSupply.hs | 30 ++++++ .../Effects/SubConversationSupply/Random.hs | 40 ++++++++ services/galley/test/integration/API/MLS.hs | 79 +++++++++++++++- .../galley/test/integration/API/MLS/Util.hs | 21 +++++ 20 files changed, 351 insertions(+), 37 deletions(-) create mode 100644 changelog.d/1-api-changes/delete-subconversation create mode 100644 changelog.d/2-features/delete-subconversation create mode 100644 services/galley/src/Galley/Effects/SubConversationSupply.hs create mode 100644 services/galley/src/Galley/Effects/SubConversationSupply/Random.hs diff --git a/changelog.d/1-api-changes/delete-subconversation b/changelog.d/1-api-changes/delete-subconversation new file mode 100644 index 00000000000..79f52ccb5d4 --- /dev/null +++ b/changelog.d/1-api-changes/delete-subconversation @@ -0,0 +1 @@ +Introduce an endpoint for deleting a subconversation diff --git a/changelog.d/2-features/delete-subconversation b/changelog.d/2-features/delete-subconversation new file mode 100644 index 00000000000..08ab83679d9 --- /dev/null +++ b/changelog.d/2-features/delete-subconversation @@ -0,0 +1 @@ +Introduce support for resetting a subconversation diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index 09a77b89a13..7b4288707da 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -23,7 +23,7 @@ module Wire.API.MLS.SubConversation where import Control.Lens (makePrisms, (?~)) import Control.Lens.Tuple (_1) import Control.Monad.Except -import qualified Crypto.Hash as Crypto +import Crypto.Hash as Crypto import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as A import Data.ByteArray @@ -163,3 +163,20 @@ deriving via Schema ConvOrSubConvId instance FromJSON ConvOrSubConvId deriving via Schema ConvOrSubConvId instance ToJSON ConvOrSubConvId deriving via Schema ConvOrSubConvId instance S.ToSchema ConvOrSubConvId + +-- | The body of the delete subconversation request +data DeleteSubConversation = DeleteSubConversation + { dscGroupId :: GroupId, + dscEpoch :: Epoch + } + deriving (Eq, Show) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema DeleteSubConversation) + +instance ToSchema DeleteSubConversation where + schema = + objectWithDocModifier + "DeleteSubConversation" + (description ?~ "Delete an MLS subconversation") + $ DeleteSubConversation + <$> dscGroupId .= field "group_id" schema + <*> dscEpoch .= field "epoch" schema diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index a8bd5a03da8..952b8afb5f7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -405,6 +405,24 @@ type ConversationAPI = PublicSubConversation ) ) + :<|> Named + "delete-subconversation" + ( Summary "Delete an MLS subconversation" + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'MLSNotEnabled + :> CanThrow 'MLSStaleMessage + :> ZLocalUser + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> "subconversations" + :> Capture "subconv" SubConvId + :> ReqBody '[JSON] DeleteSubConversation + :> MultiVerb1 + 'DELETE + '[JSON] + (Respond 200 "Deletion successful" ()) + ) :<|> Named "get-subconversation-group-info" ( Summary "Get MLS group information of subconversation" diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 6bf7ada2374..8c17924d3d1 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -111,6 +111,8 @@ library Galley.Effects.ServiceStore Galley.Effects.SparAccess Galley.Effects.SubConversationStore + Galley.Effects.SubConversationSupply + Galley.Effects.SubConversationSupply.Random Galley.Effects.TeamFeatureStore Galley.Effects.TeamMemberStore Galley.Effects.TeamNotificationStore diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index c9abdc135b8..d65da4f49fa 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -51,7 +51,6 @@ import Galley.API.MLS.Welcome (postMLSWelcome) import Galley.API.Util import Galley.Data.Conversation.Types hiding (Conversation) import qualified Galley.Data.Conversation.Types as Data -import Galley.Data.Types import Galley.Effects import Galley.Effects.BrigAccess import Galley.Effects.ConversationStore @@ -67,7 +66,7 @@ import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.Internal -import Polysemy.Resource (Resource, bracket) +import Polysemy.Resource (Resource) import Polysemy.TinyLog import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol @@ -1474,32 +1473,6 @@ instance where handleMLSProposalFailure = mapError (MLSProposalFailure . toWai) -withCommitLock :: - forall r a. - ( Members - '[ Resource, - ConversationStore, - ErrorS 'MLSStaleMessage - ] - r - ) => - GroupId -> - Epoch -> - Sem r a -> - Sem r a -withCommitLock gid epoch action = - bracket - ( acquireCommitLock gid epoch ttl >>= \lockAcquired -> - when (lockAcquired == NotAcquired) $ - throwS @'MLSStaleMessage - ) - (const $ releaseCommitLock gid epoch) - $ \_ -> do - -- FUTUREWORK: fetch epoch again and check that is matches - action - where - ttl = fromIntegral (600 :: Int) -- 10 minutes - storeGroupInfoBundle :: Members '[ ConversationStore, diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 21cd6cc4727..1512634ccc0 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -15,8 +15,17 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.API.MLS.SubConversation where +module Galley.API.MLS.SubConversation + ( getSubConversation, + getLocalSubConversation, + deleteSubConversation, + getSubConversationGroupInfo, + getSubConversationGroupInfoFromLocalConv, + MLSGetSubConvStaticErrors, + ) +where +import Control.Arrow import Data.Id import Data.Qualified import Galley.API.MLS @@ -29,19 +38,24 @@ import qualified Galley.Data.Conversation as Data import Galley.Data.Conversation.Types import Galley.Effects import Galley.Effects.FederatorAccess -import Galley.Effects.SubConversationStore +import qualified Galley.Effects.MemberStore as Eff +import Galley.Effects.SubConversationStore (SubConversationStore) import qualified Galley.Effects.SubConversationStore as Eff +import Galley.Effects.SubConversationSupply (SubConversationSupply) +import qualified Galley.Effects.SubConversationSupply as Eff import Imports +import qualified Network.Wai.Utilities.Error as Wai import Polysemy import Polysemy.Error import Polysemy.Input +import Polysemy.Resource import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley (GetSubConversationsRequest (..), GetSubConversationsResponse (..)) -import Wire.API.Federation.Error (FederationError) +import Wire.API.Federation.Error import Wire.API.MLS.PublicGroupState import Wire.API.MLS.SubConversation @@ -103,8 +117,8 @@ getLocalSubConversation qusr lconv sconv = do let groupId = initialGroupId lconv sconv epoch = Epoch 0 suite = cnvmlsCipherSuite mlsMeta - createSubConversation (tUnqualified lconv) sconv suite epoch groupId Nothing - setGroupIdForSubConversation groupId (tUntagged lconv) sconv + Eff.createSubConversation (tUnqualified lconv) sconv suite epoch groupId Nothing + Eff.setGroupIdForSubConversation groupId (tUntagged lconv) sconv let sub = SubConversation { scParentConvId = tUnqualified lconv, @@ -191,5 +205,71 @@ getSubConversationGroupInfoFromLocalConv :: Sem r OpaquePublicGroupState getSubConversationGroupInfoFromLocalConv qusr subConvId lcnvId = do void $ getLocalConvForUser qusr lcnvId - getSubConversationPublicGroupState (tUnqualified lcnvId) subConvId + Eff.getSubConversationPublicGroupState (tUnqualified lcnvId) subConvId >>= noteS @'MLSMissingGroupInfo + +deleteSubConversation :: + Members + '[ ConversationStore, + ErrorS 'ConvAccessDenied, + ErrorS 'ConvNotFound, + ErrorS 'MLSNotEnabled, + ErrorS 'MLSStaleMessage, + Error Wai.Error, + Input Env, + MemberStore, + Resource, + SubConversationStore, + SubConversationSupply + ] + r => + Local UserId -> + Qualified ConvId -> + SubConvId -> + DeleteSubConversation -> + Sem r () +deleteSubConversation lusr qconv sconv dsc = do + assertMLSEnabled + foldQualified + lusr + (\lcnv -> deleteLocalSubConversation lusr lcnv sconv dsc) + (\_rcnv -> throw federationNotImplemented) + qconv + +deleteLocalSubConversation :: + Members + '[ ConversationStore, + ErrorS 'ConvAccessDenied, + ErrorS 'ConvNotFound, + ErrorS 'MLSStaleMessage, + MemberStore, + Resource, + SubConversationStore, + SubConversationSupply + ] + r => + Local UserId -> + Local ConvId -> + SubConvId -> + DeleteSubConversation -> + Sem r () +deleteLocalSubConversation lusr lcnvId scnvId dsc = do + let cnvId = tUnqualified lcnvId + cnv <- getConversationAndCheckMembership (tUntagged lusr) lcnvId + cs <- cnvmlsCipherSuite <$> noteS @'ConvNotFound (mlsMetadata cnv) + withCommitLock (dscGroupId dsc) (dscEpoch dsc) $ do + sconv <- + Eff.getSubConversation cnvId scnvId + >>= noteS @'ConvNotFound + let (gid, epoch) = (cnvmlsGroupId &&& cnvmlsEpoch) (scMLSData sconv) + unless (dscGroupId dsc == gid) $ throwS @'ConvNotFound + unless (dscEpoch dsc == epoch) $ throwS @'MLSStaleMessage + Eff.removeAllMLSClients gid + + newGid <- Eff.makeFreshGroupId + + Eff.deleteGroupIdForSubConversation gid + Eff.setGroupIdForSubConversation newGid (tUntagged lcnvId) scnvId + + -- the following overwrites any prior information about the subconversation + Eff.createSubConversation cnvId scnvId cs (Epoch 0) newGid Nothing diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs index 1095e1ef62a..dd968bdcccb 100644 --- a/services/galley/src/Galley/API/MLS/Util.hs +++ b/services/galley/src/Galley/API/MLS/Util.hs @@ -22,12 +22,14 @@ import Data.Id import Data.Qualified import Galley.Data.Conversation.Types hiding (Conversation) import qualified Galley.Data.Conversation.Types as Data +import Galley.Data.Types import Galley.Effects import Galley.Effects.ConversationStore import Galley.Effects.MemberStore import Galley.Effects.ProposalStore import Imports import Polysemy +import Polysemy.Resource (Resource, bracket) import Polysemy.TinyLog (TinyLog) import qualified Polysemy.TinyLog as TinyLog import qualified System.Logger as Log @@ -77,3 +79,29 @@ getPendingBackendRemoveProposals gid epoch = do TinyLog.warn $ Log.msg ("found pending proposal without origin, ignoring" :: ByteString) pure Nothing ) + +withCommitLock :: + forall r a. + ( Members + '[ Resource, + ConversationStore, + ErrorS 'MLSStaleMessage + ] + r + ) => + GroupId -> + Epoch -> + Sem r a -> + Sem r a +withCommitLock gid epoch action = + bracket + ( acquireCommitLock gid epoch ttl >>= \lockAcquired -> + when (lockAcquired == NotAcquired) $ + throwS @'MLSStaleMessage + ) + (const $ releaseCommitLock gid epoch) + $ \_ -> do + -- FUTUREWORK: fetch epoch again and check that is matches + action + where + ttl = fromIntegral (600 :: Int) -- 10 minutes diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index b165dcfcf22..73632068b4e 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -50,6 +50,7 @@ conversationAPI = <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError <@> mkNamedAPI @"get-subconversation" (callsFed getSubConversation) + <@> mkNamedAPI @"delete-subconversation" deleteSubConversation <@> mkNamedAPI @"get-subconversation-group-info" (callsFed getSubConversationGroupInfo) <@> mkNamedAPI @"create-one-to-one-conversation@v2" (callsFed createOne2OneConversation) <@> mkNamedAPI @"create-one-to-one-conversation" (callsFed createOne2OneConversation) diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 5f15bf3be8f..6c965dc33c4 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -76,6 +76,7 @@ import Galley.Cassandra.TeamFeatures import Galley.Cassandra.TeamNotifications import Galley.Effects import Galley.Effects.FireAndForget (interpretFireAndForget) +import Galley.Effects.SubConversationSupply.Random import Galley.Effects.WaiRoutes.IO import Galley.Env import Galley.External @@ -107,6 +108,7 @@ import Util.Options import Wire.API.Error import Wire.API.Federation.Error import qualified Wire.Sem.Logger +import Wire.Sem.Random.IO -- Effects needed by the interpretation of other effects type GalleyEffects0 = @@ -258,6 +260,8 @@ evalGalley e = . interpretMemberStoreToCassandra . interpretLegalHoldStoreToCassandra lh . interpretCustomBackendStoreToCassandra + . randomToIO + . interpretSubConversationSupplyToRandom . interpretSubConversationStoreToCassandra . interpretConversationStoreToCassandra . interpretProposalStoreToCassandra diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index 147d76c6e79..639cf9f01e7 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -356,6 +356,10 @@ removeMLSClients groupId (Qualified usr domain) cs = retry x5 . batch $ do for_ cs $ \c -> addPrepQuery Cql.removeMLSClient (groupId, domain, usr, c) +removeAllMLSClients :: GroupId -> Client () +removeAllMLSClients groupId = do + retry x5 $ write Cql.removeAllMLSClients (params LocalQuorum (Identity groupId)) + interpretMemberStoreToCassandra :: Members '[Embed IO, Input ClientState] r => Sem (MemberStore ': r) a -> @@ -380,4 +384,5 @@ interpretMemberStoreToCassandra = interpret $ \case removeLocalMembersFromRemoteConv rcnv uids AddMLSClients lcnv quid cs -> embedClient $ addMLSClients lcnv quid cs RemoveMLSClients lcnv quid cs -> embedClient $ removeMLSClients lcnv quid cs + RemoveAllMLSClients gid -> embedClient $ removeAllMLSClients gid LookupMLSClients lcnv -> embedClient $ lookupMLSClients lcnv diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 9585f9c038e..2ee25de8233 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -334,6 +334,9 @@ updateSubConvPublicGroupState = "INSERT INTO subconversation (conv_id, subconv_i selectSubConvPublicGroupState :: PrepQuery R (ConvId, SubConvId) (Identity (Maybe OpaquePublicGroupState)) selectSubConvPublicGroupState = "SELECT public_group_state FROM subconversation WHERE conv_id = ? AND subconv_id = ?" +deleteGroupIdForSubconv :: PrepQuery W (Identity GroupId) () +deleteGroupIdForSubconv = "DELETE FROM group_id_conv_id WHERE group_id = ?" + insertGroupIdForSubConversation :: PrepQuery W (GroupId, ConvId, Domain, SubConvId) () insertGroupIdForSubConversation = "INSERT INTO group_id_conv_id (group_id, conv_id, domain, subconv_id) VALUES (?, ?, ?, ?)" @@ -449,6 +452,9 @@ addMLSClient = "insert into mls_group_member_client (group_id, user_domain, user removeMLSClient :: PrepQuery W (GroupId, Domain, UserId, ClientId) () removeMLSClient = "delete from mls_group_member_client where group_id = ? and user_domain = ? and user = ? and client = ?" +removeAllMLSClients :: PrepQuery W (Identity GroupId) () +removeAllMLSClients = "DELETE FROM mls_group_member_client WHERE group_id = ?" + lookupMLSClients :: PrepQuery R (Identity GroupId) (Domain, UserId, ClientId, KeyPackageRef) lookupMLSClients = "select user_domain, user, client, key_package_ref from mls_group_member_client where group_id = ?" diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index 259255ffe1a..efa3ab41327 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -72,6 +72,10 @@ setEpochForSubConversation :: ConvId -> SubConvId -> Epoch -> Client () setEpochForSubConversation cid sconv epoch = retry x5 (write Cql.insertEpochForSubConversation (params LocalQuorum (epoch, cid, sconv))) +deleteGroupId :: GroupId -> Client () +deleteGroupId groupId = + retry x5 $ write Cql.deleteGroupIdForSubconv (params LocalQuorum (Identity groupId)) + interpretSubConversationStoreToCassandra :: Members '[Embed IO, Input ClientState] r => Sem (SubConversationStore ': r) a -> @@ -83,3 +87,4 @@ interpretSubConversationStoreToCassandra = interpret $ \case GetSubConversationPublicGroupState convId subConvId -> embedClient (selectSubConvPublicGroupState convId subConvId) SetGroupIdForSubConversation gId cid sconv -> embedClient $ setGroupIdForSubConversation gId cid sconv SetSubConversationEpoch cid sconv epoch -> embedClient $ setEpochForSubConversation cid sconv epoch + DeleteGroupIdForSubConversation groupId -> embedClient $ deleteGroupId groupId diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index dd8195e22d3..62cd8d5f647 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -82,6 +82,7 @@ import Galley.Effects.SearchVisibilityStore import Galley.Effects.ServiceStore import Galley.Effects.SparAccess import Galley.Effects.SubConversationStore +import Galley.Effects.SubConversationSupply import Galley.Effects.TeamFeatureStore import Galley.Effects.TeamMemberStore import Galley.Effects.TeamNotificationStore @@ -95,6 +96,7 @@ import Polysemy.Input import Polysemy.TinyLog import Wire.API.Error import Wire.Sem.Paging.Cassandra +import Wire.Sem.Random -- All the possible high-level effects. type GalleyEffects1 = @@ -110,6 +112,8 @@ type GalleyEffects1 = ProposalStore, ConversationStore, SubConversationStore, + SubConversationSupply, + Random, CustomBackendStore, LegalHoldStore, MemberStore, diff --git a/services/galley/src/Galley/Effects/MemberStore.hs b/services/galley/src/Galley/Effects/MemberStore.hs index ec865b8e273..bdc61c90160 100644 --- a/services/galley/src/Galley/Effects/MemberStore.hs +++ b/services/galley/src/Galley/Effects/MemberStore.hs @@ -40,6 +40,7 @@ module Galley.Effects.MemberStore setOtherMember, addMLSClients, removeMLSClients, + removeAllMLSClients, lookupMLSClients, -- * Delete members @@ -78,6 +79,7 @@ data MemberStore m a where DeleteMembersInRemoteConversation :: Remote ConvId -> [UserId] -> MemberStore m () AddMLSClients :: GroupId -> Qualified UserId -> Set (ClientId, KeyPackageRef) -> MemberStore m () RemoveMLSClients :: GroupId -> Qualified UserId -> Set ClientId -> MemberStore m () + RemoveAllMLSClients :: GroupId -> MemberStore m () LookupMLSClients :: GroupId -> MemberStore m ClientMap makeSem ''MemberStore diff --git a/services/galley/src/Galley/Effects/SubConversationStore.hs b/services/galley/src/Galley/Effects/SubConversationStore.hs index 00120d40c38..0316e5d1f42 100644 --- a/services/galley/src/Galley/Effects/SubConversationStore.hs +++ b/services/galley/src/Galley/Effects/SubConversationStore.hs @@ -37,5 +37,6 @@ data SubConversationStore m a where GetSubConversationPublicGroupState :: ConvId -> SubConvId -> SubConversationStore m (Maybe OpaquePublicGroupState) SetGroupIdForSubConversation :: GroupId -> Qualified ConvId -> SubConvId -> SubConversationStore m () SetSubConversationEpoch :: ConvId -> SubConvId -> Epoch -> SubConversationStore m () + DeleteGroupIdForSubConversation :: GroupId -> SubConversationStore m () makeSem ''SubConversationStore diff --git a/services/galley/src/Galley/Effects/SubConversationSupply.hs b/services/galley/src/Galley/Effects/SubConversationSupply.hs new file mode 100644 index 00000000000..4b96e0b469c --- /dev/null +++ b/services/galley/src/Galley/Effects/SubConversationSupply.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.Effects.SubConversationSupply where + +import Polysemy +import Wire.API.MLS.Group + +data SubConversationSupply m a where + -- | Generate a fresh group ID. This is used for subconversations, but the + -- generator does not depend on a subconversation. + MakeFreshGroupId :: SubConversationSupply m GroupId + +makeSem ''SubConversationSupply diff --git a/services/galley/src/Galley/Effects/SubConversationSupply/Random.hs b/services/galley/src/Galley/Effects/SubConversationSupply/Random.hs new file mode 100644 index 00000000000..31f34175ab8 --- /dev/null +++ b/services/galley/src/Galley/Effects/SubConversationSupply/Random.hs @@ -0,0 +1,40 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.Effects.SubConversationSupply.Random + ( interpretSubConversationSupplyToRandom, + ) +where + +import qualified Crypto.Hash as Crypto +import Data.ByteArray (convert) +import Galley.Effects.SubConversationSupply +import Imports +import Polysemy +import Wire.API.MLS.Group +import Wire.Sem.Random + +interpretSubConversationSupplyToRandom :: + Member Random r => + Sem (SubConversationSupply ': r) a -> + Sem r a +interpretSubConversationSupplyToRandom = interpret $ \case + MakeFreshGroupId -> freshGroupId + +freshGroupId :: Member Random r => Sem r GroupId +freshGroupId = + GroupId . convert . Crypto.hash @ByteString @Crypto.SHA256 <$> bytes 100 diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 7aaf921179c..21ed77c394b 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -63,6 +63,7 @@ import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential +import Wire.API.MLS.Epoch import Wire.API.MLS.Keys import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation @@ -204,7 +205,8 @@ tests s = [ test s "cannot create MLS conversations" postMLSConvDisabled, test s "cannot send an MLS message" postMLSMessageDisabled, test s "cannot send a commit bundle" postMLSBundleDisabled, - test s "cannot get group info" getGroupInfoDisabled + test s "cannot get group info" getGroupInfoDisabled, + test s "cannot delete a subconversation" deleteSubConversationDisabled ], testGroup "SubConversation" @@ -216,7 +218,9 @@ tests s = test s "join subconversation with a client that is not in the main conv" testJoinSubNonMemberClient, test s "add another client to a subconversation" testAddClientSubConv, test s "remove another client from a subconversation" testRemoveClientSubConv, - test s "send an application message in a subconversation" testSendMessageSubConv + test s "send an application message in a subconversation" testSendMessageSubConv, + test s "reset a subconversation" testDeleteSubConv, + test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale ], testGroup "Local Sender/Remote Subconversation" @@ -2307,6 +2311,18 @@ getGroupInfoDisabled = do getGroupInfo (qUnqualified alice) (fmap Conv qcnv) !!! assertMLSNotEnabled +deleteSubConversationDisabled :: TestM () +deleteSubConversationDisabled = do + alice <- randomUser + cnvId <- Qualified <$> randomId <*> pure (Domain "www.example.com") + let scnvId = SubConvId "conference" + dsc = + DeleteSubConversation + (GroupId "MLS") + (Epoch 0) + withMLSDisabled $ + deleteSubConv alice cnvId scnvId dsc !!! assertMLSNotEnabled + testCreateSubConv :: Bool -> TestM () testCreateSubConv parentIsMLSConv = do alice <- randomQualifiedUser @@ -2495,3 +2511,62 @@ testRemoteMemberGetSubConv isAMember = do expectSubConvError :: GalleyError -> GetSubConversationsResponse -> TestM () expectSubConvError _errExpected (GetSubConversationsResponseSuccess _) = liftIO $ assertFailure "Unexpected GetSubConversationsResponseSuccess" expectSubConvError errExpected (GetSubConversationsResponseError err) = liftIO $ err @?= errExpected + +testDeleteSubConv :: TestM () +testDeleteSubConv = do + alice <- randomQualifiedUser + let sconv = SubConvId "conference" + (qcnv, sub) <- runMLSTest $ do + alice1 <- createMLSClient alice + (_, qcnv) <- setupMLSGroup alice1 + sub <- + liftTest $ + responseJsonError + =<< getSubConv (qUnqualified alice) qcnv sconv + >= sendAndConsumeCommitBundle + pure (qcnv, sub) + + let epoch = addToEpoch @Word64 1 $ pscEpoch sub + dsc = + DeleteSubConversation + (pscGroupId sub) + epoch + deleteSubConv (qUnqualified alice) qcnv sconv dsc + !!! do const 200 === statusCode + + newSub <- + responseJsonError + =<< getSubConv (qUnqualified alice) qcnv sconv + >= sendAndConsumeCommitBundle + pure (qcnv, sub) + + -- the commit was made, yet the epoch for the request body is old + let dsc = DeleteSubConversation (pscGroupId sub) (pscEpoch sub) + deleteSubConv (qUnqualified alice) qcnv sconv dsc + !!! do const 409 === statusCode diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 6d60f1f04ee..cf343b7f5bd 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -1096,6 +1096,27 @@ getSubConv u qcnv sconv = do ] . zUser u +deleteSubConv :: + UserId -> + Qualified ConvId -> + SubConvId -> + DeleteSubConversation -> + TestM ResponseLBS +deleteSubConv u qcnv sconv dsc = do + g <- viewGalley + delete $ + g + . paths + [ "conversations", + toByteString' (qDomain qcnv), + toByteString' (qUnqualified qcnv), + "subconversations", + LBS.toStrict (toLazyByteString (toEncodedUrlPiece sconv)) + ] + . zUser u + . contentJson + . json dsc + convsub :: Qualified ConvId -> Maybe Text -> Qualified ConvOrSubConvId convsub qcnv Nothing = Conv <$> qcnv convsub qcnv (Just subname) = flip SubConv (SubConvId subname) <$> qcnv From 3b6deadd0bad00097702144c5bd2209ff106901a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 12 Jan 2023 11:13:50 +0100 Subject: [PATCH 008/225] [FS-901] Update the key package reference mapping of the creator of the subconversation (#2975) * Map subconversation's creator key package ref * Test: assert a key package update via update path * Add a changelog --- changelog.d/5-internal/subconv-update-path | 1 + services/galley/src/Galley/API/MLS/Message.hs | 2 ++ services/galley/test/integration/API/MLS.hs | 6 ++++++ 3 files changed, 9 insertions(+) create mode 100644 changelog.d/5-internal/subconv-update-path diff --git a/changelog.d/5-internal/subconv-update-path b/changelog.d/5-internal/subconv-update-path new file mode 100644 index 00000000000..11737cb0bec --- /dev/null +++ b/changelog.d/5-internal/subconv-update-path @@ -0,0 +1 @@ +Via the update path update the key package of the committer in epoch 0 of a subconversation diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index d65da4f49fa..ad23ce86db4 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -949,6 +949,8 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co unless (isClientMember (mkClientIdentity qusr creatorClient) (mcMembers parentConv)) $ throwS @'MLSSubConvClientNotInParent let creatorRef = fromMaybe senderRef updatePathRef + addKeyPackageRef creatorRef qusr creatorClient $ + tUntagged (convOfConvOrSub . idForConvOrSub <$> lConvOrSub) addMLSClients (cnvmlsGroupId mlsMeta) qusr diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 21ed77c394b..10fbc5e511e 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -2362,9 +2362,15 @@ testJoinSubConv = do resetGroup bob1 (pscGroupId sub) + bobRefsBefore <- getClientsFromGroupState bob1 bob -- bob adds his first client to the subconversation void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle + bobRefsAfter <- getClientsFromGroupState bob1 bob + liftIO $ + assertBool + "Bob's key package has not been updated via the update path" + (bobRefsBefore /= bobRefsAfter) -- now alice joins with her own client void $ From 46fcd12075a8e6b5919fba2362287137e3a62aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 13 Jan 2023 14:17:14 +0100 Subject: [PATCH 009/225] [FS-1334] Reset a Remote Subconversation (#2964) * Support resetting remote subconversations * Tests: delete a remote subconversation - These tests are parameterised by whether the user is a member of the subconversation * Tests: a remote deletes a local subconversation * Add changelogs * Test utility: return subconv from `createSubConv` * Test: local case, non-member resetting subconv --- .../2-features/delete-remote-subconversation | 1 + .../delete-remote-subconversation | 1 + .../src/Wire/API/Federation/API/Galley.hs | 17 ++ libs/wire-api/src/Wire/API/MLS/Epoch.hs | 2 + .../API/Routes/Public/Galley/Conversation.hs | 1 + services/galley/src/Galley/API/Federation.hs | 30 ++++ .../src/Galley/API/MLS/SubConversation.hs | 95 ++++++++--- .../src/Galley/API/Public/Conversation.hs | 2 +- services/galley/test/integration/API/MLS.hs | 157 +++++++++++++++--- .../galley/test/integration/API/MLS/Util.hs | 10 +- 10 files changed, 266 insertions(+), 50 deletions(-) create mode 100644 changelog.d/2-features/delete-remote-subconversation create mode 100644 changelog.d/6-federation/delete-remote-subconversation diff --git a/changelog.d/2-features/delete-remote-subconversation b/changelog.d/2-features/delete-remote-subconversation new file mode 100644 index 00000000000..01c7da857e0 --- /dev/null +++ b/changelog.d/2-features/delete-remote-subconversation @@ -0,0 +1 @@ +Support deleting a remote subconversation diff --git a/changelog.d/6-federation/delete-remote-subconversation b/changelog.d/6-federation/delete-remote-subconversation new file mode 100644 index 00000000000..816d9435eb1 --- /dev/null +++ b/changelog.d/6-federation/delete-remote-subconversation @@ -0,0 +1 @@ +Introduce an endpoint for resetting a remote subconversation 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 237f6a83d7a..dd472f0b875 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 @@ -128,6 +128,7 @@ type GalleyApi = EmptyResponse :<|> FedEndpoint "on-typing-indicator-updated" TypingDataUpdateRequest EmptyResponse :<|> FedEndpoint "get-sub-conversation" GetSubConversationsRequest GetSubConversationsResponse + :<|> FedEndpoint "delete-sub-conversation" DeleteSubConversationRequest DeleteSubConversationResponse data TypingDataUpdateRequest = TypingDataUpdateRequest { tdurTypingStatus :: TypingStatus, @@ -434,3 +435,19 @@ data GetSubConversationsResponse | GetSubConversationsResponseSuccess PublicSubConversation deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded GetSubConversationsResponse) + +data DeleteSubConversationRequest = DeleteSubConversationRequest + { dscreqUser :: UserId, + dscreqConv :: ConvId, + dscreqSubConv :: SubConvId, + dscreqGroupId :: GroupId, + dscreqEpoch :: Epoch + } + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded DeleteSubConversationRequest) + +data DeleteSubConversationResponse + = DeleteSubConversationResponseError GalleyError + | DeleteSubConversationResponseSuccess + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded DeleteSubConversationResponse) diff --git a/libs/wire-api/src/Wire/API/MLS/Epoch.hs b/libs/wire-api/src/Wire/API/MLS/Epoch.hs index fb247712535..0a0bf206a67 100644 --- a/libs/wire-api/src/Wire/API/MLS/Epoch.hs +++ b/libs/wire-api/src/Wire/API/MLS/Epoch.hs @@ -19,6 +19,7 @@ module Wire.API.MLS.Epoch where +import qualified Data.Aeson as A import Data.Binary import Data.Schema import Imports @@ -28,6 +29,7 @@ import Wire.Arbitrary newtype Epoch = Epoch {epochNumber :: Word64} deriving stock (Eq, Show) deriving newtype (Arbitrary, Enum, ToSchema) + deriving (A.FromJSON, A.ToJSON) via (Schema Epoch) instance ParseMLS Epoch where parseMLS = Epoch <$> parseMLS diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 952b8afb5f7..1b21e42c5ff 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -408,6 +408,7 @@ type ConversationAPI = :<|> Named "delete-subconversation" ( Summary "Delete an MLS subconversation" + :> MakesFederatedCall 'Galley "delete-sub-conversation" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'MLSNotEnabled diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 246d5a19f34..ef197e16ab2 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -59,6 +59,7 @@ import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E import Galley.Effects.ProposalStore (ProposalStore) import Galley.Effects.SubConversationStore +import Galley.Effects.SubConversationSupply import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.UserList (UserList (UserList)) @@ -122,6 +123,7 @@ federationSitemap = :<|> Named @"on-client-removed" (callsFed onClientRemoved) :<|> Named @"on-typing-indicator-updated" onTypingIndicatorUpdated :<|> Named @"get-sub-conversation" getSubConversationForRemoteUser + :<|> Named @"delete-sub-conversation" deleteSubConversationForRemoteUser onClientRemoved :: ( Members @@ -896,3 +898,31 @@ getSubConversationForRemoteUser domain GetSubConversationsRequest {..} = let qusr = Qualified gsreqUser domain lconv <- qualifyLocal gsreqConv getLocalSubConversation qusr lconv gsreqSubConv + +deleteSubConversationForRemoteUser :: + Members + '[ ConversationStore, + Input (Local ()), + Input Env, + MemberStore, + Resource, + SubConversationStore, + SubConversationSupply + ] + r => + Domain -> + DeleteSubConversationRequest -> + Sem r DeleteSubConversationResponse +deleteSubConversationForRemoteUser domain DeleteSubConversationRequest {..} = + fmap + ( either + F.DeleteSubConversationResponseError + (\() -> F.DeleteSubConversationResponseSuccess) + ) + . runError @GalleyError + . mapToGalleyError @MLSDeleteSubConvStaticErrors + $ do + let qusr = Qualified dscreqUser domain + dsc = DeleteSubConversation dscreqGroupId dscreqEpoch + lconv <- qualifyLocal dscreqConv + deleteLocalSubConversation qusr lconv dscreqSubConv dsc diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 1512634ccc0..13039388e7c 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -19,9 +19,11 @@ module Galley.API.MLS.SubConversation ( getSubConversation, getLocalSubConversation, deleteSubConversation, + deleteLocalSubConversation, getSubConversationGroupInfo, getSubConversationGroupInfoFromLocalConv, MLSGetSubConvStaticErrors, + MLSDeleteSubConvStaticErrors, ) where @@ -44,7 +46,6 @@ import qualified Galley.Effects.SubConversationStore as Eff import Galley.Effects.SubConversationSupply (SubConversationSupply) import qualified Galley.Effects.SubConversationSupply as Eff import Imports -import qualified Network.Wai.Utilities.Error as Wai import Polysemy import Polysemy.Error import Polysemy.Input @@ -54,7 +55,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API -import Wire.API.Federation.API.Galley (GetSubConversationsRequest (..), GetSubConversationsResponse (..)) +import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.MLS.PublicGroupState import Wire.API.MLS.SubConversation @@ -208,32 +209,41 @@ getSubConversationGroupInfoFromLocalConv qusr subConvId lcnvId = do Eff.getSubConversationPublicGroupState (tUnqualified lcnvId) subConvId >>= noteS @'MLSMissingGroupInfo +type MLSDeleteSubConvStaticErrors = + '[ ErrorS 'ConvAccessDenied, + ErrorS 'ConvNotFound, + ErrorS 'MLSNotEnabled, + ErrorS 'MLSStaleMessage + ] + deleteSubConversation :: - Members - '[ ConversationStore, - ErrorS 'ConvAccessDenied, - ErrorS 'ConvNotFound, - ErrorS 'MLSNotEnabled, - ErrorS 'MLSStaleMessage, - Error Wai.Error, - Input Env, - MemberStore, - Resource, - SubConversationStore, - SubConversationSupply - ] - r => + ( Members + '[ ConversationStore, + ErrorS 'ConvAccessDenied, + ErrorS 'ConvNotFound, + ErrorS 'MLSNotEnabled, + ErrorS 'MLSStaleMessage, + Error FederationError, + FederatorAccess, + Input Env, + MemberStore, + Resource, + SubConversationStore, + SubConversationSupply + ] + r, + CallsFed 'Galley "delete-sub-conversation" + ) => Local UserId -> Qualified ConvId -> SubConvId -> DeleteSubConversation -> Sem r () -deleteSubConversation lusr qconv sconv dsc = do - assertMLSEnabled +deleteSubConversation lusr qconv sconv dsc = foldQualified lusr - (\lcnv -> deleteLocalSubConversation lusr lcnv sconv dsc) - (\_rcnv -> throw federationNotImplemented) + (\lcnv -> deleteLocalSubConversation (tUntagged lusr) lcnv sconv dsc) + (\rcnv -> deleteRemoteSubConversation lusr rcnv sconv dsc) qconv deleteLocalSubConversation :: @@ -241,21 +251,24 @@ deleteLocalSubConversation :: '[ ConversationStore, ErrorS 'ConvAccessDenied, ErrorS 'ConvNotFound, + ErrorS 'MLSNotEnabled, ErrorS 'MLSStaleMessage, + Input Env, MemberStore, Resource, SubConversationStore, SubConversationSupply ] r => - Local UserId -> + Qualified UserId -> Local ConvId -> SubConvId -> DeleteSubConversation -> Sem r () -deleteLocalSubConversation lusr lcnvId scnvId dsc = do +deleteLocalSubConversation qusr lcnvId scnvId dsc = do + assertMLSEnabled let cnvId = tUnqualified lcnvId - cnv <- getConversationAndCheckMembership (tUntagged lusr) lcnvId + cnv <- getConversationAndCheckMembership qusr lcnvId cs <- cnvmlsCipherSuite <$> noteS @'ConvNotFound (mlsMetadata cnv) withCommitLock (dscGroupId dsc) (dscEpoch dsc) $ do sconv <- @@ -273,3 +286,39 @@ deleteLocalSubConversation lusr lcnvId scnvId dsc = do -- the following overwrites any prior information about the subconversation Eff.createSubConversation cnvId scnvId cs (Epoch 0) newGid Nothing + +deleteRemoteSubConversation :: + ( Members + '[ ErrorS 'ConvAccessDenied, + ErrorS 'ConvNotFound, + ErrorS 'MLSNotEnabled, + ErrorS 'MLSStaleMessage, + Error FederationError, + FederatorAccess, + Input Env + ] + r, + CallsFed 'Galley "delete-sub-conversation" + ) => + Local UserId -> + Remote ConvId -> + SubConvId -> + DeleteSubConversation -> + Sem r () +deleteRemoteSubConversation lusr rcnvId scnvId dsc = do + assertMLSEnabled + let deleteRequest = + DeleteSubConversationRequest + { dscreqUser = tUnqualified lusr, + dscreqConv = tUnqualified rcnvId, + dscreqSubConv = scnvId, + dscreqGroupId = dscGroupId dsc, + dscreqEpoch = dscEpoch dsc + } + response <- + runFederated + rcnvId + (fedClient @'Galley @"delete-sub-conversation" deleteRequest) + case response of + DeleteSubConversationResponseError e -> rethrowErrors @MLSDeleteSubConvStaticErrors e + DeleteSubConversationResponseSuccess -> pure () diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 73632068b4e..acfdf9568a2 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -50,7 +50,7 @@ conversationAPI = <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError <@> mkNamedAPI @"get-subconversation" (callsFed getSubConversation) - <@> mkNamedAPI @"delete-subconversation" deleteSubConversation + <@> mkNamedAPI @"delete-subconversation" (callsFed deleteSubConversation) <@> mkNamedAPI @"get-subconversation-group-info" (callsFed getSubConversationGroupInfo) <@> mkNamedAPI @"create-one-to-one-conversation@v2" (callsFed createOne2OneConversation) <@> mkNamedAPI @"create-one-to-one-conversation" (callsFed createOne2OneConversation) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 10fbc5e511e..b7fa15c2e5c 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -63,7 +63,6 @@ import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential -import Wire.API.MLS.Epoch import Wire.API.MLS.Keys import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation @@ -219,20 +218,25 @@ tests s = test s "add another client to a subconversation" testAddClientSubConv, test s "remove another client from a subconversation" testRemoveClientSubConv, test s "send an application message in a subconversation" testSendMessageSubConv, - test s "reset a subconversation" testDeleteSubConv, + test s "reset a subconversation as a member" (testDeleteSubConv True), + test s "reset a subconversation as a non-member" (testDeleteSubConv False), test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale ], testGroup "Local Sender/Remote Subconversation" [ test s "get subconversation of remote conversation - member" (testGetRemoteSubConv True), test s "get subconversation of remote conversation - not member" (testGetRemoteSubConv False), - test s "join remote subconversation" testJoinRemoteSubConv + test s "join remote subconversation" testJoinRemoteSubConv, + test s "reset a subconversation - member" (testDeleteRemoteSubConv True), + test s "reset a subconversation - not member" (testDeleteRemoteSubConv False) ], testGroup "Remote Sender/Local SubConversation" [ test s "get subconversation as a remote member" (testRemoteMemberGetSubConv True), test s "get subconversation as a remote non-member" (testRemoteMemberGetSubConv False), - test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv + test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv, + test s "delete subconversation as a remote member" (testRemoteMemberDeleteSubConv True), + test s "delete subconversation as a remote non-member" (testRemoteMemberDeleteSubConv False) ] ] ] @@ -2406,7 +2410,7 @@ testSendMessageSubConv = do void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit let subname = "conference" - createSubConv qcnv bob1 subname + void $ createSubConv qcnv bob1 subname let qcs = convsub qcnv (Just subname) void $ createExternalCommit alice1 Nothing qcs >>= sendAndConsumeCommitBundle @@ -2518,32 +2522,91 @@ testRemoteMemberGetSubConv isAMember = do expectSubConvError _errExpected (GetSubConversationsResponseSuccess _) = liftIO $ assertFailure "Unexpected GetSubConversationsResponseSuccess" expectSubConvError errExpected (GetSubConversationsResponseError err) = liftIO $ err @?= errExpected -testDeleteSubConv :: TestM () -testDeleteSubConv = do - alice <- randomQualifiedUser - let sconv = SubConvId "conference" - (qcnv, sub) <- runMLSTest $ do - alice1 <- createMLSClient alice - (_, qcnv) <- setupMLSGroup alice1 +testRemoteMemberDeleteSubConv :: Bool -> TestM () +testRemoteMemberDeleteSubConv isAMember = do + -- alice is local, bob is remote + -- alice creates a local conversation and invites bob + -- bob deletes a subconversation via federated enpdoint + + let bobDomain = Domain "faraway.example.com" + scnv = SubConvId "conference" + [alice, bob] <- createAndConnectUsers [Nothing, Just (domainText bobDomain)] + + (cnv, groupId, epoch) <- runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + (_cnvGroupId, qcnv) <- setupMLSGroup alice1 + mp <- createAddCommit alice1 [bob] + + let mockedResponse fedReq = + case frRPC fedReq of + "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) + "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) + "on-conversation-updated" -> pure (Aeson.encode ()) + "get-mls-clients" -> + pure + . Aeson.encode + . Set.singleton + $ ClientInfo (ciClient bob1) True + ms -> assertFailure ("unmocked endpoint called: " <> cs ms) + + void . withTempMockFederator' mockedResponse . sendAndConsumeCommit $ mp + sub <- liftTest $ responseJsonError - =<< getSubConv (qUnqualified alice) qcnv sconv - >= sendAndConsumeCommitBundle + pure (qUnqualified qcnv, pscGroupId sub, pscEpoch sub) + + randUser <- randomId + let delReq = + DeleteSubConversationRequest + { dscreqUser = if isAMember then qUnqualified bob else randUser, + dscreqConv = cnv, + dscreqSubConv = scnv, + dscreqGroupId = groupId, + dscreqEpoch = epoch + } + + fedGalleyClient <- view tsFedGalleyClient + -- Bob is a member of the parent conversation so he's allowed to delete the + -- subconversation. + res <- + runFedClient @"delete-sub-conversation" fedGalleyClient bobDomain delReq + + if isAMember then expectSuccess res else expectFailure ConvNotFound res + where + expectSuccess :: DeleteSubConversationResponse -> TestM () + expectSuccess DeleteSubConversationResponseSuccess = pure () + expectSuccess (DeleteSubConversationResponseError err) = + liftIO . assertFailure $ + "Unexpected DeleteSubConversationResponseError: " <> show err + + expectFailure :: GalleyError -> DeleteSubConversationResponse -> TestM () + expectFailure _errExpected DeleteSubConversationResponseSuccess = + liftIO . assertFailure $ + "Unexpected DeleteSubConversationResponseSuccess" + expectFailure errExpected (DeleteSubConversationResponseError err) = + liftIO $ err @?= errExpected + +testDeleteSubConv :: Bool -> TestM () +testDeleteSubConv isAMember = do + alice <- randomQualifiedUser + randUser <- randomId + let (deleter, expectedCode) = + if isAMember + then (qUnqualified alice, 200) + else (randUser, 403) + let sconv = SubConvId "conference" + (qcnv, sub) <- runMLSTest $ do + alice1 <- createMLSClient alice + (_, qcnv) <- setupMLSGroup alice1 + sub <- createSubConv qcnv alice1 (unSubConvId sconv) pure (qcnv, sub) - let epoch = addToEpoch @Word64 1 $ pscEpoch sub - dsc = - DeleteSubConversation - (pscGroupId sub) - epoch - deleteSubConv (qUnqualified alice) qcnv sconv dsc - !!! do const 200 === statusCode + let dsc = DeleteSubConversation (pscGroupId sub) (pscEpoch sub) + deleteSubConv deleter qcnv sconv dsc !!! const expectedCode === statusCode newSub <- responseJsonError @@ -2551,7 +2614,15 @@ testDeleteSubConv = do TestM () +testDeleteRemoteSubConv isAMember = do + alice <- randomQualifiedUser + let remoteDomain = Domain "faraway.example.com" + conv <- randomId + let qconv = Qualified conv remoteDomain + sconv = SubConvId "conference" + groupId = GroupId "deadbeef" + epoch = Epoch 0 + expectedReq = + DeleteSubConversationRequest + { dscreqUser = qUnqualified alice, + dscreqConv = conv, + dscreqSubConv = sconv, + dscreqGroupId = groupId, + dscreqEpoch = epoch + } + + let mock req = case frRPC req of + "delete-sub-conversation" -> + pure $ + if isAMember + then Aeson.encode DeleteSubConversationResponseSuccess + else Aeson.encode (DeleteSubConversationResponseError ConvNotFound) + rpc -> assertFailure $ "unmocked RPC called: " <> T.unpack rpc + dsc = DeleteSubConversation groupId epoch + + (_, reqs) <- + withTempMockFederator' mock $ + deleteSubConv (qUnqualified alice) qconv sconv dsc + ClientIdentity -> Text -> MLSTest () +createSubConv :: + Qualified ConvId -> + ClientIdentity -> + Text -> + MLSTest PublicSubConversation createSubConv qcnv creator name = do let subId = SubConvId name sub <- @@ -469,6 +473,10 @@ createSubConv qcnv creator name = do >= sendAndConsumeCommitBundle + liftTest $ + responseJsonError + =<< getSubConv (ciUser creator) qcnv subId + Date: Mon, 16 Jan 2023 21:18:42 +0100 Subject: [PATCH 010/225] [FS-901] Tests for joining subconversation (#2974) * Test: a non-member attempts to join * Test utilities: adjust types * Handle subconversations when processing commits * Test: attempt to join via an internal commit Co-authored-by: Paolo Capriotti --- .../5-internal/test-joining-subconversation | 1 + services/galley/src/Galley/API/MLS/Message.hs | 153 +++++++++++------- services/galley/test/integration/API/MLS.hs | 74 +++++++-- .../galley/test/integration/API/MLS/Util.hs | 9 +- 4 files changed, 160 insertions(+), 77 deletions(-) create mode 100644 changelog.d/5-internal/test-joining-subconversation diff --git a/changelog.d/5-internal/test-joining-subconversation b/changelog.d/5-internal/test-joining-subconversation new file mode 100644 index 00000000000..41a2a42111e --- /dev/null +++ b/changelog.d/5-internal/test-joining-subconversation @@ -0,0 +1 @@ +Add more tests for joining a subconversation diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index ad23ce86db4..9b80af005d8 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -29,7 +29,8 @@ where import Control.Arrow ((>>>)) import Control.Comonad import Control.Error.Util (hush) -import Control.Lens (preview) +import Control.Lens (forOf_, preview) +import Control.Lens.Extras (is) import Data.Id import Data.Json.Util import Data.List.NonEmpty (NonEmpty, nonEmpty) @@ -785,11 +786,10 @@ processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = wi throw . mlsProtocolError $ "The external commit attempts to add another client of the user, it must only add itself" - case convOrSub of - Conv _ -> pure () - SubConv mlsConv _ -> - unless (isClientMember cid (mcMembers mlsConv)) $ - throwS @'MLSSubConvClientNotInParent + -- only members can join a subconversation + forOf_ _SubConv convOrSub $ \(mlsConv, _) -> + unless (isClientMember cid (mcMembers mlsConv)) $ + throwS @'MLSSubConvClientNotInParent -- check if there is a key package ref in the remove proposal remRef <- @@ -982,7 +982,7 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co throwS @'MLSCommitMissingReferences -- process and execute proposals - updates <- executeProposalAction lConvOrSub qusr con convOrSub action + updates <- executeProposalAction qusr con lConvOrSub action -- update key package ref if necessary postponedKeyPackageRefUpdate @@ -1218,8 +1218,7 @@ checkExternalProposalUser qusr prop = do (const $ pure ()) -- FUTUREWORK: check external proposals from remote backends qusr -executeProposalAction :: - forall r x. +type HasProposalActionEffects r = ( Member BrigAccess r, Member ConversationStore r, Member (Error InternalError) r, @@ -1244,28 +1243,35 @@ executeProposalAction :: Member TinyLog r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-conversation" + ) + +executeProposalAction :: + forall r. + ( HasProposalActionEffects r, CallsFed 'Brig "get-mls-clients" ) => - Local x -> Qualified UserId -> Maybe ConnId -> - ConvOrSubConv -> + Local ConvOrSubConv -> ProposalAction -> Sem r [LocalConversationUpdate] -executeProposalAction _loc _qusr _con (SubConv _ _) _action = pure [] -executeProposalAction loc qusr con (Conv mlsConv) action = do - let lconv = qualifyAs loc . mcConv $ mlsConv - mlsMeta = mcMLSData mlsConv - cm = mcMembers mlsConv +executeProposalAction qusr con lconvOrSub action = do + let convOrSub = tUnqualified lconvOrSub + mlsMeta = mlsMetaConvOrSub convOrSub + cm = membersConvOrSub convOrSub ss = csSignatureScheme (cnvmlsCipherSuite mlsMeta) newUserClients = Map.assocs (paAdd action) + -- no client can be directly added to a subconversation + when (is _SubConv convOrSub && not (null newUserClients)) $ + throw (mlsProtocolError "Add proposals in subconversations are not supported") + -- Note [client removal] -- We support two types of removals: -- 1. when a user is removed from a group, all their clients have to be removed -- 2. when a client is deleted, that particular client (but not necessarily - -- other clients of the same user), has to be removed. + -- other clients of the same user) has to be removed. -- -- Type 2 requires no special processing on the backend, so here we filter -- out all removals of that type, so that further checks and processing can @@ -1273,7 +1279,7 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do removedUsers <- mapMaybe hush <$$> for (Map.assocs (paRemove action)) $ \(qtarget, Map.keysSet -> clients) -> runError @() $ do -- fetch clients from brig - clientInfo <- Set.map ciId <$> getClientInfo lconv qtarget ss + clientInfo <- Set.map ciId <$> getClientInfo lconvOrSub qtarget ss -- if the clients being removed don't exist, consider this as a removal of -- type 2, and skip it when (Set.null (clientInfo `Set.intersection` clients)) $ @@ -1281,7 +1287,7 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do pure (qtarget, clients) -- FUTUREWORK: remove this check after remote admins are implemented in federation https://wearezeta.atlassian.net/browse/FS-216 - foldQualified lconv (\_ -> pure ()) (\_ -> throwS @'MLSUnsupportedProposal) qusr + foldQualified lconvOrSub (\_ -> pure ()) (\_ -> throwS @'MLSUnsupportedProposal) qusr -- for each user, we compare their clients with the ones being added to the conversation for_ newUserClients $ \(qtarget, newclients) -> case Map.lookup qtarget cm of @@ -1292,7 +1298,7 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do -- final set of clients in the conversation let clients = Map.keysSet (newclients <> Map.findWithDefault mempty qtarget cm) -- get list of mls clients from brig - clientInfo <- getClientInfo lconv qtarget ss + clientInfo <- getClientInfo lconvOrSub qtarget ss let allClients = Set.map ciId clientInfo let allMLSClients = Set.map ciId (Set.filter ciMLS clientInfo) -- We check the following condition: @@ -1316,14 +1322,21 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do membersToRemove <- catMaybes <$> for removedUsers (uncurry (checkRemoval cm)) -- add users to the conversation and send events - addEvents <- foldMap (addMembers lconv) . nonEmpty . map fst $ newUserClients + addEvents <- + foldMap (addMembers qusr con lconvOrSub) + . nonEmpty + . map fst + $ newUserClients -- add clients in the conversation state for_ newUserClients $ \(qtarget, newClients) -> do addMLSClients (cnvmlsGroupId mlsMeta) qtarget (Set.fromList (Map.assocs newClients)) -- remove users from the conversation and send events - removeEvents <- foldMap (removeMembers lconv) (nonEmpty membersToRemove) + removeEvents <- + foldMap + (removeMembers qusr con lconvOrSub) + (nonEmpty membersToRemove) -- Remove clients from the conversation state. This includes client removals -- of all types (see Note [client removal]). @@ -1346,43 +1359,63 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do throwS @'MLSSelfRemovalNotAllowed pure (Just qtarget) - existingLocalMembers :: Local Data.Conversation -> Set (Qualified UserId) - existingLocalMembers lconv = - (Set.fromList . map (fmap lmId . tUntagged)) (traverse convLocalMembers lconv) - - existingRemoteMembers :: Local Data.Conversation -> Set (Qualified UserId) - existingRemoteMembers lconv = - Set.fromList . map (tUntagged . rmId) . convRemoteMembers . tUnqualified $ - lconv - - existingMembers :: Local Data.Conversation -> Set (Qualified UserId) - existingMembers lconv = existingLocalMembers lconv <> existingRemoteMembers lconv - - addMembers :: Local Data.Conversation -> NonEmpty (Qualified UserId) -> Sem r [LocalConversationUpdate] - addMembers lconv = - -- FUTUREWORK: update key package ref mapping to reflect conversation membership - foldMap - ( handleNoChanges - . handleMLSProposalFailures @ProposalErrors - . fmap pure - . updateLocalConversationUnchecked @'ConversationJoinTag lconv qusr con - . flip ConversationJoin roleNameWireMember - ) - . nonEmpty - . filter (flip Set.notMember (existingMembers lconv)) - . toList - - removeMembers :: Local Data.Conversation -> NonEmpty (Qualified UserId) -> Sem r [LocalConversationUpdate] - removeMembers lconv = - foldMap - ( handleNoChanges - . handleMLSProposalFailures @ProposalErrors - . fmap pure - . updateLocalConversationUnchecked @'ConversationRemoveMembersTag lconv qusr con - ) - . nonEmpty - . filter (flip Set.member (existingMembers lconv)) - . toList +existingLocalMembers :: Local Data.Conversation -> Set (Qualified UserId) +existingLocalMembers lconv = + (Set.fromList . map (fmap lmId . tUntagged)) (traverse convLocalMembers lconv) + +existingRemoteMembers :: Local Data.Conversation -> Set (Qualified UserId) +existingRemoteMembers lconv = + Set.fromList . map (tUntagged . rmId) . convRemoteMembers . tUnqualified $ + lconv + +existingMembers :: Local Data.Conversation -> Set (Qualified UserId) +existingMembers lconv = existingLocalMembers lconv <> existingRemoteMembers lconv + +addMembers :: + HasProposalActionEffects r => + Qualified UserId -> + Maybe ConnId -> + Local ConvOrSubConv -> + NonEmpty (Qualified UserId) -> + Sem r [LocalConversationUpdate] +addMembers qusr con lconvOrSub users = case tUnqualified lconvOrSub of + Conv mlsConv -> do + let lconv = qualifyAs lconvOrSub (mcConv mlsConv) + -- FUTUREWORK: update key package ref mapping to reflect conversation membership + foldMap + ( handleNoChanges + . handleMLSProposalFailures @ProposalErrors + . fmap pure + . updateLocalConversationUnchecked @'ConversationJoinTag lconv qusr con + . flip ConversationJoin roleNameWireMember + ) + . nonEmpty + . filter (flip Set.notMember (existingMembers lconv)) + . toList + $ users + SubConv _ _ -> pure [] + +removeMembers :: + HasProposalActionEffects r => + Qualified UserId -> + Maybe ConnId -> + Local ConvOrSubConv -> + NonEmpty (Qualified UserId) -> + Sem r [LocalConversationUpdate] +removeMembers qusr con lconvOrSub users = case tUnqualified lconvOrSub of + Conv mlsConv -> do + let lconv = qualifyAs lconvOrSub (mcConv mlsConv) + foldMap + ( handleNoChanges + . handleMLSProposalFailures @ProposalErrors + . fmap pure + . updateLocalConversationUnchecked @'ConversationRemoveMembersTag lconv qusr con + ) + . nonEmpty + . filter (flip Set.member (existingMembers lconv)) + . toList + $ users + SubConv _ _ -> pure [] handleNoChanges :: Monoid a => Sem (Error NoChanges ': r) a -> Sem r a handleNoChanges = fmap fold . runError diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index b7fa15c2e5c..e4c4acccd46 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -214,8 +214,8 @@ tests s = [ test s "get subconversation of MLS conv - 200" (testCreateSubConv True), test s "get subconversation of Proteus conv - 404" (testCreateSubConv False), test s "join subconversation with an external commit bundle" testJoinSubConv, - test s "join subconversation with a client that is not in the main conv" testJoinSubNonMemberClient, - test s "add another client to a subconversation" testAddClientSubConv, + test s "join subconversation with a client that is not in the parent conv" testJoinSubNonMemberClient, + test s "fail to add another client to a subconversation via internal commit" testAddClientSubConvFailure, test s "remove another client from a subconversation" testRemoveClientSubConv, test s "send an application message in a subconversation" testSendMessageSubConv, test s "reset a subconversation as a member" (testDeleteSubConv True), @@ -2361,7 +2361,7 @@ testJoinSubConv = do sub <- liftTest $ responseJsonError - =<< getSubConv (qUnqualified bob) qcnv (SubConvId "conference") + =<< getSubConv (qUnqualified bob) qcnv subId >= sendAndConsumeCommitBundle --- FUTUREWORK: implement the following tests - testJoinSubNonMemberClient :: TestM () -testJoinSubNonMemberClient = pure () +testJoinSubNonMemberClient = do + [alice, bob] <- createAndConnectUsers [Nothing, Nothing] + + runMLSTest $ do + [alice1, alice2, bob1] <- + traverse createMLSClient [alice, alice, bob] + traverse_ uploadNewKeyPackage [bob1, alice2] + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [alice] >>= sendAndConsumeCommit + + let subId = SubConvId "conference" + void $ createSubConv qcnv alice1 subId + + -- now Bob attempts to get the group info so he can join via external commit + -- with his own client, but he cannot because he is not a member of the + -- parent conversation + getGroupInfo (ciUser bob1) (fmap (flip SubConv subId) qcnv) + !!! const 404 === statusCode + +testAddClientSubConvFailure :: TestM () +testAddClientSubConvFailure = do + [alice, bob] <- createAndConnectUsers [Nothing, Nothing] + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ uploadNewKeyPackage bob1 + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + + let subId = SubConvId "conference" + void $ createSubConv qcnv alice1 subId + + void $ uploadNewKeyPackage bob1 -testAddClientSubConv :: TestM () -testAddClientSubConv = pure () + commit <- createAddCommit alice1 [bob] + (createBundle commit >>= postCommitBundle (mpSender commit)) + !!! do + const 400 === statusCode + const (Just "Add proposals in subconversations are not supported") + === fmap Wai.message . responseJsonError + + finalSub <- + liftTest $ + responseJsonError + =<< getSubConv (qUnqualified alice) qcnv subId + >= sendAndConsumeCommit - let subname = "conference" - void $ createSubConv qcnv bob1 subname - let qcs = convsub qcnv (Just subname) + let subId = SubConvId "conference" + void $ createSubConv qcnv bob1 subId + let qcs = convsub qcnv (Just subId) void $ createExternalCommit alice1 Nothing qcs >>= sendAndConsumeCommitBundle void $ createExternalCommit bob2 Nothing qcs >>= sendAndConsumeCommitBundle @@ -2602,7 +2652,7 @@ testDeleteSubConv isAMember = do (qcnv, sub) <- runMLSTest $ do alice1 <- createMLSClient alice (_, qcnv) <- setupMLSGroup alice1 - sub <- createSubConv qcnv alice1 (unSubConvId sconv) + sub <- createSubConv qcnv alice1 sconv pure (qcnv, sub) let dsc = DeleteSubConversation (pscGroupId sub) (pscEpoch sub) diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 1bb7490d99b..ea62eceb7d2 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -462,10 +462,9 @@ resetGroup cid gid = do createSubConv :: Qualified ConvId -> ClientIdentity -> - Text -> + SubConvId -> MLSTest PublicSubConversation -createSubConv qcnv creator name = do - let subId = SubConvId name +createSubConv qcnv creator subId = do sub <- liftTest $ responseJsonError @@ -1125,6 +1124,6 @@ deleteSubConv u qcnv sconv dsc = do . contentJson . json dsc -convsub :: Qualified ConvId -> Maybe Text -> Qualified ConvOrSubConvId +convsub :: Qualified ConvId -> Maybe SubConvId -> Qualified ConvOrSubConvId convsub qcnv Nothing = Conv <$> qcnv -convsub qcnv (Just subname) = flip SubConv (SubConvId subname) <$> qcnv +convsub qcnv (Just sconv) = flip SubConv sconv <$> qcnv From 512028d1eaac8eb061cc82998c436c844cc17640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 18 Jan 2023 15:00:39 +0100 Subject: [PATCH 011/225] [FS-1214] Add the last commit timestamp for a subconversation (#2995) * Reorder actions in the SubConversationStore effect * Add the timestamp to the `ConversationMLSData` type * Test utility: simplify createSubConv * Tests: make sure the epoch timestamp is present * Fix golden tests in wire-api * wire-api-federation: adjust golden test * Update a changelog - From the perspective of a release, this is not a change to the subconversation GET endpoint, hence no separate changelog for it. Instead, we append to an existing changelog Co-authored-by: Stefan Matting --- changelog.d/1-api-changes/get-subconversation | 2 +- .../Federation/Golden/ConversationCreated.hs | 3 +- .../testObject_ConversationCreated2.json | 1 + .../src/Wire/API/Conversation/Protocol.hs | 9 ++++ .../src/Wire/API/MLS/SubConversation.hs | 12 ++++- .../Golden/Manual/ConversationsResponse.hs | 14 +++++- .../Wire/API/Golden/Manual/SubConversation.hs | 9 ++++ .../testObject_ConversationsResponse_1.json | 1 + ...testObject_ConversationsResponse_v2_1.json | 1 + .../testObject_PublicSubConversation_1.json | 1 + .../testObject_PublicSubConversation_2.json | 1 + services/brig/src/Brig/User/Search/Index.hs | 1 - .../brig/test/integration/API/Internal.hs | 1 - services/galley/src/Galley/API/MLS/Message.hs | 10 ++-- .../src/Galley/API/MLS/SubConversation.hs | 1 + services/galley/src/Galley/API/MLS/Types.hs | 1 + .../src/Galley/Cassandra/Conversation.hs | 46 +++++++++++++++---- .../galley/src/Galley/Cassandra/Queries.hs | 7 +-- .../src/Galley/Cassandra/SubConversation.hs | 13 ++++-- .../src/Galley/Effects/ConversationStore.hs | 2 +- .../Galley/Effects/SubConversationStore.hs | 4 +- services/galley/test/integration/API/MLS.hs | 31 +++++++++++-- .../galley/test/integration/API/MLS/Util.hs | 19 ++++---- 23 files changed, 149 insertions(+), 41 deletions(-) diff --git a/changelog.d/1-api-changes/get-subconversation b/changelog.d/1-api-changes/get-subconversation index 175ddb4b909..6a632571f6b 100644 --- a/changelog.d/1-api-changes/get-subconversation +++ b/changelog.d/1-api-changes/get-subconversation @@ -1 +1 @@ -Introduce a subconversation GET endpoint +Introduce a subconversation GET endpoint (#2869, #2995) diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs index c912a689916..cf6c637c953 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs @@ -22,6 +22,7 @@ import Data.Id import Data.Misc import Data.Qualified import qualified Data.Set as Set +import Data.Time import qualified Data.UUID as UUID import Imports import Wire.API.Conversation @@ -84,5 +85,5 @@ testObject_ConversationCreated2 = ccNonCreatorMembers = Set.fromList [], ccMessageTimer = Nothing, ccReceiptMode = Nothing, - ccProtocol = ProtocolMLS (ConversationMLSData (GroupId "group") (Epoch 3) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) + ccProtocol = ProtocolMLS (ConversationMLSData (GroupId "group") (Epoch 3) (Just (UTCTime (fromGregorian 2020 8 29) 0)) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) } diff --git a/libs/wire-api-federation/test/golden/testObject_ConversationCreated2.json b/libs/wire-api-federation/test/golden/testObject_ConversationCreated2.json index cca0a6bb7c9..ae542d36e7d 100644 --- a/libs/wire-api-federation/test/golden/testObject_ConversationCreated2.json +++ b/libs/wire-api-federation/test/golden/testObject_ConversationCreated2.json @@ -13,6 +13,7 @@ "protocol": { "cipher_suite": 1, "epoch": 3, + "epoch_timestamp": "2020-08-29T00:00:00Z", "group_id": "Z3JvdXA=", "protocol": "mls" }, diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 30ca0b6591d..0fa9fc99c14 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -36,11 +36,13 @@ import Control.Arrow import Control.Lens (makePrisms, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Schema +import Data.Time.Clock import Imports import Wire.API.Conversation.Action.Tag import Wire.API.MLS.CipherSuite import Wire.API.MLS.Epoch import Wire.API.MLS.Group +import Wire.API.MLS.SubConversation import Wire.Arbitrary data ProtocolTag = ProtocolProteusTag | ProtocolMLSTag @@ -52,6 +54,8 @@ data ConversationMLSData = ConversationMLSData cnvmlsGroupId :: GroupId, -- | The current epoch number of the corresponding MLS group. cnvmlsEpoch :: Epoch, + -- | The time stamp of the epoch. + cnvmlsEpochTimestamp :: Maybe UTCTime, -- | The cipher suite to be used in the MLS group. cnvmlsCipherSuite :: CipherSuiteTag } @@ -126,6 +130,11 @@ mlsDataSchema = "epoch" (description ?~ "The epoch number of the corresponding MLS group") schema + <*> cnvmlsEpochTimestamp + .= fieldWithDocModifier + "epoch_timestamp" + (description ?~ "The timestamp of the epoch number") + schemaEpochTimestamp <*> cnvmlsCipherSuite .= fieldWithDocModifier "cipher_suite" diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index 7b4288707da..00d616e33dc 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -29,10 +29,12 @@ import qualified Data.Aeson as A import Data.ByteArray import Data.ByteString.Conversion import Data.Id +import Data.Json.Util import Data.Qualified import Data.Schema import qualified Data.Swagger as S import qualified Data.Text as T +import Data.Time.Clock import Imports import Servant (FromHttpApiData (..), ToHttpApiData (toQueryParam)) import Test.QuickCheck @@ -77,6 +79,9 @@ data PublicSubConversation = PublicSubConversation pscSubConvId :: SubConvId, pscGroupId :: GroupId, pscEpoch :: Epoch, + -- | It is 'Nothing' when the epoch is 0, and otherwise a timestamp when the + -- epoch was bumped, i.e., it is a timestamp of the most recent commit. + pscEpochTimestamp :: Maybe UTCTime, pscCipherSuite :: CipherSuiteTag, pscMembers :: [ClientIdentity] } @@ -87,15 +92,20 @@ instance ToSchema PublicSubConversation where schema = objectWithDocModifier "PublicSubConversation" - (description ?~ "A MLS subconversation") + (description ?~ "An MLS subconversation") $ PublicSubConversation <$> pscParentConvId .= field "parent_qualified_id" schema <*> pscSubConvId .= field "subconv_id" schema <*> pscGroupId .= field "group_id" schema <*> pscEpoch .= field "epoch" schema + <*> pscEpochTimestamp .= field "epoch_timestamp" schemaEpochTimestamp <*> pscCipherSuite .= field "cipher_suite" schema <*> pscMembers .= field "members" (array schema) +schemaEpochTimestamp :: ValueSchema NamedSwaggerDoc (Maybe UTCTime) +schemaEpochTimestamp = + named "Epoch Timestamp" . nullable . unnamed $ utcTimeSchema + data ConvOrSubTag = ConvTag | SubConvTag deriving (Eq, Enum, Bounded) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs index c5d39f191a7..0e32a61ca41 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs @@ -25,6 +25,8 @@ import Data.Id (Id (Id)) import Data.Misc import Data.Qualified import qualified Data.Set as Set +import Data.Time.Calendar +import Data.Time.Clock import qualified Data.UUID as UUID import Imports import Wire.API.Conversation @@ -128,5 +130,15 @@ conv2 = }, cmOthers = [] }, - cnvProtocol = ProtocolMLS (ConversationMLSData (GroupId "test_group") (Epoch 42) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) + cnvProtocol = + ProtocolMLS + ( ConversationMLSData + (GroupId "test_group") + (Epoch 42) + (Just timestamp) + MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) } + where + timestamp :: UTCTime + timestamp = UTCTime (fromGregorian 2023 1 17) (secondsToDiffTime 42) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs index 640dde1c778..b2af34d1835 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs @@ -24,6 +24,8 @@ where import Data.Domain import Data.Id import Data.Qualified +import Data.Time.Calendar +import Data.Time.Clock import qualified Data.UUID as UUID import Imports import Wire.API.MLS.CipherSuite @@ -55,8 +57,14 @@ testObject_PublicSubConversation_1 = subConvId1 (GroupId "test_group") (Epoch 5) + (Just (UTCTime day fromMidnight)) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 [] + where + fromMidnight :: DiffTime + fromMidnight = 42 + day :: Day + day = fromGregorian 2023 1 17 testObject_PublicSubConversation_2 :: PublicSubConversation testObject_PublicSubConversation_2 = @@ -65,6 +73,7 @@ testObject_PublicSubConversation_2 = subConvId2 (GroupId "test_group_2") (Epoch 0) + Nothing MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 [mkClientIdentity user cid] where diff --git a/libs/wire-api/test/golden/testObject_ConversationsResponse_1.json b/libs/wire-api/test/golden/testObject_ConversationsResponse_1.json index 3bba9904761..1156816764e 100644 --- a/libs/wire-api/test/golden/testObject_ConversationsResponse_1.json +++ b/libs/wire-api/test/golden/testObject_ConversationsResponse_1.json @@ -72,6 +72,7 @@ "cipher_suite": 1, "creator": "00000000-0000-0000-0000-000200000001", "epoch": 42, + "epoch_timestamp": "2023-01-17T00:00:42Z", "group_id": "dGVzdF9ncm91cA==", "id": "00000000-0000-0000-0000-000000000002", "last_event": "0.0", diff --git a/libs/wire-api/test/golden/testObject_ConversationsResponse_v2_1.json b/libs/wire-api/test/golden/testObject_ConversationsResponse_v2_1.json index 0bba3b7e155..cf0cc2893b9 100644 --- a/libs/wire-api/test/golden/testObject_ConversationsResponse_v2_1.json +++ b/libs/wire-api/test/golden/testObject_ConversationsResponse_v2_1.json @@ -74,6 +74,7 @@ "cipher_suite": 1, "creator": "00000000-0000-0000-0000-000200000001", "epoch": 42, + "epoch_timestamp": "2023-01-17T00:00:42Z", "group_id": "dGVzdF9ncm91cA==", "id": "00000000-0000-0000-0000-000000000002", "last_event": "0.0", diff --git a/libs/wire-api/test/golden/testObject_PublicSubConversation_1.json b/libs/wire-api/test/golden/testObject_PublicSubConversation_1.json index d81e3853f4e..05ce835507a 100644 --- a/libs/wire-api/test/golden/testObject_PublicSubConversation_1.json +++ b/libs/wire-api/test/golden/testObject_PublicSubConversation_1.json @@ -1,6 +1,7 @@ { "cipher_suite": 1, "epoch": 5, + "epoch_timestamp": "2023-01-17T00:00:42Z", "group_id": "dGVzdF9ncm91cA==", "members": [], "parent_qualified_id": { diff --git a/libs/wire-api/test/golden/testObject_PublicSubConversation_2.json b/libs/wire-api/test/golden/testObject_PublicSubConversation_2.json index ac57e7e8e1b..a918c3161ba 100644 --- a/libs/wire-api/test/golden/testObject_PublicSubConversation_2.json +++ b/libs/wire-api/test/golden/testObject_PublicSubConversation_2.json @@ -1,6 +1,7 @@ { "cipher_suite": 1, "epoch": 0, + "epoch_timestamp": null, "group_id": "dGVzdF9ncm91cF8y", "members": [ { diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index bd460bee31d..28e3b41e5e9 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -1,5 +1,4 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE StrictData #-} -- This file is part of the Wire Server implementation. diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index cc2e3900f1e..f3fbeae676d 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -14,7 +14,6 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# LANGUAGE NumericUnderscores #-} module API.Internal ( tests, diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 9b80af005d8..f8c7ab52ada 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -1553,6 +1553,8 @@ fetchConvOrSub qusr convOrSubId = for convOrSubId $ \case incrementEpoch :: Members '[ ConversationStore, + ErrorS 'ConvNotFound, + MemberStore, SubConversationStore ] r => @@ -1561,9 +1563,11 @@ incrementEpoch :: incrementEpoch (Conv c) = do let epoch' = succ (cnvmlsEpoch (mcMLSData c)) setConversationEpoch (mcId c) epoch' - pure $ Conv c {mcMLSData = (mcMLSData c) {cnvmlsEpoch = epoch'}} + conv <- getConversation (mcId c) >>= noteS @'ConvNotFound + fmap Conv (mkMLSConversation conv >>= noteS @'ConvNotFound) incrementEpoch (SubConv c s) = do let epoch' = succ (cnvmlsEpoch (scMLSData s)) setSubConversationEpoch (scParentConvId s) (scSubConvId s) epoch' - let s' = s {scMLSData = (scMLSData s) {cnvmlsEpoch = epoch'}} - pure (SubConv c s') + subconv <- + getSubConversation (mcId c) (scSubConvId s) >>= noteS @'ConvNotFound + pure (SubConv c subconv) diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 13039388e7c..9f9617f35f3 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -128,6 +128,7 @@ getLocalSubConversation qusr lconv sconv = do ConversationMLSData { cnvmlsGroupId = groupId, cnvmlsEpoch = epoch, + cnvmlsEpochTimestamp = Nothing, cnvmlsCipherSuite = suite }, scMembers = mkClientMap [] diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index ec2643884e3..d1feefdd3ba 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -85,6 +85,7 @@ toPublicSubConv (Qualified (SubConversation {..}) domain) = pscSubConvId = scSubConvId, pscGroupId = cnvmlsGroupId scMLSData, pscEpoch = cnvmlsEpoch scMLSData, + pscEpochTimestamp = cnvmlsEpochTimestamp scMLSData, pscCipherSuite = cnvmlsCipherSuite scMLSData, pscMembers = members } diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index a6ce01494d6..7b66019eb54 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -24,6 +24,7 @@ where import Cassandra hiding (Set) import qualified Cassandra as Cql +import Cassandra.Util import Control.Error.Util import Control.Monad.Trans.Maybe import Data.ByteString.Conversion @@ -34,6 +35,7 @@ import Data.Misc import Data.Qualified import Data.Range import qualified Data.Set as Set +import Data.Time.Clock import Data.UUID.V4 (nextRandom) import Galley.Cassandra.Access import Galley.Cassandra.Conversation.MLS @@ -84,6 +86,7 @@ createMLSSelfConversation lusr = do ConversationMLSData { cnvmlsGroupId = gid, cnvmlsEpoch = Epoch 0, + cnvmlsEpochTimestamp = Nothing, cnvmlsCipherSuite = cs } retry x5 . batch $ do @@ -129,6 +132,7 @@ createConversation lcnv nc = do ConversationMLSData { cnvmlsGroupId = gid, cnvmlsEpoch = ep, + cnvmlsEpochTimestamp = Nothing, cnvmlsCipherSuite = cs }, Just gid, @@ -189,7 +193,7 @@ conversationMeta conv = (toConvMeta =<<) <$> retry x1 (query1 Cql.selectConv (params LocalQuorum (Identity conv))) where - toConvMeta (t, mc, a, r, r', n, i, _, mt, rm, _, _, _, _) = do + toConvMeta (t, mc, a, r, r', n, i, _, mt, rm, _, _, _, _, _) = do c <- mc let mbAccessRolesV2 = Set.fromList . Cql.fromSet <$> r' accessRoles = maybeRole t $ parseAccessRoles r mbAccessRolesV2 @@ -272,7 +276,10 @@ localConversation cid = toConv cid <$> UnliftIO.Concurrently (members cid) <*> UnliftIO.Concurrently (lookupRemoteMembers cid) - <*> UnliftIO.Concurrently (retry x1 $ query1 Cql.selectConv (params LocalQuorum (Identity cid))) + <*> UnliftIO.Concurrently + ( retry x1 $ + query1 Cql.selectConv (params LocalQuorum (Identity cid)) + ) localConversations :: Members '[Embed IO, Input ClientState, TinyLog] r => @@ -322,31 +329,54 @@ toProtocol :: Maybe ProtocolTag -> Maybe GroupId -> Maybe Epoch -> + Maybe UTCTime -> Maybe CipherSuiteTag -> Maybe Protocol -toProtocol Nothing _ _ _ = Just ProtocolProteus -toProtocol (Just ProtocolProteusTag) _ _ _ = Just ProtocolProteus -toProtocol (Just ProtocolMLSTag) mgid mepoch mcs = +toProtocol Nothing _ _ _ _ = Just ProtocolProteus +toProtocol (Just ProtocolProteusTag) _ _ _ _ = Just ProtocolProteus +toProtocol (Just ProtocolMLSTag) mgid mepoch mtimestamp mcs = ProtocolMLS <$> ( ConversationMLSData <$> mgid -- If there is no epoch in the database, assume the epoch is 0 <*> (mepoch <|> Just (Epoch 0)) + <*> pure (mepoch `toTimestamp` mtimestamp) <*> mcs ) + where + toTimestamp :: Maybe Epoch -> Maybe UTCTime -> Maybe UTCTime + toTimestamp Nothing _ = Nothing + toTimestamp (Just (Epoch 0)) _ = Nothing + toTimestamp (Just _) ts = ts toConv :: ConvId -> [LocalMember] -> [RemoteMember] -> - Maybe (ConvType, Maybe UserId, Maybe (Cql.Set Access), Maybe AccessRoleLegacy, Maybe (Cql.Set AccessRole), Maybe Text, Maybe TeamId, Maybe Bool, Maybe Milliseconds, Maybe ReceiptMode, Maybe ProtocolTag, Maybe GroupId, Maybe Epoch, Maybe CipherSuiteTag) -> + Maybe + ( ConvType, + Maybe UserId, + Maybe (Cql.Set Access), + Maybe AccessRoleLegacy, + Maybe (Cql.Set AccessRole), + Maybe Text, + Maybe TeamId, + Maybe Bool, + Maybe Milliseconds, + Maybe ReceiptMode, + Maybe ProtocolTag, + Maybe GroupId, + Maybe Epoch, + Maybe (Writetime Epoch), + Maybe CipherSuiteTag + ) -> Maybe Conversation toConv cid ms remoteMems mconv = do - (cty, muid, acc, role, roleV2, nme, ti, del, timer, rm, ptag, mgid, mep, mcs) <- mconv + (cty, muid, acc, role, roleV2, nme, ti, del, timer, rm, ptag, mgid, mep, mts, mcs) <- mconv uid <- muid let mbAccessRolesV2 = Set.fromList . Cql.fromSet <$> roleV2 accessRoles = maybeRole cty $ parseAccessRoles role mbAccessRolesV2 - proto <- toProtocol ptag mgid mep mcs + proto <- toProtocol ptag mgid mep (writetimeToUTC <$> mts) mcs pure Conversation { convId = cid, diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 2ee25de8233..78771cefd57 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -216,9 +216,10 @@ selectConv :: Maybe ProtocolTag, Maybe GroupId, Maybe Epoch, + Maybe (Writetime Epoch), Maybe CipherSuiteTag ) -selectConv = "select type, creator, access, access_role, access_roles_v2, name, team, deleted, message_timer, receipt_mode, protocol, group_id, epoch, cipher_suite from conversation where conv = ?" +selectConv = "select type, creator, access, access_role, access_roles_v2, name, team, deleted, message_timer, receipt_mode, protocol, group_id, epoch, WRITETIME(epoch), cipher_suite from conversation where conv = ?" selectReceiptMode :: PrepQuery R (Identity ConvId) (Identity (Maybe ReceiptMode)) selectReceiptMode = "select receipt_mode from conversation where conv = ?" @@ -322,8 +323,8 @@ lookupGroupId = "SELECT conv_id, domain, subconv_id from group_id_conv_id where -- MLS SubConversations ----------------------------------------------------- -selectSubConversation :: PrepQuery R (ConvId, SubConvId) (CipherSuiteTag, Epoch, GroupId) -selectSubConversation = "SELECT cipher_suite, epoch, group_id FROM subconversation WHERE conv_id = ? and subconv_id = ?" +selectSubConversation :: PrepQuery R (ConvId, SubConvId) (CipherSuiteTag, Epoch, Writetime Epoch, GroupId) +selectSubConversation = "SELECT cipher_suite, epoch, WRITETIME(epoch), group_id FROM subconversation WHERE conv_id = ? and subconv_id = ?" insertSubConversation :: PrepQuery W (ConvId, SubConvId, CipherSuiteTag, Epoch, GroupId, Maybe OpaquePublicGroupState) () insertSubConversation = "INSERT INTO subconversation (conv_id, subconv_id, cipher_suite, epoch, group_id, public_group_state) VALUES (?, ?, ?, ?, ?, ?)" diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index efa3ab41327..6dd6dca3ed5 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -18,8 +18,10 @@ module Galley.Cassandra.SubConversation where import Cassandra +import Cassandra.Util import Data.Id import Data.Qualified +import Data.Time.Clock import Galley.API.MLS.Types (SubConversation (..)) import Galley.Cassandra.Conversation.MLS (lookupMLSClients) import qualified Galley.Cassandra.Queries as Cql @@ -37,7 +39,7 @@ import Wire.API.MLS.SubConversation selectSubConversation :: ConvId -> SubConvId -> Client (Maybe SubConversation) selectSubConversation convId subConvId = do m <- retry x5 (query1 Cql.selectSubConversation (params LocalQuorum (convId, subConvId))) - for m $ \(suite, epoch, groupId) -> do + for m $ \(suite, epoch, epochWritetime, groupId) -> do cm <- lookupMLSClients groupId pure $ SubConversation @@ -47,10 +49,15 @@ selectSubConversation convId subConvId = do ConversationMLSData { cnvmlsGroupId = groupId, cnvmlsEpoch = epoch, + cnvmlsEpochTimestamp = epochTimestamp epoch epochWritetime, cnvmlsCipherSuite = suite }, scMembers = cm } + where + epochTimestamp :: Epoch -> Writetime Epoch -> Maybe UTCTime + epochTimestamp (Epoch 0) _ = Nothing + epochTimestamp _ (Writetime t) = Just t insertSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> Epoch -> GroupId -> Maybe OpaquePublicGroupState -> Client () insertSubConversation convId subConvId suite epoch groupId mPgs = @@ -81,10 +88,10 @@ interpretSubConversationStoreToCassandra :: Sem (SubConversationStore ': r) a -> Sem r a interpretSubConversationStoreToCassandra = interpret $ \case - GetSubConversation convId subConvId -> embedClient (selectSubConversation convId subConvId) CreateSubConversation convId subConvId suite epoch groupId mPgs -> embedClient (insertSubConversation convId subConvId suite epoch groupId mPgs) - SetSubConversationPublicGroupState convId subConvId mPgs -> embedClient (updateSubConvPublicGroupState convId subConvId mPgs) + GetSubConversation convId subConvId -> embedClient (selectSubConversation convId subConvId) GetSubConversationPublicGroupState convId subConvId -> embedClient (selectSubConvPublicGroupState convId subConvId) + SetSubConversationPublicGroupState convId subConvId mPgs -> embedClient (updateSubConvPublicGroupState convId subConvId mPgs) SetGroupIdForSubConversation gId cid sconv -> embedClient $ setGroupIdForSubConversation gId cid sconv SetSubConversationEpoch cid sconv epoch -> embedClient $ setEpochForSubConversation cid sconv epoch DeleteGroupIdForSubConversation groupId -> embedClient $ deleteGroupId groupId diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index f8e5336e3a6..c89d58dce95 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -60,7 +60,7 @@ import Data.Id import Data.Misc import Data.Qualified import Data.Range -import Data.Time (NominalDiffTime) +import Data.Time.Clock import Galley.Data.Conversation import Galley.Data.Types import Galley.Types.Conversations.Members diff --git a/services/galley/src/Galley/Effects/SubConversationStore.hs b/services/galley/src/Galley/Effects/SubConversationStore.hs index 0316e5d1f42..cb6f41b05e4 100644 --- a/services/galley/src/Galley/Effects/SubConversationStore.hs +++ b/services/galley/src/Galley/Effects/SubConversationStore.hs @@ -31,10 +31,10 @@ import Wire.API.MLS.PublicGroupState import Wire.API.MLS.SubConversation data SubConversationStore m a where - GetSubConversation :: ConvId -> SubConvId -> SubConversationStore m (Maybe SubConversation) CreateSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> Epoch -> GroupId -> Maybe OpaquePublicGroupState -> SubConversationStore m () - SetSubConversationPublicGroupState :: ConvId -> SubConvId -> Maybe OpaquePublicGroupState -> SubConversationStore m () + GetSubConversation :: ConvId -> SubConvId -> SubConversationStore m (Maybe SubConversation) GetSubConversationPublicGroupState :: ConvId -> SubConvId -> SubConversationStore m (Maybe OpaquePublicGroupState) + SetSubConversationPublicGroupState :: ConvId -> SubConvId -> Maybe OpaquePublicGroupState -> SubConversationStore m () SetGroupIdForSubConversation :: GroupId -> Qualified ConvId -> SubConvId -> SubConversationStore m () SetSubConversationEpoch :: ConvId -> SubConvId -> Epoch -> SubConversationStore m () DeleteGroupIdForSubConversation :: GroupId -> SubConversationStore m () diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index e4c4acccd46..978a6afc988 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -2341,10 +2341,22 @@ testCreateSubConv parentIsMLSConv = do cnvQualifiedId <$> liftTest (postConvQualified (qUnqualified alice) defNewProteusConv >>= responseJsonError) let sconv = SubConvId "conference" - liftTest $ - getSubConv (qUnqualified alice) qcnv sconv - !!! do - const (if parentIsMLSConv then 200 else 404) === statusCode + if parentIsMLSConv + then do + sub <- + liftTest $ + responseJsonError + =<< getSubConv (qUnqualified alice) qcnv sconv + >= sendAndConsumeCommitBundle + subAfter <- + liftTest $ + responseJsonError + =<< getSubConv (qUnqualified bob) qcnv subId + MLSTest PublicSubConversation createSubConv qcnv creator subId = do - sub <- - liftTest $ - responseJsonError - =<< getSubConv (ciUser creator) qcnv subId - >= sendAndConsumeCommitBundle - liftTest $ - responseJsonError - =<< getSubConv (ciUser creator) qcnv subId - Date: Thu, 26 Jan 2023 10:43:21 +0100 Subject: [PATCH 012/225] Fix build --- services/galley/test/integration/API/MLS.hs | 59 ++++++--------------- 1 file changed, 16 insertions(+), 43 deletions(-) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 99e5978d3d3..2bafbd250aa 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -2382,15 +2382,12 @@ testGetRemoteSubConv isAMember = do pscCipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519, pscMembers = [] } - - let mock req = case frRPC req of - "get-sub-conversation" -> - pure $ - if isAMember - then Aeson.encode (GetSubConversationsResponseSuccess fakeSubConv) - else Aeson.encode (GetSubConversationsResponseError ConvNotFound) - rpc -> assertFailure $ "unmocked RPC called: " <> T.unpack rpc - + let mock = do + guardRPC "get-sub-conversation" + mockReply $ + if isAMember + then GetSubConversationsResponseSuccess fakeSubConv + else GetSubConversationsResponseError ConvNotFound (_, reqs) <- withTempMockFederator' mock $ getSubConv (qUnqualified alice) qconv sconv @@ -2417,20 +2414,8 @@ testRemoteMemberGetSubConv isAMember = do kpb <- claimKeyPackages alice1 bob mp <- createAddCommit alice1 [bob] - let mockedResponse fedReq = - case frRPC fedReq of - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "on-conversation-updated" -> pure (Aeson.encode ()) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.singleton - $ ClientInfo (ciClient bob1) True - "claim-key-packages" -> pure . Aeson.encode $ kpb - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - - void . withTempMockFederator' mockedResponse $ + let mock = receiveCommitMock [bob1] <|> welcomeMock <|> claimKeyPackagesMock kpb + void . withTempMockFederator' mock $ sendAndConsumeCommit mp let subconv = SubConvId "conference" @@ -2479,19 +2464,8 @@ testRemoteMemberDeleteSubConv isAMember = do (_cnvGroupId, qcnv) <- setupMLSGroup alice1 mp <- createAddCommit alice1 [bob] - let mockedResponse fedReq = - case frRPC fedReq of - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "on-conversation-updated" -> pure (Aeson.encode ()) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.singleton - $ ClientInfo (ciClient bob1) True - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - - void . withTempMockFederator' mockedResponse . sendAndConsumeCommit $ mp + let mock = receiveCommitMock [bob1] <|> welcomeMock + void . withTempMockFederator' mock . sendAndConsumeCommit $ mp sub <- liftTest $ @@ -2608,13 +2582,12 @@ testDeleteRemoteSubConv isAMember = do dscreqEpoch = epoch } - let mock req = case frRPC req of - "delete-sub-conversation" -> - pure $ - if isAMember - then Aeson.encode DeleteSubConversationResponseSuccess - else Aeson.encode (DeleteSubConversationResponseError ConvNotFound) - rpc -> assertFailure $ "unmocked RPC called: " <> T.unpack rpc + let mock = do + guardRPC "delete-sub-conversation" + mockReply $ + if isAMember + then DeleteSubConversationResponseSuccess + else (DeleteSubConversationResponseError ConvNotFound) dsc = DeleteSubConversation groupId epoch (_, reqs) <- From fa9bcc799c21a62a0c1c2f25263c826b3fa343d6 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 26 Jan 2023 11:16:13 +0100 Subject: [PATCH 013/225] Fix hlint --- services/galley/test/integration/API/MLS.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 2bafbd250aa..3cc99e10e08 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -2587,7 +2587,7 @@ testDeleteRemoteSubConv isAMember = do mockReply $ if isAMember then DeleteSubConversationResponseSuccess - else (DeleteSubConversationResponseError ConvNotFound) + else DeleteSubConversationResponseError ConvNotFound dsc = DeleteSubConversation groupId epoch (_, reqs) <- From dc5aeaa48b8cf52199b55fa0a1c81ce408f5a836 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Wed, 1 Feb 2023 11:35:39 +0100 Subject: [PATCH 014/225] extend on-new-remote-conversation by the subconv ID (#2997) * extend on-new-remote-conversation by the subconv ID * invoke on-new-remote-conversation in addMembers * add end2end test for messages in a subconversation * Notify new backends about existing subconvs * Add client field to federated MLS messages * Use sender client for federated messages * Test remote user joining a local subconversation * fix deserialisation of subgroups in e2e tests * add client header to requests in e2e tests * fix comments in e2e test * abstract sendCommitBundle in e2e tests * Test joining remote subconversation * Test onrc when a new backend joins a conversation * Call onrc when a subconversation is reset * Test onrc call when resetting a subconversation * Split on-new-remote-conversation into 2 endpoints * lint: remove redundant brackets * Fix assertions of RPCs * add changelog entry * Update services/galley/test/integration/API/MLS.hs Co-authored-by: Stefan Matting * Do not call onrsc unless the subconv is new * Use bucketRemote --------- Co-authored-by: Paolo Capriotti Co-authored-by: Stefan Matting --- .../on-new-remote-subconversation | 4 + .../src/Wire/API/Federation/API/Galley.hs | 30 +- .../src/Wire/API/Conversation/Protocol.hs | 52 +-- .../API/Routes/Public/Galley/Conversation.hs | 21 ++ .../Wire/API/Routes/Public/Galley/Feature.hs | 3 +- .../API/Routes/Public/Galley/LegalHold.hs | 5 + .../src/Wire/API/Routes/Public/Galley/MLS.hs | 3 + .../Routes/Public/Galley/TeamConversation.hs | 1 + .../test/integration/Federation/End2end.hs | 357 +++++++++++++++++- .../brig/test/integration/Federation/Util.hs | 26 ++ services/brig/test/integration/Util.hs | 24 ++ services/galley/src/Galley/API/Action.hs | 52 ++- services/galley/src/Galley/API/Federation.hs | 98 +++-- services/galley/src/Galley/API/Internal.hs | 3 +- services/galley/src/Galley/API/LegalHold.hs | 40 +- services/galley/src/Galley/API/MLS/Message.hs | 55 ++- .../src/Galley/API/MLS/SubConversation.hs | 48 ++- services/galley/src/Galley/API/Teams.hs | 4 +- .../galley/src/Galley/API/Teams/Features.hs | 4 +- services/galley/src/Galley/API/Update.hs | 95 +++-- .../galley/src/Galley/Cassandra/Queries.hs | 3 + .../src/Galley/Cassandra/SubConversation.hs | 28 +- services/galley/src/Galley/Effects.hs | 1 + .../src/Galley/Effects/FederatorAccess.hs | 2 +- .../Galley/Effects/SubConversationStore.hs | 3 +- services/galley/test/integration/API.hs | 6 +- services/galley/test/integration/API/MLS.hs | 228 ++++++++--- .../galley/test/integration/API/MLS/Mocks.hs | 7 +- .../galley/test/integration/API/MLS/Util.hs | 190 ++++++++-- 29 files changed, 1163 insertions(+), 230 deletions(-) create mode 100644 changelog.d/6-federation/on-new-remote-subconversation diff --git a/changelog.d/6-federation/on-new-remote-subconversation b/changelog.d/6-federation/on-new-remote-subconversation new file mode 100644 index 00000000000..edb9c811d3d --- /dev/null +++ b/changelog.d/6-federation/on-new-remote-subconversation @@ -0,0 +1,4 @@ +Split federation endpoint into on-new-remote-conversation and on-new-remote-subconversation +Call on-new-remote-subconversation when a new subconversation is created +Call on-new-remote-subconversation for all existing subconversations when a new backend gets involved +Call on-new-remote-subconversation when a subconversation is reset 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 dd472f0b875..16947e69cea 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 @@ -56,6 +56,7 @@ type GalleyApi = -- This endpoint is called the first time a user from this backend is -- added to a remote conversation. :<|> FedEndpoint "on-new-remote-conversation" NewRemoteConversation EmptyResponse + :<|> FedEndpoint "on-new-remote-subconversation" NewRemoteSubConversation EmptyResponse :<|> FedEndpoint "get-conversations" GetConversationsRequest GetConversationsResponse -- used by the backend that owns a conversation to inform this backend of -- changes to the conversation @@ -63,7 +64,8 @@ type GalleyApi = :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-new-remote-conversation" + MakesFederatedCall 'Galley "on-new-remote-conversation", + MakesFederatedCall 'Galley "on-new-remote-subconversation" ] "leave-conversation" LeaveConversationRequest @@ -83,7 +85,8 @@ type GalleyApi = :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-mls-message-sent", MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-new-remote-conversation" + MakesFederatedCall 'Galley "on-new-remote-conversation", + MakesFederatedCall 'Galley "on-new-remote-subconversation" ] "on-user-deleted-conversations" UserDeletedConversationsNotification @@ -91,7 +94,8 @@ type GalleyApi = :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-new-remote-conversation" + MakesFederatedCall 'Galley "on-new-remote-conversation", + MakesFederatedCall 'Galley "on-new-remote-subconversation" ] "update-conversation" ConversationUpdateRequest @@ -102,6 +106,7 @@ type GalleyApi = '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", MakesFederatedCall 'Galley "on-new-remote-conversation", + MakesFederatedCall 'Galley "on-new-remote-subconversation", MakesFederatedCall 'Galley "send-mls-message", MakesFederatedCall 'Brig "get-mls-clients" ] @@ -113,6 +118,7 @@ type GalleyApi = MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", MakesFederatedCall 'Galley "on-new-remote-conversation", + MakesFederatedCall 'Galley "on-new-remote-subconversation", MakesFederatedCall 'Galley "send-mls-commit-bundle", MakesFederatedCall 'Brig "get-mls-clients" ] @@ -128,7 +134,11 @@ type GalleyApi = EmptyResponse :<|> FedEndpoint "on-typing-indicator-updated" TypingDataUpdateRequest EmptyResponse :<|> FedEndpoint "get-sub-conversation" GetSubConversationsRequest GetSubConversationsResponse - :<|> FedEndpoint "delete-sub-conversation" DeleteSubConversationRequest DeleteSubConversationResponse + :<|> FedEndpointWithMods + '[MakesFederatedCall 'Galley "on-new-remote-subconversation"] + "delete-sub-conversation" + DeleteSubConversationRequest + DeleteSubConversationResponse data TypingDataUpdateRequest = TypingDataUpdateRequest { tdurTypingStatus :: TypingStatus, @@ -226,6 +236,17 @@ data NewRemoteConversation = NewRemoteConversation deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded NewRemoteConversation) +data NewRemoteSubConversation = NewRemoteSubConversation + { -- | The ID of the parent conversation + nrscConvId :: ConvId, + -- | The subconversation ID + nrscSubConvId :: SubConvId, + -- | MLS data + nrscMlsData :: ConversationMLSData + } + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded NewRemoteSubConversation) + data ConversationUpdate = ConversationUpdate { cuTime :: UTCTime, cuOrigUserId :: Qualified UserId, @@ -325,6 +346,7 @@ data MLSMessageSendRequest = MLSMessageSendRequest -- | Sender is assumed to be owned by the origin domain, this allows us to -- protect against spoofing attacks mmsrSender :: UserId, + mmsrSenderClient :: ClientId, mmsrRawMessage :: Base64ByteString } deriving stock (Eq, Show, Generic) diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 0fa9fc99c14..f72e45ea67a 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -61,6 +61,34 @@ data ConversationMLSData = ConversationMLSData } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform ConversationMLSData + deriving (ToJSON, FromJSON) via Schema ConversationMLSData + +mlsDataSchema :: ObjectSchema SwaggerDoc ConversationMLSData +mlsDataSchema = + ConversationMLSData + <$> cnvmlsGroupId + .= fieldWithDocModifier + "group_id" + (description ?~ "An MLS group identifier (at most 256 bytes long)") + schema + <*> cnvmlsEpoch + .= fieldWithDocModifier + "epoch" + (description ?~ "The epoch number of the corresponding MLS group") + schema + <*> cnvmlsEpochTimestamp + .= fieldWithDocModifier + "epoch_timestamp" + (description ?~ "The timestamp of the epoch number") + schemaEpochTimestamp + <*> cnvmlsCipherSuite + .= fieldWithDocModifier + "cipher_suite" + (description ?~ "The cipher suite of the corresponding MLS group") + schema + +instance ToSchema ConversationMLSData where + schema = object "ConversationMLSData" mlsDataSchema -- | Conversation protocol and protocol-specific data. data Protocol @@ -116,27 +144,3 @@ deriving via (Schema Protocol) instance ToJSON Protocol protocolDataSchema :: ProtocolTag -> ObjectSchema SwaggerDoc Protocol protocolDataSchema ProtocolProteusTag = tag _ProtocolProteus (pure ()) protocolDataSchema ProtocolMLSTag = tag _ProtocolMLS mlsDataSchema - -mlsDataSchema :: ObjectSchema SwaggerDoc ConversationMLSData -mlsDataSchema = - ConversationMLSData - <$> cnvmlsGroupId - .= fieldWithDocModifier - "group_id" - (description ?~ "An MLS group identifier (at most 256 bytes long)") - schema - <*> cnvmlsEpoch - .= fieldWithDocModifier - "epoch" - (description ?~ "The epoch number of the corresponding MLS group") - schema - <*> cnvmlsEpochTimestamp - .= fieldWithDocModifier - "epoch_timestamp" - (description ?~ "The timestamp of the epoch number") - schemaEpochTimestamp - <*> cnvmlsCipherSuite - .= fieldWithDocModifier - "cipher_suite" - (description ?~ "The cipher suite of the corresponding MLS group") - schema diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index a1c9f231f0c..66dd85e252e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -423,6 +423,8 @@ type ConversationAPI = "delete-subconversation" ( Summary "Delete an MLS subconversation" :> MakesFederatedCall 'Galley "delete-sub-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'MLSNotEnabled @@ -513,6 +515,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> Until 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -537,6 +540,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> Until 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -562,6 +566,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> From 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -587,6 +592,7 @@ type ConversationAPI = ( Summary "Join a conversation by its ID (if link access enabled)" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -609,6 +615,7 @@ type ConversationAPI = \Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow 'CodeNotFound :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound @@ -739,6 +746,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> Until 'V2 :> ZLocalUser :> ZConn @@ -760,6 +768,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'RemoveConversationMember) @@ -779,6 +788,7 @@ type ConversationAPI = :> Description "Use `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> ZLocalUser :> ZConn :> CanThrow 'ConvNotFound @@ -803,6 +813,7 @@ type ConversationAPI = :> Description "**Note**: at least one field has to be provided." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> ZLocalUser :> ZConn :> CanThrow 'ConvNotFound @@ -829,6 +840,7 @@ type ConversationAPI = :> Description "Use `/conversations/:domain/:conv/name` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -849,6 +861,7 @@ type ConversationAPI = :> Description "Use `/conversations/:domain/:conv/name` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -869,6 +882,7 @@ type ConversationAPI = ( Summary "Update conversation name" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -892,6 +906,7 @@ type ConversationAPI = :> Description "Use `/conversations/:domain/:cnv/message-timer` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) @@ -913,6 +928,7 @@ type ConversationAPI = ( Summary "Update the message timer for a conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) @@ -937,6 +953,7 @@ type ConversationAPI = :> Description "Use `PUT /conversations/:domain/:cnv/receipt-mode` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Galley "update-conversation" :> ZLocalUser :> ZConn @@ -959,6 +976,7 @@ type ConversationAPI = ( Summary "Update receipt mode for a conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Galley "update-conversation" :> ZLocalUser :> ZConn @@ -985,6 +1003,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> Until 'V3 :> Description "Use PUT `/conversations/:domain/:cnv/access` instead." :> ZLocalUser @@ -1011,6 +1030,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> Until 'V3 :> ZLocalUser :> ZConn @@ -1036,6 +1056,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> From 'V3 :> ZLocalUser :> ZConn diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index a07a09fdfb1..ea443820251 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -39,7 +39,8 @@ type FeatureAPI = :<|> FeatureStatusPut '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-new-remote-conversation" + MakesFederatedCall 'Galley "on-new-remote-conversation", + MakesFederatedCall 'Galley "on-new-remote-subconversation" ] '( 'ActionDenied 'RemoveConversationMember, '( AuthenticationError, diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs index 82318d92132..645878f3758 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs @@ -66,6 +66,7 @@ type LegalHoldAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow AuthenticationError :> CanThrow OperationDenied :> CanThrow 'NotATeamMember @@ -105,6 +106,7 @@ type LegalHoldAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'InvalidOperation :> CanThrow 'TeamMemberNotFound @@ -123,6 +125,7 @@ type LegalHoldAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'NotATeamMember :> CanThrow OperationDenied @@ -154,6 +157,7 @@ type LegalHoldAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow AuthenticationError :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'NotATeamMember @@ -183,6 +187,7 @@ type LegalHoldAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow AuthenticationError :> CanThrow 'AccessDenied :> CanThrow ('ActionDenied 'RemoveConversationMember) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index fd705cf6fe7..ebea2b25bae 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -54,6 +54,7 @@ type MLSMessagingAPI = :> MakesFederatedCall 'Galley "send-mls-message" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Brig "get-mls-clients" :> Until 'V2 :> CanThrow 'ConvAccessDenied @@ -90,6 +91,7 @@ type MLSMessagingAPI = :> MakesFederatedCall 'Galley "send-mls-message" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Brig "get-mls-clients" :> From 'V2 :> CanThrow 'ConvAccessDenied @@ -127,6 +129,7 @@ type MLSMessagingAPI = :> MakesFederatedCall 'Galley "send-mls-commit-bundle" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Brig "get-mls-clients" :> From 'V3 :> CanThrow 'ConvAccessDenied diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs index 759b83c6ccd..2499b95fbd4 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs @@ -70,6 +70,7 @@ type TeamConversationAPI = ( Summary "Remove a team conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'DeleteConversation) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index a13db52313c..54f3e8a7b83 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -42,7 +42,7 @@ import Data.Qualified import Data.Range (checked) import qualified Data.Set as Set import qualified Data.Text as T -import Federation.Util (connectUsersEnd2End, generateClientPrekeys, getConvQualified) +import Federation.Util import Imports import System.FilePath import qualified System.Logger as Log @@ -63,6 +63,7 @@ import Wire.API.Internal.Notification (ntfTransient) import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation +import Wire.API.MLS.SubConversation import Wire.API.Message import Wire.API.Routes.MultiTablePaging import Wire.API.User hiding (assetKey) @@ -118,7 +119,9 @@ spec _brigOpts mg brig galley cargohold cannon _federator brigTwo galleyTwo carg test mg "download remote asset" $ testRemoteAsset brig brigTwo cargohold cargoholdTwo, test mg "claim remote key packages" $ claimRemoteKeyPackages brig brigTwo, test mg "send an MLS message to a remote user" $ - testSendMLSMessage brig brigTwo galley galleyTwo cannon cannonTwo + testSendMLSMessage brig brigTwo galley galleyTwo cannon cannonTwo, + test mg "send an MLS subconversation message to a federated user" $ + testSendMLSMessageToSubConversation brig brigTwo galley galleyTwo cannon cannonTwo ] -- | Path covered by this test: @@ -736,6 +739,7 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do ( brig1 . paths ["clients", toByteString' aliceClient] . zUser (qUnqualified (userQualifiedId alice)) + . zClient aliceClient . json update ) !!! const 200 === statusCode @@ -745,6 +749,7 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do ( brig1 . paths ["mls", "key-packages", "self", toByteString' aliceClient] . zUser (qUnqualified (userQualifiedId alice)) + . zClient aliceClient . json (KeyPackageUpload [aliceKP]) ) !!! const 201 === statusCode @@ -779,6 +784,7 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do toByteString' (qUnqualified (userQualifiedId alice)) ] . zUser (qUnqualified (userQualifiedId bob)) + . zClient bobClient ) Brig -> Galley -> Galley -> Cannon -> Cannon -> Http () +testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 = do + let cli :: String -> FilePath -> [String] -> CreateProcess + cli store tmp args = + proc "mls-test-cli" $ + ["--store", tmp (store <> ".db")] <> args + + -- create alice user and client on domain 1 + alice <- randomUser brig1 + aliceClient <- + clientId . responseJsonUnsafe + <$> addClient + brig1 + (userId alice) + (defNewClient PermanentClientType [] (Imports.head someLastPrekeys)) + let aliceClientId = + show (userId alice) + <> ":" + <> T.unpack (client aliceClient) + <> "@" + <> T.unpack (domainText (qDomain (userQualifiedId alice))) + + -- create bob user and client on domain 2 + bob <- randomUser brig2 + bobClient <- + clientId . responseJsonUnsafe + <$> addClient + brig2 + (userId bob) + (defNewClient PermanentClientType [] (someLastPrekeys !! 1)) + let bobClientId = + show (userId bob) + <> ":" + <> T.unpack (client bobClient) + <> "@" + <> T.unpack (domainText (qDomain (userQualifiedId bob))) + + withSystemTempDirectory "mls" $ \tmp -> do + -- create alice's key package + void . liftIO $ spawn (cli aliceClientId tmp ["init", aliceClientId]) Nothing + kpMLS <- liftIO $ spawn (cli aliceClientId tmp ["key-package", "create"]) Nothing + aliceKP <- liftIO $ case decodeMLS' kpMLS of + Right kp -> pure kp + Left e -> assertFailure $ "Could not decode alice Key Package: " <> T.unpack e + + -- set public key + let update = + defUpdateClient + { updateClientMLSPublicKeys = + Map.singleton + Ed25519 + (bcSignatureKey (kpCredential (rmValue aliceKP))) + } + put + ( brig1 + . paths ["clients", toByteString' aliceClient] + . zUser (qUnqualified (userQualifiedId alice)) + . json update + ) + !!! const 200 === statusCode + + -- upload key package + post + ( brig1 + . paths ["mls", "key-packages", "self", toByteString' aliceClient] + . zUser (qUnqualified (userQualifiedId alice)) + . zClient aliceClient + . json (KeyPackageUpload [aliceKP]) + ) + !!! const 201 === statusCode + + -- create bob's client state + void . liftIO $ spawn (cli bobClientId tmp ["init", bobClientId]) Nothing + + connectUsersEnd2End brig1 brig2 (userQualifiedId alice) (userQualifiedId bob) + + -- bob claims alice's key package + void $ + post + ( brig2 + . paths + [ "mls", + "key-packages", + "claim", + toByteString' (qDomain (userQualifiedId alice)), + toByteString' (qUnqualified (userQualifiedId alice)) + ] + . zUser (qUnqualified (userQualifiedId bob)) + . zClient bobClient + ) + pure (unGroupId (cnvmlsGroupId p)) + ProtocolProteus -> liftIO $ assertFailure "Expected MLS conversation" + let qconvId = cnvQualifiedId conv + groupJSON <- + liftIO $ + spawn + ( cli + bobClientId + tmp + [ "group", + "create", + T.unpack (toBase64Text groupId) + ] + ) + Nothing + liftIO $ BS.writeFile (tmp "group.json") groupJSON + + -- invite alice + liftIO $ BS.writeFile (tmp aliceClientId) (rmRaw aliceKP) + commit <- + liftIO $ + spawn + ( cli + bobClientId + tmp + [ "member", + "add", + "--in-place", + "--group", + tmp "group.json", + "--welcome-out", + tmp "welcome", + tmp aliceClientId + ] + ) + Nothing + welcome <- liftIO $ BS.readFile (tmp "welcome") + + -- send welcome and commit + WS.bracketR cannon1 (userId alice) $ \wsAlice -> do + post + ( galley2 + . paths + ["mls", "messages"] + . zUser (userId bob) + . zClient bobClient + . zConn "conn" + . header "Z-Type" "access" + . content "message/mls" + . bytes commit + ) + !!! const 201 === statusCode + + post + ( galley2 + . paths + ["mls", "welcome"] + . zUser (userId bob) + . zClient bobClient + . zConn "conn" + . header "Z-Type" "access" + . content "message/mls" + . bytes welcome + ) + !!! const 201 === statusCode + + -- verify that alice receives the welcome message + WS.assertMatch_ (5 # Second) wsAlice $ \n -> do + let e = List1.head (WS.unpackPayload n) + ntfTransient n @?= False + evtType e @?= MLSWelcome + evtFrom e @?= userQualifiedId alice + evtData e @?= EdMLSWelcome welcome + + -- verify that alice receives a join event + WS.assertMatch_ (5 # Second) wsAlice $ \n -> do + let e = List1.head (WS.unpackPayload n) + evtConv e @?= qconvId + evtType e @?= MemberJoin + evtFrom e @?= userQualifiedId bob + fmap (sort . mMembers) (evtData e ^? _EdMembersJoin) + @?= Just [SimpleMember (userQualifiedId alice) roleNameWireMember] + + -- alice creates the group + void . liftIO $ + spawn + ( cli + aliceClientId + tmp + [ "group", + "from-welcome", + "--group-out", + tmp "groupA.json", + tmp "welcome" + ] + ) + Nothing + + -- SUBCONVERSATION + -- create subconversation on domain 2 + subConv <- + responseJsonError + =<< createMLSSubConversation galley2 (userId bob) qconvId (SubConvId "sub") + "subgroup.json") subGroupJSON + + -- bob sends commit bundle for subconversation + do + subCommitRaw <- + liftIO $ + spawn + ( cli + bobClientId + tmp + [ "commit", + "--in-place", + "--group", + tmp "subgroup.json", + "--group-state-out", + tmp "subgroupstate.mls" + ] + ) + Nothing + sendCommitBundle + tmp + "subgroupstate.mls" + galley2 + (userId bob) + bobClient + subCommitRaw + + -- alice sends an external commit to add herself to the subconveration + do + subCommitRaw <- + liftIO $ + spawn + ( cli + aliceClientId + tmp + [ "external-commit", + "--group-out", + tmp "subgroupA.json", + "--group-state-in", + tmp "subgroupstate.mls", + "--group-state-out", + tmp "subgroupstateA.mls" + ] + ) + Nothing + sendCommitBundle + tmp + "subgroupstateA.mls" + galley1 + (userId alice) + aliceClient + subCommitRaw + + -- prepare bob's message to the subconversation + dove <- + liftIO $ + spawn + ( cli + bobClientId + tmp + ["message", "--group", tmp "subgroup.json", "dove"] + ) + Nothing + + -- prepare alice's reply to the subconversation + reply <- + liftIO $ + spawn + ( cli + aliceClientId + tmp + ["message", "--group", tmp "subgroupA.json", "raven"] + ) + Nothing + + -- send bob's message + WS.bracketR cannon1 (userId alice) $ \wsAlice -> do + post + ( galley2 + . paths + ["mls", "messages"] + . zUser (userId bob) + . zClient bobClient + . zConn "conn" + . header "Z-Type" "access" + . content "message/mls" + . bytes dove + ) + !!! const 201 === statusCode + + -- verify that alice receives bob's message in the subconversation + WS.assertMatch_ (5 # Second) wsAlice $ \n -> do + let e = List1.head (WS.unpackPayload n) + ntfTransient n @?= False + evtConv e @?= qconvId + evtType e @?= MLSMessageAdd + evtFrom e @?= userQualifiedId bob + evtData e @?= EdMLSMessage dove + + -- send alice's message + WS.bracketR cannon2 (userId bob) $ \wsBob -> do + post + ( galley1 + . paths + ["mls", "messages"] + . zUser (userId alice) + . zClient aliceClient + . zConn "conn" + . header "Z-Type" "access" + . content "message/mls" + . bytes reply + ) + !!! const 201 === statusCode + + -- verify that bob receives alice's message in the subconversation + WS.assertMatch_ (5 # Second) wsBob $ \n -> do + let e = List1.head (WS.unpackPayload n) + ntfTransient n @?= False + evtConv e @?= qconvId + evtType e @?= MLSMessageAdd + evtFrom e @?= userQualifiedId alice + evtData e @?= EdMLSMessage reply diff --git a/services/brig/test/integration/Federation/Util.hs b/services/brig/test/integration/Federation/Util.hs index dbc08acb711..8d56f4f4b06 100644 --- a/services/brig/test/integration/Federation/Util.hs +++ b/services/brig/test/integration/Federation/Util.hs @@ -33,6 +33,7 @@ import Control.Monad.Trans.Except import Control.Retry import Data.Aeson (FromJSON, Value, decode, (.=)) import qualified Data.Aeson as Aeson +import qualified Data.ByteString as BS import Data.ByteString.Conversion (toByteString') import Data.Domain (Domain (Domain)) import Data.Handle (fromHandle) @@ -40,6 +41,7 @@ import Data.Id import qualified Data.Map.Strict as Map import Data.Qualified (Qualified (..)) import Data.String.Conversions (cs) +import qualified Data.Text as T import qualified Data.Text as Text import qualified Database.Bloodhound as ES import qualified Federator.MockServer as Mock @@ -52,6 +54,7 @@ import Network.Socket import Network.Wai.Handler.Warp (Port) import Network.Wai.Test (Session) import qualified Network.Wai.Test as WaiTest +import System.FilePath import Test.QuickCheck (Arbitrary (arbitrary), generate) import Test.Tasty import Test.Tasty.HUnit @@ -63,6 +66,9 @@ import Wire.API.Connection import Wire.API.Conversation (Conversation (cnvMembers)) import Wire.API.Conversation.Member (OtherMember (OtherMember), cmOthers) import Wire.API.Conversation.Role (roleNameWireAdmin) +import Wire.API.MLS.CommitBundle +import Wire.API.MLS.GroupInfoBundle +import Wire.API.MLS.Serialisation import Wire.API.Team.Feature (FeatureStatus (..)) import Wire.API.User import Wire.API.User.Client @@ -111,3 +117,23 @@ connectUsersEnd2End brig1 brig2 quid1 quid2 = do !!! const 201 === statusCode putConnectionQualified brig2 (qUnqualified quid2) quid1 Accepted !!! const 200 === statusCode + +sendCommitBundle :: FilePath -> FilePath -> Galley -> UserId -> ClientId -> ByteString -> Http () +sendCommitBundle tmp subGroupStateFn galley uid cid commit = do + subGroupStateRaw <- liftIO $ BS.readFile $ tmp subGroupStateFn + subGroupState <- either (liftIO . assertFailure . T.unpack) pure . decodeMLS' $ subGroupStateRaw + subCommit <- either (liftIO . assertFailure . T.unpack) pure . decodeMLS' $ commit + let subGroupBundle = CommitBundle subCommit Nothing (GroupInfoBundle UnencryptedGroupInfo TreeFull subGroupState) + let subGroupBundleRaw = serializeCommitBundle subGroupBundle + post + ( galley + . paths + ["mls", "commit-bundles"] + . zUser uid + . zClient cid + . zConn "conn" + . header "Z-Type" "access" + . content "application/x-protobuf" + . bytes subGroupBundleRaw + ) + !!! const 201 === statusCode diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index d6bc80cf92a..48de9be3bae 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -102,6 +102,7 @@ import Test.Tasty.HUnit import Text.Printf (printf) import qualified UnliftIO.Async as Async import Util.Options +import Web.Internal.HttpApiData import Wire.API.Connection import Wire.API.Conversation import Wire.API.Conversation.Protocol @@ -109,6 +110,7 @@ import Wire.API.Conversation.Role (roleNameWireAdmin) import Wire.API.Federation.API import Wire.API.Federation.Domain import Wire.API.Internal.Notification +import Wire.API.MLS.SubConversation import Wire.API.Routes.MultiTablePaging import Wire.API.Team.Member hiding (userId) import Wire.API.User @@ -740,6 +742,25 @@ createMLSConversation galley zusr c = do . zConn "conn" . json conv +createMLSSubConversation :: + (MonadIO m, MonadHttp m) => + Galley -> + UserId -> + Qualified ConvId -> + SubConvId -> + m ResponseLBS +createMLSSubConversation galley zusr qcnv sconv = + get $ + galley + . paths + [ "conversations", + toByteString' (qDomain qcnv), + toByteString' (qUnqualified qcnv), + "subconversations", + toHeader sconv + ] + . zUser zusr + createConversation :: (MonadIO m, MonadHttp m) => Galley -> UserId -> [Qualified UserId] -> m ResponseLBS createConversation galley zusr usersToAdd = do let conv = @@ -844,6 +865,9 @@ zAuthAccess u c = header "Z-Type" "access" . zUser u . zConn c zUser :: UserId -> Request -> Request zUser = header "Z-User" . B8.pack . show +zClient :: ClientId -> Request -> Request +zClient = header "Z-Client" . toByteString' + zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index d5a33d94d7a..e14f46b70c6 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -68,6 +68,7 @@ import qualified Galley.Effects.FederatorAccess as E import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E import Galley.Effects.ProposalStore +import qualified Galley.Effects.SubConversationStore as E import qualified Galley.Effects.TeamStore as E import Galley.Options import Galley.Types.Conversations.Members @@ -120,6 +121,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con LegalHoldStore, MemberStore, ProposalStore, + SubConversationStore, TeamStore, TinyLog, ConversationStore, @@ -166,6 +168,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Input Env, MemberStore, ProposalStore, + SubConversationStore, TeamStore, TinyLog, Input UTCTime, @@ -278,12 +281,14 @@ type family PerformActionCalls tag where PerformActionCalls 'ConversationAccessDataTag = ( CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) PerformActionCalls 'ConversationJoinTag = ( CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) PerformActionCalls 'ConversationLeaveTag = ( CallsFed 'Galley "on-mls-message-sent" @@ -367,7 +372,8 @@ performConversationJoin :: ( HasConversationActionEffects 'ConversationJoinTag r, CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Qualified UserId -> Local Conversation -> @@ -449,6 +455,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do LegalHoldStore, MemberStore, ProposalStore, + SubConversationStore, TeamStore, TinyLog ] @@ -497,7 +504,8 @@ performConversationAccessData :: ( HasConversationActionEffects 'ConversationAccessDataTag r, CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Qualified UserId -> Local Conversation -> @@ -592,12 +600,14 @@ updateLocalConversation :: FederatorAccess, GundeckAccess, Input Env, - Input UTCTime + Input UTCTime, + SubConversationStore ] r, HasConversationActionEffects tag r, SingI tag, CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "on-conversation-updated", PerformActionCalls tag ) => @@ -636,8 +646,10 @@ updateLocalConversationUnchecked :: Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, + Member SubConversationStore r, HasConversationActionEffects tag r, CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "on-conversation-updated", PerformActionCalls tag ) => @@ -715,8 +727,16 @@ addMembersToLocalConversation lcnv users role = do notifyConversationAction :: forall tag r. - ( Members '[FederatorAccess, ExternalAccess, GundeckAccess, Input UTCTime] r, + ( Members + '[ FederatorAccess, + ExternalAccess, + GundeckAccess, + Input UTCTime, + SubConversationStore + ] + r, CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "on-conversation-updated" ) => Sing tag -> @@ -741,19 +761,27 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do uids (SomeConversationAction tag action) - -- call `on-new-remote-conversation` on backends that are seeing this - -- conversation for the first time + -- Backends that are seeing this conversation for the first time need to be + -- notified about this conversation and all its subconversations let newDomains = Set.difference (Set.map void (bmRemotes targets)) (Set.fromList (map (void . rmId) (convRemoteMembers conv))) - let nrc = + subConvs <- Map.assocs <$> E.listSubConversations (convId conv) + E.runFederatedConcurrently_ (toList newDomains) $ \_ -> do + void $ + fedClient @'Galley @"on-new-remote-conversation" NewRemoteConversation { nrcConvId = convId conv, nrcProtocol = convProtocol conv } - E.runFederatedConcurrently_ (toList newDomains) $ \_ -> do - void $ fedClient @'Galley @"on-new-remote-conversation" nrc + for_ subConvs $ \(mSubId, mlsData) -> + fedClient @'Galley @"on-new-remote-subconversation" + NewRemoteSubConversation + { nrscConvId = convId conv, + nrscSubConvId = mSubId, + nrscMlsData = mlsData + } update <- fmap (fromMaybe (mkUpdate []) . asum . map tUnqualified) . E.runFederatedConcurrently (toList (bmRemotes targets)) @@ -834,8 +862,10 @@ kickMember :: Member (Input UTCTime) r, Member (Input Env) r, Member MemberStore r, + Member SubConversationStore r, Member TinyLog r, CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "on-conversation-updated", PerformActionCalls 'ConversationLeaveTag ) => diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index ef197e16ab2..fbe6da83365 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -58,7 +58,7 @@ import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E import Galley.Effects.ProposalStore (ProposalStore) -import Galley.Effects.SubConversationStore +import qualified Galley.Effects.SubConversationStore as E import Galley.Effects.SubConversationSupply import Galley.Options import Galley.Types.Conversations.Members @@ -108,6 +108,7 @@ federationSitemap :: federationSitemap = Named @"on-conversation-created" onConversationCreated :<|> Named @"on-new-remote-conversation" onNewRemoteConversation + :<|> Named @"on-new-remote-subconversation" onNewRemoteSubConversation :<|> Named @"get-conversations" getConversations :<|> Named @"on-conversation-updated" onConversationUpdated :<|> Named @"leave-conversation" (callsFed leaveConversation) @@ -123,7 +124,7 @@ federationSitemap = :<|> Named @"on-client-removed" (callsFed onClientRemoved) :<|> Named @"on-typing-indicator-updated" onTypingIndicatorUpdated :<|> Named @"get-sub-conversation" getSubConversationForRemoteUser - :<|> Named @"delete-sub-conversation" deleteSubConversationForRemoteUser + :<|> Named @"delete-sub-conversation" (callsFed deleteSubConversationForRemoteUser) onClientRemoved :: ( Members @@ -203,15 +204,37 @@ onConversationCreated domain rc = do pushConversationEvent Nothing event (qualifyAs loc [qUnqualified . Public.memId $ mem]) [] onNewRemoteConversation :: - Member ConversationStore r => + Members + '[ ConversationStore, + SubConversationStore + ] + r => Domain -> F.NewRemoteConversation -> Sem r EmptyResponse onNewRemoteConversation domain nrc = do -- update group_id -> conv_id mapping for_ (preview (to F.nrcProtocol . _ProtocolMLS) nrc) $ \mls -> - E.setGroupIdForConversation (cnvmlsGroupId mls) (Qualified (F.nrcConvId nrc) domain) + E.setGroupIdForConversation + (cnvmlsGroupId mls) + (Qualified (F.nrcConvId nrc) domain) + + pure EmptyResponse +onNewRemoteSubConversation :: + Members + '[ ConversationStore, + SubConversationStore + ] + r => + Domain -> + F.NewRemoteSubConversation -> + Sem r EmptyResponse +onNewRemoteSubConversation domain nrsc = do + E.setGroupIdForSubConversation + (cnvmlsGroupId (F.nrscMlsData nrsc)) + (Qualified (F.nrscConvId nrsc) domain) + (F.nrscSubConvId nrsc) pure EmptyResponse getConversations :: @@ -349,12 +372,14 @@ leaveConversation :: Input UTCTime, MemberStore, ProposalStore, + SubConversationStore, TinyLog ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Domain -> F.LeaveConversationRequest -> @@ -487,12 +512,14 @@ onUserDeleted :: Input Env, MemberStore, ProposalStore, + SubConversationStore, TinyLog ] r, CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Domain -> F.UserDeletedConversationsNotification -> @@ -535,31 +562,33 @@ onUserDeleted origDomain udcn = do updateConversation :: forall r. ( Members - '[ BrigAccess, + '[ BotAccess, + BrigAccess, CodeStore, - BotAccess, - FireAndForget, + ConversationStore, Error FederationError, + Error InternalError, Error InvalidInput, ExternalAccess, FederatorAccess, - Error InternalError, + FireAndForget, GundeckAccess, Input Env, + Input (Local ()), Input Opts, Input UTCTime, LegalHoldStore, MemberStore, ProposalStore, + SubConversationStore, TeamStore, - TinyLog, - ConversationStore, - Input (Local ()) + TinyLog ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Domain -> F.ConversationUpdateRequest -> @@ -647,6 +676,7 @@ sendMLSCommitBundle :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "send-mls-commit-bundle", CallsFed 'Brig "get-mls-clients" ) => @@ -670,7 +700,13 @@ sendMLSCommitBundle remoteDomain msr = qConvOrSub <- E.lookupConvByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch F.MLSMessageResponseUpdates . map lcuUpdate - <$> postMLSCommitBundle loc (tUntagged sender) Nothing qConvOrSub Nothing bundle + <$> postMLSCommitBundle + loc + (tUntagged sender) + (Just (mmsrSenderClient msr)) + qConvOrSub + Nothing + bundle sendMLSMessage :: ( Members @@ -697,6 +733,7 @@ sendMLSMessage :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "send-mls-message", CallsFed 'Brig "get-mls-clients" ) => @@ -721,7 +758,13 @@ sendMLSMessage remoteDomain msr = qConvOrSub <- E.lookupConvByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch F.MLSMessageResponseUpdates . map lcuUpdate - <$> postMLSMessage loc (tUntagged sender) Nothing qConvOrSub Nothing raw + <$> postMLSMessage + loc + (tUntagged sender) + (Just (mmsrSenderClient msr)) + qConvOrSub + Nothing + raw class ToGalleyRuntimeError (effs :: EffectRow) r where mapToGalleyError :: @@ -900,16 +943,19 @@ getSubConversationForRemoteUser domain GetSubConversationsRequest {..} = getLocalSubConversation qusr lconv gsreqSubConv deleteSubConversationForRemoteUser :: - Members - '[ ConversationStore, - Input (Local ()), - Input Env, - MemberStore, - Resource, - SubConversationStore, - SubConversationSupply - ] - r => + ( Members + '[ ConversationStore, + FederatorAccess, + Input (Local ()), + Input Env, + MemberStore, + Resource, + SubConversationStore, + SubConversationSupply + ] + r, + CallsFed 'Galley "on-new-remote-subconversation" + ) => Domain -> DeleteSubConversationRequest -> Sem r DeleteSubConversationResponse diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index c334e600314..8a1582dbda9 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -133,7 +133,8 @@ type LegalHoldFeatureStatusChangeErrors = type LegalHoldFeaturesStatusChangeFederatedCalls = '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-new-remote-conversation" + MakesFederatedCall 'Galley "on-new-remote-conversation", + MakesFederatedCall 'Galley "on-new-remote-subconversation" ] type IFeatureAPI = diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index cfb7e9cbae2..c581de86dab 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -213,6 +213,7 @@ removeSettingsInternalPaging :: MemberStore, ProposalStore, P.TinyLog, + SubConversationStore, TeamFeatureStore db, TeamMemberStore InternalPaging, TeamStore, @@ -221,7 +222,8 @@ removeSettingsInternalPaging :: r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => Local UserId -> @@ -262,6 +264,7 @@ removeSettings :: MemberStore, ProposalStore, P.TinyLog, + SubConversationStore, TeamFeatureStore db, TeamMemberStore p, TeamStore @@ -269,7 +272,8 @@ removeSettings :: r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => UserId -> @@ -326,12 +330,14 @@ removeSettings' :: TeamMemberStore p, TeamStore, ProposalStore, - P.TinyLog + P.TinyLog, + SubConversationStore ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => TeamId -> Sem r () @@ -418,12 +424,14 @@ grantConsent :: MemberStore, ProposalStore, P.TinyLog, + SubConversationStore, TeamStore ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> TeamId -> @@ -468,13 +476,15 @@ requestDevice :: MemberStore, ProposalStore, P.TinyLog, + SubConversationStore, TeamFeatureStore db, TeamStore ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => Local UserId -> @@ -552,13 +562,15 @@ approveDevice :: MemberStore, ProposalStore, P.TinyLog, + SubConversationStore, TeamFeatureStore db, TeamStore ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => Local UserId -> @@ -633,12 +645,14 @@ disableForUser :: MemberStore, ProposalStore, P.TinyLog, + SubConversationStore, TeamStore ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> TeamId -> @@ -690,12 +704,14 @@ changeLegalholdStatus :: MemberStore, TeamStore, ProposalStore, - P.TinyLog + P.TinyLog, + SubConversationStore ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => TeamId -> Local UserId -> @@ -810,12 +826,14 @@ handleGroupConvPolicyConflicts :: MemberStore, ProposalStore, P.TinyLog, + SubConversationStore, TeamStore ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> UserLegalHoldStatus -> diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index f8c7ab52ada..d860a7cf306 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -151,6 +151,7 @@ postMLSMessageFromLocalUserV1 :: CallsFed 'Galley "send-mls-message", CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Brig "get-mls-clients" ) => Local UserId -> @@ -196,6 +197,7 @@ postMLSMessageFromLocalUser :: CallsFed 'Galley "send-mls-message", CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Brig "get-mls-clients" ) => Local UserId -> @@ -234,6 +236,7 @@ postMLSCommitBundle :: CallsFed 'Galley "send-mls-commit-bundle", CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Brig "get-mls-clients" ) => Local x -> @@ -247,7 +250,7 @@ postMLSCommitBundle loc qusr mc qConvOrSub conn rawBundle = foldQualified loc (postMLSCommitBundleToLocalConv qusr mc conn rawBundle) - (postMLSCommitBundleToRemoteConv loc qusr conn rawBundle) + (postMLSCommitBundleToRemoteConv loc qusr mc conn rawBundle) qConvOrSub postMLSCommitBundleFromLocalUser :: @@ -273,6 +276,7 @@ postMLSCommitBundleFromLocalUser :: CallsFed 'Galley "send-mls-commit-bundle", CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Brig "get-mls-clients" ) => Local UserId -> @@ -311,6 +315,7 @@ postMLSCommitBundleToLocalConv :: CallsFed 'Galley "mls-welcome", CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Brig "get-mls-clients" ) => Qualified UserId -> @@ -362,7 +367,8 @@ postMLSCommitBundleToLocalConv qusr mc conn bundle lConvOrSubId = do postMLSCommitBundleToRemoteConv :: ( Members MLSBundleStaticErrors r, Members - '[ Error FederationError, + '[ BrigAccess, + Error FederationError, Error MLSProtocolError, Error MLSProposalFailure, ExternalAccess, @@ -376,23 +382,33 @@ postMLSCommitBundleToRemoteConv :: ) => Local x -> Qualified UserId -> + Maybe ClientId -> Maybe ConnId -> CommitBundle -> Remote ConvOrSubConvId -> Sem r [LocalConversationUpdate] -postMLSCommitBundleToRemoteConv loc qusr con bundle rConvOrSubId = do +postMLSCommitBundleToRemoteConv loc qusr mc con bundle rConvOrSubId = do -- only local users can send messages to remote conversations lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr -- only members may send commit bundles to a remote conversation flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) (convOfConvOrSub <$> rConvOrSubId) + senderIdentity <- + noteS @'MLSMissingSenderClient + =<< getSenderIdentity + qusr + mc + SMLSPlainText + (rmValue (cbCommitMsg bundle)) + resp <- runFederated rConvOrSubId $ fedClient @'Galley @"send-mls-commit-bundle" $ MLSMessageSendRequest { mmsrConvOrSubId = tUnqualified rConvOrSubId, mmsrSender = tUnqualified lusr, + mmsrSenderClient = ciClient senderIdentity, mmsrRawMessage = Base64ByteString (serializeCommitBundle bundle) } updates <- case resp of @@ -435,6 +451,7 @@ postMLSMessage :: CallsFed 'Galley "send-mls-message", CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Brig "get-mls-clients" ) => Local x -> @@ -530,6 +547,7 @@ postMLSMessageToLocalConv :: CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Brig "get-mls-clients" ) => Qualified UserId -> @@ -577,18 +595,21 @@ postMLSMessageToRemoteConv :: RawMLS SomeMessage -> Remote ConvOrSubConvId -> Sem r [LocalConversationUpdate] -postMLSMessageToRemoteConv loc qusr _senderClient con smsg rConvOrSubId = do +postMLSMessageToRemoteConv loc qusr mc con smsg rConvOrSubId = do -- only local users can send messages to remote conversations lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr -- only members may send messages to the remote conversation flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) (convOfConvOrSub <$> rConvOrSubId) + senderClient <- noteS @'MLSMissingSenderClient mc + resp <- runFederated rConvOrSubId $ fedClient @'Galley @"send-mls-message" $ MLSMessageSendRequest { mmsrConvOrSubId = tUnqualified rConvOrSubId, mmsrSender = tUnqualified lusr, + mmsrSenderClient = senderClient, mmsrRawMessage = Base64ByteString (rmRaw smsg) } updates <- case resp of @@ -699,6 +720,7 @@ processCommit :: Member SubConversationStore r, CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-conversation-updated", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Brig "get-mls-clients" ) => @@ -854,6 +876,7 @@ processCommitWithAction :: CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Brig "get-mls-clients" ) => Qualified UserId -> @@ -893,6 +916,7 @@ processInternalCommit :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Brig "get-mls-clients" ) => Qualified UserId -> @@ -1239,11 +1263,13 @@ type HasProposalActionEffects r = Member LegalHoldStore r, Member MemberStore r, Member ProposalStore r, + Member SubConversationStore r, Member TeamStore r, Member TinyLog r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) executeProposalAction :: @@ -1343,6 +1369,25 @@ executeProposalAction qusr con lconvOrSub action = do for_ (Map.assocs (paRemove action)) $ \(qtarget, clients) -> do removeMLSClients (cnvmlsGroupId mlsMeta) qtarget (Map.keysSet clients) + -- if this is a new subconversation, call `on-new-remote-conversation` on all + -- the remote backends involved in the main conversation + forOf_ _SubConv convOrSub $ \(mlsConv, subConv) -> do + when (cnvmlsEpoch (scMLSData subConv) == Epoch 0) $ do + let remoteDomains = + Set.fromList + ( map + (void . rmId) + (mcRemoteMembers mlsConv) + ) + let nrc = + NewRemoteSubConversation + { nrscConvId = mcId mlsConv, + nrscSubConvId = scSubConvId subConv, + nrscMlsData = scMLSData subConv + } + runFederatedConcurrently_ (toList remoteDomains) $ \_ -> do + void $ fedClient @'Galley @"on-new-remote-subconversation" nrc + pure (addEvents <> removeEvents) where checkRemoval :: diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 9f9617f35f3..79ac6534d26 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -40,11 +40,12 @@ import qualified Galley.Data.Conversation as Data import Galley.Data.Conversation.Types import Galley.Effects import Galley.Effects.FederatorAccess +import qualified Galley.Effects.FederatorAccess as Eff import qualified Galley.Effects.MemberStore as Eff -import Galley.Effects.SubConversationStore (SubConversationStore) import qualified Galley.Effects.SubConversationStore as Eff import Galley.Effects.SubConversationSupply (SubConversationSupply) import qualified Galley.Effects.SubConversationSupply as Eff +import Galley.Types.Conversations.Members import Imports import Polysemy import Polysemy.Error @@ -233,7 +234,8 @@ deleteSubConversation :: SubConversationSupply ] r, - CallsFed 'Galley "delete-sub-conversation" + CallsFed 'Galley "delete-sub-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> Qualified ConvId -> @@ -248,19 +250,22 @@ deleteSubConversation lusr qconv sconv dsc = qconv deleteLocalSubConversation :: - Members - '[ ConversationStore, - ErrorS 'ConvAccessDenied, - ErrorS 'ConvNotFound, - ErrorS 'MLSNotEnabled, - ErrorS 'MLSStaleMessage, - Input Env, - MemberStore, - Resource, - SubConversationStore, - SubConversationSupply - ] - r => + ( Members + '[ ConversationStore, + ErrorS 'ConvAccessDenied, + ErrorS 'ConvNotFound, + ErrorS 'MLSNotEnabled, + ErrorS 'MLSStaleMessage, + FederatorAccess, + Input Env, + MemberStore, + Resource, + SubConversationStore, + SubConversationSupply + ] + r, + CallsFed 'Galley "on-new-remote-subconversation" + ) => Qualified UserId -> Local ConvId -> SubConvId -> @@ -271,7 +276,7 @@ deleteLocalSubConversation qusr lcnvId scnvId dsc = do let cnvId = tUnqualified lcnvId cnv <- getConversationAndCheckMembership qusr lcnvId cs <- cnvmlsCipherSuite <$> noteS @'ConvNotFound (mlsMetadata cnv) - withCommitLock (dscGroupId dsc) (dscEpoch dsc) $ do + mlsData <- withCommitLock (dscGroupId dsc) (dscEpoch dsc) $ do sconv <- Eff.getSubConversation cnvId scnvId >>= noteS @'ConvNotFound @@ -287,6 +292,17 @@ deleteLocalSubConversation qusr lcnvId scnvId dsc = do -- the following overwrites any prior information about the subconversation Eff.createSubConversation cnvId scnvId cs (Epoch 0) newGid Nothing + pure (scMLSData sconv) + + -- notify all backends that the subconversation has a new ID + let remotes = bucketRemote (map rmId (convRemoteMembers cnv)) + Eff.runFederatedConcurrently_ remotes $ \_ -> do + fedClient @'Galley @"on-new-remote-subconversation" + NewRemoteSubConversation + { nrscConvId = cnvId, + nrscSubConvId = scnvId, + nrscMlsData = mlsData + } deleteRemoteSubConversation :: ( Members diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index fcad2b51e5f..80777887331 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -1116,11 +1116,13 @@ deleteTeamConversation :: GundeckAccess, Input Env, Input UTCTime, + SubConversationStore, TeamStore ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 3da18e48480..479caa616e1 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -715,7 +715,8 @@ instance GetFeatureConfig db LegalholdConfig where instance ( CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => SetFeatureConfig db LegalholdConfig where @@ -749,6 +750,7 @@ instance ListItems LegacyPaging ConvId, MemberStore, ProposalStore, + SubConversationStore, TeamFeatureStore db, TeamStore, TeamMemberStore InternalPaging, diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 06133de467e..ee5182b13a2 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -285,6 +285,7 @@ type UpdateConversationAccessEffects = Input UTCTime, MemberStore, ProposalStore, + SubConversationStore, TeamStore, TinyLog ] @@ -293,6 +294,7 @@ updateConversationAccess :: ( Members UpdateConversationAccessEffects r, CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "on-conversation-updated" ) => Local UserId -> @@ -309,6 +311,7 @@ updateConversationAccessUnqualified :: ( Members UpdateConversationAccessEffects r, CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "on-conversation-updated" ) => Local UserId -> @@ -339,11 +342,13 @@ updateConversationReceiptMode :: Input Env, Input UTCTime, MemberStore, + SubConversationStore, TinyLog ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "update-conversation" ) => Local UserId -> @@ -419,11 +424,13 @@ updateConversationReceiptModeUnqualified :: Input Env, Input UTCTime, MemberStore, + SubConversationStore, TinyLog ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "update-conversation" ) => Local UserId -> @@ -444,11 +451,13 @@ updateConversationMessageTimer :: FederatorAccess, GundeckAccess, Input Env, - Input UTCTime + Input UTCTime, + SubConversationStore ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -482,11 +491,13 @@ updateConversationMessageTimerUnqualified :: FederatorAccess, GundeckAccess, Input Env, - Input UTCTime + Input UTCTime, + SubConversationStore ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -509,11 +520,13 @@ deleteLocalConversation :: GundeckAccess, Input Env, Input UTCTime, + SubConversationStore, TeamStore ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -718,13 +731,15 @@ joinConversationByReusableCode :: Input Opts, Input UTCTime, MemberStore, + SubConversationStore, TeamStore, TeamFeatureStore db ] r, FeaturePersistentConstraint db GuestLinksConfig, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -752,12 +767,14 @@ joinConversationById :: Input Opts, Input UTCTime, MemberStore, + SubConversationStore, TeamStore, TeamFeatureStore db ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -781,12 +798,14 @@ joinConversation :: Input Opts, Input UTCTime, MemberStore, + SubConversationStore, TeamStore, TeamFeatureStore db ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -840,13 +859,15 @@ addMembers :: LegalHoldStore, MemberStore, ProposalStore, + SubConversationStore, TeamStore, TinyLog ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -883,13 +904,15 @@ addMembersUnqualifiedV2 :: LegalHoldStore, MemberStore, ProposalStore, + SubConversationStore, TeamStore, TinyLog ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -926,13 +949,15 @@ addMembersUnqualified :: LegalHoldStore, MemberStore, ProposalStore, + SubConversationStore, TeamStore, TinyLog ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -1024,11 +1049,13 @@ updateOtherMemberLocalConv :: GundeckAccess, Input Env, Input UTCTime, - MemberStore + MemberStore, + SubConversationStore ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local ConvId -> Local UserId -> @@ -1055,11 +1082,13 @@ updateOtherMemberUnqualified :: GundeckAccess, Input Env, Input UTCTime, - MemberStore + MemberStore, + SubConversationStore ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -1086,11 +1115,13 @@ updateOtherMember :: GundeckAccess, Input Env, Input UTCTime, - MemberStore + MemberStore, + SubConversationStore ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -1126,13 +1157,15 @@ removeMemberUnqualified :: Input UTCTime, MemberStore, ProposalStore, + SubConversationStore, TinyLog ] r, CallsFed 'Galley "leave-conversation", CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -1158,13 +1191,15 @@ removeMemberQualified :: Input UTCTime, MemberStore, ProposalStore, + SubConversationStore, TinyLog ] r, CallsFed 'Galley "leave-conversation", CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -1234,12 +1269,14 @@ removeMemberFromLocalConv :: Input UTCTime, MemberStore, ProposalStore, + SubConversationStore, TinyLog ] r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local ConvId -> Local UserId -> @@ -1454,11 +1491,13 @@ updateConversationName :: FederatorAccess, GundeckAccess, Input Env, - Input UTCTime + Input UTCTime, + SubConversationStore ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -1484,11 +1523,13 @@ updateUnqualifiedConversationName :: FederatorAccess, GundeckAccess, Input Env, - Input UTCTime + Input UTCTime, + SubConversationStore ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> @@ -1510,11 +1551,13 @@ updateLocalConversationName :: FederatorAccess, GundeckAccess, Input Env, - Input UTCTime + Input UTCTime, + SubConversationStore ] r, CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) => Local UserId -> ConnId -> diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 78771cefd57..e5e5be455cd 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -347,6 +347,9 @@ lookupGroupIdForSubConversation = "SELECT conv_id, domain, subconv_id from group insertEpochForSubConversation :: PrepQuery W (Epoch, ConvId, SubConvId) () insertEpochForSubConversation = "UPDATE subconversation set epoch = ? WHERE conv_id = ? AND subconv_id = ?" +listSubConversations :: PrepQuery R (Identity ConvId) (SubConvId, CipherSuiteTag, Epoch, Writetime Epoch, GroupId) +listSubConversations = "SELECT subconv_id, cipher_suite, epoch, WRITETIME(epoch), group_id FROM subconversation WHERE conv_id = ?" + -- Members ------------------------------------------------------------------ type MemberStatus = Int32 diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index 6dd6dca3ed5..be9e188ccb3 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -20,6 +20,7 @@ module Galley.Cassandra.SubConversation where import Cassandra import Cassandra.Util import Data.Id +import qualified Data.Map as Map import Data.Qualified import Data.Time.Clock import Galley.API.MLS.Types (SubConversation (..)) @@ -54,10 +55,6 @@ selectSubConversation convId subConvId = do }, scMembers = cm } - where - epochTimestamp :: Epoch -> Writetime Epoch -> Maybe UTCTime - epochTimestamp (Epoch 0) _ = Nothing - epochTimestamp _ (Writetime t) = Just t insertSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> Epoch -> GroupId -> Maybe OpaquePublicGroupState -> Client () insertSubConversation convId subConvId suite epoch groupId mPgs = @@ -83,6 +80,21 @@ deleteGroupId :: GroupId -> Client () deleteGroupId groupId = retry x5 $ write Cql.deleteGroupIdForSubconv (params LocalQuorum (Identity groupId)) +listSubConversations :: ConvId -> Client (Map SubConvId ConversationMLSData) +listSubConversations cid = do + subs <- retry x1 (query Cql.listSubConversations (params LocalQuorum (Identity cid))) + pure . Map.fromList $ do + (subId, cs, epoch, ts, gid) <- subs + pure + ( subId, + ConversationMLSData + { cnvmlsGroupId = gid, + cnvmlsEpoch = epoch, + cnvmlsEpochTimestamp = epochTimestamp epoch ts, + cnvmlsCipherSuite = cs + } + ) + interpretSubConversationStoreToCassandra :: Members '[Embed IO, Input ClientState] r => Sem (SubConversationStore ': r) a -> @@ -95,3 +107,11 @@ interpretSubConversationStoreToCassandra = interpret $ \case SetGroupIdForSubConversation gId cid sconv -> embedClient $ setGroupIdForSubConversation gId cid sconv SetSubConversationEpoch cid sconv epoch -> embedClient $ setEpochForSubConversation cid sconv epoch DeleteGroupIdForSubConversation groupId -> embedClient $ deleteGroupId groupId + ListSubConversations cid -> embedClient $ listSubConversations cid + +-------------------------------------------------------------------------------- +-- Utilities + +epochTimestamp :: Epoch -> Writetime Epoch -> Maybe UTCTime +epochTimestamp (Epoch 0) _ = Nothing +epochTimestamp _ (Writetime t) = Just t diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index 62cd8d5f647..37fea753aa4 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -36,6 +36,7 @@ module Galley.Effects ClientStore, CodeStore, ConversationStore, + SubConversationStore, CustomBackendStore, LegalHoldStore, MemberStore, diff --git a/services/galley/src/Galley/Effects/FederatorAccess.hs b/services/galley/src/Galley/Effects/FederatorAccess.hs index 1c4a1900871..d4d5d641995 100644 --- a/services/galley/src/Galley/Effects/FederatorAccess.hs +++ b/services/galley/src/Galley/Effects/FederatorAccess.hs @@ -65,6 +65,6 @@ makeSem ''FederatorAccess runFederatedConcurrently_ :: (KnownComponent c, Foldable f, Functor f, Member FederatorAccess r) => f (Remote a) -> - (Remote [a] -> FederatorClient c ()) -> + (Remote [a] -> FederatorClient c x) -> Sem r () runFederatedConcurrently_ xs = void . runFederatedConcurrently xs diff --git a/services/galley/src/Galley/Effects/SubConversationStore.hs b/services/galley/src/Galley/Effects/SubConversationStore.hs index cb6f41b05e4..29d5d0f3ce2 100644 --- a/services/galley/src/Galley/Effects/SubConversationStore.hs +++ b/services/galley/src/Galley/Effects/SubConversationStore.hs @@ -24,8 +24,8 @@ import Data.Qualified import Galley.API.MLS.Types import Imports import Polysemy +import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite -import Wire.API.MLS.Epoch import Wire.API.MLS.Group import Wire.API.MLS.PublicGroupState import Wire.API.MLS.SubConversation @@ -38,5 +38,6 @@ data SubConversationStore m a where SetGroupIdForSubConversation :: GroupId -> Qualified ConvId -> SubConvId -> SubConversationStore m () SetSubConversationEpoch :: ConvId -> SubConvId -> Epoch -> SubConversationStore m () DeleteGroupIdForSubConversation :: GroupId -> SubConversationStore m () + ListSubConversations :: ConvId -> SubConversationStore m (Map SubConvId ConversationMLSData) makeSem ''SubConversationStore diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index e337a96aa6f..f88336c54f2 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -2954,8 +2954,10 @@ deleteRemoteMemberConvLocalQualifiedOk = do Left err -> assertFailure err Right e -> assertLeaveEvent qconvId qAlice [qChad] e - let [remote1GalleyFederatedRequest] = fedRequestsForDomain remoteDomain1 Galley federatedRequests - [remote2GalleyFederatedRequest] = fedRequestsForDomain remoteDomain2 Galley federatedRequests + remote1GalleyFederatedRequest <- + assertOne (filter ((== "on-conversation-updated") . frRPC) (fedRequestsForDomain remoteDomain1 Galley federatedRequests)) + remote2GalleyFederatedRequest <- + assertOne (filter ((== "on-conversation-updated") . frRPC) (fedRequestsForDomain remoteDomain2 Galley federatedRequests)) assertRemoveUpdate remote1GalleyFederatedRequest qconvId qAlice [qUnqualified qChad, qUnqualified qDee] qChad assertRemoveUpdate remote2GalleyFederatedRequest qconvId qAlice [qUnqualified qEve] qChad diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 3cc99e10e08..6b290889b8a 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -22,16 +22,16 @@ module API.MLS (tests) where import API.MLS.Mocks import API.MLS.Util import API.Util -import Bilge hiding (head) +import Bilge hiding (empty, head) import Bilge.Assert import Cassandra +import Control.Applicative import Control.Lens (view) import qualified Control.Monad.State as State import Crypto.Error import qualified Crypto.PubKey.Ed25519 as Ed25519 import qualified Data.Aeson as Aeson import Data.Binary.Put -import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import Data.Domain import Data.Id @@ -48,7 +48,6 @@ import Data.Time import Federator.MockServer hiding (withTempMockFederator) import Imports import qualified Network.Wai.Utilities.Error as Wai -import Test.QuickCheck (Arbitrary (arbitrary), generate) import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (Second), (#)) import qualified Test.Tasty.Cannon as WS @@ -60,6 +59,7 @@ import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error.Galley +import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential @@ -226,6 +226,7 @@ tests s = [ test s "get subconversation of remote conversation - member" (testGetRemoteSubConv True), test s "get subconversation of remote conversation - not member" (testGetRemoteSubConv False), test s "join remote subconversation" testJoinRemoteSubConv, + test s "backends are notified about subconvs when a user joins" testRemoteSubConvNotificationWhenUserJoins, test s "reset a subconversation - member" (testDeleteRemoteSubConv True), test s "reset a subconversation - not member" (testDeleteRemoteSubConv False) ], @@ -393,12 +394,9 @@ testAddUserWithBundle = do "Users added to an MLS group should find it when listing conversations" (qcnv `elem` map cnvQualifiedId convs) - returnedGS <- - fmap responseBody $ - getGroupInfo (qUnqualified alice) (fmap Conv qcnv) - returnedGS + liftIO $ mpPublicGroupState commit @?= Just returnedGS testAddUserWithBundleIncompleteWelcome :: TestM () testAddUserWithBundleIncompleteWelcome = do @@ -423,7 +421,7 @@ testAddUserWithBundleIncompleteWelcome = do bundle <- createBundle commit err <- responseJsonError - =<< postCommitBundle (mpSender commit) bundle + =<< localPostCommitBundle (mpSender commit) bundle >= sendAndConsumeCommitBundle - pgs <- - LBS.toStrict . fromJust . responseBody - <$> getGroupInfo (ciUser alice1) (fmap Conv qcnv) + pgs <- liftTest $ getGroupInfo (cidQualifiedUser alice1) (fmap Conv qcnv) mp <- createExternalCommit bob1 (Just pgs) (fmap Conv qcnv) bundle <- createBundle mp - postCommitBundle (mpSender mp) bundle + localPostCommitBundle (mpSender mp) bundle !!! const 404 === statusCode testExternalCommitSameClient :: TestM () @@ -1226,6 +1220,7 @@ testRemoteToLocal = do MLSMessageSendRequest { mmsrConvOrSubId = Conv (qUnqualified qcnv), mmsrSender = qUnqualified bob, + mmsrSenderClient = ciClient bob1, mmsrRawMessage = Base64ByteString (mpMessage message) } @@ -1269,6 +1264,7 @@ testRemoteToLocalWrongConversation = do MLSMessageSendRequest { mmsrConvOrSubId = Conv randomConfId, mmsrSender = qUnqualified bob, + mmsrSenderClient = ciClient bob1, mmsrRawMessage = Base64ByteString (mpMessage message) } @@ -1302,6 +1298,7 @@ testRemoteNonMemberToLocal = do MLSMessageSendRequest { mmsrConvOrSubId = Conv (qUnqualified qcnv), mmsrSender = qUnqualified bob, + mmsrSenderClient = ciClient bob1, mmsrRawMessage = Base64ByteString (mpMessage message) } @@ -1317,7 +1314,7 @@ propNonExistingConv = do runMLSTest $ do [alice1, bob1] <- traverse createMLSClient [alice, bob] void $ uploadNewKeyPackage bob1 - createGroup alice1 "test_group" + void $ setupFakeMLSGroup alice1 [prop] <- createAddProposals alice1 [bob] postMessage alice1 (mpMessage prop) !!! do @@ -1876,11 +1873,8 @@ testGetGroupInfoOfLocalConv = do -- check the group info matches gs <- assertJust (mpPublicGroupState commit) - returnedGS <- - fmap responseBody $ - getGroupInfo (qUnqualified alice) (fmap Conv qcnv) - returnedGS + returnedGS <- liftTest $ getGroupInfo alice (fmap Conv qcnv) + liftIO $ gs @=? returnedGS testGetGroupInfoOfRemoteConv :: TestM () testGetGroupInfoOfRemoteConv = do @@ -1895,22 +1889,18 @@ testGetGroupInfoOfRemoteConv = do mp <- createAddCommit alice1 [bob] traverse_ consumeWelcome (mpWelcome mp) - receiveNewRemoteConv qcnv groupId + receiveNewRemoteConv (fmap Conv qcnv) groupId receiveOnConvUpdated qcnv alice bob let fakeGroupState = "\xde\xad\xbe\xef" - let mock = queryGroupStateMock fakeGroupState bob + mock = queryGroupStateMock fakeGroupState bob (_, reqs) <- withTempMockFederator' mock $ do - res <- - fmap responseBody $ - getGroupInfo (qUnqualified bob) (fmap Conv qcnv) - >= sendAndConsumeCommit withMLSDisabled $ - getGroupInfo (qUnqualified alice) (fmap Conv qcnv) + localGetGroupInfo (qUnqualified alice) (fmap Conv qcnv) !!! assertMLSNotEnabled deleteSubConversationDisabled :: TestM () @@ -2247,7 +2244,7 @@ testJoinSubConv = do =<< getSubConv (qUnqualified bob) qcnv subId >= postCommitBundle (mpSender commit)) + (createBundle commit >>= localPostCommitBundle (mpSender commit)) !!! do const 400 === statusCode const (Just "Add proposals in subconversations are not supported") @@ -2328,16 +2325,135 @@ testAddClientSubConvFailure = do (Epoch 1) (pscEpoch finalSub) --- FUTUREWORK: implement the following tests +-- FUTUREWORK: implement the following test testRemoveClientSubConv :: TestM () testRemoveClientSubConv = pure () testJoinRemoteSubConv :: TestM () -testJoinRemoteSubConv = pure () +testJoinRemoteSubConv = do + [alice, bob] <- createAndConnectUsers [Just "alice.example.com", Nothing] + + runMLSTest $ do + alice1 <- createFakeMLSClient alice + bob1 <- createMLSClient bob + void $ uploadNewKeyPackage bob1 + + -- setup fake group for the subconversation + let subId = SubConvId "conference" + (subGroupId, qcnv) <- setupFakeMLSGroup alice1 + let qcs = convsub qcnv (Just subId) + initialCommit <- createPendingProposalCommit alice1 + + -- create a fake group ID for the main (we don't need the actual group) + mainGroupId <- fakeGroupId + + -- inform backend about the main conversation + receiveNewRemoteConv (fmap Conv qcnv) mainGroupId + receiveOnConvUpdated qcnv alice bob + + -- inform backend about the subconversation + receiveNewRemoteConv qcs subGroupId + + -- bob joins subconversation + let pgs = mpPublicGroupState initialCommit + let mock = queryGroupStateMock (fold pgs) bob <|> sendMessageMock + (_, reqs) <- withTempMockFederator' mock $ do + commit <- createExternalCommit bob1 Nothing qcs + sendAndConsumeCommitBundle commit + + -- check that commit bundle is sent to remote backend + fr <- assertOne (filter ((== "send-mls-commit-bundle") . frRPC) reqs) + liftIO $ do + mmsr <- assertJust (Aeson.decode (frBody fr)) + mmsrConvOrSubId mmsr @?= qUnqualified qcs + mmsrSender mmsr @?= ciUser bob1 + mmsrSenderClient mmsr @?= ciClient bob1 + +testRemoteSubConvNotificationWhenUserJoins :: TestM () +testRemoteSubConvNotificationWhenUserJoins = do + [alice, bob] <- createAndConnectUsers [Nothing, Just "bob.example.com"] + + runMLSTest $ do + alice1 <- createMLSClient alice + bob1 <- createFakeMLSClient bob + + (_, qcnv) <- setupMLSGroup alice1 + void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + let subId = SubConvId "conference" + s <- State.get + void $ createSubConv qcnv alice1 subId + -- revert first commit and subconv + void . replicateM 2 $ rollBackClient alice1 + State.put s + + (_, reqs) <- + withTempMockFederator' (receiveCommitMock [bob1] <|> welcomeMock) $ + createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + do + req <- assertOne $ filter ((== "on-new-remote-conversation") . frRPC) reqs + nrc <- assertOne (toList (Aeson.decode (frBody req))) + liftIO $ nrcConvId nrc @?= qUnqualified qcnv + do + req <- assertOne $ filter ((== "on-new-remote-subconversation") . frRPC) reqs + nrsc <- assertOne (toList (Aeson.decode (frBody req))) + liftIO $ nrscConvId nrsc @?= qUnqualified qcnv + liftIO $ nrscSubConvId nrsc @?= subId testRemoteUserJoinSubConv :: TestM () -testRemoteUserJoinSubConv = pure () +testRemoteUserJoinSubConv = do + [alice, bob] <- createAndConnectUsers [Nothing, Just "bob.example.com"] + + runMLSTest $ do + alice1 <- createMLSClient alice + (_, qcnv) <- setupMLSGroup alice1 + + bob1 <- createFakeMLSClient bob + void $ do + commit <- createAddCommit alice1 [bob] + withTempMockFederator' (receiveCommitMock [bob1] <|> welcomeMock) $ + sendAndConsumeCommit commit + + let mock = + asum + [ "on-new-remote-subconversation" ~> EmptyResponse, + messageSentMock + ] + let subId = SubConvId "conference" + (psc, reqs) <- withTempMockFederator' mock $ createSubConv qcnv alice1 subId + let qcs = convsub qcnv (Just subId) + + -- check that the remote backend is notified when a subconversation is + -- created locally + req <- assertOne $ filter ((== "on-new-remote-subconversation") . frRPC) reqs + nrsc <- assertOne . toList $ Aeson.decode (frBody req) + liftIO $ do + nrscConvId nrsc @?= qUnqualified qcnv + nrscSubConvId nrsc @?= subId + let mls = nrscMlsData nrsc + cnvmlsGroupId mls @?= pscGroupId psc + cnvmlsEpoch mls @?= Epoch 0 + + -- bob joins the subconversation + void $ createExternalCommit bob1 Nothing qcs >>= sendAndConsumeCommitBundle + + -- check that bob is now part of the subconversation + liftTest $ do + psc' <- + responseJsonError + =<< getSubConv (qUnqualified alice) qcnv subId + >= sendAndConsumeCommitBundle + + liftIO $ + assertBool "Unexpected on-new-remote-subconversation" $ + all ((/= "on-new-remote-subconversation") . frRPC) reqs' testSendMessageSubConv :: TestM () testSendMessageSubConv = do @@ -2471,7 +2587,7 @@ testRemoteMemberDeleteSubConv isAMember = do liftTest $ responseJsonError =<< getSubConv (qUnqualified alice) qcnv scnv - resetGroup alice1 (pscGroupId sub) + resetGroup alice1 (fmap (flip SubConv scnv) qcnv) (pscGroupId sub) pure (qUnqualified qcnv, pscGroupId sub, pscEpoch sub) @@ -2485,11 +2601,18 @@ testRemoteMemberDeleteSubConv isAMember = do dscreqEpoch = epoch } - fedGalleyClient <- view tsFedGalleyClient -- Bob is a member of the parent conversation so he's allowed to delete the -- subconversation. - res <- - runFedClient @"delete-sub-conversation" fedGalleyClient bobDomain delReq + (res, reqs) <- + withTempMockFederator' ("on-new-remote-subconversation" ~> EmptyResponse) $ do + fedGalleyClient <- view tsFedGalleyClient + runFedClient @"delete-sub-conversation" fedGalleyClient bobDomain delReq + when isAMember $ do + req <- assertOne (filter ((== "on-new-remote-subconversation") . frRPC) reqs) + nrsc <- assertOne (toList (Aeson.decode (frBody req))) + liftIO $ do + nrscConvId nrsc @?= cnv + nrscSubConvId nrsc @?= scnv if isAMember then expectSuccess res else expectFailure ConvNotFound res where @@ -2553,7 +2676,7 @@ testDeleteSubConvStale = do =<< getSubConv (qUnqualified alice) qcnv sconv >= sendAndConsumeCommitBundle @@ -2594,7 +2717,8 @@ testDeleteRemoteSubConv isAMember = do withTempMockFederator' mock $ deleteSubConv (qUnqualified alice) qconv sconv dsc (), "on-new-remote-conversation" ~> EmptyResponse, + "on-new-remote-subconversation" ~> EmptyResponse, "get-mls-clients" ~> Set.fromList ( map (flip ClientInfo True . ciClient) clients @@ -56,7 +57,11 @@ welcomeMock :: Mock LByteString welcomeMock = "mls-welcome" ~> MLSWelcomeSent sendMessageMock :: Mock LByteString -sendMessageMock = "send-mls-message" ~> MLSMessageResponseUpdates [] +sendMessageMock = + asum + [ "send-mls-message" ~> MLSMessageResponseUpdates [], + "send-mls-commit-bundle" ~> MLSMessageResponseUpdates [] + ] claimKeyPackagesMock :: KeyPackageBundle -> Mock LByteString claimKeyPackagesMock kpb = "claim-key-packages" ~> kpb diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index f1df1f5f325..96166ac18fd 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -126,7 +126,7 @@ postMessage sender msg = do . bytes msg ) -postCommitBundle :: +localPostCommitBundle :: ( HasCallStack, MonadIO m, MonadCatch m, @@ -137,7 +137,7 @@ postCommitBundle :: ClientIdentity -> ByteString -> m ResponseLBS -postCommitBundle sender bundle = do +localPostCommitBundle sender bundle = do galley <- viewGalley post ( galley @@ -149,6 +149,58 @@ postCommitBundle sender bundle = do . bytes bundle ) +remotePostCommitBundle :: + ( MonadIO m, + MonadReader TestSetup m + ) => + Remote ClientIdentity -> + Qualified ConvOrSubConvId -> + ByteString -> + m [Event] +remotePostCommitBundle rsender qcs bundle = do + client <- view tsFedGalleyClient + let msr = + MLSMessageSendRequest + { mmsrConvOrSubId = qUnqualified qcs, + mmsrSender = ciUser (tUnqualified rsender), + mmsrSenderClient = ciClient (tUnqualified rsender), + mmsrRawMessage = Base64ByteString bundle + } + runFedClient + @"send-mls-commit-bundle" + client + (tDomain rsender) + msr + >>= liftIO . \case + MLSMessageResponseError e -> + assertFailure $ + "error while receiving commit bundle: " <> show e + MLSMessageResponseProtocolError e -> + assertFailure $ + "protocol error while receiving commit bundle: " <> T.unpack e + MLSMessageResponseProposalFailure e -> + assertFailure $ + "proposal failure while receiving commit bundle: " <> displayException e + MLSMessageResponseUpdates _ -> pure [] + +postCommitBundle :: + HasCallStack => + ClientIdentity -> + Qualified ConvOrSubConvId -> + ByteString -> + TestM [Event] +postCommitBundle sender qcs bundle = do + loc <- qualifyLocal () + foldQualified + loc + ( \_ -> + fmap mmssEvents . responseJsonError + =<< localPostCommitBundle sender bundle + remotePostCommitBundle rsender qcs bundle) + (cidQualifiedUser sender $> sender) + postWelcome :: (MonadIO m, MonadHttp m, HasGalley m, HasCallStack) => UserId -> ByteString -> m ResponseLBS postWelcome uid welcome = do galley <- viewGalley @@ -198,6 +250,7 @@ data MLSState = MLSState -- | users expected to receive a welcome message after the next commit mlsNewMembers :: Set ClientIdentity, mlsGroupId :: Maybe GroupId, + mlsConvId :: Maybe (Qualified ConvOrSubConvId), mlsEpoch :: Word64 } @@ -243,6 +296,7 @@ runMLSTest (MLSTest m) = mlsMembers = mempty, mlsNewMembers = mempty, mlsGroupId = Nothing, + mlsConvId = Nothing, mlsEpoch = 0 } @@ -413,8 +467,9 @@ setupMLSGroupWithConv convAction creator = do fromJust (preview (to cnvProtocol . _ProtocolMLS . to cnvmlsGroupId) conv) - createGroup creator groupId - pure (groupId, cnvQualifiedId conv) + let qcnv = cnvQualifiedId conv + createGroup creator (fmap Conv qcnv) groupId + pure (groupId, qcnv) -- | Create conversation and corresponding group. setupMLSGroup :: HasCallStack => ClientIdentity -> MLSTest (GroupId, Qualified ConvId) @@ -439,27 +494,34 @@ setupMLSSelfGroup creator = setupMLSGroupWithConv action creator (getSelfConv (ciUser creator)) GroupId -> MLSTest () -createGroup cid gid = do +createGroup :: ClientIdentity -> Qualified ConvOrSubConvId -> GroupId -> MLSTest () +createGroup cid qcs gid = do State.gets mlsGroupId >>= \case Just _ -> liftIO $ assertFailure "only one group can be created" Nothing -> pure () - resetGroup cid gid + resetGroup cid qcs gid -resetGroup :: ClientIdentity -> GroupId -> MLSTest () -resetGroup cid gid = do +resetGroup :: ClientIdentity -> Qualified ConvOrSubConvId -> GroupId -> MLSTest () +resetGroup cid qcs gid = do groupJSON <- mlscli cid ["group", "create", T.unpack (toBase64Text (unGroupId gid))] Nothing g <- nextGroupFile cid liftIO $ BS.writeFile g groupJSON State.modify $ \s -> s { mlsGroupId = Just gid, + mlsConvId = Just qcs, mlsMembers = Set.singleton cid, mlsEpoch = 0, mlsNewMembers = mempty } +getConvId :: MLSTest (Qualified ConvOrSubConvId) +getConvId = + State.gets mlsConvId + >>= maybe (liftIO (assertFailure "Uninitialised test conversation")) pure + createSubConv :: + HasCallStack => Qualified ConvId -> ClientIdentity -> SubConvId -> @@ -471,21 +533,27 @@ createSubConv qcnv creator subId = do =<< getSubConv (ciUser creator) qcnv subId >= sendAndConsumeCommitBundle getSC -- | Create a local group only without a conversation. This simulates creating -- an MLS conversation on a remote backend. -setupFakeMLSGroup :: ClientIdentity -> MLSTest (GroupId, Qualified ConvId) +setupFakeMLSGroup :: + HasCallStack => + ClientIdentity -> + MLSTest (GroupId, Qualified ConvId) setupFakeMLSGroup creator = do - groupId <- - liftIO $ - fmap (GroupId . BS.pack) (replicateM 32 (generate arbitrary)) - createGroup creator groupId + groupId <- fakeGroupId qcnv <- randomQualifiedId (ciDomain creator) + createGroup creator (fmap Conv qcnv) groupId pure (groupId, qcnv) +fakeGroupId :: MLSTest GroupId +fakeGroupId = + liftIO $ + fmap (GroupId . BS.pack) (replicateM 32 (generate arbitrary)) + keyPackageFile :: HasCallStack => ClientIdentity -> KeyPackageRef -> MLSTest FilePath keyPackageFile qcid ref = State.gets $ \mls -> @@ -561,6 +629,7 @@ bundleKeyPackages bundle = do createAddCommit :: HasCallStack => ClientIdentity -> [Qualified UserId] -> MLSTest MessagePackage createAddCommit cid users = do kps <- concat <$> traverse (bundleKeyPackages <=< claimKeyPackages cid) users + liftIO $ assertBool "no key packages could be claimed" (not (null kps)) createAddCommitWithKeyPackages cid kps createExternalCommit :: @@ -574,11 +643,7 @@ createExternalCommit qcid mpgs qcs = do gNew <- nextGroupFile qcid pgsFile <- liftIO $ emptyTempFile bd "pgs" pgs <- case mpgs of - Nothing -> - LBS.toStrict . fromJust . responseBody - <$> ( getGroupInfo (ciUser qcid) qcs - liftTest $ getGroupInfo (cidQualifiedUser qcid) qcs Just v -> pure v commit <- mlscli @@ -912,12 +977,9 @@ sendAndConsumeCommitBundle :: MessagePackage -> MLSTest [Event] sendAndConsumeCommitBundle mp = do + qcs <- getConvId bundle <- createBundle mp - events <- - fmap mmssEvents - . responseJsonError - =<< postCommitBundle (mpSender mp) bundle - - Qualified ConvId -> + Qualified ConvOrSubConvId -> GroupId -> m () -receiveNewRemoteConv conv gid = do +receiveNewRemoteConv qcs gid = do client <- view tsFedGalleyClient - let nrc = - NewRemoteConversation (qUnqualified conv) $ - ProtocolMLS - ( ConversationMLSData + case qUnqualified qcs of + Conv c -> do + let nrc = + NewRemoteConversation c $ + ProtocolMLS + ( ConversationMLSData + gid + (Epoch 1) + (Just (UTCTime (fromGregorian 2020 8 29) 0)) + MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) + void $ + runFedClient + @"on-new-remote-conversation" + client + (qDomain qcs) + nrc + SubConv c s -> do + let nrc = + NewRemoteSubConversation c s $ + ConversationMLSData gid (Epoch 1) (Just (UTCTime (fromGregorian 2020 8 29) 0)) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - ) - void $ - runFedClient - @"on-new-remote-conversation" - client - (qDomain conv) - nrc + void $ + runFedClient + @"on-new-remote-subconversation" + client + (qDomain qcs) + nrc receiveOnConvUpdated :: (MonadReader TestSetup m, MonadIO m) => @@ -1026,7 +1104,22 @@ receiveOnConvUpdated conv origUser joiner = do (qDomain conv) cu -getGroupInfo :: +getGroupInfo :: HasCallStack => Qualified UserId -> Qualified ConvOrSubConvId -> TestM ByteString +getGroupInfo qusr qcs = do + loc <- qualifyLocal () + foldQualified + loc + ( \lusr -> + fmap (LBS.toStrict . fromJust . responseBody) $ + localGetGroupInfo + (tUnqualified lusr) + qcs + remoteGetGroupInfo rusr qcs) + qusr + +localGetGroupInfo :: ( HasCallStack, MonadIO m, MonadCatch m, @@ -1037,7 +1130,7 @@ getGroupInfo :: UserId -> Qualified ConvOrSubConvId -> m ResponseLBS -getGroupInfo sender qcs = do +localGetGroupInfo sender qcs = do galley <- viewGalley case qUnqualified qcs of Conv cnv -> @@ -1067,6 +1160,23 @@ getGroupInfo sender qcs = do . zConn "conn" ) +remoteGetGroupInfo :: + Remote UserId -> + Qualified ConvOrSubConvId -> + TestM ByteString +remoteGetGroupInfo rusr qcs = do + client <- view tsFedGalleyClient + GetGroupInfoResponseState (Base64ByteString pgs) <- + runFedClient + @"query-group-info" + client + (tDomain rusr) + GetGroupInfoRequest + { ggireqConv = qUnqualified qcs, + ggireqSender = tUnqualified rusr + } + pure pgs + getSelfConv :: UserId -> TestM ResponseLBS From 4eb9be0bfd18f3bba79318e98ebd782498548b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 2 Feb 2023 16:55:31 +0100 Subject: [PATCH 015/225] [FS-1488] Fix self-conversation creator key package mapping (#3055) * Add a failing test for recreating a client * Add a function description * A better name for a backend submitting a remove proposal function * Add to Brig the creator key package ref for self convs * Fix the test (remove alice1 from the local state) * Map the self conv creator's ref in Brig * Streamline creator mapping for subconvs --------- Co-authored-by: Stefan Matting --- .../3-bug-fixes/mls-self-conv-creator-ref | 1 + services/brig/src/Brig/Data/MLS/KeyPackage.hs | 6 ++- services/galley/src/Galley/API/Clients.hs | 3 ++ services/galley/src/Galley/API/MLS/Message.hs | 15 ++------ services/galley/src/Galley/API/MLS/Removal.hs | 10 ++--- services/galley/test/integration/API/MLS.hs | 38 +++++++++++++++++++ 6 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 changelog.d/3-bug-fixes/mls-self-conv-creator-ref diff --git a/changelog.d/3-bug-fixes/mls-self-conv-creator-ref b/changelog.d/3-bug-fixes/mls-self-conv-creator-ref new file mode 100644 index 00000000000..8ba14ebd2f9 --- /dev/null +++ b/changelog.d/3-bug-fixes/mls-self-conv-creator-ref @@ -0,0 +1 @@ +Map the MLS self-conversation creator's key package reference in Brig diff --git a/services/brig/src/Brig/Data/MLS/KeyPackage.hs b/services/brig/src/Brig/Data/MLS/KeyPackage.hs index 2bdd3abaaca..17d4b0617cb 100644 --- a/services/brig/src/Brig/Data/MLS/KeyPackage.hs +++ b/services/brig/src/Brig/Data/MLS/KeyPackage.hs @@ -186,7 +186,7 @@ keyPackageRefSetConvId ref convId = do q = "UPDATE mls_key_package_refs SET conv_domain = ?, conv = ? WHERE ref = ? IF EXISTS" addKeyPackageRef :: MonadClient m => KeyPackageRef -> NewKeyPackageRef -> m () -addKeyPackageRef ref nkpr = do +addKeyPackageRef ref nkpr = retry x5 $ write q @@ -207,7 +207,9 @@ updateKeyPackageRef :: MonadClient m => KeyPackageRef -> KeyPackageRef -> m () updateKeyPackageRef prevRef newRef = void . runMaybeT $ do backup <- backupKeyPackageMeta prevRef - lift $ restoreKeyPackageMeta newRef backup >> deleteKeyPackage prevRef + lift $ do + restoreKeyPackageMeta newRef backup + deleteKeyPackage prevRef -------------------------------------------------------------------------------- -- Utilities diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index d671b33621b..cac39d4655e 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -85,6 +85,9 @@ addClientH (usr ::: clt) = do E.createClient usr clt pure empty +-- | Remove a client from conversations it is part of according to the +-- conversation protocol (Proteus or MLS). In addition, remove the client from +-- the "clients" table in Galley. rmClientH :: forall p1 r. ( p1 ~ CassandraPaging, diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index d860a7cf306..555fd9892c8 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -833,7 +833,7 @@ processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = wi -- fetch backend remove proposals of the previous epoch kpRefs <- getPendingBackendRemoveProposals (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub') epoch -- requeue backend remove proposals for the current epoch - removeClientsWithClientMap lConvOrSub' kpRefs qusr + createAndSendRemoveProposals lConvOrSub' kpRefs qusr where derefUser :: ClientMap -> Qualified UserId -> Sem r (ClientIdentity, KeyPackageRef) derefUser cm user = case Map.assocs cm of @@ -947,10 +947,7 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co (True, SelfConv, [], Conv _) -> do creatorClient <- noteS @'MLSMissingSenderClient senderClient let creatorRef = fromMaybe senderRef updatePathRef - addMLSClients - (cnvmlsGroupId mlsMeta) - qusr - (Set.singleton (creatorClient, creatorRef)) + updateKeyPackageMapping lConvOrSub qusr creatorClient Nothing creatorRef (True, SelfConv, _, _) -> -- this is a newly created (sub)conversation, and it should -- contain exactly one client (the creator) @@ -973,13 +970,7 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co unless (isClientMember (mkClientIdentity qusr creatorClient) (mcMembers parentConv)) $ throwS @'MLSSubConvClientNotInParent let creatorRef = fromMaybe senderRef updatePathRef - addKeyPackageRef creatorRef qusr creatorClient $ - tUntagged (convOfConvOrSub . idForConvOrSub <$> lConvOrSub) - addMLSClients - (cnvmlsGroupId mlsMeta) - qusr - (Set.singleton (creatorClient, creatorRef)) - -- uninitialised conversations should contain exactly one client + updateKeyPackageMapping lConvOrSub qusr creatorClient Nothing creatorRef (_, _, _, _) -> throw (InternalErrorWithDescription "Unexpected creator client set") pure $ pure () -- no key package ref update necessary diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 3507ba50a9e..227d3d32182 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -16,7 +16,7 @@ -- with this program. If not, see . module Galley.API.MLS.Removal - ( removeClientsWithClientMap, + ( createAndSendRemoveProposals, removeClient, removeUser, ) @@ -51,7 +51,7 @@ import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation -- | Send remove proposals for a set of clients to clients in the ClientMap. -removeClientsWithClientMap :: +createAndSendRemoveProposals :: ( Members '[ Input UTCTime, TinyLog, @@ -69,7 +69,7 @@ removeClientsWithClientMap :: t KeyPackageRef -> Qualified UserId -> Sem r () -removeClientsWithClientMap lConvOrSubConv cs qusr = do +createAndSendRemoveProposals lConvOrSubConv cs qusr = do let meta = mlsMetaConvOrSub (tUnqualified lConvOrSubConv) mKeyPair <- getMLSRemovalKey case mKeyPair of @@ -113,7 +113,7 @@ removeClient lc qusr cid = do for_ mMlsConv $ \mlsConv -> do -- FUTUREWORK: also remove the client from from subconversations of lc let cidAndKPs = maybeToList (cmLookupRef (mkClientIdentity qusr cid) (mcMembers mlsConv)) - removeClientsWithClientMap (qualifyAs lc (Conv mlsConv)) cidAndKPs qusr + createAndSendRemoveProposals (qualifyAs lc (Conv mlsConv)) cidAndKPs qusr -- | Send remove proposals for all clients of the user to the local conversation. removeUser :: @@ -139,4 +139,4 @@ removeUser lc qusr = do for_ mMlsConv $ \mlsConv -> do -- FUTUREWORK: also remove the client from from subconversations of lc let kprefs = toList (Map.findWithDefault mempty qusr (mcMembers mlsConv)) - removeClientsWithClientMap (qualifyAs lc (Conv mlsConv)) kprefs qusr + createAndSendRemoveProposals (qualifyAs lc (Conv mlsConv)) kprefs qusr diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 6b290889b8a..9b4fbcb1e21 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -162,6 +162,7 @@ tests s = testGroup "Backend-side External Remove Proposals" [ test s "local conversation, local user deleted" testBackendRemoveProposalLocalConvLocalUser, + test s "local conversation, recreate client" testBackendRemoveProposalRecreateClient, test s "local conversation, remote user deleted" testBackendRemoveProposalLocalConvRemoteUser, test s "local conversation, creator leaving" testBackendRemoveProposalLocalConvLocalLeaverCreator, test s "local conversation, local committer leaving" testBackendRemoveProposalLocalConvLocalLeaverCommitter, @@ -1577,6 +1578,40 @@ propUnsupported = do -- support AppAck proposals postMessage alice1 msgData !!! const 201 === statusCode +testBackendRemoveProposalRecreateClient :: TestM () +testBackendRemoveProposalRecreateClient = do + alice <- randomQualifiedUser + runMLSTest $ do + alice1 <- createMLSClient alice + (_, qcnv) <- setupMLSSelfGroup alice1 + + let cnv = Conv <$> qcnv + + void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + + (_, ref) <- assertOne =<< getClientsFromGroupState alice1 alice + + liftTest $ + deleteClient (qUnqualified alice) (ciClient alice1) (Just defPassword) + !!! const 200 === statusCode + State.modify $ \mls -> + mls + { mlsMembers = Set.difference (mlsMembers mls) (Set.singleton alice1) + } + + alice2 <- createMLSClient alice + proposal <- mlsBracket [alice2] $ \[wsA] -> do + void $ + createExternalCommit alice2 Nothing cnv + >>= sendAndConsumeCommitBundle + WS.assertMatch (5 # WS.Second) wsA $ + wsAssertBackendRemoveProposal alice qcnv ref + + consumeMessage1 alice2 proposal + void $ createPendingProposalCommit alice2 >>= sendAndConsumeCommitBundle + + void $ createApplicationMessage alice2 "hello" >>= sendAndConsumeMessage + testBackendRemoveProposalLocalConvLocalUser :: TestM () testBackendRemoveProposalLocalConvLocalUser = do [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) @@ -2046,6 +2081,9 @@ testRemoteUserPostsCommitBundle = do pure () +-- FUTUREWORK: New clients should be adding themselves via external commits, and +-- they shouldn't be added by another client. Change the test so external +-- commits are used. testSelfConversation :: TestM () testSelfConversation = do alice <- randomQualifiedUser From 85b5289f643317b4fb919f5a908a9412cc1d8682 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 3 Feb 2023 10:31:35 +0100 Subject: [PATCH 016/225] MLS integration testing: make client group state transparent in state (#3057) --- services/galley/test/integration/API/MLS.hs | 25 ++- .../galley/test/integration/API/MLS/Util.hs | 196 ++++++++---------- 2 files changed, 108 insertions(+), 113 deletions(-) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 9b4fbcb1e21..71e45bd5732 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -636,11 +636,14 @@ testStaleCommit = do traverse_ uploadNewKeyPackage clients void $ setupMLSGroup alice1 + gsBackup <- getClientGroupState alice1 + -- add the first batch of users to the conversation void $ createAddCommit alice1 users1 >>= sendAndConsumeCommit -- now roll back alice1 and try to add the second batch of users - void $ rollBackClient alice1 + setClientGroupState alice1 gsBackup + commit <- createAddCommit alice1 users2 err <- responseJsonError @@ -772,12 +775,14 @@ testCommitNotReferencingAllProposals = do void $ setupMLSGroup alice1 traverse_ uploadNewKeyPackage [bob1, charlie1] + gsBackup <- getClientGroupState alice1 + -- create proposals for bob and charlie createAddProposals alice1 [bob, charlie] >>= traverse_ sendAndConsumeMessage -- now create a commit referencing only the first proposal - void $ rollBackClient alice1 + setClientGroupState alice1 gsBackup commit <- createPendingProposalCommit alice1 -- send commit and expect and error @@ -1342,11 +1347,13 @@ propInvalidEpoch = do -- Add bob -> epoch 1 void $ uploadNewKeyPackage bob1 + gsBackup <- getClientGroupState alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + gsBackup2 <- getClientGroupState alice1 -- try to send a proposal from an old epoch (0) do - groupState <- rollBackClient alice1 + setClientGroupState alice1 gsBackup void $ uploadNewKeyPackage dee1 [prop] <- createAddProposals alice1 [dee] err <- @@ -1354,12 +1361,12 @@ propInvalidEpoch = do =<< postMessage alice1 (mpMessage prop) mls {mlsNewMembers = mempty} -- alice send a well-formed proposal and commits it void $ uploadNewKeyPackage dee1 + setClientGroupState alice1 gsBackup2 createAddProposals alice1 [dee] >>= traverse_ sendAndConsumeMessage void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommit @@ -1559,7 +1566,8 @@ propUnsupported = do (gid, _) <- setupMLSGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit - mems <- currentGroupFile alice1 >>= liftIO . readGroupState + mems <- readGroupState <$> getClientGroupState alice1 + (_, ref) <- assertJust $ find ((== alice1) . fst) mems (priv, pub) <- clientKeyPair alice1 msg <- @@ -2417,12 +2425,15 @@ testRemoteSubConvNotificationWhenUserJoins = do bob1 <- createFakeMLSClient bob (_, qcnv) <- setupMLSGroup alice1 + gsBackup <- getClientGroupState alice1 void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle let subId = SubConvId "conference" s <- State.get void $ createSubConv qcnv alice1 subId + -- revert first commit and subconv - void . replicateM 2 $ rollBackClient alice1 + setClientGroupState alice1 gsBackup + State.put s (_, reqs) <- diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 96166ac18fd..19f2604b3c1 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -47,11 +47,12 @@ import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Time +import qualified Data.UUID as UUID +import qualified Data.UUID.V4 as UUIDV4 import Galley.Keys import Galley.Options import qualified Galley.Options as Opts import Imports hiding (getSymbolicLinkTarget) -import System.Directory (getSymbolicLinkTarget) import System.FilePath import System.IO.Temp import System.Posix hiding (createDirectory) @@ -249,6 +250,7 @@ data MLSState = MLSState mlsMembers :: Set ClientIdentity, -- | users expected to receive a welcome message after the next commit mlsNewMembers :: Set ClientIdentity, + mlsClientGroupState :: Map ClientIdentity ByteString, mlsGroupId :: Maybe GroupId, mlsConvId :: Maybe (Qualified ConvOrSubConvId), mlsEpoch :: Word64 @@ -295,6 +297,7 @@ runMLSTest (MLSTest m) = mlsUnusedPrekeys = someLastPrekeys, mlsMembers = mempty, mlsNewMembers = mempty, + mlsClientGroupState = mempty, mlsGroupId = Nothing, mlsConvId = Nothing, mlsEpoch = 0 @@ -316,12 +319,54 @@ takeLastPrekeyNG = do pure pk [] -> error "no prekeys left" +toRandomFile :: ByteString -> MLSTest FilePath +toRandomFile bs = do + p <- randomFileName + liftIO $ BS.writeFile p bs + pure p + +randomFileName :: MLSTest FilePath +randomFileName = do + bd <- State.gets mlsBaseDir + (bd ) . UUID.toString <$> liftIO UUIDV4.nextRandom + mlscli :: HasCallStack => ClientIdentity -> [String] -> Maybe ByteString -> MLSTest ByteString mlscli qcid args mbstdin = do bd <- State.gets mlsBaseDir let cdir = bd cid2Str qcid - liftIO $ do - spawn (proc "mls-test-cli" (["--store", cdir "store"] <> args)) mbstdin + + groupOut <- randomFileName + let substOut = argSubst "" groupOut + + hasState <- hasClientGroupState qcid + substIn <- + if hasState + then do + gs <- getClientGroupState qcid + fn <- toRandomFile gs + pure (argSubst "" fn) + else pure mempty + + out <- + liftIO $ + spawn + ( proc + "mls-test-cli" + ( ["--store", cdir "store"] + <> map (substIn . substOut) args + ) + ) + mbstdin + + groupOutWritten <- liftIO $ doesFileExist groupOut + when groupOutWritten $ do + gs <- liftIO (BS.readFile groupOut) + setClientGroupState qcid gs + pure out + +argSubst :: String -> String -> String -> String +argSubst from to_ s = + if s == from then to_ else s createWireClient :: HasCallStack => Qualified UserId -> MLSTest ClientIdentity createWireClient qusr = do @@ -392,65 +437,21 @@ generateKeyPackage qcid = do liftIO $ BS.writeFile fp (rmRaw kp) pure (kp, ref) -groupFileLink :: HasCallStack => ClientIdentity -> MLSTest FilePath -groupFileLink qcid = State.gets $ \mls -> - mlsBaseDir mls cid2Str qcid "group.latest" - -currentGroupFile :: HasCallStack => ClientIdentity -> MLSTest FilePath -currentGroupFile = liftIO . getSymbolicLinkTarget <=< groupFileLink - -parseGroupFileName :: FilePath -> IO (FilePath, Int) -parseGroupFileName fp = do - let base = takeFileName fp - (prefix, version) <- case break (== '.') base of - (p, '.' : v) -> pure (p, v) - _ -> assertFailure "invalid group file name" - n <- case reads version of - [(v, "")] -> pure (v :: Int) - _ -> assertFailure "could not parse group file version" - pure $ (prefix, n) - --- sets symlink and creates empty file -nextGroupFile :: HasCallStack => ClientIdentity -> MLSTest FilePath -nextGroupFile qcid = do - bd <- State.gets mlsBaseDir - link <- groupFileLink qcid - exists <- doesFileExist link - base' <- - liftIO $ - if exists - then -- group file exists, bump version and update link - do - (prefix, n) <- parseGroupFileName =<< getSymbolicLinkTarget link - removeFile link - pure $ prefix <> "." <> show (n + 1) - else -- group file does not exist yet, point link to version 0 - pure "group.0" - - let groupFile = bd cid2Str qcid base' - createFileLink groupFile link - pure groupFile - -rollBackClient :: HasCallStack => ClientIdentity -> MLSTest ByteString -rollBackClient cid = do - link <- groupFileLink cid - groupFile <- liftIO $ getSymbolicLinkTarget link - (prefix, n) <- - liftIO $ parseGroupFileName groupFile - when (n == 0) $ do - liftIO . assertFailure $ "Cannot roll back client " <> cid2Str cid - state <- liftIO $ BS.readFile groupFile - removeFile groupFile - removeFile link - bd <- State.gets mlsBaseDir - let newGroupFile = bd cid2Str cid (prefix <> "." <> show (n - 1)) - createFileLink newGroupFile link - pure state +setClientGroupState :: HasCallStack => ClientIdentity -> ByteString -> MLSTest () +setClientGroupState cid g = + State.modify $ \s -> + s {mlsClientGroupState = Map.insert cid g (mlsClientGroupState s)} + +getClientGroupState :: HasCallStack => ClientIdentity -> MLSTest ByteString +getClientGroupState cid = do + mgs <- State.gets (Map.lookup cid . mlsClientGroupState) + case mgs of + Nothing -> liftIO $ assertFailure ("Attempted to get non-existing group state for client " <> show cid) + Just g -> pure g -setGroupState :: HasCallStack => ClientIdentity -> ByteString -> MLSTest () -setGroupState cid state = do - fp <- nextGroupFile cid - liftIO $ BS.writeFile fp state +hasClientGroupState :: HasCallStack => ClientIdentity -> MLSTest Bool +hasClientGroupState cid = + State.gets (isJust . Map.lookup cid . mlsClientGroupState) -- | Create a conversation from a provided action and then create a -- corresponding group. @@ -504,8 +505,6 @@ createGroup cid qcs gid = do resetGroup :: ClientIdentity -> Qualified ConvOrSubConvId -> GroupId -> MLSTest () resetGroup cid qcs gid = do groupJSON <- mlscli cid ["group", "create", T.unpack (toBase64Text (unGroupId gid))] Nothing - g <- nextGroupFile cid - liftIO $ BS.writeFile g groupJSON State.modify $ \s -> s { mlsGroupId = Just gid, @@ -514,6 +513,7 @@ resetGroup cid qcs gid = do mlsEpoch = 0, mlsNewMembers = mempty } + setClientGroupState cid groupJSON getConvId :: MLSTest (Qualified ConvOrSubConvId) getConvId = @@ -640,7 +640,6 @@ createExternalCommit :: MLSTest MessagePackage createExternalCommit qcid mpgs qcs = do bd <- State.gets mlsBaseDir - gNew <- nextGroupFile qcid pgsFile <- liftIO $ emptyTempFile bd "pgs" pgs <- case mpgs of Nothing -> liftTest $ getGroupInfo (cidQualifiedUser qcid) qcs @@ -654,7 +653,7 @@ createExternalCommit qcid mpgs qcs = do "--group-state-out", pgsFile, "--group-out", - gNew + "" ] (Just pgs) @@ -686,11 +685,10 @@ createApplicationMessage :: String -> MLSTest MessagePackage createApplicationMessage cid messageContent = do - groupFile <- currentGroupFile cid message <- mlscli cid - ["message", "--group", groupFile, messageContent] + ["message", "--group", "", messageContent] Nothing pure $ @@ -707,8 +705,6 @@ createAddCommitWithKeyPackages :: MLSTest MessagePackage createAddCommitWithKeyPackages qcid clientsAndKeyPackages = do bd <- State.gets mlsBaseDir - g <- currentGroupFile qcid - gNew <- nextGroupFile qcid welcomeFile <- liftIO $ emptyTempFile bd "welcome" pgsFile <- liftIO $ emptyTempFile bd "pgs" commit <- @@ -717,13 +713,13 @@ createAddCommitWithKeyPackages qcid clientsAndKeyPackages = do ( [ "member", "add", "--group", - g, + "", "--welcome-out", welcomeFile, "--group-state-out", pgsFile, "--group-out", - gNew + "" ] <> map snd clientsAndKeyPackages ) @@ -749,12 +745,10 @@ createAddProposalWithKeyPackage :: (ClientIdentity, FilePath) -> MLSTest MessagePackage createAddProposalWithKeyPackage cid (_, kp) = do - g <- currentGroupFile cid - gNew <- nextGroupFile cid prop <- mlscli cid - ["proposal", "--group-in", g, "--group-out", gNew, "add", kp] + ["proposal", "--group-in", "", "--group-out", "", "add", kp] Nothing pure MessagePackage @@ -769,16 +763,14 @@ createPendingProposalCommit qcid = do bd <- State.gets mlsBaseDir welcomeFile <- liftIO $ emptyTempFile bd "welcome" pgsFile <- liftIO $ emptyTempFile bd "pgs" - g <- currentGroupFile qcid - gNew <- nextGroupFile qcid commit <- mlscli qcid [ "commit", "--group", - g, + "", "--group-out", - gNew, + "", "--welcome-out", welcomeFile, "--group-state-out", @@ -808,10 +800,10 @@ createRemoveCommit cid targets = do bd <- State.gets mlsBaseDir welcomeFile <- liftIO $ emptyTempFile bd "welcome" pgsFile <- liftIO $ emptyTempFile bd "pgs" - g <- currentGroupFile cid - gNew <- nextGroupFile cid - kprefByClient <- liftIO $ Map.fromList <$> readGroupState g + g <- getClientGroupState cid + + let kprefByClient = Map.fromList (readGroupState g) let fetchKeyPackage c = keyPackageFile c (kprefByClient Map.! c) kps <- traverse fetchKeyPackage targets @@ -821,9 +813,9 @@ createRemoveCommit cid targets = do ( [ "member", "remove", "--group", - g, + "", "--group-out", - gNew, + "", "--welcome-out", welcomeFile, "--group-state-out", @@ -877,18 +869,15 @@ consumeWelcome :: HasCallStack => ByteString -> MLSTest () consumeWelcome welcome = do qcids <- State.gets mlsNewMembers for_ qcids $ \qcid -> do - link <- groupFileLink qcid - liftIO $ - doesFileExist link >>= \e -> - assertBool "Existing clients in a conversation should not consume commits" (not e) - groupFile <- nextGroupFile qcid + hasState <- hasClientGroupState qcid + liftIO $ assertBool "Existing clients in a conversation should not consume commits" (not hasState) void $ mlscli qcid [ "group", "from-welcome", "--group-out", - groupFile, + "", "-" ] (Just welcome) @@ -903,16 +892,14 @@ consumeMessage msg = do consumeMessage1 :: HasCallStack => ClientIdentity -> ByteString -> MLSTest () consumeMessage1 cid msg = do bd <- State.gets mlsBaseDir - g <- currentGroupFile cid - gNew <- nextGroupFile cid void $ mlscli cid [ "consume", "--group", - g, + "", "--group-out", - gNew, + "", "--signer-key", bd "removal.key", "-" @@ -1002,25 +989,22 @@ mlsBracket clients k = do c <- view tsCannon WS.bracketAsClientRN c (map (ciUser &&& ciClient) clients) k -readGroupState :: FilePath -> IO [(ClientIdentity, KeyPackageRef)] -readGroupState fp = do - j <- BS.readFile fp - pure $ do - node <- j ^.. key "group" . key "tree" . key "tree" . key "nodes" . _Array . traverse - leafNode <- node ^.. key "node" . key "LeafNode" - identity <- - either (const []) pure . decodeMLS' . BS.pack . map fromIntegral $ - leafNode ^.. key "key_package" . key "payload" . key "credential" . key "credential" . key "Basic" . key "identity" . key "vec" . _Array . traverse . _Integer - kpr <- (unhexM . T.encodeUtf8 =<<) $ leafNode ^.. key "key_package_ref" . _String - pure (identity, KeyPackageRef kpr) +readGroupState :: ByteString -> [(ClientIdentity, KeyPackageRef)] +readGroupState j = do + node <- j ^.. key "group" . key "tree" . key "tree" . key "nodes" . _Array . traverse + leafNode <- node ^.. key "node" . key "LeafNode" + identity <- + either (const []) pure . decodeMLS' . BS.pack . map fromIntegral $ + leafNode ^.. key "key_package" . key "payload" . key "credential" . key "credential" . key "Basic" . key "identity" . key "vec" . _Array . traverse . _Integer + kpr <- (unhexM . T.encodeUtf8 =<<) $ leafNode ^.. key "key_package_ref" . _String + pure (identity, KeyPackageRef kpr) getClientsFromGroupState :: ClientIdentity -> Qualified UserId -> MLSTest [(ClientIdentity, KeyPackageRef)] getClientsFromGroupState cid u = do - groupFile <- currentGroupFile cid - groupState <- liftIO $ readGroupState groupFile + groupState <- readGroupState <$> getClientGroupState cid pure $ filter (\(cid', _) -> cidQualifiedUser cid' == u) groupState clientKeyPair :: ClientIdentity -> MLSTest (ByteString, ByteString) From 9a2f4a0f2b88affccb7d06b1eaecdaa921d7ed11 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 3 Feb 2023 10:43:13 +0100 Subject: [PATCH 017/225] Leave "" untouched in case there is no group state --- services/galley/test/integration/API/MLS/Util.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 19f2604b3c1..6a846278634 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -345,7 +345,7 @@ mlscli qcid args mbstdin = do gs <- getClientGroupState qcid fn <- toRandomFile gs pure (argSubst "" fn) - else pure mempty + else pure id out <- liftIO $ From 59e50a0a8024ff9bc33f7ec00dbb67a95ca6d704 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 3 Feb 2023 11:07:27 +0100 Subject: [PATCH 018/225] hi ci From 27ef3df47d30d72c6a4d4f95385df1f88154bba8 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 3 Feb 2023 14:11:33 +0100 Subject: [PATCH 019/225] [FS-1336] Leaving subconversations (#2969) * Implement local case of leaving subconv * Implement leaving remote subconversations * Small refactoring - Remove utility function `convsub` and inline its uses. - `createSubConv` now returns a `Qualified ConvOrSubId`. * Test leaving a subconversation * Test leaving a subconversation as a non-member * Test leaving a remote subconversation * integration test: remote user leaving local conv * Remove empty test case --------- Co-authored-by: Stefan Berthold --- changelog.d/2-features/subconv-leave | 1 + .../src/Wire/API/Federation/API/Galley.hs | 22 ++ .../API/Routes/Public/Galley/Conversation.hs | 20 ++ services/galley/src/Galley/API/Action.hs | 1 - services/galley/src/Galley/API/Clients.hs | 1 - services/galley/src/Galley/API/Federation.hs | 79 ++++-- services/galley/src/Galley/API/Internal.hs | 1 - services/galley/src/Galley/API/LegalHold.hs | 1 - services/galley/src/Galley/API/MLS/Message.hs | 24 +- .../src/Galley/API/MLS/SubConversation.hs | 113 ++++++++- .../src/Galley/API/Public/Conversation.hs | 1 + .../galley/src/Galley/API/Teams/Features.hs | 1 - services/galley/src/Galley/API/Update.hs | 2 +- services/galley/src/Galley/Effects.hs | 1 + services/galley/test/integration/API/MLS.hs | 230 +++++++++++++++--- .../galley/test/integration/API/MLS/Util.hs | 94 ++++++- 16 files changed, 501 insertions(+), 91 deletions(-) create mode 100644 changelog.d/2-features/subconv-leave diff --git a/changelog.d/2-features/subconv-leave b/changelog.d/2-features/subconv-leave new file mode 100644 index 00000000000..73daf536158 --- /dev/null +++ b/changelog.d/2-features/subconv-leave @@ -0,0 +1 @@ +Implement endpoint for leaving a subconversation 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 16947e69cea..704b1518088 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 @@ -139,6 +139,11 @@ type GalleyApi = "delete-sub-conversation" DeleteSubConversationRequest DeleteSubConversationResponse + :<|> FedEndpointWithMods + '[MakesFederatedCall 'Galley "on-mls-message-sent"] + "leave-sub-conversation" + LeaveSubConversationRequest + LeaveSubConversationResponse data TypingDataUpdateRequest = TypingDataUpdateRequest { tdurTypingStatus :: TypingStatus, @@ -458,6 +463,23 @@ data GetSubConversationsResponse deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded GetSubConversationsResponse) +data LeaveSubConversationRequest = LeaveSubConversationRequest + { lscrUser :: UserId, + lscrClient :: ClientId, + lscrConv :: ConvId, + lscrSubConv :: SubConvId + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform LeaveSubConversationRequest) + deriving (ToJSON, FromJSON) via (CustomEncoded LeaveSubConversationRequest) + +data LeaveSubConversationResponse + = LeaveSubConversationResponseError GalleyError + | LeaveSubConversationResponseProtocolError Text + | LeaveSubConversationResponseOk + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded LeaveSubConversationResponse) + data DeleteSubConversationRequest = DeleteSubConversationRequest { dscreqUser :: UserId, dscreqConv :: ConvId, diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 66dd85e252e..ebdc37f0d1a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -419,6 +419,26 @@ type ConversationAPI = PublicSubConversation ) ) + :<|> Named + "leave-subconversation" + ( Summary "Leave an MLS subconversation" + :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Galley "leave-sub-conversation" + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvAccessDenied + :> CanThrow 'MLSProtocolErrorTag + :> ZLocalUser + :> ZClient + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> "subconversations" + :> Capture "subconv" SubConvId + :> "self" + :> MultiVerb1 + 'DELETE + '[JSON] + (RespondEmpty 200 "OK") + ) :<|> Named "delete-subconversation" ( Summary "Delete an MLS subconversation" diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index e14f46b70c6..a13eb0bfb0b 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -67,7 +67,6 @@ import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FederatorAccess as E import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E -import Galley.Effects.ProposalStore import qualified Galley.Effects.SubConversationStore as E import qualified Galley.Effects.TeamStore as E import Galley.Options diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index cac39d4655e..b16dc16f0f0 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -40,7 +40,6 @@ import qualified Galley.Effects.BrigAccess as E import qualified Galley.Effects.ClientStore as E import Galley.Effects.ConversationStore (getConversation) import Galley.Effects.FederatorAccess -import Galley.Effects.ProposalStore (ProposalStore) import Galley.Env import Galley.Types.Clients (clientIds, fromUserClients) import Imports diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index fbe6da83365..a21ce22e2ed 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -17,7 +17,12 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -module Galley.API.Federation where +module Galley.API.Federation + ( FederationAPI, + federationSitemap, + onConversationUpdated, + ) +where import Control.Error import Control.Lens (itraversed, preview, to, (<.>)) @@ -44,7 +49,7 @@ import Galley.API.MLS.GroupInfo import Galley.API.MLS.KeyPackage import Galley.API.MLS.Message import Galley.API.MLS.Removal -import Galley.API.MLS.SubConversation +import Galley.API.MLS.SubConversation hiding (leaveSubConversation) import Galley.API.MLS.Welcome import qualified Galley.API.Mapping as Mapping import Galley.API.Message @@ -57,7 +62,6 @@ import qualified Galley.Effects.BrigAccess as E import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E -import Galley.Effects.ProposalStore (ProposalStore) import qualified Galley.Effects.SubConversationStore as E import Galley.Effects.SubConversationSupply import Galley.Options @@ -125,6 +129,7 @@ federationSitemap = :<|> Named @"on-typing-indicator-updated" onTypingIndicatorUpdated :<|> Named @"get-sub-conversation" getSubConversationForRemoteUser :<|> Named @"delete-sub-conversation" (callsFed deleteSubConversationForRemoteUser) + :<|> Named @"leave-sub-conversation" (callsFed leaveSubConversation) onClientRemoved :: ( Members @@ -766,29 +771,6 @@ sendMLSMessage remoteDomain msr = Nothing raw -class ToGalleyRuntimeError (effs :: EffectRow) r where - mapToGalleyError :: - Member (Error GalleyError) r => - Sem (Append effs r) a -> - Sem r a - -instance ToGalleyRuntimeError '[] r where - mapToGalleyError = id - -instance - forall (err :: GalleyError) effs r. - ( ToGalleyRuntimeError effs r, - SingI err, - Member (Error GalleyError) (Append effs r) - ) => - ToGalleyRuntimeError (ErrorS err ': effs) r - where - mapToGalleyError act = - mapToGalleyError @effs @r $ - runError act >>= \case - Left _ -> throw (demote @err) - Right res -> pure res - mlsSendWelcome :: Members '[ BrigAccess, @@ -942,6 +924,25 @@ getSubConversationForRemoteUser domain GetSubConversationsRequest {..} = lconv <- qualifyLocal gsreqConv getLocalSubConversation qusr lconv gsreqSubConv +leaveSubConversation :: + ( HasLeaveSubConversationEffects r, + Members '[Input (Local ())] r + ) => + Domain -> + LeaveSubConversationRequest -> + Sem r LeaveSubConversationResponse +leaveSubConversation domain lscr = do + let rusr = toRemoteUnsafe domain (lscrUser lscr) + cid = mkClientIdentity (tUntagged rusr) (lscrClient lscr) + lcnv <- qualifyLocal (lscrConv lscr) + fmap (either (LeaveSubConversationResponseProtocolError . unTagged) id) + . runError @MLSProtocolError + . fmap (either LeaveSubConversationResponseError id) + . runError @GalleyError + . mapToGalleyError @LeaveSubConversationStaticErrors + $ leaveLocalSubConversation cid lcnv (lscrSubConv lscr) + $> LeaveSubConversationResponseOk + deleteSubConversationForRemoteUser :: ( Members '[ ConversationStore, @@ -972,3 +973,29 @@ deleteSubConversationForRemoteUser domain DeleteSubConversationRequest {..} = dsc = DeleteSubConversation dscreqGroupId dscreqEpoch lconv <- qualifyLocal dscreqConv deleteLocalSubConversation qusr lconv dscreqSubConv dsc + +-------------------------------------------------------------------------------- +-- Error handling machinery + +class ToGalleyRuntimeError (effs :: EffectRow) r where + mapToGalleyError :: + Member (Error GalleyError) r => + Sem (Append effs r) a -> + Sem r a + +instance ToGalleyRuntimeError '[] r where + mapToGalleyError = id + +instance + forall (err :: GalleyError) effs r. + ( ToGalleyRuntimeError effs r, + SingI err, + Member (Error GalleyError) (Append effs r) + ) => + ToGalleyRuntimeError (ErrorS err ': effs) r + where + mapToGalleyError act = + mapToGalleyError @effs @r $ + runError act >>= \case + Left _ -> throw (demote @err) + Right res -> pure res diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 8a1582dbda9..8e5d1b647a8 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -60,7 +60,6 @@ import Galley.Effects.FederatorAccess import Galley.Effects.GundeckAccess import Galley.Effects.LegalHoldStore as LegalHoldStore import Galley.Effects.MemberStore -import Galley.Effects.ProposalStore import Galley.Effects.TeamStore import qualified Galley.Intra.Push as Intra import Galley.Monad diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index c581de86dab..fd4a9df6aab 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -54,7 +54,6 @@ import Galley.Effects import Galley.Effects.BrigAccess import Galley.Effects.FireAndForget import qualified Galley.Effects.LegalHoldStore as LegalHoldData -import Galley.Effects.ProposalStore import qualified Galley.Effects.TeamFeatureStore as TeamFeatures import Galley.Effects.TeamMemberStore import Galley.Effects.TeamStore diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 555fd9892c8..bc37e88340e 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -1293,15 +1293,21 @@ executeProposalAction qusr con lconvOrSub action = do -- Type 2 requires no special processing on the backend, so here we filter -- out all removals of that type, so that further checks and processing can -- be applied only to type 1 removals. - removedUsers <- mapMaybe hush <$$> for (Map.assocs (paRemove action)) $ - \(qtarget, Map.keysSet -> clients) -> runError @() $ do - -- fetch clients from brig - clientInfo <- Set.map ciId <$> getClientInfo lconvOrSub qtarget ss - -- if the clients being removed don't exist, consider this as a removal of - -- type 2, and skip it - when (Set.null (clientInfo `Set.intersection` clients)) $ - throw () - pure (qtarget, clients) + -- + -- Furthermore, subconversation clients can be removed arbitrarily, so this + -- processing is only necessary for main conversations. In the + -- subconversation case, an empty list is returned. + removedUsers <- case convOrSub of + SubConv _ _ -> pure [] + Conv _ -> mapMaybe hush <$$> for (Map.assocs (paRemove action)) $ + \(qtarget, Map.keysSet -> clients) -> runError @() $ do + -- fetch clients from brig + clientInfo <- Set.map ciId <$> getClientInfo lconvOrSub qtarget ss + -- if the clients being removed don't exist, consider this as a removal of + -- type 2, and skip it + when (Set.null (clientInfo `Set.intersection` clients)) $ + throw () + pure (qtarget, clients) -- FUTUREWORK: remove this check after remote admins are implemented in federation https://wearezeta.atlassian.net/browse/FS-216 foldQualified lconvOrSub (\_ -> pure ()) (\_ -> throwS @'MLSUnsupportedProposal) qusr diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 79ac6534d26..b3949650ecb 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -22,6 +22,10 @@ module Galley.API.MLS.SubConversation deleteLocalSubConversation, getSubConversationGroupInfo, getSubConversationGroupInfoFromLocalConv, + leaveSubConversation, + HasLeaveSubConversationEffects, + LeaveSubConversationStaticErrors, + leaveLocalSubConversation, MLSGetSubConvStaticErrors, MLSDeleteSubConvStaticErrors, ) @@ -30,8 +34,11 @@ where import Control.Arrow import Data.Id import Data.Qualified +import Data.Time.Clock import Galley.API.MLS +import Galley.API.MLS.Conversation import Galley.API.MLS.GroupInfo +import Galley.API.MLS.Removal import Galley.API.MLS.Types import Galley.API.MLS.Util import Galley.API.Util @@ -51,6 +58,7 @@ import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.Resource +import Polysemy.TinyLog import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.Error @@ -58,6 +66,7 @@ import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.MLS.Credential import Wire.API.MLS.PublicGroupState import Wire.API.MLS.SubConversation @@ -164,8 +173,8 @@ getRemoteSubConversation lusr rcnv sconv = do gsreqSubConv = sconv } case res of - GetSubConversationsResponseError err -> - rethrowErrors @MLSGetSubConvStaticErrors @r err + GetSubConversationsResponseError e -> + rethrowErrors @MLSGetSubConvStaticErrors @r e GetSubConversationsResponseSuccess subconv -> pure subconv @@ -339,3 +348,103 @@ deleteRemoteSubConversation lusr rcnvId scnvId dsc = do case response of DeleteSubConversationResponseError e -> rethrowErrors @MLSDeleteSubConvStaticErrors e DeleteSubConversationResponseSuccess -> pure () + +type HasLeaveSubConversationEffects r = + ( Members + '[ ConversationStore, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Input Env, + Input UTCTime, + MemberStore, + ProposalStore, + SubConversationStore, + TinyLog + ] + r, + CallsFed 'Galley "on-mls-message-sent" + ) + +type LeaveSubConversationStaticErrors = + '[ErrorS 'ConvNotFound, ErrorS 'ConvAccessDenied] + +leaveSubConversation :: + ( HasLeaveSubConversationEffects r, + Members + '[ Error MLSProtocolError, + Error FederationError + ] + r, + Members LeaveSubConversationStaticErrors r, + CallsFed 'Galley "leave-sub-conversation" + ) => + Local UserId -> + ClientId -> + Qualified ConvId -> + SubConvId -> + Sem r () +leaveSubConversation lusr cli qcnv sub = + foldQualified + lusr + (leaveLocalSubConversation cid) + (leaveRemoteSubConversation cid) + qcnv + sub + where + cid = mkClientIdentity (tUntagged lusr) cli + +leaveLocalSubConversation :: + ( HasLeaveSubConversationEffects r, + Members '[Error MLSProtocolError] r, + Members LeaveSubConversationStaticErrors r + ) => + ClientIdentity -> + Local ConvId -> + SubConvId -> + Sem r () +leaveLocalSubConversation cid lcnv sub = do + cnv <- getConversationAndCheckMembership (cidQualifiedUser cid) lcnv + mlsConv <- noteS @'ConvNotFound =<< mkMLSConversation cnv + subConv <- + noteS @'ConvNotFound + =<< Eff.getSubConversation (tUnqualified lcnv) sub + kp <- + note (mlsProtocolError "Client is not a member of the subconversation") $ + cmLookupRef cid (scMembers subConv) + removeClientsWithClientMap + (qualifyAs lcnv (SubConv mlsConv subConv)) + (Identity kp) + (cidQualifiedUser cid) + +leaveRemoteSubConversation :: + ( Members + '[ ErrorS 'ConvNotFound, + ErrorS 'ConvAccessDenied, + Error FederationError, + Error MLSProtocolError, + FederatorAccess + ] + r, + CallsFed 'Galley "leave-sub-conversation" + ) => + ClientIdentity -> + Remote ConvId -> + SubConvId -> + Sem r () +leaveRemoteSubConversation cid rcnv sub = do + res <- + runFederated rcnv $ + fedClient @'Galley @"leave-sub-conversation" $ + LeaveSubConversationRequest + { lscrUser = ciUser cid, + lscrClient = ciClient cid, + lscrConv = tUnqualified rcnv, + lscrSubConv = sub + } + case res of + LeaveSubConversationResponseError e -> + rethrowErrors @'[ErrorS 'ConvNotFound, ErrorS 'ConvAccessDenied] e + LeaveSubConversationResponseProtocolError e -> + throw (mlsProtocolError e) + LeaveSubConversationResponseOk -> pure () diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 8690b3e9026..bf19c40a8f7 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -51,6 +51,7 @@ conversationAPI = <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError <@> mkNamedAPI @"get-subconversation" (callsFed getSubConversation) + <@> mkNamedAPI @"leave-subconversation" (callsFed leaveSubConversation) <@> mkNamedAPI @"delete-subconversation" (callsFed deleteSubConversation) <@> mkNamedAPI @"get-subconversation-group-info" (callsFed getSubConversationGroupInfo) <@> mkNamedAPI @"create-one-to-one-conversation@v2" (callsFed createOne2OneConversation) diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 479caa616e1..1189ea41e2c 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -57,7 +57,6 @@ import Galley.Effects import Galley.Effects.BrigAccess (getAccountConferenceCallingConfigClient, updateSearchVisibilityInbound) import Galley.Effects.ConversationStore as ConversationStore import Galley.Effects.GundeckAccess -import Galley.Effects.ProposalStore import qualified Galley.Effects.SearchVisibilityStore as SearchVisibilityData import Galley.Effects.TeamFeatureStore import qualified Galley.Effects.TeamFeatureStore as TeamFeatures diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index ee5182b13a2..71bf2b31fc6 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -98,7 +98,6 @@ import qualified Galley.Effects.ExternalAccess as E import qualified Galley.Effects.FederatorAccess as E import qualified Galley.Effects.GundeckAccess as E import qualified Galley.Effects.MemberStore as E -import Galley.Effects.ProposalStore import qualified Galley.Effects.ServiceStore as E import Galley.Effects.TeamFeatureStore (FeaturePersistentConstraint) import Galley.Effects.WaiRoutes @@ -406,6 +405,7 @@ updateRemoteConversation rcnv lusr conn action = getUpdateResult $ do ConversationUpdateResponseError err' -> rethrowErrors @(HasConversationActionGalleyErrors tag) err' ConversationUpdateResponseUpdate convUpdate -> pure convUpdate + -- FUTUREWORK: Should we really be calling a federation handler here? onConversationUpdated (tDomain rcnv) convUpdate notifyRemoteConversationAction lusr (qualifyAs rcnv convUpdate) (Just conn) diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index 37fea753aa4..0440dba79ae 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -40,6 +40,7 @@ module Galley.Effects CustomBackendStore, LegalHoldStore, MemberStore, + ProposalStore, SearchVisibilityStore, ServiceStore, TeamFeatureStore, diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 71e45bd5732..f8fa0f18359 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -220,7 +220,9 @@ tests s = test s "send an application message in a subconversation" testSendMessageSubConv, test s "reset a subconversation as a member" (testDeleteSubConv True), test s "reset a subconversation as a non-member" (testDeleteSubConv False), - test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale + test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale, + test s "leave a subconversation" testLeaveSubConv, + test s "leave a subconversation as a non-member" testLeaveSubConvNonMember ], testGroup "Local Sender/Remote Subconversation" @@ -229,7 +231,8 @@ tests s = test s "join remote subconversation" testJoinRemoteSubConv, test s "backends are notified about subconvs when a user joins" testRemoteSubConvNotificationWhenUserJoins, test s "reset a subconversation - member" (testDeleteRemoteSubConv True), - test s "reset a subconversation - not member" (testDeleteRemoteSubConv False) + test s "reset a subconversation - not member" (testDeleteRemoteSubConv False), + test s "leave a remote subconversation" testLeaveRemoteSubConv ], testGroup "Remote Sender/Local SubConversation" @@ -1077,7 +1080,7 @@ testExternalCommitNewClientResendBackendProposal = do liftIO $ assertBool "No events after external commit expected" (null ecEvents) WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ - wsAssertMLSMessage (convsub qcnv Nothing) bob (mpMessage mp) + wsAssertMLSMessage (fmap Conv qcnv) bob (mpMessage mp) -- The backend proposals for bob2 are replayed, but the external add -- proposal for bob3 has to replayed by the client and is thus not found @@ -1102,7 +1105,7 @@ testAppMessage = do liftIO $ events @?= [] liftIO $ WS.assertMatchN_ (5 # WS.Second) wss $ - wsAssertMLSMessage (convsub qcnv Nothing) alice (mpMessage message) + wsAssertMLSMessage (fmap Conv qcnv) alice (mpMessage message) testAppMessage2 :: TestM () testAppMessage2 = do @@ -1133,7 +1136,7 @@ testAppMessage2 = do liftIO $ WS.assertMatchN_ (5 # WS.Second) wss $ - wsAssertMLSMessage (convsub conversation Nothing) bob (mpMessage message) + wsAssertMLSMessage (fmap Conv conversation) bob (mpMessage message) testRemoteToRemote :: TestM () testRemoteToRemote = do @@ -1183,8 +1186,8 @@ testRemoteToRemote = do void $ runFedClient @"on-mls-message-sent" fedGalleyClient bdom rm liftIO $ do -- alice should receive the message on her first client - WS.assertMatch_ (5 # Second) wsA1 $ \n -> wsAssertMLSMessage (convsub qconv Nothing) qbob txt n - WS.assertMatch_ (5 # Second) wsA2 $ \n -> wsAssertMLSMessage (convsub qconv Nothing) qbob txt n + WS.assertMatch_ (5 # Second) wsA1 $ \n -> wsAssertMLSMessage (fmap Conv qconv) qbob txt n + WS.assertMatch_ (5 # Second) wsA2 $ \n -> wsAssertMLSMessage (fmap Conv qconv) qbob txt n -- eve should not receive the message WS.assertNoEvent (1 # Second) [wsE] @@ -1235,7 +1238,7 @@ testRemoteToLocal = do liftIO $ do resp @?= MLSMessageResponseUpdates [] WS.assertMatch_ (5 # Second) ws $ - wsAssertMLSMessage (convsub qcnv Nothing) bob (mpMessage message) + wsAssertMLSMessage (fmap Conv qcnv) bob (mpMessage message) testRemoteToLocalWrongConversation :: TestM () testRemoteToLocalWrongConversation = do @@ -1431,7 +1434,7 @@ testExternalAddProposal = do void $ sendAndConsumeMessage msg liftTest $ WS.assertMatchN_ (5 # Second) wss $ - wsAssertMLSMessage (convsub qcnv Nothing) alice (mpMessage msg) + wsAssertMLSMessage (fmap Conv qcnv) alice (mpMessage msg) -- bob adds charlie putOtherMemberQualified @@ -2326,13 +2329,12 @@ testJoinSubNonMemberClient = do (_, qcnv) <- setupMLSGroup alice1 void $ createAddCommit alice1 [alice] >>= sendAndConsumeCommit - let subId = SubConvId "conference" - void $ createSubConv qcnv alice1 subId + qcs <- createSubConv qcnv alice1 (SubConvId "conference") -- now Bob attempts to get the group info so he can join via external commit -- with his own client, but he cannot because he is not a member of the -- parent conversation - localGetGroupInfo (ciUser bob1) (fmap (flip SubConv subId) qcnv) + localGetGroupInfo (ciUser bob1) qcs !!! const 404 === statusCode testAddClientSubConvFailure :: TestM () @@ -2388,7 +2390,7 @@ testJoinRemoteSubConv = do -- setup fake group for the subconversation let subId = SubConvId "conference" (subGroupId, qcnv) <- setupFakeMLSGroup alice1 - let qcs = convsub qcnv (Just subId) + let qcs = fmap (flip SubConv subId) qcnv initialCommit <- createPendingProposalCommit alice1 -- create a fake group ID for the main (we don't need the actual group) @@ -2470,8 +2472,11 @@ testRemoteUserJoinSubConv = do messageSentMock ] let subId = SubConvId "conference" - (psc, reqs) <- withTempMockFederator' mock $ createSubConv qcnv alice1 subId - let qcs = convsub qcnv (Just subId) + (qcs, reqs) <- withTempMockFederator' mock $ createSubConv qcnv alice1 subId + psc <- + liftTest $ + responseJsonError + =<< getSubConv (ciUser alice1) qcnv subId >= sendAndConsumeCommit + runMLSTest $ do + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit - let subId = SubConvId "conference" - void $ createSubConv qcnv bob1 subId - let qcs = convsub qcnv (Just subId) + qcs <- createSubConv qcnv bob1 (SubConvId "conference") - void $ createExternalCommit alice1 Nothing qcs >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob2 Nothing qcs >>= sendAndConsumeCommitBundle + void $ createExternalCommit alice1 Nothing qcs >>= sendAndConsumeCommitBundle + void $ createExternalCommit bob2 Nothing qcs >>= sendAndConsumeCommitBundle - message <- createApplicationMessage alice1 "some text" - mlsBracket [bob1, bob2] $ \wss -> do - events <- sendAndConsumeMessage message - liftIO $ events @?= [] - liftIO $ - WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do - wsAssertMLSMessage qcs alice (mpMessage message) n + message <- createApplicationMessage alice1 "some text" + mlsBracket [bob1, bob2] $ \wss -> do + events <- sendAndConsumeMessage message + liftIO $ events @?= [] + liftIO $ + WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do + wsAssertMLSMessage qcs alice (mpMessage message) n testGetRemoteSubConv :: Bool -> TestM () testGetRemoteSubConv isAMember = do @@ -2687,12 +2689,16 @@ testDeleteSubConv isAMember = do then (qUnqualified alice, 200) else (randUser, 403) let sconv = SubConvId "conference" - (qcnv, sub) <- runMLSTest $ do + qcnv <- runMLSTest $ do alice1 <- createMLSClient alice (_, qcnv) <- setupMLSGroup alice1 - sub <- createSubConv qcnv alice1 sconv - pure (qcnv, sub) + void $ createSubConv qcnv alice1 sconv + pure qcnv + sub <- + responseJsonError + =<< getSubConv (qUnqualified alice) qcnv sconv + welcomeMock + ) + $ do + void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommit + + qsub <- createSubConv qcnv bob1 subId + void $ createExternalCommit alice1 Nothing qsub >>= sendAndConsumeCommitBundle + void $ createExternalCommit bob2 Nothing qsub >>= sendAndConsumeCommitBundle + void $ createExternalCommit charlie1 Nothing qsub >>= sendAndConsumeCommitBundle + pure qsub + + -- bob1 (the creator of the subconv) leaves + [bob1KP] <- + map snd . filter (\(cid, _) -> cid == bob1) + <$> getClientsFromGroupState alice1 bob + mlsBracket [alice1, bob2] $ \wss -> do + (_, reqs) <- withTempMockFederator' messageSentMock $ leaveCurrentConv bob1 qsub + req <- + assertOne + ( toList . Aeson.decode . frBody + =<< filter ((== "on-mls-message-sent") . frRPC) reqs + ) + let msg = fromBase64ByteString $ rmmMessage req + liftIO $ + rmmRecipients req @?= [(ciUser charlie1, ciClient charlie1)] + consumeMessage1 charlie1 msg + + msgs <- + WS.assertMatchN (5 # WS.Second) wss $ + wsAssertBackendRemoveProposal bob qcnv bob1KP + traverse_ (uncurry consumeMessage1) (zip [alice1, bob2] msgs) + + -- alice commits the pending proposal + void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + + -- check that only 3 clients are left in the subconv + do + psc <- + liftTest $ + responseJsonError + =<< getSubConv (qUnqualified alice) qcnv subId + cid == charlie1) + <$> getClientsFromGroupState alice1 charlie + mlsBracket [alice1, bob2] $ \wss -> do + leaveCurrentConv charlie1 qsub + + msgs <- + WS.assertMatchN (5 # WS.Second) wss $ + wsAssertBackendRemoveProposal charlie qcnv charlie1KP + traverse_ (uncurry consumeMessage1) (zip [alice1, bob2] msgs) + + -- alice commits the pending proposal + void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + + -- check that only 2 clients are left in the subconv + do + psc <- + liftTest $ + responseJsonError + =<< getSubConv (qUnqualified alice) qcnv subId + >= sendAndConsumeCommit + + let subId = SubConvId "conference" + _qsub <- createSubConv qcnv bob1 subId + + -- alice attempts to leave + liftTest $ do + e <- + responseJsonError + =<< leaveSubConv (ciUser alice1) (ciClient alice1) qcnv subId + sendMessageMock + <|> ("leave-sub-conversation" ~> LeaveSubConversationResponseOk) + (_, reqs) <- withTempMockFederator' mock $ do + -- bob joins subconversation + void $ createExternalCommit bob1 Nothing qcs >>= sendAndConsumeCommitBundle + + -- bob leaves + liftTest $ + leaveSubConv (ciUser bob1) (ciClient bob1) qcnv subId + !!! const 200 === statusCode + + -- check that leave-sub-conversation is called + void $ assertOne (filter ((== "leave-sub-conversation") . frRPC) reqs) diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 6a846278634..66fb3aec683 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -525,17 +525,17 @@ createSubConv :: Qualified ConvId -> ClientIdentity -> SubConvId -> - MLSTest PublicSubConversation + MLSTest (Qualified ConvOrSubConvId) createSubConv qcnv creator subId = do - let getSC = - liftTest $ - responseJsonError - =<< getSubConv (ciUser creator) qcnv subId - >= sendAndConsumeCommitBundle - getSC + pure qcs -- | Create a local group only without a conversation. This simulates creating -- an MLS conversation on a remote backend. @@ -1217,6 +1217,76 @@ deleteSubConv u qcnv sconv dsc = do . contentJson . json dsc -convsub :: Qualified ConvId -> Maybe SubConvId -> Qualified ConvOrSubConvId -convsub qcnv Nothing = Conv <$> qcnv -convsub qcnv (Just sconv) = flip SubConv sconv <$> qcnv +leaveSubConv :: + UserId -> + ClientId -> + Qualified ConvId -> + SubConvId -> + TestM ResponseLBS +leaveSubConv u c qcnv subId = do + g <- viewGalley + delete $ + g + . paths + [ "conversations", + toByteString' (qDomain qcnv), + toByteString' (qUnqualified qcnv), + "subconversations", + toHeader subId, + "self" + ] + . zUser u + . zClient c + +remoteLeaveCurrentConv :: + Remote ClientIdentity -> + Qualified ConvId -> + SubConvId -> + TestM () +remoteLeaveCurrentConv rcid qcnv subId = do + client <- view tsFedGalleyClient + let lscr = + LeaveSubConversationRequest + { lscrUser = ciUser $ tUnqualified rcid, + lscrClient = ciClient $ tUnqualified rcid, + lscrConv = qUnqualified qcnv, + lscrSubConv = subId + } + runFedClient + @"leave-sub-conversation" + client + (tDomain rcid) + lscr + >>= liftIO . \case + LeaveSubConversationResponseError e -> + assertFailure $ + "error while leaving remote conversation: " <> show e + LeaveSubConversationResponseProtocolError e -> + assertFailure $ + "protocol error while leaving remote conversation: " <> T.unpack e + LeaveSubConversationResponseOk -> pure () + +leaveCurrentConv :: + HasCallStack => + ClientIdentity -> + Qualified ConvOrSubConvId -> + MLSTest () +leaveCurrentConv cid qsub = case qUnqualified qsub of + -- TODO: implement leaving main conversation as well + Conv _ -> liftIO $ assertFailure "Leaving conversations is not supported" + SubConv cnv subId -> do + liftTest $ do + loc <- qualifyLocal () + foldQualified + loc + ( \_ -> + leaveSubConv (ciUser cid) (ciClient cid) (qsub $> cnv) subId + !!! const 200 === statusCode + ) + ( \rcid -> remoteLeaveCurrentConv rcid (qsub $> cnv) subId + ) + (cidQualifiedUser cid $> cid) + State.modify $ \mls -> + mls + { mlsMembers = Set.difference (mlsMembers mls) (Set.singleton cid) + } From 9023a0e1c0d84200a82772e05a2ba367147aa258 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Wed, 8 Feb 2023 16:14:34 +0100 Subject: [PATCH 020/225] Renamed function in leavel local sub. (#3063) It was referencing a renamed function that no longer exists. --- services/galley/src/Galley/API/MLS/SubConversation.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index b3949650ecb..5dae68a4ce2 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -412,7 +412,7 @@ leaveLocalSubConversation cid lcnv sub = do kp <- note (mlsProtocolError "Client is not a member of the subconversation") $ cmLookupRef cid (scMembers subConv) - removeClientsWithClientMap + createAndSendRemoveProposals (qualifyAs lcnv (SubConv mlsConv subConv)) (Identity kp) (cidQualifiedUser cid) From a646eb379866c56c6237cb88a41f95f74c0de7d2 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 13 Feb 2023 18:29:49 +0100 Subject: [PATCH 021/225] MLS: Propagate deletion of parent conv to subconvs (#3006) --- .../src/Wire/API/Federation/API/Galley.hs | 25 ++- libs/wire-api/src/Wire/API/MLS/Group.hs | 2 +- .../src/Wire/API/MLS/SubConversation.hs | 10 +- .../src/Wire/API/Routes/Internal/Brig.hs | 85 ++++++---- .../API/Routes/Public/Galley/Conversation.hs | 9 +- .../src/Wire/API/Routes/Public/Galley/MLS.hs | 9 +- .../Routes/Public/Galley/TeamConversation.hs | 1 + services/brig/src/Brig/API/Internal.hs | 20 ++- services/brig/src/Brig/Data/MLS/KeyPackage.hs | 9 ++ services/galley/src/Galley/API/Action.hs | 55 ++++++- services/galley/src/Galley/API/Federation.hs | 26 ++- services/galley/src/Galley/API/MLS/Message.hs | 14 +- .../src/Galley/API/MLS/SubConversation.hs | 35 ++-- services/galley/src/Galley/API/Teams.hs | 8 +- services/galley/src/Galley/API/Update.hs | 20 ++- .../src/Galley/Cassandra/Conversation.hs | 17 ++ .../galley/src/Galley/Cassandra/Proposal.hs | 5 + .../galley/src/Galley/Cassandra/Queries.hs | 10 +- .../src/Galley/Cassandra/SubConversation.hs | 7 +- services/galley/src/Galley/Effects.hs | 2 +- .../galley/src/Galley/Effects/BrigAccess.hs | 2 + .../src/Galley/Effects/ConversationStore.hs | 4 + .../src/Galley/Effects/ProposalStore.hs | 3 + .../Galley/Effects/SubConversationStore.hs | 1 + services/galley/src/Galley/Intra/Client.hs | 12 ++ services/galley/src/Galley/Intra/Effects.hs | 3 + services/galley/test/integration/API/MLS.hs | 149 ++++++++++++++++-- .../galley/test/integration/API/MLS/Mocks.hs | 9 ++ .../galley/test/integration/API/MLS/Util.hs | 8 +- services/galley/test/integration/TestSetup.hs | 3 +- 30 files changed, 459 insertions(+), 104 deletions(-) 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 704b1518088..82e72852251 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 @@ -35,7 +35,7 @@ import Wire.API.Conversation.Typing import Wire.API.Error.Galley import Wire.API.Federation.API.Common import Wire.API.Federation.Endpoint -import Wire.API.MLS.SubConversation +import Wire.API.MLS.SubConversation hiding (DeleteSubConversationRequest (..)) import Wire.API.MakesFederatedCall import Wire.API.Message import Wire.API.Routes.Public.Galley.Messaging @@ -65,7 +65,8 @@ type GalleyApi = '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", MakesFederatedCall 'Galley "on-new-remote-conversation", - MakesFederatedCall 'Galley "on-new-remote-subconversation" + MakesFederatedCall 'Galley "on-new-remote-subconversation", + MakesFederatedCall 'Galley "on-delete-mls-conversation" ] "leave-conversation" LeaveConversationRequest @@ -94,6 +95,7 @@ type GalleyApi = :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", + MakesFederatedCall 'Galley "on-delete-mls-conversation", MakesFederatedCall 'Galley "on-new-remote-conversation", MakesFederatedCall 'Galley "on-new-remote-subconversation" ] @@ -108,6 +110,7 @@ type GalleyApi = MakesFederatedCall 'Galley "on-new-remote-conversation", MakesFederatedCall 'Galley "on-new-remote-subconversation", MakesFederatedCall 'Galley "send-mls-message", + MakesFederatedCall 'Galley "on-delete-mls-conversation", MakesFederatedCall 'Brig "get-mls-clients" ] "send-mls-message" @@ -116,6 +119,7 @@ type GalleyApi = :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "mls-welcome", MakesFederatedCall 'Galley "on-conversation-updated", + MakesFederatedCall 'Galley "on-delete-mls-conversation", MakesFederatedCall 'Galley "on-mls-message-sent", MakesFederatedCall 'Galley "on-new-remote-conversation", MakesFederatedCall 'Galley "on-new-remote-subconversation", @@ -135,15 +139,18 @@ type GalleyApi = :<|> FedEndpoint "on-typing-indicator-updated" TypingDataUpdateRequest EmptyResponse :<|> FedEndpoint "get-sub-conversation" GetSubConversationsRequest GetSubConversationsResponse :<|> FedEndpointWithMods - '[MakesFederatedCall 'Galley "on-new-remote-subconversation"] + '[ MakesFederatedCall 'Galley "on-new-remote-subconversation", + MakesFederatedCall 'Galley "on-delete-mls-conversation" + ] "delete-sub-conversation" - DeleteSubConversationRequest + DeleteSubConversationFedRequest DeleteSubConversationResponse :<|> FedEndpointWithMods '[MakesFederatedCall 'Galley "on-mls-message-sent"] "leave-sub-conversation" LeaveSubConversationRequest LeaveSubConversationResponse + :<|> FedEndpoint "on-delete-mls-conversation" OnDeleteMLSConversationRequest EmptyResponse data TypingDataUpdateRequest = TypingDataUpdateRequest { tdurTypingStatus :: TypingStatus, @@ -480,7 +487,7 @@ data LeaveSubConversationResponse deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded LeaveSubConversationResponse) -data DeleteSubConversationRequest = DeleteSubConversationRequest +data DeleteSubConversationFedRequest = DeleteSubConversationFedRequest { dscreqUser :: UserId, dscreqConv :: ConvId, dscreqSubConv :: SubConvId, @@ -488,10 +495,16 @@ data DeleteSubConversationRequest = DeleteSubConversationRequest dscreqEpoch :: Epoch } deriving stock (Eq, Show, Generic) - deriving (ToJSON, FromJSON) via (CustomEncoded DeleteSubConversationRequest) + deriving (ToJSON, FromJSON) via (CustomEncoded DeleteSubConversationFedRequest) data DeleteSubConversationResponse = DeleteSubConversationResponseError GalleyError | DeleteSubConversationResponseSuccess deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded DeleteSubConversationResponse) + +newtype OnDeleteMLSConversationRequest = OnDeleteMLSConversationRequest + { odmcGroupIds :: [GroupId] + } + deriving stock (Eq, Show, Generic) + deriving (FromJSON, ToJSON) via (CustomEncoded OnDeleteMLSConversationRequest) diff --git a/libs/wire-api/src/Wire/API/MLS/Group.hs b/libs/wire-api/src/Wire/API/MLS/Group.hs index 48e4ad9c79e..c693ddd2a21 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group.hs @@ -31,7 +31,7 @@ import Wire.API.MLS.Serialisation import Wire.Arbitrary newtype GroupId = GroupId {unGroupId :: ByteString} - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, Ord) deriving (Arbitrary) via (GenericUniform GroupId) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema GroupId) diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index 00d616e33dc..15e533d31e0 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -175,18 +175,18 @@ deriving via Schema ConvOrSubConvId instance ToJSON ConvOrSubConvId deriving via Schema ConvOrSubConvId instance S.ToSchema ConvOrSubConvId -- | The body of the delete subconversation request -data DeleteSubConversation = DeleteSubConversation +data DeleteSubConversationRequest = DeleteSubConversationRequest { dscGroupId :: GroupId, dscEpoch :: Epoch } deriving (Eq, Show) - deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema DeleteSubConversation) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema DeleteSubConversationRequest) -instance ToSchema DeleteSubConversation where +instance ToSchema DeleteSubConversationRequest where schema = objectWithDocModifier - "DeleteSubConversation" + "DeleteSubConversationRequest" (description ?~ "Delete an MLS subconversation") - $ DeleteSubConversation + $ DeleteSubConversationRequest <$> dscGroupId .= field "group_id" schema <*> dscEpoch .= field "epoch" schema 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 c42b16e0297..a87ec9db964 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -33,6 +33,7 @@ module Wire.API.Routes.Internal.Brig NewKeyPackageRef (..), NewKeyPackage (..), NewKeyPackageResult (..), + DeleteKeyPackageRefsRequest (..), ) where @@ -210,44 +211,66 @@ instance ToSchema NewKeyPackageResult where <$> nkpresClientIdentity .= field "client_identity" schema <*> nkpresKeyPackageRef .= field "key_package_ref" schema +newtype DeleteKeyPackageRefsRequest = DeleteKeyPackageRefsRequest {unDeleteKeyPackageRefsRequest :: [KeyPackageRef]} + deriving (Eq, Show) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema DeleteKeyPackageRefsRequest) + +instance ToSchema DeleteKeyPackageRefsRequest where + schema = + object "DeleteKeyPackageRefsRequest" $ + DeleteKeyPackageRefsRequest + <$> unDeleteKeyPackageRefsRequest .= field "key_package_refs" (array schema) + type MLSAPI = "mls" :> ( ( "key-packages" - :> Capture "ref" KeyPackageRef - :> ( Named - "get-client-by-key-package-ref" - ( Summary "Resolve an MLS key package ref to a qualified client ID" - :> MultiVerb - 'GET - '[Servant.JSON] - '[ RespondEmpty 404 "Key package ref not found", - Respond 200 "Key package ref found" ClientIdentity - ] - (Maybe ClientIdentity) - ) - :<|> ( "conversation" - :> ( PutConversationByKeyPackageRef - :<|> GetConversationByKeyPackageRef - ) + :> ( ( Capture "ref" KeyPackageRef + :> ( Named + "get-client-by-key-package-ref" + ( Summary "Resolve an MLS key package ref to a qualified client ID" + :> MultiVerb + 'GET + '[Servant.JSON] + '[ RespondEmpty 404 "Key package ref not found", + Respond 200 "Key package ref found" ClientIdentity + ] + (Maybe ClientIdentity) + ) + :<|> ( "conversation" + :> ( PutConversationByKeyPackageRef + :<|> GetConversationByKeyPackageRef + ) + ) + :<|> Named + "put-key-package-ref" + ( Summary "Create a new KeyPackageRef mapping" + :> ReqBody '[Servant.JSON] NewKeyPackageRef + :> MultiVerb + 'PUT + '[Servant.JSON] + '[RespondEmpty 201 "Key package ref mapping created"] + () + ) + :<|> Named + "post-key-package-ref" + ( Summary "Update a KeyPackageRef in mapping" + :> ReqBody '[Servant.JSON] KeyPackageRef + :> MultiVerb + 'POST + '[Servant.JSON] + '[RespondEmpty 201 "Key package ref mapping updated"] + () + ) ) + ) :<|> Named - "put-key-package-ref" - ( Summary "Create a new KeyPackageRef mapping" - :> ReqBody '[Servant.JSON] NewKeyPackageRef - :> MultiVerb - 'PUT - '[Servant.JSON] - '[RespondEmpty 201 "Key package ref mapping created"] - () - ) - :<|> Named - "post-key-package-ref" - ( Summary "Update a KeyPackageRef in mapping" - :> ReqBody '[Servant.JSON] KeyPackageRef + "delete-key-package-refs" + ( Summary "Delete a batch of KeyPackageRef mappings" + :> ReqBody '[Servant.JSON] DeleteKeyPackageRefsRequest :> MultiVerb - 'POST + 'DELETE '[Servant.JSON] - '[RespondEmpty 201 "Key package ref mapping updated"] + '[RespondEmpty 200 "Key package ref mappings deleted"] () ) ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index ebdc37f0d1a..03ec88545c8 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -445,6 +445,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "delete-sub-conversation" :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Galley "on-new-remote-subconversation" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'MLSNotEnabled @@ -454,7 +455,7 @@ type ConversationAPI = :> QualifiedCapture "cnv" ConvId :> "subconversations" :> Capture "subconv" SubConvId - :> ReqBody '[JSON] DeleteSubConversation + :> ReqBody '[JSON] DeleteSubConversationRequest :> MultiVerb1 'DELETE '[JSON] @@ -533,6 +534,7 @@ type ConversationAPI = "add-members-to-conversation-unqualified" ( Summary "Add members to an existing conversation (deprecated)" :> MakesFederatedCall 'Galley "on-conversation-updated" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Galley "on-new-remote-subconversation" @@ -561,6 +563,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Galley "on-new-remote-subconversation" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> Until 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -587,6 +590,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Galley "on-new-remote-subconversation" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> From 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -1021,6 +1025,7 @@ type ConversationAPI = "update-conversation-access-unqualified" ( Summary "Update access modes for a conversation (deprecated)" :> MakesFederatedCall 'Galley "on-conversation-updated" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Galley "on-new-remote-subconversation" @@ -1051,6 +1056,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Galley "on-new-remote-subconversation" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> Until 'V3 :> ZLocalUser :> ZConn @@ -1074,6 +1080,7 @@ type ConversationAPI = "update-conversation-access" ( Summary "Update access modes for a conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Galley "on-new-remote-subconversation" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index ebea2b25bae..d0197fb63f5 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -50,12 +50,13 @@ type MLSMessagingAPI = :<|> Named "mls-message-v1" ( Summary "Post an MLS message" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "send-mls-message" + :> MakesFederatedCall 'Brig "get-mls-clients" :> MakesFederatedCall 'Galley "on-conversation-updated" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" + :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Galley "on-new-remote-subconversation" - :> MakesFederatedCall 'Brig "get-mls-clients" + :> MakesFederatedCall 'Galley "send-mls-message" :> Until 'V2 :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvMemberNotFound @@ -93,6 +94,7 @@ type MLSMessagingAPI = :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Brig "get-mls-clients" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> From 'V2 :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvMemberNotFound @@ -131,6 +133,7 @@ type MLSMessagingAPI = :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Brig "get-mls-clients" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> From 'V3 :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvMemberNotFound diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs index 2499b95fbd4..bbaa683c781 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs @@ -69,6 +69,7 @@ type TeamConversationAPI = "delete-team-conversation" ( Summary "Remove a team conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'DeleteConversation) diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index d01481190bf..c0355c36cb8 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -139,13 +139,15 @@ ejpdAPI = mlsAPI :: ServerT BrigIRoutes.MLSAPI (Handler r) mlsAPI = - ( \ref -> - Named @"get-client-by-key-package-ref" (getClientByKeyPackageRef ref) - :<|> ( Named @"put-conversation-by-key-package-ref" (putConvIdByKeyPackageRef ref) - :<|> Named @"get-conversation-by-key-package-ref" (getConvIdByKeyPackageRef ref) - ) - :<|> Named @"put-key-package-ref" (putKeyPackageRef ref) - :<|> Named @"post-key-package-ref" (postKeyPackageRef ref) + ( ( \ref -> + Named @"get-client-by-key-package-ref" (getClientByKeyPackageRef ref) + :<|> ( Named @"put-conversation-by-key-package-ref" (putConvIdByKeyPackageRef ref) + :<|> Named @"get-conversation-by-key-package-ref" (getConvIdByKeyPackageRef ref) + ) + :<|> Named @"put-key-package-ref" (putKeyPackageRef ref) + :<|> Named @"post-key-package-ref" (postKeyPackageRef ref) + ) + :<|> Named @"delete-key-package-refs" deleteKeyPackageRefs ) :<|> getMLSClients :<|> mapKeyPackageRefsInternal @@ -246,6 +248,10 @@ upsertKeyPackage nkp = do noteH errMsg Nothing = mlsProtocolError errMsg noteH _ (Just y) = pure y +deleteKeyPackageRefs :: DeleteKeyPackageRefsRequest -> Handler r () +deleteKeyPackageRefs (DeleteKeyPackageRefsRequest refs) = + lift . wrapClient $ pooledForConcurrentlyN_ 16 refs Data.deleteKeyPackageRef + getMLSClients :: UserId -> SignatureSchemeTag -> Handler r (Set ClientInfo) getMLSClients usr _ss = do -- FUTUREWORK: check existence of key packages with a given ciphersuite diff --git a/services/brig/src/Brig/Data/MLS/KeyPackage.hs b/services/brig/src/Brig/Data/MLS/KeyPackage.hs index 17d4b0617cb..2f99e355bcb 100644 --- a/services/brig/src/Brig/Data/MLS/KeyPackage.hs +++ b/services/brig/src/Brig/Data/MLS/KeyPackage.hs @@ -25,6 +25,7 @@ module Brig.Data.MLS.KeyPackage keyPackageRefSetConvId, addKeyPackageRef, updateKeyPackageRef, + deleteKeyPackageRef, ) where @@ -211,6 +212,14 @@ updateKeyPackageRef prevRef newRef = restoreKeyPackageMeta newRef backup deleteKeyPackage prevRef +deleteKeyPackageRef :: MonadClient m => KeyPackageRef -> m () +deleteKeyPackageRef ref = do + retry x5 $ + write q (params LocalQuorum (Identity ref)) + where + q :: PrepQuery W (Identity KeyPackageRef) x + q = "DELETE FROM mls_key_package_refs WHERE ref = ?" + -------------------------------------------------------------------------------- -- Utilities diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index a13eb0bfb0b..c16ddb655d8 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -53,10 +53,12 @@ import Data.Singletons import Data.Time.Clock import Galley.API.Error import Galley.API.MLS.Removal +import Galley.API.MLS.Types (cmAssocs) import Galley.API.Util import Galley.App import Galley.Data.Conversation import qualified Galley.Data.Conversation as Data +import Galley.Data.Conversation.Types (mlsMetadata) import Galley.Data.Services import Galley.Data.Types import Galley.Effects @@ -67,6 +69,7 @@ import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FederatorAccess as E import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E +import qualified Galley.Effects.ProposalStore as E import qualified Galley.Effects.SubConversationStore as E import qualified Galley.Effects.TeamStore as E import Galley.Options @@ -147,7 +150,19 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con HasConversationActionEffects 'ConversationMemberUpdateTag r = (Members '[MemberStore, ErrorS 'ConvMemberNotFound] r) HasConversationActionEffects 'ConversationDeleteTag r = - Members '[Error FederationError, ErrorS 'NotATeamMember, CodeStore, TeamStore, ConversationStore] r + Members + '[ Error FederationError, + ErrorS 'NotATeamMember, + BrigAccess, + CodeStore, + ConversationStore, + FederatorAccess, + MemberStore, + TeamStore, + ProposalStore, + SubConversationStore + ] + r HasConversationActionEffects 'ConversationRenameTag r = Members '[Error InvalidInput, ConversationStore] r HasConversationActionEffects 'ConversationAccessDataTag r = @@ -279,6 +294,7 @@ type PerformActionCalls :: ConversationActionTag -> Constraint type family PerformActionCalls tag where PerformActionCalls 'ConversationAccessDataTag = ( CallsFed 'Galley "on-conversation-updated", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation" @@ -287,11 +303,15 @@ type family PerformActionCalls tag where ( CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-new-remote-subconversation" + CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation" ) PerformActionCalls 'ConversationLeaveTag = ( CallsFed 'Galley "on-mls-message-sent" ) + PerformActionCalls 'ConversationDeleteTag = + ( CallsFed 'Galley "on-delete-mls-conversation" + ) PerformActionCalls tag = () -- | Returns additional members that resulted from the action (e.g. ConversationJoin) @@ -345,11 +365,36 @@ performAction tag origUser lconv action = do E.setOtherMember lcnv (cmuTarget action) (cmuUpdate action) pure (mempty, action) SConversationDeleteTag -> do + let deleteGroup groupId = do + cm <- E.lookupMLSClients groupId + let refs = cm & cmAssocs & map (snd . snd) + E.deleteKeyPackageRefs refs + E.removeAllMLSClients groupId + E.deleteAllProposals groupId + + let cid = convId conv + for_ (conv & mlsMetadata <&> cnvmlsGroupId) $ \gidParent -> do + sconvs <- E.listSubConversations cid + gidSubs <- for (Map.assocs sconvs) $ \(subid, mlsData) -> do + let gidSub = cnvmlsGroupId mlsData + E.deleteSubConversation cid subid + E.deleteGroupIdForSubConversation gidSub + deleteGroup gidSub + pure gidSub + E.deleteGroupIdForConversation gidParent + deleteGroup gidParent + + let odr = OnDeleteMLSConversationRequest ([gidParent] <> gidSubs) + let remotes = bucketRemote (map rmId (Data.convRemoteMembers conv)) + E.runFederatedConcurrently_ remotes $ \_ -> do + void $ fedClient @'Galley @"on-delete-mls-conversation" odr + key <- E.makeKey (tUnqualified lcnv) E.deleteCode key ReusableCode case convTeam conv of Nothing -> E.deleteConversation (tUnqualified lcnv) Just tid -> E.deleteTeamConversation tid (tUnqualified lcnv) + pure (mempty, action) SConversationRenameTag -> do cn <- rangeChecked (cupName action) @@ -371,8 +416,9 @@ performConversationJoin :: ( HasConversationActionEffects 'ConversationJoinTag r, CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-conversation-updated", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-new-remote-subconversation" + CallsFed 'Galley "on-delete-mls-conversation" ) => Qualified UserId -> Local Conversation -> @@ -503,8 +549,9 @@ performConversationAccessData :: ( HasConversationActionEffects 'ConversationAccessDataTag r, CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-conversation-updated", + CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-new-remote-subconversation" + CallsFed 'Galley "on-delete-mls-conversation" ) => Qualified UserId -> Local Conversation -> diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index a21ce22e2ed..950e63abeb7 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -59,6 +59,7 @@ import Galley.App import qualified Galley.Data.Conversation as Data import Galley.Effects import qualified Galley.Effects.BrigAccess as E +import Galley.Effects.ConversationStore (deleteGroupIds) import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E @@ -101,7 +102,7 @@ import Wire.API.MLS.SubConversation import Wire.API.MLS.Welcome import Wire.API.Message import Wire.API.Routes.Internal.Brig.Connection -import Wire.API.Routes.Named +import Wire.API.Routes.Named (Named (Named)) import Wire.API.ServantProto type FederationAPI = "federation" :> FedApi 'Galley @@ -130,6 +131,7 @@ federationSitemap = :<|> Named @"get-sub-conversation" getSubConversationForRemoteUser :<|> Named @"delete-sub-conversation" (callsFed deleteSubConversationForRemoteUser) :<|> Named @"leave-sub-conversation" (callsFed leaveSubConversation) + :<|> Named @"on-delete-mls-conversation" onDeleteMLSConversation onClientRemoved :: ( Members @@ -382,6 +384,7 @@ leaveConversation :: ] r, CallsFed 'Galley "on-conversation-updated", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation" @@ -591,6 +594,7 @@ updateConversation :: ] r, CallsFed 'Galley "on-conversation-updated", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation" @@ -683,6 +687,7 @@ sendMLSCommitBundle :: CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "send-mls-commit-bundle", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Brig "get-mls-clients" ) => Domain -> @@ -740,6 +745,7 @@ sendMLSMessage :: CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "send-mls-message", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Brig "get-mls-clients" ) => Domain -> @@ -955,12 +961,13 @@ deleteSubConversationForRemoteUser :: SubConversationSupply ] r, - CallsFed 'Galley "on-new-remote-subconversation" + CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation" ) => Domain -> - DeleteSubConversationRequest -> + DeleteSubConversationFedRequest -> Sem r DeleteSubConversationResponse -deleteSubConversationForRemoteUser domain DeleteSubConversationRequest {..} = +deleteSubConversationForRemoteUser domain DeleteSubConversationFedRequest {..} = fmap ( either F.DeleteSubConversationResponseError @@ -970,10 +977,19 @@ deleteSubConversationForRemoteUser domain DeleteSubConversationRequest {..} = . mapToGalleyError @MLSDeleteSubConvStaticErrors $ do let qusr = Qualified dscreqUser domain - dsc = DeleteSubConversation dscreqGroupId dscreqEpoch + dsc = DeleteSubConversationRequest dscreqGroupId dscreqEpoch lconv <- qualifyLocal dscreqConv deleteLocalSubConversation qusr lconv dscreqSubConv dsc +onDeleteMLSConversation :: + Members '[ConversationStore] r => + Domain -> + OnDeleteMLSConversationRequest -> + Sem r EmptyResponse +onDeleteMLSConversation _domain OnDeleteMLSConversationRequest {..} = do + deleteGroupIds odmcGroupIds + pure EmptyResponse + -------------------------------------------------------------------------------- -- Error handling machinery diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index bc37e88340e..70afc665cf9 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -152,6 +152,7 @@ postMLSMessageFromLocalUserV1 :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Brig "get-mls-clients" ) => Local UserId -> @@ -198,6 +199,7 @@ postMLSMessageFromLocalUser :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Brig "get-mls-clients" ) => Local UserId -> @@ -237,6 +239,7 @@ postMLSCommitBundle :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Brig "get-mls-clients" ) => Local x -> @@ -277,6 +280,7 @@ postMLSCommitBundleFromLocalUser :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Brig "get-mls-clients" ) => Local UserId -> @@ -314,6 +318,7 @@ postMLSCommitBundleToLocalConv :: CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "mls-welcome", CallsFed 'Galley "on-conversation-updated", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Brig "get-mls-clients" @@ -378,6 +383,7 @@ postMLSCommitBundleToRemoteConv :: TinyLog ] r, + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Galley "send-mls-commit-bundle" ) => Local x -> @@ -452,6 +458,7 @@ postMLSMessage :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Brig "get-mls-clients" ) => Local x -> @@ -548,6 +555,7 @@ postMLSMessageToLocalConv :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Brig "get-mls-clients" ) => Qualified UserId -> @@ -722,6 +730,7 @@ processCommit :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-subconversation", CallsFed 'Galley "on-new-remote-conversation", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Brig "get-mls-clients" ) => Qualified UserId -> @@ -877,6 +886,7 @@ processCommitWithAction :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Brig "get-mls-clients" ) => Qualified UserId -> @@ -917,6 +927,7 @@ processInternalCommit :: CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Brig "get-mls-clients" ) => Qualified UserId -> @@ -1260,7 +1271,8 @@ type HasProposalActionEffects r = CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-new-remote-subconversation" + CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation" ) executeProposalAction :: diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 5dae68a4ce2..aea894f6145 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -244,12 +244,13 @@ deleteSubConversation :: ] r, CallsFed 'Galley "delete-sub-conversation", - CallsFed 'Galley "on-new-remote-subconversation" + CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation" ) => Local UserId -> Qualified ConvId -> SubConvId -> - DeleteSubConversation -> + DeleteSubConversationRequest -> Sem r () deleteSubConversation lusr qconv sconv dsc = foldQualified @@ -270,22 +271,24 @@ deleteLocalSubConversation :: MemberStore, Resource, SubConversationStore, + SubConversationSupply, SubConversationSupply ] r, - CallsFed 'Galley "on-new-remote-subconversation" + CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation" ) => Qualified UserId -> Local ConvId -> SubConvId -> - DeleteSubConversation -> + DeleteSubConversationRequest -> Sem r () deleteLocalSubConversation qusr lcnvId scnvId dsc = do assertMLSEnabled let cnvId = tUnqualified lcnvId cnv <- getConversationAndCheckMembership qusr lcnvId cs <- cnvmlsCipherSuite <$> noteS @'ConvNotFound (mlsMetadata cnv) - mlsData <- withCommitLock (dscGroupId dsc) (dscEpoch dsc) $ do + (mlsData, oldGid) <- withCommitLock (dscGroupId dsc) (dscEpoch dsc) $ do sconv <- Eff.getSubConversation cnvId scnvId >>= noteS @'ConvNotFound @@ -301,17 +304,21 @@ deleteLocalSubConversation qusr lcnvId scnvId dsc = do -- the following overwrites any prior information about the subconversation Eff.createSubConversation cnvId scnvId cs (Epoch 0) newGid Nothing - pure (scMLSData sconv) + + pure (scMLSData sconv, gid) -- notify all backends that the subconversation has a new ID let remotes = bucketRemote (map rmId (convRemoteMembers cnv)) Eff.runFederatedConcurrently_ remotes $ \_ -> do - fedClient @'Galley @"on-new-remote-subconversation" - NewRemoteSubConversation - { nrscConvId = cnvId, - nrscSubConvId = scnvId, - nrscMlsData = mlsData - } + void $ + fedClient @'Galley @"on-new-remote-subconversation" + NewRemoteSubConversation + { nrscConvId = cnvId, + nrscSubConvId = scnvId, + nrscMlsData = mlsData + } + fedClient @'Galley @"on-delete-mls-conversation" + (OnDeleteMLSConversationRequest [oldGid]) deleteRemoteSubConversation :: ( Members @@ -329,12 +336,12 @@ deleteRemoteSubConversation :: Local UserId -> Remote ConvId -> SubConvId -> - DeleteSubConversation -> + DeleteSubConversationRequest -> Sem r () deleteRemoteSubConversation lusr rcnvId scnvId dsc = do assertMLSEnabled let deleteRequest = - DeleteSubConversationRequest + DeleteSubConversationFedRequest { dscreqUser = tUnqualified lusr, dscreqConv = tUnqualified rcnvId, dscreqSubConv = scnvId, diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 80777887331..6f0cdb14621 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -1103,7 +1103,8 @@ getTeamConversation zusr tid cid = do deleteTeamConversation :: ( Members - '[ CodeStore, + '[ BrigAccess, + CodeStore, ConversationStore, Error FederationError, Error InvalidInput, @@ -1111,6 +1112,8 @@ deleteTeamConversation :: ErrorS 'InvalidOperation, ErrorS 'NotATeamMember, ErrorS ('ActionDenied 'DeleteConversation), + MemberStore, + ProposalStore, ExternalAccess, FederatorAccess, GundeckAccess, @@ -1122,7 +1125,8 @@ deleteTeamConversation :: r, CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-new-remote-subconversation" + CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation" ) => Local UserId -> ConnId -> diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 71bf2b31fc6..c84c2bc4706 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -294,7 +294,8 @@ updateConversationAccess :: CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", - CallsFed 'Galley "on-conversation-updated" + CallsFed 'Galley "on-conversation-updated", + CallsFed 'Galley "on-delete-mls-conversation" ) => Local UserId -> ConnId -> @@ -311,7 +312,8 @@ updateConversationAccessUnqualified :: CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation", - CallsFed 'Galley "on-conversation-updated" + CallsFed 'Galley "on-conversation-updated", + CallsFed 'Galley "on-delete-mls-conversation" ) => Local UserId -> ConnId -> @@ -508,7 +510,8 @@ updateConversationMessageTimerUnqualified lusr zcon cnv = updateConversationMess deleteLocalConversation :: ( Members - '[ CodeStore, + '[ BrigAccess, + CodeStore, ConversationStore, Error FederationError, ErrorS 'NotATeamMember, @@ -518,6 +521,9 @@ deleteLocalConversation :: ExternalAccess, FederatorAccess, GundeckAccess, + SubConversationStore, + MemberStore, + ProposalStore, Input Env, Input UTCTime, SubConversationStore, @@ -525,6 +531,7 @@ deleteLocalConversation :: ] r, CallsFed 'Galley "on-conversation-updated", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation" ) => @@ -865,6 +872,7 @@ addMembers :: ] r, CallsFed 'Galley "on-conversation-updated", + CallsFed 'Galley "on-delete-mls-conversation", CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", CallsFed 'Galley "on-new-remote-subconversation" @@ -912,7 +920,8 @@ addMembersUnqualifiedV2 :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-new-remote-subconversation" + CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation" ) => Local UserId -> ConnId -> @@ -957,7 +966,8 @@ addMembersUnqualified :: CallsFed 'Galley "on-conversation-updated", CallsFed 'Galley "on-mls-message-sent", CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-new-remote-subconversation" + CallsFed 'Galley "on-new-remote-subconversation", + CallsFed 'Galley "on-delete-mls-conversation" ) => Local UserId -> ConnId -> diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 7b66019eb54..852086a7cb7 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -401,6 +401,10 @@ setGroupIdForConversation :: GroupId -> Qualified ConvId -> Client () setGroupIdForConversation gId conv = write Cql.insertGroupIdForConversation (params LocalQuorum (gId, qUnqualified conv, qDomain conv)) +deleteGroupIdForConversation :: GroupId -> Client () +deleteGroupIdForConversation groupId = + retry x5 $ write Cql.deleteGroupId (params LocalQuorum (Identity groupId)) + lookupConvByGroupId :: GroupId -> Client (Maybe (Qualified ConvOrSubConvId)) lookupConvByGroupId gId = toConvOrSubConv <$$> retry x1 (query1 Cql.lookupGroupId (params LocalQuorum (Identity gId))) @@ -411,6 +415,17 @@ lookupConvByGroupId gId = Nothing -> Qualified (Conv convId) domain Just subConvId -> Qualified (SubConv convId subConvId) domain +deleteGroupIds :: + Members + '[ Embed IO, + Input ClientState + ] + r => + [GroupId] -> + Sem r () +deleteGroupIds = + embedClient . UnliftIO.pooledMapConcurrentlyN_ 8 deleteGroupIdForConversation + interpretConversationStoreToCassandra :: Members '[Embed IO, Input ClientState, TinyLog] r => Sem (ConversationStore ': r) a -> @@ -435,6 +450,8 @@ interpretConversationStoreToCassandra = interpret $ \case SetConversationEpoch cid epoch -> embedClient $ updateConvEpoch cid epoch DeleteConversation cid -> embedClient $ deleteConversation cid SetGroupIdForConversation gId cid -> embedClient $ setGroupIdForConversation gId cid + DeleteGroupIdForConversation gId -> embedClient $ deleteGroupIdForConversation gId SetPublicGroupState cid gib -> embedClient $ setPublicGroupState cid gib AcquireCommitLock gId epoch ttl -> embedClient $ acquireCommitLock gId epoch ttl ReleaseCommitLock gId epoch -> embedClient $ releaseCommitLock gId epoch + DeleteGroupIds gIds -> deleteGroupIds gIds diff --git a/services/galley/src/Galley/Cassandra/Proposal.hs b/services/galley/src/Galley/Cassandra/Proposal.hs index 7b5089631cb..b4e6935b51f 100644 --- a/services/galley/src/Galley/Cassandra/Proposal.hs +++ b/services/galley/src/Galley/Cassandra/Proposal.hs @@ -54,6 +54,8 @@ interpretProposalStoreToCassandra = runIdentity <$$> retry x1 (query getAllPendingRef (params LocalQuorum (groupId, epoch))) GetAllPendingProposals groupId epoch -> retry x1 (query getAllPending (params LocalQuorum (groupId, epoch))) + DeleteAllProposals groupId -> + retry x5 (write deleteAllProposalsForGroup (params LocalQuorum (Identity groupId))) storeQuery :: Timeout -> PrepQuery W (GroupId, Epoch, ProposalRef, ProposalOrigin, RawMLS Proposal) () storeQuery ttl = @@ -70,3 +72,6 @@ getAllPendingRef = "select ref from mls_proposal_refs where group_id = ? and epo getAllPending :: PrepQuery R (GroupId, Epoch) (Maybe ProposalOrigin, RawMLS Proposal) getAllPending = "select origin, proposal from mls_proposal_refs where group_id = ? and epoch = ?" + +deleteAllProposalsForGroup :: PrepQuery W (Identity GroupId) () +deleteAllProposalsForGroup = "delete from mls_proposal_refs where group_id = ?" diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index e5e5be455cd..0fed085240f 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -335,8 +335,8 @@ updateSubConvPublicGroupState = "INSERT INTO subconversation (conv_id, subconv_i selectSubConvPublicGroupState :: PrepQuery R (ConvId, SubConvId) (Identity (Maybe OpaquePublicGroupState)) selectSubConvPublicGroupState = "SELECT public_group_state FROM subconversation WHERE conv_id = ? AND subconv_id = ?" -deleteGroupIdForSubconv :: PrepQuery W (Identity GroupId) () -deleteGroupIdForSubconv = "DELETE FROM group_id_conv_id WHERE group_id = ?" +deleteGroupId :: PrepQuery W (Identity GroupId) () +deleteGroupId = "DELETE FROM group_id_conv_id WHERE group_id = ?" insertGroupIdForSubConversation :: PrepQuery W (GroupId, ConvId, Domain, SubConvId) () insertGroupIdForSubConversation = "INSERT INTO group_id_conv_id (group_id, conv_id, domain, subconv_id) VALUES (?, ?, ?, ?)" @@ -350,6 +350,12 @@ insertEpochForSubConversation = "UPDATE subconversation set epoch = ? WHERE conv listSubConversations :: PrepQuery R (Identity ConvId) (SubConvId, CipherSuiteTag, Epoch, Writetime Epoch, GroupId) listSubConversations = "SELECT subconv_id, cipher_suite, epoch, WRITETIME(epoch), group_id FROM subconversation WHERE conv_id = ?" +selectSubConversations :: PrepQuery R (Identity ConvId) (Identity SubConvId) +selectSubConversations = "SELECT subconv_id FROM subconversation WHERE conv_id = ?" + +deleteSubConversation :: PrepQuery W (ConvId, SubConvId) () +deleteSubConversation = "DELETE FROM subconversation where conv_id = ? and subconv_id = ?" + -- Members ------------------------------------------------------------------ type MemberStatus = Int32 diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index be9e188ccb3..574e32d3b3c 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -78,7 +78,11 @@ setEpochForSubConversation cid sconv epoch = deleteGroupId :: GroupId -> Client () deleteGroupId groupId = - retry x5 $ write Cql.deleteGroupIdForSubconv (params LocalQuorum (Identity groupId)) + retry x5 $ write Cql.deleteGroupId (params LocalQuorum (Identity groupId)) + +deleteSubConversation :: ConvId -> SubConvId -> Client () +deleteSubConversation cid sconv = + retry x5 $ write Cql.deleteSubConversation (params LocalQuorum (cid, sconv)) listSubConversations :: ConvId -> Client (Map SubConvId ConversationMLSData) listSubConversations cid = do @@ -108,6 +112,7 @@ interpretSubConversationStoreToCassandra = interpret $ \case SetSubConversationEpoch cid sconv epoch -> embedClient $ setEpochForSubConversation cid sconv epoch DeleteGroupIdForSubConversation groupId -> embedClient $ deleteGroupId groupId ListSubConversations cid -> embedClient $ listSubConversations cid + DeleteSubConversation convId subConvId -> embedClient $ deleteSubConversation convId subConvId -------------------------------------------------------------------------------- -- Utilities diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index 0440dba79ae..099dfb747c0 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -36,13 +36,13 @@ module Galley.Effects ClientStore, CodeStore, ConversationStore, - SubConversationStore, CustomBackendStore, LegalHoldStore, MemberStore, ProposalStore, SearchVisibilityStore, ServiceStore, + SubConversationStore, TeamFeatureStore, TeamMemberStore, TeamNotificationStore, diff --git a/services/galley/src/Galley/Effects/BrigAccess.hs b/services/galley/src/Galley/Effects/BrigAccess.hs index 570672a0b9c..da209359f24 100644 --- a/services/galley/src/Galley/Effects/BrigAccess.hs +++ b/services/galley/src/Galley/Effects/BrigAccess.hs @@ -53,6 +53,7 @@ module Galley.Effects.BrigAccess addKeyPackageRef, validateAndAddKeyPackageRef, updateKeyPackageRef, + deleteKeyPackageRefs, -- * Features getAccountConferenceCallingConfigClient, @@ -134,6 +135,7 @@ data BrigAccess m a where AddKeyPackageRef :: KeyPackageRef -> Qualified UserId -> ClientId -> Qualified ConvId -> BrigAccess m () ValidateAndAddKeyPackageRef :: NewKeyPackage -> BrigAccess m (Either Text NewKeyPackageResult) UpdateKeyPackageRef :: KeyPackageUpdate -> BrigAccess m () + DeleteKeyPackageRefs :: [KeyPackageRef] -> BrigAccess m () UpdateSearchVisibilityInbound :: Multi.TeamStatus SearchVisibilityInboundConfig -> BrigAccess m () diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index c89d58dce95..0e0763e6af3 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -45,7 +45,9 @@ module Galley.Effects.ConversationStore setConversationEpoch, acceptConnectConversation, setGroupIdForConversation, + deleteGroupIdForConversation, setPublicGroupState, + deleteGroupIds, -- * Delete conversation deleteConversation, @@ -98,12 +100,14 @@ data ConversationStore m a where SetConversationMessageTimer :: ConvId -> Maybe Milliseconds -> ConversationStore m () SetConversationEpoch :: ConvId -> Epoch -> ConversationStore m () SetGroupIdForConversation :: GroupId -> Qualified ConvId -> ConversationStore m () + DeleteGroupIdForConversation :: GroupId -> ConversationStore m () SetPublicGroupState :: ConvId -> OpaquePublicGroupState -> ConversationStore m () AcquireCommitLock :: GroupId -> Epoch -> NominalDiffTime -> ConversationStore m LockAcquired ReleaseCommitLock :: GroupId -> Epoch -> ConversationStore m () + DeleteGroupIds :: [GroupId] -> ConversationStore m () makeSem ''ConversationStore diff --git a/services/galley/src/Galley/Effects/ProposalStore.hs b/services/galley/src/Galley/Effects/ProposalStore.hs index 4dfd5993177..cf549d576c3 100644 --- a/services/galley/src/Galley/Effects/ProposalStore.hs +++ b/services/galley/src/Galley/Effects/ProposalStore.hs @@ -47,5 +47,8 @@ data ProposalStore m a where GroupId -> Epoch -> ProposalStore m [(Maybe ProposalOrigin, RawMLS Proposal)] + DeleteAllProposals :: + GroupId -> + ProposalStore m () makeSem ''ProposalStore diff --git a/services/galley/src/Galley/Effects/SubConversationStore.hs b/services/galley/src/Galley/Effects/SubConversationStore.hs index 29d5d0f3ce2..056eec34d81 100644 --- a/services/galley/src/Galley/Effects/SubConversationStore.hs +++ b/services/galley/src/Galley/Effects/SubConversationStore.hs @@ -39,5 +39,6 @@ data SubConversationStore m a where SetSubConversationEpoch :: ConvId -> SubConvId -> Epoch -> SubConversationStore m () DeleteGroupIdForSubConversation :: GroupId -> SubConversationStore m () ListSubConversations :: ConvId -> SubConversationStore m (Map SubConvId ConversationMLSData) + DeleteSubConversation :: ConvId -> SubConvId -> SubConversationStore m () makeSem ''SubConversationStore diff --git a/services/galley/src/Galley/Intra/Client.hs b/services/galley/src/Galley/Intra/Client.hs index 80caf138706..2740ecd8a14 100644 --- a/services/galley/src/Galley/Intra/Client.hs +++ b/services/galley/src/Galley/Intra/Client.hs @@ -27,6 +27,7 @@ module Galley.Intra.Client addKeyPackageRef, updateKeyPackageRef, validateAndAddKeyPackageRef, + deleteKeyPackageRefs, ) where @@ -207,6 +208,17 @@ getLocalMLSClients lusr ss = ) >>= parseResponse (mkError status502 "server-error") +deleteKeyPackageRefs :: [KeyPackageRef] -> App () +deleteKeyPackageRefs refs = + void $ + call + Brig + ( method DELETE + . paths ["i", "mls", "key-packages"] + . json (DeleteKeyPackageRefsRequest refs) + . expect2xx + ) + addKeyPackageRef :: KeyPackageRef -> Qualified UserId -> ClientId -> Qualified ConvId -> App () addKeyPackageRef ref qusr cl qcnv = void $ diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs index c42b7f1d632..011c4183b24 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/services/galley/src/Galley/Intra/Effects.hs @@ -88,6 +88,9 @@ interpretBrigAccess = interpret $ \case UpdateKeyPackageRef update -> embedApp $ updateKeyPackageRef update + DeleteKeyPackageRefs refs -> + embedApp $ + deleteKeyPackageRefs refs UpdateSearchVisibilityInbound status -> embedApp $ updateSearchVisibilityInbound status diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index f8fa0f18359..45083bbf815 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -222,7 +222,8 @@ tests s = test s "reset a subconversation as a non-member" (testDeleteSubConv False), test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale, test s "leave a subconversation" testLeaveSubConv, - test s "leave a subconversation as a non-member" testLeaveSubConvNonMember + test s "leave a subconversation as a non-member" testLeaveSubConvNonMember, + test s "delete parent conversation of a subconversation" testDeleteParentOfSubConv ], testGroup "Local Sender/Remote Subconversation" @@ -240,7 +241,8 @@ tests s = test s "get subconversation as a remote non-member" (testRemoteMemberGetSubConv False), test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv, test s "delete subconversation as a remote member" (testRemoteMemberDeleteSubConv True), - test s "delete subconversation as a remote non-member" (testRemoteMemberDeleteSubConv False) + test s "delete subconversation as a remote non-member" (testRemoteMemberDeleteSubConv False), + test s "delete parent conversation of a remote subconveration" testDeleteRemoteParentOfSubConv ] ] ] @@ -2238,7 +2240,7 @@ deleteSubConversationDisabled = do cnvId <- Qualified <$> randomId <*> pure (Domain "www.example.com") let scnvId = SubConvId "conference" dsc = - DeleteSubConversation + DeleteSubConversationRequest (GroupId "MLS") (Epoch 0) withMLSDisabled $ @@ -2616,7 +2618,7 @@ testRemoteMemberGetSubConv isAMember = do expectSubConvError _errExpected (GetSubConversationsResponseSuccess _) = liftIO $ assertFailure "Unexpected GetSubConversationsResponseSuccess" expectSubConvError errExpected (GetSubConversationsResponseError err) = liftIO $ err @?= errExpected -testRemoteMemberDeleteSubConv :: Bool -> TestM () +testRemoteMemberDeleteSubConv :: HasCallStack => Bool -> TestM () testRemoteMemberDeleteSubConv isAMember = do -- alice is local, bob is remote -- alice creates a local conversation and invites bob @@ -2644,7 +2646,7 @@ testRemoteMemberDeleteSubConv isAMember = do randUser <- randomId let delReq = - DeleteSubConversationRequest + DeleteSubConversationFedRequest { dscreqUser = if isAMember then qUnqualified bob else randUser, dscreqConv = cnv, dscreqSubConv = scnv, @@ -2655,16 +2657,26 @@ testRemoteMemberDeleteSubConv isAMember = do -- Bob is a member of the parent conversation so he's allowed to delete the -- subconversation. (res, reqs) <- - withTempMockFederator' ("on-new-remote-subconversation" ~> EmptyResponse) $ do + withTempMockFederator' deleteMLSConvMock $ do fedGalleyClient <- view tsFedGalleyClient runFedClient @"delete-sub-conversation" fedGalleyClient bobDomain delReq + when isAMember $ do - req <- assertOne (filter ((== "on-new-remote-subconversation") . frRPC) reqs) - nrsc <- assertOne (toList (Aeson.decode (frBody req))) liftIO $ do + req <- assertOne (filter ((== "on-new-remote-subconversation") . frRPC) reqs) + nrsc <- assertOne (toList (Aeson.decode (frBody req))) nrscConvId nrsc @?= cnv nrscSubConvId nrsc @?= scnv + liftIO $ do + fr <- assertOne (filter ((== "on-delete-mls-conversation") . frRPC) reqs) + frTargetDomain fr @?= bobDomain + frRPC fr @?= "on-delete-mls-conversation" + bdy <- case Aeson.eitherDecode (frBody fr) of + Right b -> pure b + Left e -> assertFailure $ "Could not parse delete-sub-conversation request body: " <> e + odmcGroupIds bdy @?= [groupId] + if isAMember then expectSuccess res else expectFailure ConvNotFound res where expectSuccess :: DeleteSubConversationResponse -> TestM () @@ -2699,7 +2711,7 @@ testDeleteSubConv isAMember = do responseJsonError =<< getSubConv (qUnqualified alice) qcnv sconv >= sendAndConsumeCommit + createSubConv qcnv alice1 sconv + + subGid <- getCurrentGroupId + + resetGroup arthur1 qcs subGid + void $ createExternalCommit arthur1 Nothing qcs >>= sendAndConsumeCommitBundle + + resetGroup bob1 qcs subGid + void $ createExternalCommit bob1 Nothing qcs >>= sendAndConsumeCommitBundle + + sub' <- + responseJsonError + =<< liftTest + ( getSubConv (qUnqualified alice) qcnv sconv + sendMessageMock + void $ withTempMockFederator' mock $ do + -- bob joins subconversation + commit <- createExternalCommit bob1 Nothing qcs + void $ sendAndConsumeCommitBundle commit + + -- bob can send to remote conversation + void $ + withTempMockFederator' sendMessageMock $ do + message <- createApplicationMessage bob1 "hi" + postMessage (mpSender message) (mpMessage message) + !!! const 201 === statusCode + + -- remote notifies about deletion of group + liftTest $ do + client <- view tsFedGalleyClient + let odm = OnDeleteMLSConversationRequest [mainGroupId, subGroupId] + void $ + runFedClient + @"on-delete-mls-conversation" + client + (qDomain alice) + odm + + -- bob's backend has no longer a mapping of the group id + void $ + withTempMockFederator' sendMessageMock $ do + message <- createApplicationMessage bob1 "hi" + postMessage (mpSender message) (mpMessage message) + !!! const 404 === statusCode + testDeleteRemoteSubConv :: Bool -> TestM () testDeleteRemoteSubConv isAMember = do alice <- randomQualifiedUser @@ -2752,7 +2875,7 @@ testDeleteRemoteSubConv isAMember = do groupId = GroupId "deadbeef" epoch = Epoch 0 expectedReq = - DeleteSubConversationRequest + DeleteSubConversationFedRequest { dscreqUser = qUnqualified alice, dscreqConv = conv, dscreqSubConv = sconv, @@ -2766,7 +2889,7 @@ testDeleteRemoteSubConv isAMember = do if isAMember then DeleteSubConversationResponseSuccess else DeleteSubConversationResponseError ConvNotFound - dsc = DeleteSubConversation groupId epoch + dsc = DeleteSubConversationRequest groupId epoch (_, reqs) <- withTempMockFederator' mock $ @@ -2774,7 +2897,7 @@ testDeleteRemoteSubConv isAMember = do EmptyResponse, + "on-new-remote-subconversation" ~> EmptyResponse, + "on-conversation-updated" ~> EmptyResponse + ] diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 66fb3aec683..fb41fbe667b 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -1200,7 +1200,7 @@ deleteSubConv :: UserId -> Qualified ConvId -> SubConvId -> - DeleteSubConversation -> + DeleteSubConversationRequest -> TestM ResponseLBS deleteSubConv u qcnv sconv dsc = do g <- viewGalley @@ -1290,3 +1290,9 @@ leaveCurrentConv cid qsub = case qUnqualified qsub of mls { mlsMembers = Set.difference (mlsMembers mls) (Set.singleton cid) } + +getCurrentGroupId :: MLSTest GroupId +getCurrentGroupId = do + State.gets mlsGroupId >>= \case + Nothing -> liftIO $ assertFailure "Creating add proposal for non-existing group" + Just g -> pure g diff --git a/services/galley/test/integration/TestSetup.hs b/services/galley/test/integration/TestSetup.hs index 5936d3e4fd6..4c1fc576a19 100644 --- a/services/galley/test/integration/TestSetup.hs +++ b/services/galley/test/integration/TestSetup.hs @@ -135,7 +135,8 @@ runFedClient :: forall (name :: Symbol) comp m api. ( HasUnsafeFedEndpoint comp api name, Servant.HasClient Servant.ClientM api, - MonadIO m + MonadIO m, + HasCallStack ) => FedClient comp -> Domain -> From a7ba9f67c1ced1cd02dc39bddeff821c2d926c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 14 Feb 2023 11:00:47 +0100 Subject: [PATCH 022/225] [FS-1534] A subconversation leaver gets a remove proposal (#3080) --- changelog.d/2-features/subconv-leave | 2 +- services/galley/src/Galley/API/MLS/Message.hs | 9 ++++--- .../galley/src/Galley/API/MLS/Propagate.hs | 24 ++++++++++++------- services/galley/src/Galley/API/MLS/Removal.hs | 17 +++++++++---- .../src/Galley/API/MLS/SubConversation.hs | 3 +++ services/galley/src/Galley/API/MLS/Types.hs | 7 ++++++ services/galley/test/integration/API/MLS.hs | 5 +++- 7 files changed, 49 insertions(+), 18 deletions(-) diff --git a/changelog.d/2-features/subconv-leave b/changelog.d/2-features/subconv-leave index 73daf536158..6eb2aa59c0d 100644 --- a/changelog.d/2-features/subconv-leave +++ b/changelog.d/2-features/subconv-leave @@ -1 +1 @@ -Implement endpoint for leaving a subconversation +Implement endpoint for leaving a subconversation (#2969, #3080) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 70afc665cf9..68682c76cbe 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -362,7 +362,8 @@ postMLSCommitBundleToLocalConv qusr mc conn bundle lConvOrSubId = do ApplicationMessage _ -> throwS @'MLSUnsupportedMessage ProposalMessage _ -> throwS @'MLSUnsupportedMessage - propagateMessage qusr lConvOrSub conn (rmRaw (cbCommitMsg bundle)) + let cm = membersConvOrSub (tUnqualified lConvOrSub) + propagateMessage qusr lConvOrSub conn (rmRaw (cbCommitMsg bundle)) cm for_ (cbWelcome bundle) $ postMLSWelcome lConvOrSub conn @@ -582,7 +583,8 @@ postMLSMessageToLocalConv qusr senderClient con smsg convOrSubId = case rmValue Right ApplicationMessageTag -> pure mempty Left _ -> throwS @'MLSUnsupportedMessage - propagateMessage qusr lConvOrSub con (rmRaw smsg) + let cm = membersConvOrSub (tUnqualified lConvOrSub) + propagateMessage qusr lConvOrSub con (rmRaw smsg) cm pure events @@ -842,7 +844,8 @@ processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = wi -- fetch backend remove proposals of the previous epoch kpRefs <- getPendingBackendRemoveProposals (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub') epoch -- requeue backend remove proposals for the current epoch - createAndSendRemoveProposals lConvOrSub' kpRefs qusr + let cm = membersConvOrSub (tUnqualified lConvOrSub') + createAndSendRemoveProposals lConvOrSub' kpRefs qusr cm where derefUser :: ClientMap -> Qualified UserId -> Sem r (ClientIdentity, KeyPackageRef) derefUser cm user = case Map.assocs cm of diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 374cefafaa5..67619c21ce8 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -59,11 +59,17 @@ propagateMessage :: Local ConvOrSubConv -> Maybe ConnId -> ByteString -> + -- | The client map that has all the recipients of the message. This is an + -- argument, and not constructed within the function, because of a special + -- case of subconversations where everyone but the subconversation leaver + -- client should get the remove proposal message; in this case the recipients + -- are a strict subset of all the clients represented by the in-memory + -- conversation/subconversation client maps. + ClientMap -> Sem r () -propagateMessage qusr lConvOrSub con raw = do +propagateMessage qusr lConvOrSub con raw cm = do now <- input @UTCTime - let cm = membersConvOrSub (tUnqualified lConvOrSub) - mlsConv = convOfConvOrSub <$> lConvOrSub + let mlsConv = convOfConvOrSub <$> lConvOrSub lmems = mcLocalMembers . tUnqualified $ mlsConv botMap = Map.fromList $ do m <- lmems @@ -80,7 +86,7 @@ propagateMessage qusr lConvOrSub con raw = do mkPush :: UserId -> ClientId -> MessagePush 'NormalMessage mkPush u c = newMessagePush mlsConv botMap con mm (u, c) e runMessagePush mlsConv (Just qcnv) $ - foldMap (uncurry mkPush) (lmems >>= localMemberMLSClients mlsConv cm) + foldMap (uncurry mkPush) (lmems >>= localMemberMLSClients mlsConv) -- send to remotes traverse_ handleError @@ -92,20 +98,20 @@ propagateMessage qusr lConvOrSub con raw = do rmmSender = qusr, rmmMetadata = mm, rmmConversation = qUnqualified qcnv, - rmmRecipients = rs >>= remoteMemberMLSClients cm, + rmmRecipients = rs >>= remoteMemberMLSClients, rmmMessage = Base64ByteString raw } where - localMemberMLSClients :: Local x -> ClientMap -> LocalMember -> [(UserId, ClientId)] - localMemberMLSClients loc cm lm = + localMemberMLSClients :: Local x -> LocalMember -> [(UserId, ClientId)] + localMemberMLSClients loc lm = let localUserQId = tUntagged (qualifyAs loc localUserId) localUserId = lmId lm in map (\(c, _) -> (localUserId, c)) (Map.assocs (Map.findWithDefault mempty localUserQId cm)) - remoteMemberMLSClients :: ClientMap -> RemoteMember -> [(UserId, ClientId)] - remoteMemberMLSClients cm rm = + remoteMemberMLSClients :: RemoteMember -> [(UserId, ClientId)] + remoteMemberMLSClients rm = let remoteUserQId = tUntagged (rmId rm) remoteUserId = qUnqualified remoteUserQId in map diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 227d3d32182..72c0d83560b 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -68,8 +68,15 @@ createAndSendRemoveProposals :: Local ConvOrSubConv -> t KeyPackageRef -> Qualified UserId -> + -- | The client map that has all the recipients of the message. This is an + -- argument, and not constructed within the function, because of a special + -- case of subconversations where everyone but the subconversation leaver + -- client should get the remove proposal message; in this case the recipients + -- are a strict subset of all the clients represented by the in-memory + -- conversation/subconversation client maps. + ClientMap -> Sem r () -createAndSendRemoveProposals lConvOrSubConv cs qusr = do +createAndSendRemoveProposals lConvOrSubConv cs qusr cm = do let meta = mlsMetaConvOrSub (tUnqualified lConvOrSubConv) mKeyPair <- getMLSRemovalKey case mKeyPair of @@ -86,7 +93,7 @@ createAndSendRemoveProposals lConvOrSubConv cs qusr = do (proposalRef (cnvmlsCipherSuite meta) proposal) ProposalOriginBackend proposal - propagateMessage qusr lConvOrSubConv Nothing msgEncoded + propagateMessage qusr lConvOrSubConv Nothing msgEncoded cm -- | Send remove proposals for a single client of a user to the local conversation. removeClient :: @@ -113,7 +120,8 @@ removeClient lc qusr cid = do for_ mMlsConv $ \mlsConv -> do -- FUTUREWORK: also remove the client from from subconversations of lc let cidAndKPs = maybeToList (cmLookupRef (mkClientIdentity qusr cid) (mcMembers mlsConv)) - createAndSendRemoveProposals (qualifyAs lc (Conv mlsConv)) cidAndKPs qusr + cm = mcMembers mlsConv + createAndSendRemoveProposals (qualifyAs lc (Conv mlsConv)) cidAndKPs qusr cm -- | Send remove proposals for all clients of the user to the local conversation. removeUser :: @@ -139,4 +147,5 @@ removeUser lc qusr = do for_ mMlsConv $ \mlsConv -> do -- FUTUREWORK: also remove the client from from subconversations of lc let kprefs = toList (Map.findWithDefault mempty qusr (mcMembers mlsConv)) - createAndSendRemoveProposals (qualifyAs lc (Conv mlsConv)) kprefs qusr + cm = mcMembers mlsConv + createAndSendRemoveProposals (qualifyAs lc (Conv mlsConv)) kprefs qusr cm diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index aea894f6145..412b6c7c52a 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -419,10 +419,13 @@ leaveLocalSubConversation cid lcnv sub = do kp <- note (mlsProtocolError "Client is not a member of the subconversation") $ cmLookupRef cid (scMembers subConv) + -- remove the leaver from the member list + let cm = cmRemoveClient cid (scMembers subConv) createAndSendRemoveProposals (qualifyAs lcnv (SubConv mlsConv subConv)) (Identity kp) (cidQualifiedUser cid) + cm leaveRemoteSubConversation :: ( Members diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index d1feefdd3ba..38d6dff532a 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -44,6 +44,13 @@ cmLookupRef cid cm = do clients <- Map.lookup (cidQualifiedUser cid) cm Map.lookup (ciClient cid) clients +cmRemoveClient :: ClientIdentity -> ClientMap -> ClientMap +cmRemoveClient cid cm = case Map.lookup (cidQualifiedUser cid) cm of + Nothing -> cm + Just clients -> + let clients' = Map.delete (ciClient cid) clients + in Map.insert (cidQualifiedUser cid) clients' cm + isClientMember :: ClientIdentity -> ClientMap -> Bool isClientMember ci = isJust . cmLookupRef ci diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 45083bbf815..94a51a58b27 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -2914,6 +2914,7 @@ testLeaveSubConv = do (qsub, _) <- withTempMockFederator' ( receiveCommitMock [charlie1] <|> welcomeMock + <|> ("on-mls-message-sent" ~> RemoteMLSMessageOk) ) $ do void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommit @@ -2928,7 +2929,7 @@ testLeaveSubConv = do [bob1KP] <- map snd . filter (\(cid, _) -> cid == bob1) <$> getClientsFromGroupState alice1 bob - mlsBracket [alice1, bob2] $ \wss -> do + mlsBracket [bob1, alice1, bob2] $ \(wsBob1 : wss) -> do (_, reqs) <- withTempMockFederator' messageSentMock $ leaveCurrentConv bob1 qsub req <- assertOne @@ -2944,6 +2945,8 @@ testLeaveSubConv = do WS.assertMatchN (5 # WS.Second) wss $ wsAssertBackendRemoveProposal bob qcnv bob1KP traverse_ (uncurry consumeMessage1) (zip [alice1, bob2] msgs) + -- assert the leaver gets no proposal or event + void . liftIO $ WS.assertNoEvent (5 # WS.Second) [wsBob1] -- alice commits the pending proposal void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle From e1253d4bb465d8b4498beaef53584ed725c1ea8c Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 15 Feb 2023 19:19:34 +0100 Subject: [PATCH 023/225] [FS-1335] Remove clients from subconversations when user is removed from main conversation (#2942) --- .../reflect-user-removal-from-parent-in-sub | 1 + libs/wire-api/src/Wire/API/User/Client.hs | 19 ++ services/galley/src/Galley/API/Action.hs | 53 ++-- services/galley/src/Galley/API/Clients.hs | 3 +- services/galley/src/Galley/API/Federation.hs | 1 + services/galley/src/Galley/API/Internal.hs | 36 ++- services/galley/src/Galley/API/MLS/Message.hs | 140 +++++----- services/galley/src/Galley/API/MLS/Removal.hs | 64 ++++- services/galley/src/Galley/API/Util.hs | 26 +- .../src/Galley/Cassandra/SubConversation.hs | 5 +- services/galley/test/integration/API/MLS.hs | 261 +++++++++++++++++- .../galley/test/integration/API/MLS/Util.hs | 8 +- services/galley/test/integration/API/Util.hs | 27 +- 13 files changed, 515 insertions(+), 129 deletions(-) create mode 100644 changelog.d/2-features/reflect-user-removal-from-parent-in-sub diff --git a/changelog.d/2-features/reflect-user-removal-from-parent-in-sub b/changelog.d/2-features/reflect-user-removal-from-parent-in-sub new file mode 100644 index 00000000000..8f411ae91f2 --- /dev/null +++ b/changelog.d/2-features/reflect-user-removal-from-parent-in-sub @@ -0,0 +1 @@ +Removing or kicking a user from a conversation also removes the user's clients from any subconversation. diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index 230e926982b..a6e0911acb0 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -63,6 +63,9 @@ module Wire.API.User.Client longitude, Latitude (..), Longitude (..), + + -- * List of MLS client ids + ClientList (..), ) where @@ -472,6 +475,22 @@ instance ToSchema Client where mlsPublicKeysFieldSchema :: ObjectSchema SwaggerDoc MLSPublicKeys mlsPublicKeysFieldSchema = fromMaybe mempty <$> optField "mls_public_keys" mlsPublicKeysSchema +-------------------------------------------------------------------------------- +-- ClientList + +-- | Client list for internal API. +data ClientList = ClientList {clClients :: [ClientId]} + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform ClientList) + deriving (FromJSON, ToJSON, Swagger.ToSchema) via Schema ClientList + +instance ToSchema ClientList where + schema = + object "ClientList" $ + ClientList + <$> clClients + .= field "client_ids" (array schema) + -------------------------------------------------------------------------------- -- PubClient diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index bbd8ff3b235..dc471c8e870 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -132,21 +132,36 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con r HasConversationActionEffects 'ConversationLeaveTag r = ( Members - '[ MemberStore, - Error InternalError, + '[ Error InternalError, Error NoChanges, ExternalAccess, FederatorAccess, GundeckAccess, - Input UTCTime, Input Env, + Input UTCTime, + MemberStore, ProposalStore, + SubConversationStore, TinyLog ] r ) HasConversationActionEffects 'ConversationRemoveMembersTag r = - (Members '[MemberStore, Error NoChanges] r) + ( Members + '[ MemberStore, + SubConversationStore, + ProposalStore, + Input Env, + Input UTCTime, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Error InternalError, + Error NoChanges, + TinyLog + ] + r + ) HasConversationActionEffects 'ConversationMemberUpdateTag r = (Members '[MemberStore, ErrorS 'ConvMemberNotFound] r) HasConversationActionEffects 'ConversationDeleteTag r = @@ -309,6 +324,9 @@ type family PerformActionCalls tag where PerformActionCalls 'ConversationLeaveTag = ( CallsFed 'Galley "on-mls-message-sent" ) + PerformActionCalls 'ConversationRemoveMembersTag = + ( CallsFed 'Galley "on-mls-message-sent" + ) PerformActionCalls 'ConversationDeleteTag = ( CallsFed 'Galley "on-delete-mls-conversation" ) @@ -334,31 +352,16 @@ performAction tag origUser lconv action = do performConversationJoin origUser lconv action SConversationLeaveTag -> do let victims = [origUser] - E.deleteMembers (tUnqualified lcnv) (toUserList lconv victims) - -- update in-memory view of the conversation - let lconv' = - lconv <&> \c -> - foldQualified - lconv - ( \lu -> - c - { convLocalMembers = - filter (\lm -> lmId lm /= tUnqualified lu) (convLocalMembers c) - } - ) - ( \ru -> - c - { convRemoteMembers = - filter (\rm -> rmId rm /= ru) (convRemoteMembers c) - } - ) - origUser + lconv' <- traverse (convDeleteMembers (toUserList lconv victims)) lconv + -- send remove proposals in the MLS case traverse_ (removeUser lconv') victims pure (mempty, action) SConversationRemoveMembersTag -> do let presentVictims = filter (isConvMemberL lconv) (toList action) when (null presentVictims) noChanges - E.deleteMembers (tUnqualified lcnv) (toUserList lconv presentVictims) + traverse_ (convDeleteMembers (toUserList lconv presentVictims)) lconv + -- send remove proposals in the MLS case + traverse_ (removeUser lconv) presentVictims pure (mempty, action) -- FUTUREWORK: should we return the filtered action here? SConversationMemberUpdateTag -> do void $ ensureOtherMember lconv (cmuTarget action) conv @@ -487,8 +490,8 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do '[ ConversationStore, Error InternalError, ErrorS ('ActionDenied 'LeaveConversation), - ErrorS 'InvalidOperation, ErrorS 'ConvNotFound, + ErrorS 'InvalidOperation, ErrorS 'MissingLegalholdConsent, ExternalAccess, FederatorAccess, diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index b16dc16f0f0..ce563439f38 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -104,7 +104,8 @@ rmClientH :: MemberStore, Error InternalError, ProposalStore, - P.TinyLog + P.TinyLog, + SubConversationStore ] r, CallsFed 'Galley "on-client-removed", diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 7008b3f247a..f017c1fe831 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -145,6 +145,7 @@ onClientRemoved :: Input UTCTime, MemberStore, ProposalStore, + SubConversationStore, TinyLog ] r, diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 8e5d1b647a8..11ea5e607da 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -28,6 +28,7 @@ import Control.Exception.Safe (catchAny) import Control.Lens hiding (Getter, Setter, (.=)) import Data.Id as Id import Data.List1 (maybeList1) +import qualified Data.Map as Map import Data.Qualified import Data.Range import Data.Singletons @@ -60,6 +61,7 @@ import Galley.Effects.FederatorAccess import Galley.Effects.GundeckAccess import Galley.Effects.LegalHoldStore as LegalHoldStore import Galley.Effects.MemberStore +import qualified Galley.Effects.MemberStore as E import Galley.Effects.TeamStore import qualified Galley.Intra.Push as Intra import Galley.Monad @@ -86,7 +88,7 @@ import qualified Servant hiding (WithStatus) import System.Logger.Class hiding (Path, name) import qualified System.Logger.Class as Log import Wire.API.ApplyMods -import Wire.API.Conversation hiding (Member) +import Wire.API.Conversation import Wire.API.Conversation.Action import Wire.API.Conversation.Role import Wire.API.CustomBackend @@ -96,6 +98,7 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.MLS.Group import Wire.API.Provider.Service hiding (Service) import Wire.API.Routes.API import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti @@ -109,6 +112,7 @@ import Wire.API.Team import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.SearchVisibility +import Wire.API.User.Client import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra @@ -271,6 +275,19 @@ type InternalAPIBase = :> ReqBody '[Servant.JSON] Connect :> ConversationVerb ) + -- This endpoint is meant for testing membership of a conversation + :<|> Named + "get-conversation-clients" + ( Summary "Get mls conversation client list" + :> ZLocalUser + :> CanThrow 'ConvNotFound + :> "conversation" + :> Capture "cnv" ConvId + :> MultiVerb1 + 'GET + '[Servant.JSON] + (Respond 200 "Clients" ClientList) + ) :<|> Named "guard-legalhold-policy-conflicts" ( "guard-legalhold-policy-conflicts" @@ -479,6 +496,7 @@ internalAPI = mkNamedAPI @"status" (pure ()) <@> mkNamedAPI @"delete-user" (callsFed rmUser) <@> mkNamedAPI @"connect" (callsFed Create.createConnectConversation) + <@> mkNamedAPI @"get-conversation-clients" iGetMLSClientListForConv <@> mkNamedAPI @"guard-legalhold-policy-conflicts" guardLegalholdPolicyConflictsH <@> legalholdWhitelistedTeamsAPI <@> iTeamsAPI @@ -688,6 +706,7 @@ rmUser :: MemberStore, ProposalStore, P.TinyLog, + SubConversationStore, TeamStore ] r, @@ -842,3 +861,18 @@ guardLegalholdPolicyConflictsH :: guardLegalholdPolicyConflictsH glh = do mapError @LegalholdConflicts (const $ Tagged @'MissingLegalholdConsent ()) $ guardLegalholdPolicyConflicts (glhProtectee glh) (glhUserClients glh) + +-- | Get an MLS conversation client list +iGetMLSClientListForConv :: + forall r. + Members + '[ MemberStore, + ErrorS 'ConvNotFound + ] + r => + Local UserId -> + ConvId -> + Sem r ClientList +iGetMLSClientListForConv lusr cnv = do + cm <- E.lookupMLSClients (convToGroupId (qualifyAs lusr cnv)) + pure $ ClientList (concatMap (Map.keys . snd) (Map.assocs cm)) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index de873b1224a..b4836b518b9 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -662,6 +662,7 @@ data ProposalAction = ProposalAction -- to know if a commit has one when processing external commits paExternalInit :: Any } + deriving (Show) instance Semigroup ProposalAction where ProposalAction add1 rem1 init1 <> ProposalAction add2 rem2 init2 = @@ -769,71 +770,72 @@ processExternalCommit :: ProposalAction -> Maybe UpdatePath -> Sem r () -processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = withCommitLock (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub) epoch $ do - let convOrSub = tUnqualified lConvOrSub - newKeyPackage <- - upLeaf - <$> note - (mlsProtocolError "External commits need an update path") - updatePath - when (paExternalInit action == mempty) $ - throw . mlsProtocolError $ - "The external commit is missing an external init proposal" - unless (paAdd action == mempty) $ - throw . mlsProtocolError $ - "The external commit must not have add proposals" - - newRef <- - kpRef' newKeyPackage - & note (mlsProtocolError "An invalid key package in the update path") - - -- validate and update mapping in brig - eithCid <- - nkpresClientIdentity - <$$> validateAndAddKeyPackageRef - NewKeyPackage - { nkpConversation = tUntagged (convOfConvOrSub . idForConvOrSub <$> lConvOrSub), - nkpKeyPackage = KeyPackageData (rmRaw newKeyPackage) - } - cid <- either (\errMsg -> throw (mlsProtocolError ("Tried to add invalid KeyPackage: " <> errMsg))) pure eithCid - - unless (cidQualifiedUser cid == qusr) $ - throw . mlsProtocolError $ - "The external commit attempts to add another user" - - senderClient <- noteS @'MLSMissingSenderClient mSenderClient - - unless (ciClient cid == senderClient) $ - throw . mlsProtocolError $ - "The external commit attempts to add another client of the user, it must only add itself" - - -- only members can join a subconversation - forOf_ _SubConv convOrSub $ \(mlsConv, _) -> - unless (isClientMember cid (mcMembers mlsConv)) $ - throwS @'MLSSubConvClientNotInParent - - -- check if there is a key package ref in the remove proposal - remRef <- - if Map.null (paRemove action) - then pure Nothing - else do - (remCid, r) <- derefUser (paRemove action) qusr - unless (cidQualifiedUser cid == cidQualifiedUser remCid) - . throw - . mlsProtocolError - $ "The external commit attempts to remove a client from a user other than themselves" - pure (Just r) - - updateKeyPackageMapping lConvOrSub qusr (ciClient cid) remRef newRef - - -- increment epoch number - lConvOrSub' <- for lConvOrSub incrementEpoch - - -- fetch backend remove proposals of the previous epoch - kpRefs <- getPendingBackendRemoveProposals (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub') epoch - -- requeue backend remove proposals for the current epoch - let cm = membersConvOrSub (tUnqualified lConvOrSub') - createAndSendRemoveProposals lConvOrSub' kpRefs qusr cm +processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = + withCommitLock (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub) epoch $ do + let convOrSub = tUnqualified lConvOrSub + newKeyPackage <- + upLeaf + <$> note + (mlsProtocolError "External commits need an update path") + updatePath + when (paExternalInit action == mempty) $ + throw . mlsProtocolError $ + "The external commit is missing an external init proposal" + unless (paAdd action == mempty) $ + throw . mlsProtocolError $ + "The external commit must not have add proposals" + + newRef <- + kpRef' newKeyPackage + & note (mlsProtocolError "An invalid key package in the update path") + + -- validate and update mapping in brig + eithCid <- + nkpresClientIdentity + <$$> validateAndAddKeyPackageRef + NewKeyPackage + { nkpConversation = tUntagged (convOfConvOrSub . idForConvOrSub <$> lConvOrSub), + nkpKeyPackage = KeyPackageData (rmRaw newKeyPackage) + } + cid <- either (\errMsg -> throw (mlsProtocolError ("Tried to add invalid KeyPackage: " <> errMsg))) pure eithCid + + unless (cidQualifiedUser cid == qusr) $ + throw . mlsProtocolError $ + "The external commit attempts to add another user" + + senderClient <- noteS @'MLSMissingSenderClient mSenderClient + + unless (ciClient cid == senderClient) $ + throw . mlsProtocolError $ + "The external commit attempts to add another client of the user, it must only add itself" + + -- only members can join a subconversation + forOf_ _SubConv convOrSub $ \(mlsConv, _) -> + unless (isClientMember cid (mcMembers mlsConv)) $ + throwS @'MLSSubConvClientNotInParent + + -- check if there is a key package ref in the remove proposal + remRef <- + if Map.null (paRemove action) + then pure Nothing + else do + (remCid, r) <- derefUser (paRemove action) qusr + unless (cidQualifiedUser cid == cidQualifiedUser remCid) + . throw + . mlsProtocolError + $ "The external commit attempts to remove a client from a user other than themselves" + pure (Just r) + + updateKeyPackageMapping lConvOrSub qusr (ciClient cid) remRef newRef + + -- increment epoch number + lConvOrSub' <- for lConvOrSub incrementEpoch + + -- fetch backend remove proposals of the previous epoch + kpRefs <- getPendingBackendRemoveProposals (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub') epoch + -- requeue backend remove proposals for the current epoch + let cm = membersConvOrSub (tUnqualified lConvOrSub') + createAndSendRemoveProposals lConvOrSub' kpRefs qusr cm where derefUser :: ClientMap -> Qualified UserId -> Sem r (ClientIdentity, KeyPackageRef) derefUser cm user = case Map.assocs cm of @@ -1331,7 +1333,7 @@ executeProposalAction qusr con lconvOrSub action = do -- FUTUREWORK: turn this error into a proper response throwS @'MLSClientMismatch - membersToRemove <- catMaybes <$> for removedUsers (uncurry (checkRemoval cm)) + membersToRemove <- catMaybes <$> for removedUsers (uncurry (checkRemoval (is _SubConv convOrSub) cm)) -- add users to the conversation and send events addEvents <- @@ -1377,13 +1379,15 @@ executeProposalAction qusr con lconvOrSub action = do pure (addEvents <> removeEvents) where checkRemoval :: + Bool -> ClientMap -> Qualified UserId -> Set ClientId -> Sem r (Maybe (Qualified UserId)) - checkRemoval cm qtarget clients = do + checkRemoval isSubConv cm qtarget clients = do let clientsInConv = Map.keysSet (Map.findWithDefault mempty qtarget cm) - when (clients /= clientsInConv) $ do + -- FUTUREWORK: add tests against this situation for conv v subconv + when (not isSubConv && clients /= clientsInConv) $ do -- FUTUREWORK: turn this error into a proper response throwS @'MLSClientMismatch when (qusr == qtarget) $ diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 72c0d83560b..914a55fa2de 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -34,6 +34,8 @@ import Galley.API.MLS.Types import qualified Galley.Data.Conversation.Types as Data import Galley.Effects import Galley.Effects.ProposalStore +import Galley.Effects.SubConversationStore +import qualified Galley.Effects.SubConversationStore as E import Galley.Env import Imports import Polysemy @@ -62,7 +64,7 @@ createAndSendRemoveProposals :: Input Env ] r, - Traversable t, + Foldable t, CallsFed 'Galley "on-mls-message-sent" ) => Local ConvOrSubConv -> @@ -95,6 +97,41 @@ createAndSendRemoveProposals lConvOrSubConv cs qusr cm = do proposal propagateMessage qusr lConvOrSubConv Nothing msgEncoded cm +removeClientsWithClientMapRecursively :: + ( Members + '[ Input UTCTime, + TinyLog, + ExternalAccess, + FederatorAccess, + GundeckAccess, + ProposalStore, + SubConversationStore, + Input Env + ] + r, + Foldable f, + CallsFed 'Galley "on-mls-message-sent" + ) => + Local MLSConversation -> + (ConvOrSubConv -> f KeyPackageRef) -> + Qualified UserId -> + Sem r () +removeClientsWithClientMapRecursively lMlsConv getKPs qusr = do + let mainConv = fmap Conv lMlsConv + cm = mcMembers (tUnqualified lMlsConv) + createAndSendRemoveProposals mainConv (getKPs (tUnqualified mainConv)) qusr cm + + -- remove this client from all subconversations + subs <- listSubConversations' (mcId (tUnqualified lMlsConv)) + for_ subs $ \sub -> do + let subConv = fmap (flip SubConv sub) lMlsConv + + createAndSendRemoveProposals + subConv + (getKPs (tUnqualified subConv)) + qusr + cm + -- | Send remove proposals for a single client of a user to the local conversation. removeClient :: ( Members @@ -106,6 +143,7 @@ removeClient :: Input UTCTime, MemberStore, ProposalStore, + SubConversationStore, TinyLog ] r, @@ -118,10 +156,8 @@ removeClient :: removeClient lc qusr cid = do mMlsConv <- mkMLSConversation (tUnqualified lc) for_ mMlsConv $ \mlsConv -> do - -- FUTUREWORK: also remove the client from from subconversations of lc - let cidAndKPs = maybeToList (cmLookupRef (mkClientIdentity qusr cid) (mcMembers mlsConv)) - cm = mcMembers mlsConv - createAndSendRemoveProposals (qualifyAs lc (Conv mlsConv)) cidAndKPs qusr cm + let getKPs = cmLookupRef (mkClientIdentity qusr cid) . membersConvOrSub + removeClientsWithClientMapRecursively (qualifyAs lc mlsConv) getKPs qusr -- | Send remove proposals for all clients of the user to the local conversation. removeUser :: @@ -134,6 +170,7 @@ removeUser :: Input UTCTime, MemberStore, ProposalStore, + SubConversationStore, TinyLog ] r, @@ -145,7 +182,16 @@ removeUser :: removeUser lc qusr = do mMlsConv <- mkMLSConversation (tUnqualified lc) for_ mMlsConv $ \mlsConv -> do - -- FUTUREWORK: also remove the client from from subconversations of lc - let kprefs = toList (Map.findWithDefault mempty qusr (mcMembers mlsConv)) - cm = mcMembers mlsConv - createAndSendRemoveProposals (qualifyAs lc (Conv mlsConv)) kprefs qusr cm + let getKPs = Map.findWithDefault mempty qusr . membersConvOrSub + removeClientsWithClientMapRecursively (qualifyAs lc mlsConv) getKPs qusr + +-- | Convert cassandra subconv maps into SubConversations +listSubConversations' :: + Member SubConversationStore r => + ConvId -> + Sem r [SubConversation] +listSubConversations' cid = do + subs <- E.listSubConversations cid + msubs <- for (Map.assocs subs) $ \(subId, _) -> do + getSubConversation cid subId + pure (catMaybes msubs) diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index bebf559bcea..a4a24524f0c 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -39,7 +39,7 @@ import Galley.API.Error import Galley.API.Mapping import qualified Galley.Data.Conversation as Data import Galley.Data.Services (BotMember, newBotMember) -import qualified Galley.Data.Types as DataTypes +import qualified Galley.Data.Types as Data import Galley.Effects import Galley.Effects.BrigAccess import Galley.Effects.CodeStore @@ -329,6 +329,24 @@ memberJoinEvent lorig qconv t lmems rmems = localToSimple u = SimpleMember (tUntagged (qualifyAs lorig (lmId u))) (lmConvRoleName u) remoteToSimple u = SimpleMember (tUntagged (rmId u)) (rmConvRoleName u) +convDeleteMembers :: + Members '[MemberStore] r => + UserList UserId -> + Data.Conversation -> + Sem r Data.Conversation +convDeleteMembers ul conv = do + deleteMembers (Data.convId conv) ul + let locals = Set.fromList (ulLocals ul) + remotes = Set.fromList (ulRemotes ul) + -- update in-memory view of the conversation + pure $ + conv + { Data.convLocalMembers = + filter (\lm -> Set.notMember (lmId lm) locals) (Data.convLocalMembers conv), + Data.convRemoteMembers = + filter (\rm -> Set.notMember (rmId rm) remotes) (Data.convRemoteMembers conv) + } + isMember :: Foldable m => UserId -> m LocalMember -> Bool isMember u = isJust . find ((u ==) . lmId) @@ -579,12 +597,12 @@ pushConversationEvent conn e lusers bots = do verifyReusableCode :: Members '[CodeStore, ErrorS 'CodeNotFound] r => ConversationCode -> - Sem r DataTypes.Code + Sem r Data.Code verifyReusableCode convCode = do c <- - getCode (conversationKey convCode) DataTypes.ReusableCode + getCode (conversationKey convCode) Data.ReusableCode >>= noteS @'CodeNotFound - unless (DataTypes.codeValue c == conversationCode convCode) $ + unless (Data.codeValue c == conversationCode convCode) $ throwS @'CodeNotFound pure c diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index 574e32d3b3c..ad143121146 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -15,7 +15,10 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Cassandra.SubConversation where +module Galley.Cassandra.SubConversation + ( interpretSubConversationStoreToCassandra, + ) +where import Cassandra import Cassandra.Util diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 94a51a58b27..1dc6f91f91f 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -70,6 +70,7 @@ import Wire.API.MLS.Welcome import Wire.API.Message import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.Version +import Wire.API.User.Client tests :: IO TestSetup -> TestTree tests s = @@ -223,6 +224,9 @@ tests s = test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale, test s "leave a subconversation" testLeaveSubConv, test s "leave a subconversation as a non-member" testLeaveSubConvNonMember, + test s "remove user from parent conversation" testRemoveUserParent, + test s "remove creator from parent conversation" testRemoveCreatorParent, + test s "creator removes user from parent conversation" testCreatorRemovesUserFromParent, test s "delete parent conversation of a subconversation" testDeleteParentOfSubConv ], testGroup @@ -813,7 +817,12 @@ testAdminRemovesUserFromConv = do do convs <- getAllConvs (qUnqualified bob) - liftIO $ + clients <- getConvClients (qUnqualified alice) (qUnqualified qcnv) + liftIO $ do + assertEqual + ("Expected only one client, got " <> show clients) + (length . clClients $ clients) + 1 assertBool "bob is not longer part of conversation after the commit" (qcnv `notElem` map cnvQualifiedId convs) @@ -1618,7 +1627,7 @@ testBackendRemoveProposalRecreateClient = do createExternalCommit alice2 Nothing cnv >>= sendAndConsumeCommitBundle WS.assertMatch (5 # WS.Second) wsA $ - wsAssertBackendRemoveProposal alice qcnv ref + wsAssertBackendRemoveProposal alice (Conv <$> qcnv) ref consumeMessage1 alice2 proposal void $ createPendingProposalCommit alice2 >>= sendAndConsumeCommitBundle @@ -1645,7 +1654,7 @@ testBackendRemoveProposalLocalConvLocalUser = do for bobClients $ \(_, ref) -> do [msg] <- WS.assertMatchN (5 # Second) wss $ \n -> - wsAssertBackendRemoveProposal bob qcnv ref n + wsAssertBackendRemoveProposal bob (Conv <$> qcnv) ref n consumeMessage1 alice1 msg -- alice commits the external proposals @@ -1680,7 +1689,7 @@ testBackendRemoveProposalLocalConvRemoteUser = do for_ bobClients $ \(_, ref) -> WS.assertMatch (5 # WS.Second) wsA $ - wsAssertBackendRemoveProposal bob qcnv ref + wsAssertBackendRemoveProposal bob (Conv <$> qcnv) ref sendRemoteMLSWelcome :: TestM () sendRemoteMLSWelcome = do @@ -1756,7 +1765,7 @@ testBackendRemoveProposalLocalConvLocalLeaverCreator = do for_ aliceClients $ \(_, ref) -> do -- only bob's clients should receive the external proposals msgs <- WS.assertMatchN (5 # Second) (drop 1 wss) $ \n -> - wsAssertBackendRemoveProposal alice qcnv ref n + wsAssertBackendRemoveProposal alice (Conv <$> qcnv) ref n traverse_ (uncurry consumeMessage1) (zip [bob1, bob2] msgs) -- but everyone should receive leave events @@ -1801,7 +1810,7 @@ testBackendRemoveProposalLocalConvLocalLeaverCommitter = do for_ bobClients $ \(_, ref) -> do -- only alice and charlie should receive the external proposals msgs <- WS.assertMatchN (5 # Second) (take 2 wss) $ \n -> - wsAssertBackendRemoveProposal bob qcnv ref n + wsAssertBackendRemoveProposal bob (Conv <$> qcnv) ref n traverse_ (uncurry consumeMessage1) (zip [alice1, charlie1] msgs) -- but everyone should receive leave events @@ -1844,7 +1853,7 @@ testBackendRemoveProposalLocalConvRemoteLeaver = do for_ bobClients $ \(_, ref) -> WS.assertMatch_ (5 # WS.Second) wsA $ - wsAssertBackendRemoveProposal bob qcnv ref + wsAssertBackendRemoveProposal bob (Conv <$> qcnv) ref testBackendRemoveProposalLocalConvLocalClient :: TestM () testBackendRemoveProposalLocalConvLocalClient = do @@ -1871,7 +1880,7 @@ testBackendRemoveProposalLocalConvLocalClient = do wsAssertClientRemoved (ciClient bob1) msg <- WS.assertMatch (5 # WS.Second) wsA $ \notification -> do - wsAssertBackendRemoveProposal bob qcnv kpBob1 notification + wsAssertBackendRemoveProposal bob (Conv <$> qcnv) kpBob1 notification for_ [alice1, bob2, charlie1] $ flip consumeMessage1 msg @@ -1905,7 +1914,7 @@ testBackendRemoveProposalLocalConvRemoteClient = do WS.assertMatch_ (5 # WS.Second) wsA $ \notification -> - void $ wsAssertBackendRemoveProposal bob qcnv bob1KP notification + void $ wsAssertBackendRemoveProposal bob (Conv <$> qcnv) bob1KP notification testGetGroupInfoOfLocalConv :: TestM () testGetGroupInfoOfLocalConv = do @@ -2943,7 +2952,7 @@ testLeaveSubConv = do msgs <- WS.assertMatchN (5 # WS.Second) wss $ - wsAssertBackendRemoveProposal bob qcnv bob1KP + wsAssertBackendRemoveProposal bob (Conv <$> qcnv) bob1KP traverse_ (uncurry consumeMessage1) (zip [alice1, bob2] msgs) -- assert the leaver gets no proposal or event void . liftIO $ WS.assertNoEvent (5 # WS.Second) [wsBob1] @@ -2970,7 +2979,7 @@ testLeaveSubConv = do msgs <- WS.assertMatchN (5 # WS.Second) wss $ - wsAssertBackendRemoveProposal charlie qcnv charlie1KP + wsAssertBackendRemoveProposal charlie (Conv <$> qcnv) charlie1KP traverse_ (uncurry consumeMessage1) (zip [alice1, bob2] msgs) -- alice commits the pending proposal @@ -3055,3 +3064,233 @@ testLeaveRemoteSubConv = do -- check that leave-sub-conversation is called void $ assertOne (filter ((== "leave-sub-conversation") . frRPC) reqs) + +testRemoveUserParent :: TestM () +testRemoveUserParent = do + [alice, bob, charlie] <- createAndConnectUsers [Nothing, Nothing, Nothing] + + runMLSTest $ + do + [alice1, bob1, bob2, charlie1, charlie2] <- + traverse + createMLSClient + [alice, bob, bob, charlie, charlie] + traverse_ uploadNewKeyPackage [bob1, bob2, charlie1, charlie2] + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommit + + let subname = SubConvId "conference" + void $ createSubConv qcnv bob1 subname + let qcs = fmap (flip SubConv subname) qcnv + + -- all clients join + for_ [alice1, bob2, charlie1, charlie2] $ \c -> + void $ createExternalCommit c Nothing qcs >>= sendAndConsumeCommitBundle + + [(_, kpref1), (_, kpref2)] <- getClientsFromGroupState alice1 charlie + + -- charlie leaves the main conversation + mlsBracket [alice1, bob1, bob2] $ \wss -> do + liftTest $ do + deleteMemberQualified (qUnqualified charlie) charlie qcnv + !!! const 200 === statusCode + + -- Remove charlie from our state as well + State.modify $ \mls -> + mls + { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [charlie1, charlie2]) + } + + msg1 <- WS.assertMatchN (5 # Second) wss $ \n -> + wsAssertBackendRemoveProposal charlie (Conv <$> qcnv) kpref1 n + + traverse_ (uncurry consumeMessage1) (zip [alice1, bob1, bob2] msg1) + + msg2 <- WS.assertMatchN (5 # Second) wss $ \n -> + wsAssertBackendRemoveProposal charlie (Conv <$> qcnv) kpref2 n + + traverse_ (uncurry consumeMessage1) (zip [alice1, bob1, bob2] msg2) + + void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + + liftTest $ do + getSubConv (qUnqualified charlie) qcnv (SubConvId "conference") + !!! const 403 === statusCode + + sub :: PublicSubConversation <- + responseJsonError + =<< getSubConv (qUnqualified bob) qcnv (SubConvId "conference") + >= sendAndConsumeCommit + + let subname = SubConvId "conference" + void $ createSubConv qcnv alice1 subname + let qcs = fmap (flip SubConv subname) qcnv + + -- all clients join + for_ [bob1, bob2, charlie1, charlie2] $ \c -> + void $ createExternalCommit c Nothing qcs >>= sendAndConsumeCommitBundle + + [(_, kpref1)] <- getClientsFromGroupState alice1 alice + + -- creator leaves the main conversation + mlsBracket [bob1, bob2, charlie1, charlie2] $ \wss -> do + liftTest $ do + deleteMemberQualified (qUnqualified alice) alice qcnv + !!! const 200 === statusCode + + -- Remove alice1 from our state as well + State.modify $ \mls -> + mls + { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [alice1]) + } + + msg <- WS.assertMatchN (5 # Second) wss $ \n -> + -- Checks proposal for subconv, parent doesn't get one + -- since alice is not notified of her own removal + wsAssertBackendRemoveProposal alice (Conv <$> qcnv) kpref1 n + + traverse_ (uncurry consumeMessage1) (zip [bob1, bob2, charlie1, charlie2] msg) + + void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle + + liftTest $ do + getSubConv (qUnqualified alice) qcnv subname + !!! const 403 === statusCode + + -- charlie sees updated memberlist + sub :: PublicSubConversation <- + responseJsonError + =<< getSubConv (qUnqualified charlie) qcnv subname + >= sendAndConsumeCommit + + stateParent <- State.get + + let subId = SubConvId "conference" + qcs <- createSubConv qcnv alice1 subId + liftTest $ + getSubConv (qUnqualified alice) qcnv subId + !!! do const 200 === statusCode + + for_ [bob1, bob2, charlie1, charlie2] $ \c -> do + void $ createExternalCommit c Nothing qcs >>= sendAndConsumeCommitBundle + + stateSub <- State.get + State.put stateParent + + mlsBracket [alice1, charlie1, charlie2] $ \wss -> do + events <- createRemoveCommit alice1 [bob1, bob2] >>= sendAndConsumeCommitBundle + State.modify $ \s -> s {mlsMembers = Set.difference (mlsMembers s) (Set.fromList [bob1, bob2])} + + liftIO $ assertOne events >>= assertLeaveEvent qcnv alice [bob] + + WS.assertMatchN_ (5 # Second) wss $ \n -> do + wsAssertMemberLeave qcnv alice [bob] n + + State.put stateSub + -- Get client state for alice and fetch bob client identities + [(_, kprefBob1), (_, kprefBob2)] <- getClientsFromGroupState alice1 bob + + -- handle bob1 removal + msgs <- WS.assertMatchN (5 # Second) wss $ \n -> do + -- it was an alice proposal for the parent, + -- but it's a backend proposal for the sub + wsAssertBackendRemoveProposal bob qcs kprefBob1 n + + traverse_ (uncurry consumeMessage1) (zip [alice1, charlie1, charlie2] msgs) + + -- handle bob2 removal + msgs2 <- WS.assertMatchN (5 # Second) wss $ \n -> do + -- it was an alice proposal for the parent, + -- but it's a backend proposal for the sub + wsAssertBackendRemoveProposal bob qcs kprefBob2 n + + traverse_ (uncurry consumeMessage1) (zip [alice1, charlie1, charlie2] msgs2) + + -- Remove bob from our state as well + State.modify $ \mls -> + mls + { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [bob1, bob2]) + } + -- alice commits the proposal and sends over for the backend to also process it + void $ + createPendingProposalCommit alice1 + >>= sendAndConsumeCommitBundle + + liftTest $ do + getSubConv (qUnqualified bob) qcnv (SubConvId "conference") + !!! const 403 === statusCode + + -- charlie sees updated memberlist + sub1 :: PublicSubConversation <- + responseJsonError + =<< getSubConv (qUnqualified charlie) qcnv (SubConvId "conference") + (show . length . pscMembers $ sub1) + ) + (sort [alice1, charlie1, charlie2]) + (sort $ pscMembers sub1) + + -- alice also sees updated memberlist + sub2 :: PublicSubConversation <- + responseJsonError + =<< getSubConv (qUnqualified alice) qcnv (SubConvId "conference") + (show . length . pscMembers $ sub2) + ) + (sort [alice1, charlie1, charlie2]) + (sort $ pscMembers sub2) diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 10ab5f2ca37..4e3da83c5ec 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -253,6 +253,7 @@ data MLSState = MLSState mlsConvId :: Maybe (Qualified ConvOrSubConvId), mlsEpoch :: Word64 } + deriving (Show) newtype MLSTest a = MLSTest {unMLSTest :: StateT MLSState TestM a} deriving newtype @@ -307,6 +308,7 @@ data MessagePackage = MessagePackage mpWelcome :: Maybe ByteString, mpPublicGroupState :: Maybe ByteString } + deriving (Show) takeLastPrekeyNG :: HasCallStack => MLSTest LastPrekey takeLastPrekeyNG = do @@ -502,7 +504,6 @@ createGroup cid qcs gid = do resetGroup :: ClientIdentity -> Qualified ConvOrSubConvId -> GroupId -> MLSTest () resetGroup cid qcs gid = do - groupJSON <- mlscli cid ["group", "create", T.unpack (toBase64Text (unGroupId gid))] Nothing State.modify $ \s -> s { mlsGroupId = Just gid, @@ -511,6 +512,11 @@ resetGroup cid qcs gid = do mlsEpoch = 0, mlsNewMembers = mempty } + resetClientGroup cid gid + +resetClientGroup :: ClientIdentity -> GroupId -> MLSTest () +resetClientGroup cid gid = do + groupJSON <- mlscli cid ["group", "create", T.unpack (toBase64Text (unGroupId gid))] Nothing setClientGroupState cid groupJSON getConvId :: MLSTest (Qualified ConvOrSubConvId) diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 49a496195f2..d68b47df74e 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -188,7 +188,7 @@ createBindingTeam' :: HasCallStack => TestM (User, TeamId) createBindingTeam' = do owner <- randomTeamCreator' teams <- getTeams (userId owner) [] - let [team] = view teamListTeams teams + team <- assertOne $ view teamListTeams teams let tid = view teamId team SQS.assertTeamActivate "create team" tid refreshIndex @@ -998,6 +998,17 @@ getConvs u cids = do . zConn "conn" . json (ListConversations (unsafeRange cids)) +getConvClients :: HasCallStack => UserId -> ConvId -> TestM ClientList +getConvClients usr cnv = do + g <- viewGalley + responseJsonError + =<< get + ( g + . paths ["i", "conversation", toByteString' cnv] + . zUser usr + . zConn "conn" + ) + getAllConvs :: HasCallStack => UserId -> TestM [Conversation] getAllConvs u = do g <- viewGalley @@ -1811,7 +1822,7 @@ assertRemoveUpdate :: (MonadIO m, HasCallStack) => FederatedRequest -> Qualified assertRemoveUpdate req qconvId remover alreadyPresentUsers victim = liftIO $ do frRPC req @?= "on-conversation-updated" frOriginDomain req @?= qDomain qconvId - let Just cu = decode (frBody req) + cu <- assertJust $ decode (frBody req) cuOrigUserId cu @?= remover cuConvId cu @?= qUnqualified qconvId sort (cuAlreadyPresentUsers cu) @?= sort alreadyPresentUsers @@ -1821,7 +1832,7 @@ assertLeaveUpdate :: (MonadIO m, HasCallStack) => FederatedRequest -> Qualified assertLeaveUpdate req qconvId remover alreadyPresentUsers = liftIO $ do frRPC req @?= "on-conversation-updated" frOriginDomain req @?= qDomain qconvId - let Just cu = decode (frBody req) + cu <- assertJust $ decode (frBody req) cuOrigUserId cu @?= remover cuConvId cu @?= qUnqualified qconvId sort (cuAlreadyPresentUsers cu) @?= sort alreadyPresentUsers @@ -2153,7 +2164,7 @@ getInternalClientsFull userSet = do ensureClientCaps :: HasCallStack => UserId -> ClientId -> Client.ClientCapabilityList -> TestM () ensureClientCaps uid cid caps = do UserClientsFull (Map.lookup uid -> (Just clnts)) <- getInternalClientsFull (UserSet $ Set.singleton uid) - let [clnt] = filter ((== cid) . clientId) $ Set.toList clnts + clnt <- assertOne . filter ((== cid) . clientId) $ Set.toList clnts liftIO $ assertEqual ("ensureClientCaps: " <> show (uid, cid, caps)) (clientCapabilities clnt) caps -- TODO: Refactor, as used also in brig @@ -2857,17 +2868,17 @@ wsAssertConvReceiptModeUpdate conv usr new n = do wsAssertBackendRemoveProposalWithEpoch :: HasCallStack => Qualified UserId -> Qualified ConvId -> KeyPackageRef -> Epoch -> Notification -> IO ByteString wsAssertBackendRemoveProposalWithEpoch fromUser convId kpref epoch n = do - bs <- wsAssertBackendRemoveProposal fromUser convId kpref n + bs <- wsAssertBackendRemoveProposal fromUser (Conv <$> convId) kpref n let msg = fromRight (error "Failed to parse Message 'MLSPlaintext") $ decodeMLS' @(Message 'MLSPlainText) bs let tbs = rmValue . msgTBS $ msg tbsMsgEpoch tbs @?= epoch pure bs -wsAssertBackendRemoveProposal :: HasCallStack => Qualified UserId -> Qualified ConvId -> KeyPackageRef -> Notification -> IO ByteString -wsAssertBackendRemoveProposal fromUser convId kpref n = do +wsAssertBackendRemoveProposal :: HasCallStack => Qualified UserId -> Qualified ConvOrSubConvId -> KeyPackageRef -> Notification -> IO ByteString +wsAssertBackendRemoveProposal fromUser cnvOrSubCnv kpref n = do let e = List1.head (WS.unpackPayload n) ntfTransient n @?= False - evtConv e @?= convId + evtConv e @?= convOfConvOrSub <$> cnvOrSubCnv evtType e @?= MLSMessageAdd evtFrom e @?= fromUser let bs = getMLSMessageData (evtData e) From 9afe459345698bbf9df288cd542937c1c3c591f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 16 Feb 2023 10:44:21 +0100 Subject: [PATCH 024/225] [FS-1541] After the last leaver the subconversation should be reset (#3074) Co-authored-by: Stefan Berthold --- .../src/Wire/API/Federation/API/Galley.hs | 5 +- .../API/Routes/Public/Galley/Conversation.hs | 4 ++ services/galley/src/Galley/API/Federation.hs | 7 ++- .../src/Galley/API/MLS/SubConversation.hs | 46 +++++++++++++++---- services/galley/src/Galley/API/MLS/Types.hs | 4 +- services/galley/test/integration/API/MLS.hs | 33 +++++++++++++ 6 files changed, 87 insertions(+), 12 deletions(-) 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 82e72852251..69361f59df1 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 @@ -146,7 +146,10 @@ type GalleyApi = DeleteSubConversationFedRequest DeleteSubConversationResponse :<|> FedEndpointWithMods - '[MakesFederatedCall 'Galley "on-mls-message-sent"] + '[ MakesFederatedCall 'Galley "on-mls-message-sent", + MakesFederatedCall 'Galley "on-delete-mls-conversation", + MakesFederatedCall 'Galley "on-new-remote-subconversation" + ] "leave-sub-conversation" LeaveSubConversationRequest LeaveSubConversationResponse diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 03ec88545c8..f9edf4e7908 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -424,9 +424,13 @@ type ConversationAPI = ( Summary "Leave an MLS subconversation" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "leave-sub-conversation" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> CanThrow 'MLSProtocolErrorTag + :> CanThrow 'MLSStaleMessage + :> CanThrow 'MLSNotEnabled :> ZLocalUser :> ZClient :> "conversations" diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index f017c1fe831..871c14b4ef1 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -932,7 +932,12 @@ getSubConversationForRemoteUser domain GetSubConversationsRequest {..} = leaveSubConversation :: ( HasLeaveSubConversationEffects r, - Members '[Input (Local ())] r + Members + '[ Input (Local ()), + Resource, + SubConversationSupply + ] + r ) => Domain -> LeaveSubConversationRequest -> diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 30b58c82a75..55c586e1518 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -33,6 +33,7 @@ where import Control.Arrow import Data.Id +import qualified Data.Map as Map import Data.Qualified import Data.Time.Clock import Galley.API.MLS @@ -369,17 +370,27 @@ type HasLeaveSubConversationEffects r = TinyLog ] r, - CallsFed 'Galley "on-mls-message-sent" + CallsFed 'Galley "on-mls-message-sent", + CallsFed 'Galley "on-delete-mls-conversation", + CallsFed 'Galley "on-new-remote-subconversation" ) type LeaveSubConversationStaticErrors = - '[ErrorS 'ConvNotFound, ErrorS 'ConvAccessDenied] + '[ ErrorS 'ConvNotFound, + ErrorS 'ConvAccessDenied, + ErrorS 'MLSStaleMessage, + ErrorS 'MLSNotEnabled + ] leaveSubConversation :: ( HasLeaveSubConversationEffects r, Members '[ Error MLSProtocolError, - Error FederationError + Error FederationError, + ErrorS 'MLSStaleMessage, + ErrorS 'MLSNotEnabled, + Resource, + SubConversationSupply ] r, Members LeaveSubConversationStaticErrors r, @@ -402,7 +413,14 @@ leaveSubConversation lusr cli qcnv sub = leaveLocalSubConversation :: ( HasLeaveSubConversationEffects r, - Members '[Error MLSProtocolError] r, + Members + '[ Error MLSProtocolError, + ErrorS 'MLSStaleMessage, + ErrorS 'MLSNotEnabled, + Resource, + SubConversationSupply + ] + r, Members LeaveSubConversationStaticErrors r ) => ClientIdentity -> @@ -410,6 +428,7 @@ leaveLocalSubConversation :: SubConvId -> Sem r () leaveLocalSubConversation cid lcnv sub = do + assertMLSEnabled cnv <- getConversationAndCheckMembership (cidQualifiedUser cid) lcnv mlsConv <- noteS @'ConvNotFound =<< mkMLSConversation cnv subConv <- @@ -420,11 +439,20 @@ leaveLocalSubConversation cid lcnv sub = do cmLookupRef cid (scMembers subConv) -- remove the leaver from the member list let cm = cmRemoveClient cid (scMembers subConv) - createAndSendRemoveProposals - (qualifyAs lcnv (SubConv mlsConv subConv)) - (Identity kp) - (cidQualifiedUser cid) - cm + if Map.null cm + then do + let (gid, epoch) = (cnvmlsGroupId &&& cnvmlsEpoch) (scMLSData subConv) + deleteLocalSubConversation + (cidQualifiedUser cid) + lcnv + sub + $ DeleteSubConversationRequest gid epoch + else + createAndSendRemoveProposals + (qualifyAs lcnv (SubConv mlsConv subConv)) + (Identity kp) + (cidQualifiedUser cid) + cm leaveRemoteSubConversation :: ( Members diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index 38d6dff532a..69f0f795a00 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -49,7 +49,9 @@ cmRemoveClient cid cm = case Map.lookup (cidQualifiedUser cid) cm of Nothing -> cm Just clients -> let clients' = Map.delete (ciClient cid) clients - in Map.insert (cidQualifiedUser cid) clients' cm + in if Map.null clients' + then Map.delete (cidQualifiedUser cid) cm + else Map.insert (cidQualifiedUser cid) clients' cm isClientMember :: ClientIdentity -> ClientMap -> Bool isClientMember ci = isJust . cmLookupRef ci diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 1dc6f91f91f..5853fc74ad4 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -223,6 +223,7 @@ tests s = test s "reset a subconversation as a non-member" (testDeleteSubConv False), test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale, test s "leave a subconversation" testLeaveSubConv, + test s "last to leave a subconversation" testLastLeaverSubConv, test s "leave a subconversation as a non-member" testLeaveSubConvNonMember, test s "remove user from parent conversation" testRemoveUserParent, test s "remove creator from parent conversation" testRemoveCreatorParent, @@ -2910,6 +2911,38 @@ testDeleteRemoteSubConv isAMember = do Aeson.decode (frBody actualReq) liftIO $ req @?= Just expectedReq +testLastLeaverSubConv :: TestM () +testLastLeaverSubConv = do + alice <- randomQualifiedUser + + runMLSTest $ do + [alice1, alice2] <- traverse createMLSClient [alice, alice] + void $ uploadNewKeyPackage alice2 + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [alice] >>= sendAndConsumeCommitBundle + + let subId = SubConvId "conference" + qsub <- createSubConv qcnv alice1 subId + prePsc <- + liftTest $ + responseJsonError + =<< getSubConv (qUnqualified alice) qcnv subId + Date: Thu, 16 Feb 2023 12:10:18 +0100 Subject: [PATCH 025/225] [FS-1534] The subconversation non-creator gets a remove proposal when leaving (#3085) * Test: subconversation (non-)creator leaving Co-authored-by: Stefan Berthold --- changelog.d/2-features/subconv-leave | 2 +- services/galley/test/integration/API/MLS.hs | 72 ++++++++++++++------- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/changelog.d/2-features/subconv-leave b/changelog.d/2-features/subconv-leave index 6eb2aa59c0d..3ef40409c42 100644 --- a/changelog.d/2-features/subconv-leave +++ b/changelog.d/2-features/subconv-leave @@ -1 +1 @@ -Implement endpoint for leaving a subconversation (#2969, #3080) +Implement endpoint for leaving a subconversation (#2969, #3080, #3085) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 5853fc74ad4..a239c07e06f 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -222,7 +222,8 @@ tests s = test s "reset a subconversation as a member" (testDeleteSubConv True), test s "reset a subconversation as a non-member" (testDeleteSubConv False), test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale, - test s "leave a subconversation" testLeaveSubConv, + test s "leave a subconversation as a creator" (testLeaveSubConv True), + test s "leave a subconversation as a non-creator" (testLeaveSubConv False), test s "last to leave a subconversation" testLastLeaverSubConv, test s "leave a subconversation as a non-member" testLeaveSubConvNonMember, test s "remove user from parent conversation" testRemoveUserParent, @@ -2943,12 +2944,13 @@ testLastLeaverSubConv = do assertBool "group ID unchanged" $ pscGroupId prePsc /= pscGroupId psc length (pscMembers psc) @?= 0 -testLeaveSubConv :: TestM () -testLeaveSubConv = do +testLeaveSubConv :: Bool -> TestM () +testLeaveSubConv isSubConvCreator = do [alice, bob, charlie] <- createAndConnectUsers [Nothing, Nothing, Just "charlie.example.com"] runMLSTest $ do - [alice1, bob1, bob2, charlie1] <- traverse createMLSClient [alice, bob, bob, charlie] + charlie1 : allLocals@[alice1, bob1, bob2] <- + traverse createMLSClient [charlie, alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] (_, qcnv) <- setupMLSGroup alice1 @@ -2967,12 +2969,18 @@ testLeaveSubConv = do void $ createExternalCommit charlie1 Nothing qsub >>= sendAndConsumeCommitBundle pure qsub - -- bob1 (the creator of the subconv) leaves - [bob1KP] <- - map snd . filter (\(cid, _) -> cid == bob1) - <$> getClientsFromGroupState alice1 bob - mlsBracket [bob1, alice1, bob2] $ \(wsBob1 : wss) -> do - (_, reqs) <- withTempMockFederator' messageSentMock $ leaveCurrentConv bob1 qsub + let firstLeaver = if isSubConvCreator then bob1 else alice1 + -- a member leaves the subconversation + [firstLeaverKP] <- + map snd . filter (\(cid, _) -> cid == firstLeaver) + <$> getClientsFromGroupState + alice1 + (cidQualifiedUser firstLeaver) + let others = leaverAndOthers firstLeaver allLocals + mlsBracket (firstLeaver : others) $ \(wsLeaver : wss) -> do + (_, reqs) <- + withTempMockFederator' messageSentMock $ + leaveCurrentConv firstLeaver qsub req <- assertOne ( toList . Aeson.decode . frBody @@ -2985,20 +2993,23 @@ testLeaveSubConv = do msgs <- WS.assertMatchN (5 # WS.Second) wss $ - wsAssertBackendRemoveProposal bob (Conv <$> qcnv) bob1KP - traverse_ (uncurry consumeMessage1) (zip [alice1, bob2] msgs) + wsAssertBackendRemoveProposal + (cidQualifiedUser firstLeaver) + (Conv <$> qcnv) + firstLeaverKP + traverse_ (uncurry consumeMessage1) (zip others msgs) -- assert the leaver gets no proposal or event - void . liftIO $ WS.assertNoEvent (5 # WS.Second) [wsBob1] + void . liftIO $ WS.assertNoEvent (5 # WS.Second) [wsLeaver] - -- alice commits the pending proposal - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + -- a member commits the pending proposal + void $ createPendingProposalCommit (head others) >>= sendAndConsumeCommitBundle -- check that only 3 clients are left in the subconv do psc <- liftTest $ responseJsonError - =<< getSubConv (qUnqualified alice) qcnv subId + =<< getSubConv (ciUser (head others)) qcnv subId cid == charlie1) - <$> getClientsFromGroupState alice1 charlie - mlsBracket [alice1, bob2] $ \wss -> do + <$> getClientsFromGroupState (head others) charlie + mlsBracket others $ \wss -> do leaveCurrentConv charlie1 qsub msgs <- WS.assertMatchN (5 # WS.Second) wss $ wsAssertBackendRemoveProposal charlie (Conv <$> qcnv) charlie1KP - traverse_ (uncurry consumeMessage1) (zip [alice1, bob2] msgs) + traverse_ (uncurry consumeMessage1) (zip others msgs) - -- alice commits the pending proposal - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + -- a member commits the pending proposal + void $ createPendingProposalCommit (head others) >>= sendAndConsumeCommitBundle -- check that only 2 clients are left in the subconv do psc <- liftTest $ responseJsonError - =<< getSubConv (qUnqualified alice) qcnv subId + =<< getSubConv (ciUser (head others)) qcnv subId [(a, [a])] + allLocalsButLeaver xs = + ( \(l, i) -> + let s = splitAt i xs + in (l, fst s ++ drop 1 (snd s)) + ) + <$> zip xs [0 ..] + leaverAndOthers :: Eq a => a -> [a] -> [a] + leaverAndOthers leaver xs = + let (Just (_, others)) = + find (\(l, _) -> l == leaver) (allLocalsButLeaver xs) + in others testLeaveSubConvNonMember :: TestM () testLeaveSubConvNonMember = do From 918de9f948f993954eca186c292953e53e078e84 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 23 Feb 2023 11:09:10 +0100 Subject: [PATCH 026/225] nginz: Increase body size limit on /mls/commit-bundles --- charts/nginz/values.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 89b581ab1d9..507ff2a30e1 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -488,6 +488,8 @@ nginx_conf: - path: /mls/commit-bundles envs: - all + max_body_size: 70m + body_buffer_size: 256k - path: /mls/public-keys envs: - all From 9fc7acfab684128985dfd2186ff4efc188081bf8 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 23 Feb 2023 14:30:57 +0100 Subject: [PATCH 027/225] [FS-1564] no messages sent to clients who left subconv (#3096) --- changelog.d/5-internal/FS-1564 | 1 + services/galley/src/Galley/API/Action.hs | 2 +- .../src/Galley/API/MLS/SubConversation.hs | 7 ++-- services/galley/test/integration/API/MLS.hs | 36 +++++++++++-------- 4 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 changelog.d/5-internal/FS-1564 diff --git a/changelog.d/5-internal/FS-1564 b/changelog.d/5-internal/FS-1564 new file mode 100644 index 00000000000..9bb9235cc7a --- /dev/null +++ b/changelog.d/5-internal/FS-1564 @@ -0,0 +1 @@ +remove leaving clients immediately from subconversations diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 70df405218c..bbf7d6e8b0f 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -654,7 +654,7 @@ updateLocalConversation lcnv qusr con action = do unless (protocolValidAction (convProtocol conv) (fromSing tag)) $ throwS @'InvalidOperation - -- perform all authorisation checks and, if successful, the update itself + -- perform all authorisation checks and, if successful, then update itself updateLocalConversationUnchecked @tag (qualifyAs lcnv conv) qusr con action -- | Similar to 'updateLocalConversationWithLocalUser', but takes a diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 55c586e1518..b3d809bd56d 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -35,6 +35,7 @@ import Control.Arrow import Data.Id import qualified Data.Map as Map import Data.Qualified +import qualified Data.Set as Set import Data.Time.Clock import Galley.API.MLS import Galley.API.MLS.Conversation @@ -418,7 +419,8 @@ leaveLocalSubConversation :: ErrorS 'MLSStaleMessage, ErrorS 'MLSNotEnabled, Resource, - SubConversationSupply + SubConversationSupply, + MemberStore ] r, Members LeaveSubConversationStaticErrors r @@ -438,10 +440,11 @@ leaveLocalSubConversation cid lcnv sub = do note (mlsProtocolError "Client is not a member of the subconversation") $ cmLookupRef cid (scMembers subConv) -- remove the leaver from the member list + let (gid, epoch) = (cnvmlsGroupId &&& cnvmlsEpoch) (scMLSData subConv) + Eff.removeMLSClients gid (cidQualifiedUser cid) . Set.singleton . ciClient $ cid let cm = cmRemoveClient cid (scMembers subConv) if Map.null cm then do - let (gid, epoch) = (cnvmlsGroupId &&& cnvmlsEpoch) (scMLSData subConv) deleteLocalSubConversation (cidQualifiedUser cid) lcnv diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index a239c07e06f..4fb675754ea 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -1890,6 +1890,8 @@ testBackendRemoveProposalLocalConvLocalClient = do mp <- createPendingProposalCommit charlie1 events <- sendAndConsumeCommit mp liftIO $ events @?= [] + WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ \n -> do + wsAssertMLSMessage (Conv <$> qcnv) charlie (mpMessage mp) n testBackendRemoveProposalLocalConvRemoteClient :: TestM () testBackendRemoveProposalLocalConvRemoteClient = do @@ -2976,7 +2978,7 @@ testLeaveSubConv isSubConvCreator = do <$> getClientsFromGroupState alice1 (cidQualifiedUser firstLeaver) - let others = leaverAndOthers firstLeaver allLocals + let others = filter (/= firstLeaver) allLocals mlsBracket (firstLeaver : others) $ \(wsLeaver : wss) -> do (_, reqs) <- withTempMockFederator' messageSentMock $ @@ -3002,7 +3004,24 @@ testLeaveSubConv isSubConvCreator = do void . liftIO $ WS.assertNoEvent (5 # WS.Second) [wsLeaver] -- a member commits the pending proposal - void $ createPendingProposalCommit (head others) >>= sendAndConsumeCommitBundle + do + leaveCommit <- createPendingProposalCommit (head others) + mlsBracket (firstLeaver : others) $ \(wsLeaver : wss) -> do + events <- sendAndConsumeCommit leaveCommit + liftIO $ events @?= [] + WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do + wsAssertMLSMessage qsub (cidQualifiedUser . head $ others) (mpMessage leaveCommit) n + void $ WS.assertNoEvent (5 # WS.Second) [wsLeaver] + + -- send an application message + do + message <- createApplicationMessage (head others) "some text" + mlsBracket (firstLeaver : others) $ \(wsLeaver : wss) -> do + events <- sendAndConsumeMessage message + liftIO $ events @?= [] + WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do + wsAssertMLSMessage qsub (cidQualifiedUser . head $ others) (mpMessage message) n + void $ WS.assertNoEvent (5 # WS.Second) [wsLeaver] -- check that only 3 clients are left in the subconv do @@ -3040,19 +3059,6 @@ testLeaveSubConv isSubConvCreator = do liftIO $ do length (pscMembers psc) @?= 2 sort (pscMembers psc) @?= sort others - where - allLocalsButLeaver :: [a] -> [(a, [a])] - allLocalsButLeaver xs = - ( \(l, i) -> - let s = splitAt i xs - in (l, fst s ++ drop 1 (snd s)) - ) - <$> zip xs [0 ..] - leaverAndOthers :: Eq a => a -> [a] -> [a] - leaverAndOthers leaver xs = - let (Just (_, others)) = - find (\(l, _) -> l == leaver) (allLocalsButLeaver xs) - in others testLeaveSubConvNonMember :: TestM () testLeaveSubConvNonMember = do From 708d41ff89aa045af2cb6af783836c4c5a59fc09 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 27 Feb 2023 10:42:55 +0100 Subject: [PATCH 028/225] Raise HardTrunctationLimit to 100k (#3105) --- libs/wire-api/src/Wire/API/Team/Member.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index 2fc27f12d5e..1ef7d800c9f 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -271,7 +271,8 @@ instance ToSchema (TeamMember' tag) => ToSchema (TeamMemberList' tag) where <*> _teamMemberListType .= fieldWithDocModifier "hasMore" (description ?~ "true if 'members' doesn't contain all team members") schema -type HardTruncationLimit = (2000 :: Nat) +-- TODO: Revert this to 2000 before mergin 'mls' to the develop branch +type HardTruncationLimit = (100000 :: Nat) hardTruncationLimit :: Integral a => a hardTruncationLimit = fromIntegral $ natVal (Proxy @HardTruncationLimit) From b44f7d736157737942a1d7d3ec5423aa98cc8b68 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Tue, 28 Feb 2023 09:06:50 +0100 Subject: [PATCH 029/225] skip pending proposals included in external commit (#3107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * skip pending proposals included in external commit External proposals can contain remove proposals to remove key packages of the same client, "resync" in CoreCrypto speak. * Test: rejoin subconversation with the same client --------- Co-authored-by: Marko Dimjašević --- changelog.d/2-features/subconv-leave | 2 +- services/galley/src/Galley/API/MLS/Message.hs | 5 ++- services/galley/test/integration/API/MLS.hs | 31 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/changelog.d/2-features/subconv-leave b/changelog.d/2-features/subconv-leave index 3ef40409c42..0fac932a479 100644 --- a/changelog.d/2-features/subconv-leave +++ b/changelog.d/2-features/subconv-leave @@ -1 +1 @@ -Implement endpoint for leaving a subconversation (#2969, #3080, #3085) +Implement endpoint for leaving a subconversation (#2969, #3080, #3085, #3107) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 6cd546d00cd..9e27cd95a2f 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -801,7 +801,10 @@ processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = lConvOrSub' <- for lConvOrSub incrementEpoch -- fetch backend remove proposals of the previous epoch - kpRefs <- getPendingBackendRemoveProposals (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub') epoch + kpRefs <- + -- skip remove proposals of already removed by the external commit + filter (maybe (const True) (/=) remRef) + <$> getPendingBackendRemoveProposals (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub') epoch -- requeue backend remove proposals for the current epoch let cm = membersConvOrSub (tUnqualified lConvOrSub') createAndSendRemoveProposals lConvOrSub' kpRefs qusr cm diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 4fb675754ea..32828380260 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -215,6 +215,7 @@ tests s = [ test s "get subconversation of MLS conv - 200" (testCreateSubConv True), test s "get subconversation of Proteus conv - 404" (testCreateSubConv False), test s "join subconversation with an external commit bundle" testJoinSubConv, + test s "rejoin a subconversation with the same client" testExternalCommitSameClientSubConv, test s "join subconversation with a client that is not in the parent conv" testJoinSubNonMemberClient, test s "fail to add another client to a subconversation via internal commit" testAddClientSubConvFailure, test s "remove another client from a subconversation" testRemoveClientSubConv, @@ -2333,6 +2334,36 @@ testJoinSubConv = do createExternalCommit alice1 Nothing (fmap (flip SubConv subId) qcnv) >>= sendAndConsumeCommitBundle +testExternalCommitSameClientSubConv :: TestM () +testExternalCommitSameClientSubConv = do + [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) + + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ uploadNewKeyPackage bob1 + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + let subId = SubConvId "conference" + + -- alice1 and bob1 create and join a subconversation, respectively + qsub <- createSubConv qcnv alice1 subId + void $ + createExternalCommit bob1 Nothing qsub + >>= sendAndConsumeCommitBundle + + Just (_, kpBob1) <- find (\(ci, _) -> ci == bob1) <$> getClientsFromGroupState alice1 bob + + -- bob1 leaves and immediately rejoins + mlsBracket [alice1, bob1] $ \[wsA, wsB] -> do + void $ leaveCurrentConv bob1 qsub + WS.assertMatchN_ (5 # WS.Second) [wsA] $ + wsAssertBackendRemoveProposal bob qsub kpBob1 + void $ + createExternalCommit bob1 Nothing qsub + >>= sendAndConsumeCommitBundle + WS.assertNoEvent (2 # WS.Second) [wsB] + testJoinSubNonMemberClient :: TestM () testJoinSubNonMemberClient = do [alice, bob] <- createAndConnectUsers [Nothing, Nothing] From 0fcf0cbaf3651a8da524601016143e3e241dcbed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 7 Mar 2023 12:51:29 +0100 Subject: [PATCH 030/225] [FS-1558] Add a test: delete a subconversation as a conversation member (#3119) * Test: delete a subconv as a conv member * Add a changelog --- .../1-api-changes/delete-subconversation | 2 +- services/galley/test/integration/API/MLS.hs | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/changelog.d/1-api-changes/delete-subconversation b/changelog.d/1-api-changes/delete-subconversation index 79f52ccb5d4..2f190691417 100644 --- a/changelog.d/1-api-changes/delete-subconversation +++ b/changelog.d/1-api-changes/delete-subconversation @@ -1 +1 @@ -Introduce an endpoint for deleting a subconversation +Introduce an endpoint for deleting a subconversation (#2956, #3119) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 7a048fcb75f..2feb46ca980 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -220,8 +220,9 @@ tests s = test s "fail to add another client to a subconversation via internal commit" testAddClientSubConvFailure, test s "remove another client from a subconversation" testRemoveClientSubConv, test s "send an application message in a subconversation" testSendMessageSubConv, - test s "reset a subconversation as a member" (testDeleteSubConv True), - test s "reset a subconversation as a non-member" (testDeleteSubConv False), + test s "reset a subconversation as a creator" (testDeleteSubConv SubConvMember), + test s "reset a subconversation as a conversation member" (testDeleteSubConv ConvMember), + test s "reset a subconversation as a random user" (testDeleteSubConv RandomUser), test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale, test s "leave a subconversation as a creator" (testLeaveSubConv True), test s "leave a subconversation as a non-creator" (testLeaveSubConv False), @@ -2745,18 +2746,23 @@ testRemoteMemberDeleteSubConv isAMember = do expectFailure errExpected (DeleteSubConversationResponseError err) = liftIO $ err @?= errExpected -testDeleteSubConv :: Bool -> TestM () -testDeleteSubConv isAMember = do - alice <- randomQualifiedUser +-- | A choice on who is deleting a subconversation +data SubConvDeleterType + = ConvMember + | SubConvMember + | RandomUser + deriving (Eq) + +testDeleteSubConv :: SubConvDeleterType -> TestM () +testDeleteSubConv deleterType = do + [alice, bob] <- createAndConnectUsers [Nothing, Nothing] randUser <- randomId - let (deleter, expectedCode) = - if isAMember - then (qUnqualified alice, 200) - else (randUser, 403) let sconv = SubConvId "conference" qcnv <- runMLSTest $ do - alice1 <- createMLSClient alice + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ uploadNewKeyPackage bob1 (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle void $ createSubConv qcnv alice1 sconv pure qcnv @@ -2765,6 +2771,10 @@ testDeleteSubConv isAMember = do =<< getSubConv (qUnqualified alice) qcnv sconv (qUnqualified bob, 200) + SubConvMember -> (qUnqualified alice, 200) + RandomUser -> (randUser, 403) deleteSubConv deleter qcnv sconv dsc !!! const expectedCode === statusCode newSub <- @@ -2773,7 +2783,7 @@ testDeleteSubConv isAMember = do Date: Thu, 9 Mar 2023 15:33:29 +0100 Subject: [PATCH 031/225] [FS-1588] No proposals after deleting a subconversation (#3123) * Test for no leftover proposals --- .../1-api-changes/delete-subconversation | 2 +- services/galley/test/integration/API/MLS.hs | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/changelog.d/1-api-changes/delete-subconversation b/changelog.d/1-api-changes/delete-subconversation index 2f190691417..c3a53610acd 100644 --- a/changelog.d/1-api-changes/delete-subconversation +++ b/changelog.d/1-api-changes/delete-subconversation @@ -1 +1 @@ -Introduce an endpoint for deleting a subconversation (#2956, #3119) +Introduce an endpoint for deleting a subconversation (#2956, #3119, #3123) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 2feb46ca980..a47634c4d32 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -223,6 +223,7 @@ tests s = test s "reset a subconversation as a creator" (testDeleteSubConv SubConvMember), test s "reset a subconversation as a conversation member" (testDeleteSubConv ConvMember), test s "reset a subconversation as a random user" (testDeleteSubConv RandomUser), + test s "reset a subconversation and assert no leftover proposals" testJoinDeletedSubConvWithRemoval, test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale, test s "leave a subconversation as a creator" (testLeaveSubConv True), test s "leave a subconversation as a non-creator" (testLeaveSubConv False), @@ -2793,6 +2794,47 @@ testDeleteSubConv deleterType = do "Old and new subconversation are not equal" (sub == newSub) +-- In this test case, Alice creates a subconversation, Bob joins and Alice +-- leaves. The leaving causes the backend to generate an external remove +-- proposal for the client by Alice. Next, Bob does not commit (simulating his +-- client crashing), and then deleting the subconversation after coming back up. +-- Then Bob creates a subconversation with the same subconversation ID and the +-- test asserts that both Alice and Bob get no events, which means the backend +-- does not resubmit the pending remove proposal for Alice's client. +testJoinDeletedSubConvWithRemoval :: TestM () +testJoinDeletedSubConvWithRemoval = do + [alice, bob] <- createAndConnectUsers [Nothing, Nothing] + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ uploadNewKeyPackage bob1 + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + let subConvId = SubConvId "conference" + qsconvId <- createSubConv qcnv alice1 subConvId + void $ + createExternalCommit bob1 Nothing qsconvId + >>= sendAndConsumeCommitBundle + liftTest $ + leaveSubConv (ciUser alice1) (ciClient alice1) qcnv subConvId + !!! const 200 === statusCode + -- no committing by Bob of the backend-generated remove proposal for alice1 + -- (simulating his client crashing) + + do + sub <- + liftTest $ + responseJsonError + =<< getSubConv (qUnqualified bob) qcnv subConvId + do + void $ createSubConv qcnv bob1 subConvId + void . liftIO $ WS.assertNoEvent (3 # WS.Second) wss + testDeleteSubConvStale :: TestM () testDeleteSubConvStale = do alice <- randomQualifiedUser From 8f5780e70c6d560f8fdd84c277a367ba79345717 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Mon, 27 Mar 2023 08:37:53 +0000 Subject: [PATCH 032/225] Sanitised PR --- services/galley/src/Galley/API/MLS/Propagate.hs | 4 ++-- services/galley/test/integration/API/MLS.hs | 2 +- services/galley/test/integration/API/MLS/Util.hs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 7b6ae7003fa..31a60d97eb6 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -43,8 +43,8 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error -import Wire.API.MLS.SubConversation import Wire.API.MLS.Message +import Wire.API.MLS.SubConversation import Wire.API.Message -- | Propagate a message. @@ -61,7 +61,7 @@ propagateMessage :: ByteString -> ClientMap -> Sem r UnreachableUsers -propagateMessage qusr lConvOrSub con raw cm = do +propagateMessage qusr lConvOrSub con raw cm = do now <- input @UTCTime let mlsConv = convOfConvOrSub <$> lConvOrSub lmems = mcLocalMembers . tUnqualified $ mlsConv diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index bc31927a0b0..76eebb4a6fd 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -61,8 +61,8 @@ import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error.Galley -import Wire.API.Federation.API.Common import Wire.API.Event.Conversation +import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index ac80998f6ee..07d2e510e53 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -49,9 +49,9 @@ import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Time +import qualified Data.Tuple.Extra as Tuple import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUIDV4 -import qualified Data.Tuple.Extra as Tuple import Galley.Keys import Galley.Options import qualified Galley.Options as Opts From fad02337bc710135c194d3d1adf9f8632d48e479 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Mon, 27 Mar 2023 08:57:13 +0000 Subject: [PATCH 033/225] Fixed failing tests --- services/galley/test/integration/API/MLS.hs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 76eebb4a6fd..acf54e5279e 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -2530,7 +2530,10 @@ testJoinRemoteSubConv = do -- bob joins subconversation let pgs = mpPublicGroupState initialCommit - let mock = queryGroupStateMock (fold pgs) bob <|> sendMessageMock + let mock = + ("send-mls-commit-bundle" ~> MLSMessageResponseUpdates [] (UnreachableUsers [])) + <|> queryGroupStateMock (fold pgs) bob + <|> sendMessageMock (_, reqs) <- withTempMockFederator' mock $ do commit <- createExternalCommit bob1 Nothing qcs sendAndConsumeCommitBundle commit @@ -3007,7 +3010,10 @@ testDeleteRemoteParentOfSubConv = do receiveNewRemoteConv qcs subGroupId let pgs = mpPublicGroupState initialCommit - let mock = queryGroupStateMock (fold pgs) bob <|> sendMessageMock + let mock = + ("send-mls-commit-bundle" ~> MLSMessageResponseUpdates [] (UnreachableUsers [])) + <|> queryGroupStateMock (fold pgs) bob + <|> sendMessageMock void $ withTempMockFederator' mock $ do -- bob joins subconversation commit <- createExternalCommit bob1 Nothing qcs @@ -3275,7 +3281,8 @@ testLeaveRemoteSubConv = do let pgs = mpPublicGroupState initialCommit let mock = - queryGroupStateMock (fold pgs) bob + ("send-mls-commit-bundle" ~> MLSMessageResponseUpdates [] (UnreachableUsers [])) + <|> queryGroupStateMock (fold pgs) bob <|> sendMessageMock <|> ("leave-sub-conversation" ~> LeaveSubConversationResponseOk) (_, reqs) <- withTempMockFederator' mock $ do From 5b7458e6044ed07ff25bc5cf577dc8559284dbfa Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 2 May 2023 10:40:49 +0200 Subject: [PATCH 034/225] Introduce "mixed" protocol (#3258) * wire-api: add ProtocolMixedTag (with undefined stubs) * wip * wire-api * fix bug in schema * galley int test: fix some types * libs/api-client fix type * brig-integration: fix some types * Add endpoint type * wip * add updateMixedProtocol * wire-api add json instances to ProtocolTag * add test * Rename test group * fix bug * Add TODO * Add failing test * Add creator client to conversation * Revert "Add TODO" This reverts commit c3ed0c8d51c0d90fa76cbe7d9c2a010d55bc2f80. * refactor tests and add failing test step * Allow proteus messages on mixed protocol * Add changelog entry * Add nginz route * hi ci * hi ci * finish leftover TODOs in protocolValidAction * Fix bug in schema of ProtocolUpdate * update docs: start-serices-only -> run-services * Allow no-op state transitions --- README.md | 2 +- changelog.d/2-features/mixed-protocol | 1 + charts/nginz/values.yaml | 3 + docs/src/developer/developer/how-to.md | 2 +- .../Network/Wire/Client/API/Conversation.hs | 2 +- libs/wire-api/src/Wire/API/Conversation.hs | 26 +++++- .../src/Wire/API/Conversation/Protocol.hs | 27 ++++++- libs/wire-api/src/Wire/API/Error/Galley.hs | 3 + .../API/Routes/Public/Galley/Conversation.hs | 21 +++++ .../Wire/API/Golden/Generated/NewConv_user.hs | 5 +- services/brig/test/integration/API/OAuth.hs | 5 +- .../brig/test/integration/API/Provider.hs | 3 +- .../brig/test/integration/API/Team/Util.hs | 3 +- .../test/integration/Federation/End2end.hs | 4 +- services/brig/test/integration/Util.hs | 5 +- services/galley/src/Galley/API/Create.hs | 17 ++-- services/galley/src/Galley/API/Message.hs | 2 +- services/galley/src/Galley/API/One2One.hs | 3 +- .../src/Galley/API/Public/Conversation.hs | 1 + services/galley/src/Galley/API/Update.hs | 58 +++++++++++++- .../src/Galley/Cassandra/Conversation.hs | 46 ++++++++--- .../galley/src/Galley/Cassandra/Queries.hs | 5 ++ .../src/Galley/Data/Conversation/Types.hs | 3 +- .../src/Galley/Effects/ConversationStore.hs | 3 + services/galley/test/integration/API.hs | 4 +- services/galley/test/integration/API/MLS.hs | 80 +++++++++++++++++++ .../galley/test/integration/API/MLS/Util.hs | 7 +- services/galley/test/integration/API/Util.hs | 34 ++++++-- .../integration-test/conf/nginz/nginx.conf | 5 ++ services/spar/test-scim-suite/README.md | 2 +- tools/stern/README.md | 2 +- 31 files changed, 323 insertions(+), 61 deletions(-) create mode 100644 changelog.d/2-features/mixed-protocol diff --git a/README.md b/README.md index 9ac41294e72..8c8c7fcdb7d 100644 --- a/README.md +++ b/README.md @@ -95,4 +95,4 @@ You have two options: * Option 1. (recommended) Install wire-server on kubernetes using the configuration and instructions provided in [wire-server-deploy](https://github.com/wireapp/wire-server-deploy). This is the best option to run it on a server and recommended if you want to self-host wire-server. -* Option 2. Compile everything in this repo, then you can use the `services/start-services-only.sh`. This option is intended as a way to try out wire-server on your local development machine and not suited for production. +* Option 2. Compile everything in this repo, then you can use the `services/run-services`. This option is intended as a way to try out wire-server on your local development machine and not suited for production. diff --git a/changelog.d/2-features/mixed-protocol b/changelog.d/2-features/mixed-protocol new file mode 100644 index 00000000000..507a8a7d586 --- /dev/null +++ b/changelog.d/2-features/mixed-protocol @@ -0,0 +1 @@ +Introduce a "mixed" conversation protocol type. A conversation of "mixed" protocol functions as a Proteus converation as well as a MLS conversations. It's intended to be used for migrating conversations from Proteus to MLS. diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index cf574b2de36..1834258a404 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -427,6 +427,9 @@ nginx_conf: - all max_body_size: 40m body_buffer_size: 256k + - path: /conversations/([^/]*)/([^/]*)/protocol + envs: + - all - path: /broadcast envs: - all diff --git a/docs/src/developer/developer/how-to.md b/docs/src/developer/developer/how-to.md index 71d12c0851b..d202f519f47 100644 --- a/docs/src/developer/developer/how-to.md +++ b/docs/src/developer/developer/how-to.md @@ -16,7 +16,7 @@ Terminal 1: Terminal 2: * Compile all services: `make c` -* Run services including nginz: `./services/start-services-only.sh`. +* Run services including nginz: `./services/run-services`. Open your browser at: [http://localhost:8080/api/swagger-ui](http://localhost:8080/api/swagger-ui) for diff --git a/libs/api-client/src/Network/Wire/Client/API/Conversation.hs b/libs/api-client/src/Network/Wire/Client/API/Conversation.hs index 3dc4ace781d..280d096dc53 100644 --- a/libs/api-client/src/Network/Wire/Client/API/Conversation.hs +++ b/libs/api-client/src/Network/Wire/Client/API/Conversation.hs @@ -141,6 +141,6 @@ createConv users name = sessionRequest req rsc readBody method POST . path "conversations" . acceptJson - . json (NewConv users [] (name >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin M.ProtocolProteusTag) + . json (NewConv users [] (name >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin M.ProtocolCreateProteusTag) $ empty rsc = status201 :| [] diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index e1d444b29eb..cee39c64c8d 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -62,6 +62,8 @@ module Wire.API.Conversation maybeRole, -- * create + ProtocolCreateTag (..), + protocolCreateToProtocolTag, NewConv (..), ConvTeamInfo (..), @@ -632,6 +634,26 @@ instance ToSchema ReceiptMode where -------------------------------------------------------------------------------- -- create +-- | This is distinct from 'ProtocolTag', which also include ProtocolMixedTag +data ProtocolCreateTag = ProtocolCreateProteusTag | ProtocolCreateMLSTag + deriving stock (Eq, Show, Enum, Bounded, Generic) + deriving (Arbitrary) via GenericUniform ProtocolCreateTag + +instance ToSchema ProtocolCreateTag where + schema = + enum @Text "ProtocolCreateTag" $ + mconcat + [ element "proteus" ProtocolCreateProteusTag, + element "mls" ProtocolCreateMLSTag + ] + +protocolCreateToProtocolTag :: ProtocolCreateTag -> ProtocolTag +protocolCreateToProtocolTag ProtocolCreateProteusTag = ProtocolProteusTag +protocolCreateToProtocolTag ProtocolCreateMLSTag = ProtocolMLSTag + +protocolCreateTagSchema :: ObjectSchema SwaggerDoc ProtocolCreateTag +protocolCreateTagSchema = fmap (fromMaybe ProtocolCreateProteusTag) (optField "protocol" schema) + data NewConv = NewConv { newConvUsers :: [UserId], -- | A list of qualified users, which can include some local qualified users @@ -646,7 +668,7 @@ data NewConv = NewConv -- | Every member except for the creator will have this role newConvUsersRole :: RoleName, -- | The protocol of the conversation. It can be Proteus or MLS (1.0). - newConvProtocol :: ProtocolTag + newConvProtocol :: ProtocolCreateTag } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewConv) @@ -705,7 +727,7 @@ newConvSchema sch = .= ( fieldWithDocModifier "conversation_role" (description ?~ usersRoleDesc) schema <|> pure roleNameWireAdmin ) - <*> newConvProtocol .= protocolTagSchema + <*> newConvProtocol .= protocolCreateTagSchema where usersDesc = "List of user IDs (excluding the requestor) to be \ diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index f72e45ea67a..4ee31fc2cdb 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -26,9 +26,11 @@ module Wire.API.Conversation.Protocol Epoch (..), Protocol (..), _ProtocolMLS, + _ProtocolMixed, _ProtocolProteus, protocolSchema, ConversationMLSData (..), + ProtocolUpdate (..), ) where @@ -36,6 +38,7 @@ import Control.Arrow import Control.Lens (makePrisms, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Schema +import qualified Data.Swagger as S import Data.Time.Clock import Imports import Wire.API.Conversation.Action.Tag @@ -45,7 +48,7 @@ import Wire.API.MLS.Group import Wire.API.MLS.SubConversation import Wire.Arbitrary -data ProtocolTag = ProtocolProteusTag | ProtocolMLSTag +data ProtocolTag = ProtocolProteusTag | ProtocolMLSTag | ProtocolMixedTag deriving stock (Eq, Show, Enum, Bounded, Generic) deriving (Arbitrary) via GenericUniform ProtocolTag @@ -94,6 +97,7 @@ instance ToSchema ConversationMLSData where data Protocol = ProtocolProteus | ProtocolMLS ConversationMLSData + | ProtocolMixed ConversationMLSData deriving (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform Protocol @@ -102,6 +106,7 @@ $(makePrisms ''Protocol) protocolTag :: Protocol -> ProtocolTag protocolTag ProtocolProteus = ProtocolProteusTag protocolTag (ProtocolMLS _) = ProtocolMLSTag +protocolTag (ProtocolMixed _) = ProtocolMixedTag -- | Certain actions need to be performed at the level of the underlying -- protocol (MLS, mostly) before being applied to conversations. This function @@ -109,6 +114,7 @@ protocolTag (ProtocolMLS _) = ProtocolMLSTag -- with the given protocol. protocolValidAction :: Protocol -> ConversationActionTag -> Bool protocolValidAction ProtocolProteus _ = True +protocolValidAction (ProtocolMixed _) _ = True protocolValidAction (ProtocolMLS _) ConversationJoinTag = False protocolValidAction (ProtocolMLS _) ConversationLeaveTag = True protocolValidAction (ProtocolMLS _) ConversationRemoveMembersTag = False @@ -120,9 +126,14 @@ instance ToSchema ProtocolTag where enum @Text "Protocol" $ mconcat [ element "proteus" ProtocolProteusTag, - element "mls" ProtocolMLSTag + element "mls" ProtocolMLSTag, + element "mixed" ProtocolMixedTag ] +deriving via (Schema ProtocolTag) instance FromJSON ProtocolTag + +deriving via (Schema ProtocolTag) instance ToJSON ProtocolTag + protocolTagSchema :: ObjectSchema SwaggerDoc ProtocolTag protocolTagSchema = fmap (fromMaybe ProtocolProteusTag) (optField "protocol" schema) @@ -144,3 +155,15 @@ deriving via (Schema Protocol) instance ToJSON Protocol protocolDataSchema :: ProtocolTag -> ObjectSchema SwaggerDoc Protocol protocolDataSchema ProtocolProteusTag = tag _ProtocolProteus (pure ()) protocolDataSchema ProtocolMLSTag = tag _ProtocolMLS mlsDataSchema +protocolDataSchema ProtocolMixedTag = tag _ProtocolMixed mlsDataSchema + +newtype ProtocolUpdate = ProtocolUpdate {unProtocolUpdate :: ProtocolTag} + +instance ToSchema ProtocolUpdate where + schema = object "ProtocolUpdate" (ProtocolUpdate <$> unProtocolUpdate .= protocolTagSchema) + +deriving via (Schema ProtocolUpdate) instance FromJSON ProtocolUpdate + +deriving via (Schema ProtocolUpdate) instance ToJSON ProtocolUpdate + +deriving via (Schema ProtocolUpdate) instance S.ToSchema ProtocolUpdate diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index 8bc3062b7a5..b06efe03004 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -67,6 +67,7 @@ data GalleyError | InvalidTarget | ConvNotFound | ConvAccessDenied + | ConvInvalidProtocolTransition | -- MLS Errors MLSNotEnabled | MLSNonEmptyMemberList @@ -185,6 +186,8 @@ type instance MapError 'ConvNotFound = 'StaticError 404 "no-conversation" "Conve type instance MapError 'ConvAccessDenied = 'StaticError 403 "access-denied" "Conversation access denied" +type instance MapError 'ConvInvalidProtocolTransition = 'StaticError 403 "invalid-protocol-transition" "Protocol transition is invalid" + type instance MapError 'InvalidTeamNotificationId = 'StaticError 400 "invalid-notification-id" "Could not parse notification id (must be UUIDv1)." type instance diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index ae8781d9278..7519255d916 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -27,6 +27,7 @@ import Servant hiding (WithStatus) import Servant.Swagger.Internal.Orphans () import Wire.API.Conversation import Wire.API.Conversation.Code +import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Error @@ -1252,3 +1253,23 @@ type ConversationAPI = '[RespondEmpty 200 "Update successful"] () ) + :<|> Named + "update-conversation-protocol" + ( Summary "Update the protocol of the conversation" + :> Description "**Note**: Only proteus->mixed upgrade is supported." + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvInvalidProtocolTransition + :> CanThrow 'ConvMemberNotFound + :> ZLocalUser + :> ZClient + :> ZConn + :> "conversations" + :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId + :> "protocol" + :> ReqBody '[JSON] ProtocolUpdate + :> MultiVerb + 'PUT + '[JSON] + '[RespondEmpty 200 "Update successful"] + () + ) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs index cbd89a82607..e4a4deca6a2 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs @@ -26,7 +26,6 @@ import qualified Data.Set as Set (fromList) import qualified Data.UUID as UUID (fromString) import Imports import Wire.API.Conversation -import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role testDomain :: Domain @@ -52,7 +51,7 @@ testObject_NewConv_user_1 = newConvMessageTimer = Just (Ms {ms = 3320987366258987}), newConvReceiptMode = Just (ReceiptMode {unReceiptMode = 1}), newConvUsersRole = fromJust (parseRoleName "8tp2gs7b6"), - newConvProtocol = ProtocolProteusTag + newConvProtocol = ProtocolCreateProteusTag } testObject_NewConv_user_3 :: NewConv @@ -71,5 +70,5 @@ testObject_NewConv_user_3 = ( parseRoleName "y3otpiwu615lvvccxsq0315jj75jquw01flhtuf49t6mzfurvwe3_sh51f4s257e2x47zo85rif_xyiyfldpan3g4r6zr35rbwnzm0k" ), - newConvProtocol = ProtocolMLSTag + newConvProtocol = ProtocolCreateMLSTag } diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 3efdd15fa43..dd3276ca215 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -53,10 +53,9 @@ import Text.RawString.QQ import URI.ByteString import Util import Web.FormUrlEncoded -import Wire.API.Conversation (Access (..), Conversation (cnvQualifiedId)) +import Wire.API.Conversation (Access (..), Conversation (cnvQualifiedId), ProtocolCreateTag (..)) import qualified Wire.API.Conversation as Conv import Wire.API.Conversation.Code (CreateConversationCodeRequest (CreateConversationCodeRequest)) -import Wire.API.Conversation.Protocol (ProtocolTag (ProtocolProteusTag)) import qualified Wire.API.Conversation.Role as Role import Wire.API.OAuth import Wire.API.Routes.Bearer (Bearer (Bearer, unBearer)) @@ -703,7 +702,7 @@ createTeamConv :: Http ResponseLBS createTeamConv svc mkHeader token tid name = do let tinfo = Conv.ConvTeamInfo tid - let conv = Conv.NewConv [] [] (checked name) (Set.fromList [CodeAccess]) Nothing (Just tinfo) Nothing Nothing Role.roleNameWireAdmin ProtocolProteusTag + let conv = Conv.NewConv [] [] (checked name) (Set.fromList [CodeAccess]) Nothing (Just tinfo) Nothing Nothing Role.roleNameWireAdmin ProtocolCreateProteusTag post $ svc . path "conversations" diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index bf5b7db8fd7..0674f61d96a 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -84,7 +84,6 @@ import Wire.API.Asset hiding (Asset) import Wire.API.Connection import Wire.API.Conversation import Wire.API.Conversation.Bot -import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Event.Conversation import Wire.API.Internal.Notification @@ -1415,7 +1414,7 @@ createConvWithAccessRoles ars g u us = . contentJson . body (RequestBodyLBS (encode conv)) where - conv = NewConv us [] Nothing Set.empty ars Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag + conv = NewConv us [] Nothing Set.empty ars Nothing Nothing Nothing roleNameWireAdmin ProtocolCreateProteusTag postMessage :: Galley -> diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index ff2f9c7ab4c..44633941969 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -40,7 +40,6 @@ import Test.Tasty.HUnit import Util import Web.Cookie (parseSetCookie, setCookieName) import Wire.API.Conversation -import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import qualified Wire.API.Routes.Internal.Galley.TeamsIntra as Team import Wire.API.Team hiding (newTeam) @@ -235,7 +234,7 @@ createTeamConvWithRole role g tid u us mtimer = do mtimer Nothing role - ProtocolProteusTag + ProtocolCreateProteusTag r <- post ( g diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index 9a0d0a5c4ff..353516b10e4 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -281,7 +281,7 @@ testAddRemoteUsersToLocalConv brig1 galley1 brig2 galley2 = do Nothing Nothing roleNameWireAdmin - ProtocolProteusTag + ProtocolCreateProteusTag convId <- fmap cnvQualifiedId . responseJsonError =<< post @@ -803,6 +803,7 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do groupId <- case cnvProtocol conv of ProtocolMLS p -> pure (unGroupId (cnvmlsGroupId p)) ProtocolProteus -> liftIO $ assertFailure "Expected MLS conversation" + ProtocolMixed _ -> liftIO $ assertFailure "Expected MLS conversation" let qconvId = cnvQualifiedId conv groupJSON <- liftIO $ @@ -1066,6 +1067,7 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 groupId <- case cnvProtocol conv of ProtocolMLS p -> pure (unGroupId (cnvmlsGroupId p)) ProtocolProteus -> liftIO $ assertFailure "Expected MLS conversation" + ProtocolMixed _ -> liftIO $ assertFailure "Expected MLS conversation" let qconvId = cnvQualifiedId conv groupJSON <- liftIO $ diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 8b9487238c0..88d5bfcc71c 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -107,7 +107,6 @@ import Util.Options import Web.Internal.HttpApiData import Wire.API.Connection import Wire.API.Conversation -import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role (roleNameWireAdmin) import Wire.API.Federation.API import Wire.API.Federation.Domain @@ -735,7 +734,7 @@ createMLSConversation galley zusr c = do Nothing Nothing roleNameWireAdmin - ProtocolMLSTag + ProtocolCreateMLSTag post $ galley . path "/conversations" @@ -776,7 +775,7 @@ createConversation galley zusr usersToAdd = do Nothing Nothing roleNameWireAdmin - ProtocolProteusTag + ProtocolCreateProteusTag post $ galley . path "/conversations" diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 7ddb0b3fbc7..58ed1273dd6 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -203,12 +203,12 @@ createGroupConversationGeneric lusr mCreatorClient conn newConv convCreated = do ensureNoLegalholdConflicts allUsers case newConvProtocol newConv of - ProtocolMLSTag -> do + ProtocolCreateMLSTag -> do -- Here we fail early in order to notify users of this misconfiguration assertMLSEnabled unlessM (isJust <$> getMLSRemovalKey) $ throw (InternalErrorWithDescription "No backend removal key is configured (See 'mlsPrivateKeyPaths' in galley's config). Refusing to create MLS conversation.") - ProtocolProteusTag -> pure () + ProtocolCreateProteusTag -> pure () lcnv <- traverse (const E.createConversationId) lusr -- FUTUREWORK: Invoke the creating a conversation action only once @@ -224,6 +224,7 @@ createGroupConversationGeneric lusr mCreatorClient conn newConv convCreated = do (ProtocolMLS mlsMeta, Just c) -> E.addMLSClients (cnvmlsGroupId mlsMeta) (tUntagged lusr) (Set.singleton (c, nullKeyPackageRef)) (ProtocolMLS _mlsMeta, Nothing) -> throwS @'MLSMissingSenderClient + (ProtocolMixed _mlsMeta, _) -> pure () -- NOTE: We only send (conversation) events to members of the conversation failedToNotify <- notifyCreatedConversation lusr conn conv @@ -312,7 +313,7 @@ createProteusSelfConversation lusr = do NewConversation { ncMetadata = (defConversationMetadata (tUnqualified lusr)) {cnvmType = SelfConv}, ncUsers = ulFromLocals [toUserRole (tUnqualified lusr)], - ncProtocol = ProtocolProteusTag + ncProtocol = ProtocolCreateProteusTag } c <- E.createConversation lcnv nc conversationCreated lusr c @@ -407,7 +408,7 @@ createLegacyOne2OneConversationUnchecked self zcon name mtid other = do let nc = NewConversation { ncUsers = ulFromLocals (map (toUserRole . tUnqualified) [self, other]), - ncProtocol = ProtocolProteusTag, + ncProtocol = ProtocolCreateProteusTag, ncMetadata = meta } mc <- E.getConversation (tUnqualified lcnv) @@ -472,7 +473,7 @@ createOne2OneConversationLocally lcnv self zcon name mtid other = do NewConversation { ncMetadata = meta, ncUsers = fmap toUserRole (toUserList lcnv [tUntagged self, other]), - ncProtocol = ProtocolProteusTag + ncProtocol = ProtocolCreateProteusTag } c <- E.createConversation lcnv nc void $ notifyCreatedConversation self (Just zcon) c @@ -521,7 +522,7 @@ createConnectConversation lusr conn j = do { -- We add only one member, second one gets added later, -- when the other user accepts the connection request. ncUsers = ulFromLocals (map (toUserRole . tUnqualified) [lusr]), - ncProtocol = ProtocolProteusTag, + ncProtocol = ProtocolCreateProteusTag, ncMetadata = meta } E.getConversation (tUnqualified lcnv) @@ -595,8 +596,8 @@ newRegularConversation lusr newConv = do o <- input let uncheckedUsers = newConvMembers lusr newConv users <- case newConvProtocol newConv of - ProtocolProteusTag -> checkedConvSize o uncheckedUsers - ProtocolMLSTag -> do + ProtocolCreateProteusTag -> checkedConvSize o uncheckedUsers + ProtocolCreateMLSTag -> do unless (null uncheckedUsers) $ throwS @'MLSNonEmptyMemberList pure mempty let nc = diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index 585c721cb9b..511098b563f 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -393,7 +393,7 @@ postQualifiedOtrMessage senderType sender mconn lcnv msg = let senderClient = qualifiedNewOtrSender msg conv <- getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound - unless (protocolTag (convProtocol conv) == ProtocolProteusTag) $ + unless (protocolTag (convProtocol conv) `elem` [ProtocolProteusTag, ProtocolMixedTag]) $ throwS @'InvalidOperation let localMemberIds = lmId <$> convLocalMembers conv diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index c2a98414ada..8bd2b4d9d1f 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -35,7 +35,6 @@ import Galley.Types.UserList import Imports import Polysemy import Wire.API.Conversation hiding (Member) -import Wire.API.Conversation.Protocol import Wire.API.Routes.Internal.Galley.ConversationsIntra (Actor (..), DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) newConnectConversationWithRemote :: @@ -49,7 +48,7 @@ newConnectConversationWithRemote creator users = { cnvmType = One2OneConv }, ncUsers = fmap toUserRole users, - ncProtocol = ProtocolProteusTag + ncProtocol = ProtocolCreateProteusTag } iUpsertOne2OneConversation :: diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 0ef1c8b66ae..3efe3445caa 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -88,3 +88,4 @@ conversationAPI = <@> mkNamedAPI @"get-conversation-self-unqualified" getLocalSelf <@> mkNamedAPI @"update-conversation-self-unqualified" updateUnqualifiedSelfMember <@> mkNamedAPI @"update-conversation-self" updateSelfMember + <@> mkNamedAPI @"update-conversation-protocol" updateConversationProtocolWithLocalUser diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 6b8c2bc8cea..45c36abf16d 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE OverloadedRecordDot #-} -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -15,6 +14,7 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE RecordWildCards #-} module Galley.API.Update @@ -39,6 +39,7 @@ module Galley.API.Update updateConversationAccess, deleteLocalConversation, updateRemoteConversation, + updateConversationProtocolWithLocalUser, -- * Managing Members addMembersUnqualified, @@ -85,6 +86,7 @@ import Data.Time import Galley.API.Action import Galley.API.Error import Galley.API.Federation (onConversationUpdated) +import Galley.API.MLS.KeyPackage (nullKeyPackageRef) import Galley.API.Mapping import Galley.API.Message import qualified Galley.API.Query as Query @@ -123,6 +125,7 @@ import System.Logger (Msg) import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Action import Wire.API.Conversation.Code +import Wire.API.Conversation.Protocol (ProtocolTag (..), ProtocolUpdate (ProtocolUpdate), protocolTag) import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Error @@ -131,6 +134,8 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Group import Wire.API.Message import Wire.API.Password (mkSafePassword) import Wire.API.Provider.Service (ServiceRef) @@ -679,6 +684,57 @@ checkReusableCode convCode = do mapErrorS @'GuestLinksDisabled @'CodeNotFound $ Query.ensureGuestLinksEnabled @db (Data.convTeam conv) +updateConversationProtocolWithLocalUser :: + forall r. + ( Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'ConvInvalidProtocolTransition) r, + Member (ErrorS 'ConvMemberNotFound) r, + Member (Error FederationError) r, + Member MemberStore r, + Member ConversationStore r + ) => + Local UserId -> + ClientId -> + ConnId -> + Qualified ConvId -> + ProtocolUpdate -> + Sem r () +updateConversationProtocolWithLocalUser lusr client conn qcnv update = + foldQualified + lusr + (\lcnv -> updateLocalConversationProtocol (tUntagged lusr) client (Just conn) lcnv update) + (\_rcnv -> throw FederationNotImplemented) + qcnv + +updateLocalConversationProtocol :: + forall r. + ( Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'ConvInvalidProtocolTransition) r, + Member (ErrorS 'ConvMemberNotFound) r, + Member MemberStore r, + Member ConversationStore r + ) => + Qualified UserId -> + ClientId -> + Maybe ConnId -> + Local ConvId -> + ProtocolUpdate -> + Sem r () +updateLocalConversationProtocol qusr client _mconn lcnv (ProtocolUpdate newProtocol) = do + conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound + void $ ensureOtherMember lcnv qusr conv + case (protocolTag (convProtocol conv), newProtocol) of + (ProtocolProteusTag, ProtocolMixedTag) -> do + E.updateToMixedProtocol lcnv MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + E.addMLSClients (convToGroupId lcnv) qusr (Set.singleton (client, nullKeyPackageRef)) + (ProtocolProteusTag, ProtocolProteusTag) -> + pure () + (ProtocolMixedTag, ProtocolMixedTag) -> + pure () + (ProtocolMLSTag, ProtocolMLSTag) -> + pure () + (_, _) -> throwS @'ConvInvalidProtocolTransition + joinConversationByReusableCode :: forall db r. ( Member BrigAccess r, diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 4d7c09bc067..520cd3ea9c2 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -72,7 +72,7 @@ createMLSSelfConversation lusr = do { ncMetadata = (defConversationMetadata usr) {cnvmType = SelfConv}, ncUsers = ulFromLocals [toUserRole usr], - ncProtocol = ProtocolMLSTag + ncProtocol = ProtocolCreateMLSTag } meta = ncMetadata nc gid = convToGroupId . qualifyAs lusr $ cnv @@ -123,8 +123,8 @@ createConversation :: Local ConvId -> NewConversation -> Client Conversation createConversation lcnv nc = do let meta = ncMetadata nc (proto, mgid, mep, mcs) = case ncProtocol nc of - ProtocolProteusTag -> (ProtocolProteus, Nothing, Nothing, Nothing) - ProtocolMLSTag -> + ProtocolCreateProteusTag -> (ProtocolProteus, Nothing, Nothing, Nothing) + ProtocolCreateMLSTag -> let gid = convToGroupId lcnv ep = Epoch 0 cs = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 @@ -158,7 +158,7 @@ createConversation lcnv nc = do cnvmTeam meta, cnvmMessageTimer meta, cnvmReceiptMode meta, - ncProtocol nc, + protocolCreateToProtocolTag (ncProtocol nc), mgid, mep, mcs @@ -337,15 +337,17 @@ toProtocol :: Maybe Protocol toProtocol Nothing _ _ _ _ = Just ProtocolProteus toProtocol (Just ProtocolProteusTag) _ _ _ _ = Just ProtocolProteus -toProtocol (Just ProtocolMLSTag) mgid mepoch mtimestamp mcs = - ProtocolMLS - <$> ( ConversationMLSData - <$> mgid - -- If there is no epoch in the database, assume the epoch is 0 - <*> (mepoch <|> Just (Epoch 0)) - <*> pure (mepoch `toTimestamp` mtimestamp) - <*> mcs - ) +toProtocol (Just ProtocolMLSTag) mgid mepoch mtimestamp mcs = ProtocolMLS <$> toConversationMLSData mgid mepoch mtimestamp mcs +toProtocol (Just ProtocolMixedTag) mgid mepoch mtimestamp mcs = ProtocolMixed <$> toConversationMLSData mgid mepoch mtimestamp mcs + +toConversationMLSData :: Maybe GroupId -> Maybe Epoch -> Maybe UTCTime -> Maybe CipherSuiteTag -> Maybe ConversationMLSData +toConversationMLSData mgid mepoch mtimestamp mcs = + ConversationMLSData + <$> mgid + -- If there is no epoch in the database, assume the epoch is 0 + <*> (mepoch <|> Just (Epoch 0)) + <*> pure (mepoch `toTimestamp` mtimestamp) + <*> mcs where toTimestamp :: Maybe Epoch -> Maybe UTCTime -> Maybe UTCTime toTimestamp Nothing _ = Nothing @@ -429,6 +431,23 @@ deleteGroupIds :: deleteGroupIds = embedClient . UnliftIO.pooledMapConcurrentlyN_ 8 deleteGroupIdForConversation +updateToMixedProtocol :: + Members + '[ Embed IO, + Input ClientState + ] + r => + Local ConvId -> + CipherSuiteTag -> + Sem r () +updateToMixedProtocol lcnv cs = + embedClient . retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + let gid = convToGroupId lcnv + addPrepQuery Cql.insertGroupIdForConversation (gid, tUnqualified lcnv, tDomain lcnv) + addPrepQuery Cql.updateToMixedConv (tUnqualified lcnv, ProtocolMixedTag, gid, Epoch 0, cs) + interpretConversationStoreToCassandra :: ( Member (Embed IO) r, Member (Input ClientState) r, @@ -461,3 +480,4 @@ interpretConversationStoreToCassandra = interpret $ \case AcquireCommitLock gId epoch ttl -> embedClient $ acquireCommitLock gId epoch ttl ReleaseCommitLock gId epoch -> embedClient $ releaseCommitLock gId epoch DeleteGroupIds gIds -> deleteGroupIds gIds + UpdateToMixedProtocol cid cs -> updateToMixedProtocol cid cs diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 35c8ae7c325..c838ddd00f2 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -256,6 +256,11 @@ insertMLSSelfConv = <> show (fromEnum ProtocolMLSTag) <> ", ?, ?)" +updateToMixedConv :: PrepQuery W (ConvId, ProtocolTag, GroupId, Epoch, CipherSuiteTag) () +updateToMixedConv = + fromString $ + "insert into conversation (conv, protocol, group_id, epoch, cipher_suite) values (?, ?, ?, ?, ?)" + updateConvAccess :: PrepQuery W (C.Set Access, C.Set AccessRole, ConvId) () updateConvAccess = "update conversation set access = ?, access_roles_v2 = ? where conv = ?" diff --git a/services/galley/src/Galley/Data/Conversation/Types.hs b/services/galley/src/Galley/Data/Conversation/Types.hs index a8ad662a210..edff99fa5c0 100644 --- a/services/galley/src/Galley/Data/Conversation/Types.hs +++ b/services/galley/src/Galley/Data/Conversation/Types.hs @@ -41,7 +41,7 @@ data Conversation = Conversation data NewConversation = NewConversation { ncMetadata :: ConversationMetadata, ncUsers :: UserList (UserId, RoleName), - ncProtocol :: ProtocolTag + ncProtocol :: ProtocolCreateTag } mlsMetadata :: Conversation -> Maybe ConversationMLSData @@ -49,3 +49,4 @@ mlsMetadata conv = case convProtocol conv of ProtocolProteus -> Nothing ProtocolMLS meta -> pure meta + ProtocolMixed meta -> pure meta diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index 0e0763e6af3..5d9fa1d51cf 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -48,6 +48,7 @@ module Galley.Effects.ConversationStore deleteGroupIdForConversation, setPublicGroupState, deleteGroupIds, + updateToMixedProtocol, -- * Delete conversation deleteConversation, @@ -69,6 +70,7 @@ import Galley.Types.Conversations.Members import Imports import Polysemy import Wire.API.Conversation hiding (Conversation, Member) +import Wire.API.MLS.CipherSuite (CipherSuiteTag) import Wire.API.MLS.Epoch import Wire.API.MLS.PublicGroupState import Wire.API.MLS.SubConversation @@ -108,6 +110,7 @@ data ConversationStore m a where AcquireCommitLock :: GroupId -> Epoch -> NominalDiffTime -> ConversationStore m LockAcquired ReleaseCommitLock :: GroupId -> Epoch -> ConversationStore m () DeleteGroupIds :: [GroupId] -> ConversationStore m () + UpdateToMixedProtocol :: Local ConvId -> CipherSuiteTag -> ConversationStore m () makeSem ''ConversationStore diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 6c54caee22c..fd179818c65 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -2396,7 +2396,7 @@ postConvQualifiedFederationNotEnabled = do -- FUTUREWORK: figure out how to use functions in the TestM monad inside withSettingsOverrides and remove this duplication postConvHelper :: MonadHttp m => (Request -> Request) -> UserId -> [Qualified UserId] -> m ResponseLBS postConvHelper g zusr newUsers = do - let conv = NewConv [] newUsers (checked "gossip") (Set.fromList []) Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag + let conv = NewConv [] newUsers (checked "gossip") (Set.fromList []) Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolCreateProteusTag post $ g . path "/conversations" . zUser zusr . zConn "conn" . zType "access" . json conv postSelfConvOk :: TestM () @@ -2424,7 +2424,7 @@ postConvO2OFailWithSelf :: TestM () postConvO2OFailWithSelf = do g <- viewGalley alice <- randomUser - let inv = NewConv [alice] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag + let inv = NewConv [alice] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolCreateProteusTag post (g . path "/conversations/one2one" . zUser alice . zConn "conn" . zType "access" . json inv) !!! do const 403 === statusCode const (Just "invalid-op") === fmap label . responseJsonUnsafe diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index acf54e5279e..b6c6bd1023a 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -259,6 +259,11 @@ tests s = test s "delete subconversation as a remote non-member" (testRemoteMemberDeleteSubConv False), test s "delete parent conversation of a remote subconveration" testDeleteRemoteParentOfSubConv ] + ], + testGroup + "MixedProtocol" + [ test s "Upgrade a conv from proteus to mixed" testMixedUpgrade, + test s "Add clients to a mixed conversation and send proteus message" testMixedAddClients ] ] @@ -3526,3 +3531,78 @@ testCreatorRemovesUserFromParent = do ) (sort [alice1, charlie1, charlie2]) (sort $ pscMembers sub2) + +testMixedUpgrade :: TestM () +testMixedUpgrade = do + [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) + + runMLSTest $ do + [alice1] <- traverse createMLSClient [alice] + + qcnv <- + cnvQualifiedId + <$> liftTest + ( postConvQualified (qUnqualified alice) Nothing defNewProteusConv {newConvQualifiedUsers = [bob]} + >>= responseJsonError + ) + + putConversationProtocol (qUnqualified alice) (ciClient alice1) qcnv ProtocolMixedTag + !!! const 200 === statusCode + + conv <- + responseJsonError + =<< getConvQualified (qUnqualified alice) qcnv + liftTest + ( postConvQualified (qUnqualified alice) Nothing defNewProteusConv {newConvQualifiedUsers = [bob, charlie]} + >>= responseJsonError + ) + + -- bob upgrades to mixed + putConversationProtocol (qUnqualified bob) (ciClient bob1) qcnv ProtocolMixedTag + !!! const 200 === statusCode + + conv <- + responseJsonError + =<< getConvQualified (qUnqualified alice) qcnv + do + void $ sendAndConsumeCommitBundle commit + for_ (zip [alice1, charlie1] wss) $ \(c, ws) -> + WS.assertMatch (5 # Second) ws $ + wsAssertMLSWelcome (cidQualifiedUser c) welcome + + -- charlie sends a Proteus message + let msgs = + [ (qUnqualified alice, ciClient alice1, toBase64Text "ciphertext-to-alice"), + (qUnqualified bob, ciClient bob1, toBase64Text "ciphertext-to-bob") + ] + liftTest $ + postOtrMessage id (qUnqualified charlie) (ciClient charlie1) (qUnqualified qcnv) msgs !!! do + const 201 === statusCode + assertMismatch [] [] [] diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 07d2e510e53..bb59cb8cdb2 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -476,8 +476,11 @@ setupMLSGroupWithConv convAction creator = do conv <- convAction let groupId = fromJust - (preview (to cnvProtocol . _ProtocolMLS . to cnvmlsGroupId) conv) - + ( asum + [ preview (to cnvProtocol . _ProtocolMLS . to cnvmlsGroupId) conv, + preview (to cnvProtocol . _ProtocolMixed . to cnvmlsGroupId) conv + ] + ) let qcnv = cnvQualifiedId conv createGroup creator (fmap Conv qcnv) groupId pure (groupId, qcnv) diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index a0de4e36bd3..0d35e8fafe2 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -623,7 +623,7 @@ createTeamConvAccessRaw u tid us name acc role mtimer convRole = do g <- viewGalley let tinfo = ConvTeamInfo tid let conv = - NewConv us [] (name >>= checked) (fromMaybe (Set.fromList []) acc) role (Just tinfo) mtimer Nothing (fromMaybe roleNameWireAdmin convRole) ProtocolProteusTag + NewConv us [] (name >>= checked) (fromMaybe (Set.fromList []) acc) role (Just tinfo) mtimer Nothing (fromMaybe roleNameWireAdmin convRole) ProtocolCreateProteusTag post ( g . path "/conversations" @@ -659,7 +659,7 @@ createMLSTeamConv lusr c tid users name access role timer convRole = do newConvMessageTimer = timer, newConvUsersRole = fromMaybe roleNameWireAdmin convRole, newConvReceiptMode = Nothing, - newConvProtocol = ProtocolMLSTag + newConvProtocol = ProtocolCreateMLSTag } r <- post @@ -690,7 +690,7 @@ createOne2OneTeamConv :: UserId -> UserId -> Maybe Text -> TeamId -> TestM Respo createOne2OneTeamConv u1 u2 n tid = do g <- viewGalley let conv = - NewConv [u2] [] (n >>= checked) mempty Nothing (Just $ ConvTeamInfo tid) Nothing Nothing roleNameWireAdmin ProtocolProteusTag + NewConv [u2] [] (n >>= checked) mempty Nothing (Just $ ConvTeamInfo tid) Nothing Nothing roleNameWireAdmin ProtocolCreateProteusTag post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv postConv :: @@ -704,12 +704,12 @@ postConv :: postConv u us name a r mtimer = postConvWithRole u us name a r mtimer roleNameWireAdmin defNewProteusConv :: NewConv -defNewProteusConv = NewConv [] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag +defNewProteusConv = NewConv [] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolCreateProteusTag defNewMLSConv :: NewConv defNewMLSConv = defNewProteusConv - { newConvProtocol = ProtocolMLSTag + { newConvProtocol = ProtocolCreateMLSTag } postConvQualified :: @@ -748,7 +748,7 @@ postConvWithRemoteUsers u c n = postTeamConv :: TeamId -> UserId -> [UserId] -> Maybe Text -> [Access] -> Maybe (Set AccessRole) -> Maybe Milliseconds -> TestM ResponseLBS postTeamConv tid u us name a r mtimer = do g <- viewGalley - let conv = NewConv us [] (name >>= checked) (Set.fromList a) r (Just (ConvTeamInfo tid)) mtimer Nothing roleNameWireAdmin ProtocolProteusTag + let conv = NewConv us [] (name >>= checked) (Set.fromList a) r (Just (ConvTeamInfo tid)) mtimer Nothing roleNameWireAdmin ProtocolCreateProteusTag post $ g . path "/conversations" . zUser u . zConn "conn" . zType "access" . json conv deleteTeamConv :: (HasGalley m, MonadIO m, MonadHttp m) => TeamId -> ConvId -> UserId -> m ResponseLBS @@ -786,7 +786,7 @@ postConvWithRole u members name access arole timer role = postConvWithReceipt :: UserId -> [UserId] -> Maybe Text -> [Access] -> Maybe (Set AccessRole) -> Maybe Milliseconds -> ReceiptMode -> TestM ResponseLBS postConvWithReceipt u us name a r mtimer rcpt = do g <- viewGalley - let conv = NewConv us [] (name >>= checked) (Set.fromList a) r Nothing mtimer (Just rcpt) roleNameWireAdmin ProtocolProteusTag + let conv = NewConv us [] (name >>= checked) (Set.fromList a) r Nothing mtimer (Just rcpt) roleNameWireAdmin ProtocolCreateProteusTag post $ g . path "/conversations" . zUser u . zConn "conn" . zType "access" . json conv postSelfConv :: UserId -> TestM ResponseLBS @@ -797,7 +797,7 @@ postSelfConv u = do postO2OConv :: UserId -> UserId -> Maybe Text -> TestM ResponseLBS postO2OConv u1 u2 n = do g <- viewGalley - let conv = NewConv [u2] [] (n >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag + let conv = NewConv [u2] [] (n >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolCreateProteusTag post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv postConnectConv :: UserId -> UserId -> Text -> Text -> Maybe Text -> TestM ResponseLBS @@ -2980,3 +2980,21 @@ createAndConnectUsers domains = do (False, True) -> connectWithRemoteUser (qUnqualified b) a (False, False) -> pure () pure users + +putConversationProtocol :: (MonadIO m, MonadHttp m, HasGalley m, HasCallStack) => UserId -> ClientId -> Qualified ConvId -> ProtocolTag -> m ResponseLBS +putConversationProtocol uid client (Qualified conv domain) protocol = do + galley <- viewGalley + put + ( galley + . paths ["conversations", toByteString' domain, toByteString' conv, "protocol"] + . zUser uid + . zConn "conn" + . zClient client + . Bilge.json (object ["protocol" .= protocol]) + ) + +assertMixedProtocol :: (MonadIO m, HasCallStack) => Conversation -> m ConversationMLSData +assertMixedProtocol conv = do + case cnvProtocol conv of + ProtocolMixed mlsData -> pure mlsData + _ -> liftIO $ assertFailure "Unexpected protocol" diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index a2449bbbd54..d4128443c41 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -360,6 +360,11 @@ http { proxy_pass http://galley; } + location ~* ^/conversations/([^/]*)/([^/]*)/protocol { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + location /broadcast { include common_response_with_zauth.conf; proxy_pass http://galley; diff --git a/services/spar/test-scim-suite/README.md b/services/spar/test-scim-suite/README.md index e7dfaaf85d2..d2da32d467d 100644 --- a/services/spar/test-scim-suite/README.md +++ b/services/spar/test-scim-suite/README.md @@ -4,7 +4,7 @@ The scripts in this directory allow to run the [SCIM Test Suite](https://github. How to run: ```sh -./services/start-services-only.sh +./services/run-services ./services/spar/test-scim-suite/runsuite.sh ``` diff --git a/tools/stern/README.md b/tools/stern/README.md index a5ad9a345bf..884f4861348 100644 --- a/tools/stern/README.md +++ b/tools/stern/README.md @@ -19,7 +19,7 @@ TODO: This section is under construction ## How to run stern locally -Start local services via `services/start-services-only.sh` +Start local services via `services/run-services` Open in a browser. From 6d7447b2686b81b6f031875e5dd7d6f4a3b0eda0 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 3 May 2023 15:50:55 +0200 Subject: [PATCH 035/225] MLS upgrade (#3172) * Add variable-sized integer serialisation * Implement new MLS structures * Fix KeyPackage parser * Fix MLS signature verification Signatures in MLS are computed on a special `SignContent` structure, so we need to replicate that for verification. * Update paths now contain leaf nodes * Remove proposals now have indices instead of refs * Adapt integration tests to remove proposal changes * Compute new node index for add proposals * New commit bundle API Also replace PublicGroupState with GroupInfo * Add instances for roundtrip tests of MLS types * fix adding users to MLS conversations * change content-type of commit bundle in integration tests * fix keypackage ref serialisation * add context to commit bundle parsing * fix integration test: send other user's commit * keep track of index map while processing proposals * add creator client to ProposalAction in epoch 0 * readGroupState for the new group.json format * Generate welcome recipients when processing bundle Also remove old unsupported welcome endpoints. All welcome messages now need to be sent through commit bundles. * Send recipients as part of a welcome RPC * Use commit bundles in failure tests * Implement new proposal ref computation * fix integration test admin removes user from a conversation * switch mls-test-cli call to external-proposal * Implement validation of leaf nodes in galley - extract core validation function to wire-api - generalise validation of leaf node source - implement validation of key packages and leaf nodes in galley - remove all internal brig endpoints related to validation - validate leaf node in external commits - validate leaf node signature * Apply proposals in the correct order * Remove redundant GroupContext structure * Re-implement processing of external commits * add references from data types to MLS spec * Remove key package mapping code * fix more integration tests * track client scheduled for removal in Cassandra [ ] conversations [x] subconversations * minor typos * split executing proposals for int and ext commits * execute remove proposals before add proposals This makes sure that all leaf indices are freed in the database before they are occupied again. * rename Word32 and ref to LeafIndex and idx * Remove MissingSenderClient error * Remove some prefixes from MLS structures * Remove prefixes from RawMLS fields * Reorganise TODOs * Check epoch again after taking commit lock * Remove MLSPackageRefNotFound error * Simplify testRemoveUserParent * Simplify testRemoveCreatorParent * Pass correct list of clients to planClientRemoval * Fix assertion in external add proposal test * Propagate actual message, not just commit * Fix signature calculation when generating messages * Pass removal key to mls-test-cli on group creation * Take pending clients into account in removal logic * Fix assertion in remove proposal test * apply linter suggestions * fix unit test: MLS remove proposal * Upgrade mls-test-cli in the nix environment * Update cassandra-schema.cql * disable testing the keypackage lifetime * remove checks for keypackage assignments * validate bare proposals and inline proposal * rephrase and filter the left TODOs * Verify that capabilities include basic credentials * Add nonce to PreSharedKeyID structure * Split Galley.API.MLS.Message * Inline executeIntCommitProposalAction * Use more specific type for external commit actions * Re-organise TODOs * Simplify processProposal arguments * Remove LWT in planMLSClientRemoval * Restore unsupported proposal test * Restore disabled MLS unit tests * Add CHANGELOG entries * Document IndexMap and ClientMap * fixup! Restore unsupported proposal test * Linter fix * fixup! Upgrade mls-test-cli in the nix environment * Fix: make git-add-cassandra-schema-impl lists to many keyspaces * postMLSMessageToLocalConv: return no events * Remove unused paExternalInit * Renew certificates for e2e integration tests (#3243) * Renew certificates for e2e integration tests * Document how to renew e2e integration test certs Co-authored-by: Igor Ranieri * fix broken tests * ExternalCommitAction: remove superfluous ClientIdentity --------- Co-authored-by: Stefan Matting Co-authored-by: Stefan Berthold Co-authored-by: Akshay Mankar Co-authored-by: Igor Ranieri --- Makefile | 6 +- cassandra-schema.cql | 2 + changelog.d/1-api-changes/mls-upgrade | 7 + changelog.d/5-internal/key-package-mapping | 1 + hack/bin/cassandra_dump_schema | 32 + hack/python/wire/mlscli.py | 2 +- .../src/Wire/API/Federation/API/Brig.hs | 2 +- .../src/Wire/API/Federation/API/Galley.hs | 7 +- libs/wire-api/default.nix | 1 + libs/wire-api/src/Wire/API/Error/Galley.hs | 7 +- .../src/Wire/API/MLS/AuthenticatedContent.hs | 111 ++ .../wire-api/src/Wire/API/MLS/Capabilities.hs | 55 + libs/wire-api/src/Wire/API/MLS/CipherSuite.hs | 145 +- libs/wire-api/src/Wire/API/MLS/Commit.hs | 61 +- .../wire-api/src/Wire/API/MLS/CommitBundle.hs | 113 +- libs/wire-api/src/Wire/API/MLS/Context.hs | 13 +- libs/wire-api/src/Wire/API/MLS/Credential.hs | 115 +- libs/wire-api/src/Wire/API/MLS/Extension.hs | 119 +- libs/wire-api/src/Wire/API/MLS/Group.hs | 4 +- libs/wire-api/src/Wire/API/MLS/GroupInfo.hs | 140 ++ .../src/Wire/API/MLS/GroupInfoBundle.hs | 98 -- .../src/Wire/API/MLS/HPKEPublicKey.hs | 34 + libs/wire-api/src/Wire/API/MLS/KeyPackage.hs | 122 +- libs/wire-api/src/Wire/API/MLS/Keys.hs | 1 + libs/wire-api/src/Wire/API/MLS/LeafNode.hs | 201 +++ libs/wire-api/src/Wire/API/MLS/Lifetime.hs | 48 + libs/wire-api/src/Wire/API/MLS/Message.hs | 563 ++++---- libs/wire-api/src/Wire/API/MLS/Proposal.hs | 220 +-- libs/wire-api/src/Wire/API/MLS/ProposalTag.hs | 40 + .../src/Wire/API/MLS/ProtocolVersion.hs | 53 + .../src/Wire/API/MLS/PublicGroupState.hs | 121 -- .../src/Wire/API/MLS/Serialisation.hs | 86 +- libs/wire-api/src/Wire/API/MLS/Servant.hs | 20 +- libs/wire-api/src/Wire/API/MLS/Validation.hs | 125 ++ libs/wire-api/src/Wire/API/MLS/Welcome.hs | 20 +- libs/wire-api/src/Wire/API/OAuth.hs | 1 - .../src/Wire/API/Routes/Internal/Brig.hs | 146 +- .../API/Routes/Public/Galley/Conversation.hs | 13 +- .../src/Wire/API/Routes/Public/Galley/MLS.hs | 135 +- libs/wire-api/src/Wire/API/User/Client.hs | 4 +- libs/wire-api/test/golden.hs | 5 + .../Wire/API/Golden/Generated/Client_user.hs | 2 +- .../API/Golden/Generated/NewClient_user.hs | 2 +- .../API/Golden/Generated/UpdateClient_user.hs | 2 +- .../{Main.hs => Test/Wire/API/Golden/Run.hs} | 5 +- libs/wire-api/test/resources/key_package1.mls | Bin 262 -> 0 bytes libs/wire-api/test/unit.hs | 5 + libs/wire-api/test/unit/Test/Wire/API/MLS.hs | 305 ++-- .../test/unit/Test/Wire/API/Roundtrip/MLS.hs | 171 ++- .../unit/{Main.hs => Test/Wire/API/Run.hs} | 5 +- libs/wire-api/wire-api.cabal | 245 ++-- nix/pkgs/mls-test-cli/default.nix | 31 +- services/brig/brig.cabal | 404 ++---- services/brig/schema/main.hs | 5 + services/brig/schema/src/{Main.hs => Run.hs} | 2 +- .../schema/src/V69_MLSKeyPackageRefMapping.hs | 1 + services/brig/src/Brig/API/Internal.hs | 83 +- services/brig/src/Brig/API/MLS/KeyPackages.hs | 18 +- .../Brig/API/MLS/KeyPackages/Validation.hs | 119 +- services/brig/src/Brig/Data/Client.hs | 2 +- services/brig/src/Brig/Data/MLS/KeyPackage.hs | 126 +- services/brig/test/integration.hs | 5 + .../brig/test/integration/API/Federation.hs | 15 +- .../brig/test/integration/API/Internal.hs | 127 +- services/brig/test/integration/API/MLS.hs | 42 +- .../brig/test/integration/API/MLS/Util.hs | 3 +- .../brig/test/integration/API/User/Client.hs | 2 +- .../test/integration/Federation/End2end.hs | 101 +- .../brig/test/integration/Federation/Util.hs | 23 +- .../brig/test/integration/{Main.hs => Run.hs} | 2 +- services/brig/test/unit.hs | 1 + services/brig/test/unit/Main.hs | 2 +- services/brig/test/unit/Run.hs | 43 + services/brig/test/unit/Test/Brig/MLS.hs | 80 +- services/galley/galley.cabal | 410 ++---- services/galley/migrate-data/main.hs | 1 + .../migrate-data/src/{Main.hs => Run.hs} | 2 +- services/galley/schema/main.hs | 5 + .../galley/schema/src/{Main.hs => Run.hs} | 6 +- .../src/V82_MLSDraft17.hs} | 30 +- services/galley/src/Galley/API/Action.hs | 4 - services/galley/src/Galley/API/Create.hs | 28 +- services/galley/src/Galley/API/Federation.hs | 68 +- services/galley/src/Galley/API/MLS.hs | 3 - services/galley/src/Galley/API/MLS/Commit.hs | 28 + .../galley/src/Galley/API/MLS/Commit/Core.hs | 195 +++ .../Galley/API/MLS/Commit/ExternalCommit.hs | 197 +++ .../Galley/API/MLS/Commit/InternalCommit.hs | 269 ++++ .../galley/src/Galley/API/MLS/Conversation.hs | 5 +- .../galley/src/Galley/API/MLS/GroupInfo.hs | 12 +- .../src/Galley/API/MLS/IncomingMessage.hs | 131 ++ services/galley/src/Galley/API/MLS/Message.hs | 1246 ++--------------- .../galley/src/Galley/API/MLS/Propagate.hs | 9 +- .../galley/src/Galley/API/MLS/Proposal.hs | 295 ++++ services/galley/src/Galley/API/MLS/Removal.hs | 66 +- .../src/Galley/API/MLS/SubConversation.hs | 25 +- services/galley/src/Galley/API/MLS/Types.hs | 91 +- services/galley/src/Galley/API/MLS/Util.hs | 22 +- services/galley/src/Galley/API/MLS/Welcome.hs | 83 +- services/galley/src/Galley/API/Public/MLS.hs | 4 +- services/galley/src/Galley/API/Update.hs | 16 +- .../src/Galley/Cassandra/Conversation.hs | 39 +- .../src/Galley/Cassandra/Conversation/MLS.hs | 13 +- .../Galley/Cassandra/Conversation/Members.hs | 23 +- .../galley/src/Galley/Cassandra/Instances.hs | 12 +- .../galley/src/Galley/Cassandra/Queries.hs | 38 +- .../src/Galley/Cassandra/SubConversation.hs | 48 +- .../galley/src/Galley/Effects/BrigAccess.hs | 14 +- .../src/Galley/Effects/ConversationStore.hs | 17 +- .../galley/src/Galley/Effects/MemberStore.hs | 9 +- .../Galley/Effects/SubConversationStore.hs | 9 +- services/galley/src/Galley/Intra/Client.hs | 77 +- services/galley/src/Galley/Intra/Effects.hs | 14 - services/galley/src/Galley/Keys.hs | 1 + services/galley/test/integration.hs | 1 + services/galley/test/integration/API/MLS.hs | 549 +++----- .../galley/test/integration/API/MLS/Util.hs | 291 ++-- services/galley/test/integration/API/Util.hs | 62 +- .../test/integration/{Main.hs => Run.hs} | 2 +- services/galley/test/unit.hs | 1 + services/galley/test/unit/{Main.hs => Run.hs} | 2 +- .../integration-test/conf/nginz/README.md | 7 + .../conf/nginz/integration-ca-key.pem | 50 +- .../conf/nginz/integration-ca.pem | 34 +- .../conf/nginz/integration-leaf-key.pem | 50 +- .../conf/nginz/integration-leaf.pem | 34 +- 126 files changed, 4563 insertions(+), 5063 deletions(-) create mode 100644 changelog.d/1-api-changes/mls-upgrade create mode 100644 changelog.d/5-internal/key-package-mapping create mode 100755 hack/bin/cassandra_dump_schema create mode 100644 libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs create mode 100644 libs/wire-api/src/Wire/API/MLS/Capabilities.hs create mode 100644 libs/wire-api/src/Wire/API/MLS/GroupInfo.hs delete mode 100644 libs/wire-api/src/Wire/API/MLS/GroupInfoBundle.hs create mode 100644 libs/wire-api/src/Wire/API/MLS/HPKEPublicKey.hs create mode 100644 libs/wire-api/src/Wire/API/MLS/LeafNode.hs create mode 100644 libs/wire-api/src/Wire/API/MLS/Lifetime.hs create mode 100644 libs/wire-api/src/Wire/API/MLS/ProposalTag.hs create mode 100644 libs/wire-api/src/Wire/API/MLS/ProtocolVersion.hs delete mode 100644 libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs create mode 100644 libs/wire-api/src/Wire/API/MLS/Validation.hs create mode 100644 libs/wire-api/test/golden.hs rename libs/wire-api/test/golden/{Main.hs => Test/Wire/API/Golden/Run.hs} (96%) delete mode 100644 libs/wire-api/test/resources/key_package1.mls create mode 100644 libs/wire-api/test/unit.hs rename libs/wire-api/test/unit/{Main.hs => Test/Wire/API/Run.hs} (98%) create mode 100644 services/brig/schema/main.hs rename services/brig/schema/src/{Main.hs => Run.hs} (99%) create mode 100644 services/brig/test/integration.hs rename services/brig/test/integration/{Main.hs => Run.hs} (99%) create mode 100644 services/brig/test/unit.hs create mode 100644 services/brig/test/unit/Run.hs create mode 100644 services/galley/migrate-data/main.hs rename services/galley/migrate-data/src/{Main.hs => Run.hs} (98%) create mode 100644 services/galley/schema/main.hs rename services/galley/schema/src/{Main.hs => Run.hs} (98%) rename services/galley/{src/Galley/API/MLS/KeyPackage.hs => schema/src/V82_MLSDraft17.hs} (59%) create mode 100644 services/galley/src/Galley/API/MLS/Commit.hs create mode 100644 services/galley/src/Galley/API/MLS/Commit/Core.hs create mode 100644 services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs create mode 100644 services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs create mode 100644 services/galley/src/Galley/API/MLS/IncomingMessage.hs create mode 100644 services/galley/src/Galley/API/MLS/Proposal.hs create mode 100644 services/galley/test/integration.hs rename services/galley/test/integration/{Main.hs => Run.hs} (99%) create mode 100644 services/galley/test/unit.hs rename services/galley/test/unit/{Main.hs => Run.hs} (99%) create mode 100644 services/nginz/integration-test/conf/nginz/README.md diff --git a/Makefile b/Makefile index dd490691003..8e7bb9c56c1 100644 --- a/Makefile +++ b/Makefile @@ -225,11 +225,7 @@ git-add-cassandra-schema: db-migrate git-add-cassandra-schema-impl .PHONY: git-add-cassandra-schema-impl git-add-cassandra-schema-impl: - $(eval CASSANDRA_CONTAINER := $(shell docker ps | grep '/cassandra:' | perl -ne '/^(\S+)\s/ && print $$1')) - ( echo '-- automatically generated with `make git-add-cassandra-schema`'; \ - docker exec -i $(CASSANDRA_CONTAINER) /usr/bin/cqlsh -e "DESCRIBE schema;" ) \ - | sed "s/CREATE TABLE galley_test.member_client/-- NOTE: this table is unused. It was replaced by mls_group_member_client\nCREATE TABLE galley_test.member_client/g" \ - > ./cassandra-schema.cql + ./hack/bin/cassandra_dump_schema > ./cassandra-schema.cql git add ./cassandra-schema.cql .PHONY: cqlsh diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 3bc45633bca..6e8b8ea6921 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -444,6 +444,8 @@ CREATE TABLE galley_test.mls_group_member_client ( user uuid, client text, key_package_ref blob, + leaf_node_index int, + removal_pending boolean, PRIMARY KEY (group_id, user_domain, user, client) ) WITH CLUSTERING ORDER BY (user_domain ASC, user ASC, client ASC) AND bloom_filter_fp_chance = 0.01 diff --git a/changelog.d/1-api-changes/mls-upgrade b/changelog.d/1-api-changes/mls-upgrade new file mode 100644 index 00000000000..de9bd3f4d81 --- /dev/null +++ b/changelog.d/1-api-changes/mls-upgrade @@ -0,0 +1,7 @@ +Switch to MLS draft 20. The following endpoints are affected by the change: + + - All endpoints with `message/mls` content type now expect and return draft-20 MLS structures. + - `POST /conversations` does not require `creator_client` anymore. + - `POST /mls/commit-bundles` now expects a "stream" of MLS messages, i.e. a sequence of TLS-serialised messages, one after the other, in any order. Its protobuf interface has been removed. + - `POST /mls/welcome` has been removed. Welcome messages can now only be sent as part of a commit bundle. + - `POST /mls/message` does not accept commit messages anymore. All commit messages must be sent as part of a commit bundle. diff --git a/changelog.d/5-internal/key-package-mapping b/changelog.d/5-internal/key-package-mapping new file mode 100644 index 00000000000..e861208c19d --- /dev/null +++ b/changelog.d/5-internal/key-package-mapping @@ -0,0 +1 @@ +Brig does not perform key package ref mapping anymore. Claimed key packages are simply removed from the `mls_key_packages` table. The `mls_key_package_refs` table is now unused, and will be removed in the future. diff --git a/hack/bin/cassandra_dump_schema b/hack/bin/cassandra_dump_schema new file mode 100755 index 00000000000..624e4a0a180 --- /dev/null +++ b/hack/bin/cassandra_dump_schema @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +import subprocess +from subprocess import PIPE +from itertools import zip_longest +import re + +def run_cqlsh(container, expr): + p = subprocess.run(["docker", "exec", "-i", container, '/usr/bin/cqlsh', '-e', expr], stdout=PIPE, check=True).stdout.decode('utf8').strip() + return p + +def transpose(a): + return [x for col in zip_longest(*a, fillvalue='') for x in col] + +def main(): + container = subprocess.run(["docker", "ps", "--filter=name=cassandra", "--format={{.ID}}"], stdout=PIPE, check=True).stdout.decode('utf8').rstrip() + s = run_cqlsh(container, 'DESCRIBE keyspaces;') + + ks = [] + for line in s.splitlines(): + ks.append(re.split('\s+', line)) + + keyspaces = transpose(ks) + print("-- automatically generated with `make git-add-cassandra-schema`\n") + for keyspace in keyspaces: + if keyspace.endswith('_test'): + s = run_cqlsh(container, f'DESCRIBE keyspace {keyspace}') + print(s.replace('CREATE TABLE galley_test.member_client','-- NOTE: this table is unused. It was replaced by mls_group_member_client\nCREATE TABLE galley_test.member_client')) + print() + +if __name__ == '__main__': + main() diff --git a/hack/python/wire/mlscli.py b/hack/python/wire/mlscli.py index 99eca439d5b..be53f849f1f 100644 --- a/hack/python/wire/mlscli.py +++ b/hack/python/wire/mlscli.py @@ -189,7 +189,7 @@ def add_member(state, kpfiles): "", "--welcome-out", welcome_file, - "--group-state-out", + "--group-info-out", pgs_file, "--group-out", "", diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs index f9c36367bd0..d6fc8ee3cc7 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs @@ -27,7 +27,7 @@ import Test.QuickCheck (Arbitrary) import Wire.API.Federation.API.Common import Wire.API.Federation.Endpoint import Wire.API.Federation.Version -import Wire.API.MLS.Credential +import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage import Wire.API.User (UserProfile) import Wire.API.User.Client 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 319a4470c8e..06d3217cbdc 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 @@ -445,8 +445,11 @@ data ConversationUpdateResponse via (CustomEncoded ConversationUpdateResponse) -- | A wrapper around a raw welcome message -newtype MLSWelcomeRequest = MLSWelcomeRequest - { unMLSWelcomeRequest :: Base64ByteString +data MLSWelcomeRequest = MLSWelcomeRequest + { -- | A serialised welcome message. + welcomeMessage :: Base64ByteString, + -- | Recipients local to the target backend. + recipients :: [(UserId, ClientId)] } deriving stock (Eq, Generic, Show) deriving (Arbitrary) via (GenericUniform MLSWelcomeRequest) diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 8981908490a..59bfd696721 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -247,6 +247,7 @@ mkDerivation { process proto-lens QuickCheck + random saml2-web-sso schema-profunctor servant diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index b06efe03004..b412fa2cdd0 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -72,7 +72,7 @@ data GalleyError MLSNotEnabled | MLSNonEmptyMemberList | MLSDuplicatePublicKey - | MLSKeyPackageRefNotFound + | MLSInvalidLeafNodeIndex | MLSUnsupportedMessage | MLSProposalNotFound | MLSUnsupportedProposal @@ -85,7 +85,6 @@ data GalleyError | MLSClientSenderUserMismatch | MLSWelcomeMismatch | MLSMissingGroupInfo - | MLSMissingSenderClient | MLSUnexpectedSenderClient | MLSSubConvUnsupportedConvType | MLSSubConvClientNotInParent @@ -201,7 +200,7 @@ type instance MapError 'MLSNonEmptyMemberList = 'StaticError 400 "non-empty-memb type instance MapError 'MLSDuplicatePublicKey = 'StaticError 400 "mls-duplicate-public-key" "MLS public key for the given signature scheme already exists" -type instance MapError 'MLSKeyPackageRefNotFound = 'StaticError 404 "mls-key-package-ref-not-found" "A referenced key package could not be mapped to a known client" +type instance MapError 'MLSInvalidLeafNodeIndex = 'StaticError 400 "mls-invalid-leaf-node-index" "A referenced leaf node index points to a blank or non-existing node" type instance MapError 'MLSUnsupportedMessage = 'StaticError 422 "mls-unsupported-message" "Attempted to send a message with an unsupported combination of content type and wire format" @@ -227,8 +226,6 @@ type instance MapError 'MLSWelcomeMismatch = 'StaticError 400 "mls-welcome-misma type instance MapError 'MLSMissingGroupInfo = 'StaticError 404 "mls-missing-group-info" "The conversation has no group information" -type instance MapError 'MLSMissingSenderClient = 'StaticError 403 "mls-missing-sender-client" "The client has to refresh their access token and provide their client ID" - type instance MapError 'MLSSubConvUnsupportedConvType = 'StaticError 403 "mls-subconv-unsupported-convtype" "MLS subconversations are only supported for regular conversations" type instance MapError 'MLSSubConvClientNotInParent = 'StaticError 403 "mls-subconv-join-parent-missing" "MLS client cannot join the subconversation because it is not member of the parent conversation" diff --git a/libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs b/libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs new file mode 100644 index 00000000000..394a18ede1a --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs @@ -0,0 +1,111 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.MLS.AuthenticatedContent + ( AuthenticatedContent (..), + TaggedSender (..), + authContentRef, + publicMessageRef, + mkSignedPublicMessage, + ) +where + +import Crypto.PubKey.Ed25519 +import Imports +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Context +import Wire.API.MLS.Epoch +import Wire.API.MLS.Group +import Wire.API.MLS.LeafNode +import Wire.API.MLS.Message +import Wire.API.MLS.Proposal +import Wire.API.MLS.ProtocolVersion +import Wire.API.MLS.Serialisation + +-- | Needed to compute proposal refs. +-- https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6-7 +data AuthenticatedContent = AuthenticatedContent + { wireFormat :: WireFormatTag, + content :: RawMLS FramedContent, + authData :: RawMLS FramedContentAuthData + } + deriving (Eq, Show) + +instance SerialiseMLS AuthenticatedContent where + serialiseMLS ac = do + serialiseMLS ac.wireFormat + serialiseMLS ac.content + serialiseMLS ac.authData + +msgAuthContent :: PublicMessage -> AuthenticatedContent +msgAuthContent msg = + AuthenticatedContent + { wireFormat = WireFormatPublicTag, + content = msg.content, + authData = msg.authData + } + +-- | Compute the proposal ref given a ciphersuite and the raw proposal data. +authContentRef :: CipherSuiteTag -> AuthenticatedContent -> ProposalRef +authContentRef cs = ProposalRef . csHash cs proposalContext . mkRawMLS + +publicMessageRef :: CipherSuiteTag -> PublicMessage -> ProposalRef +publicMessageRef cs = authContentRef cs . msgAuthContent + +-- | Sender, plus with a membership tag in the case of a member sender. +data TaggedSender + = TaggedSenderMember LeafIndex ByteString + | TaggedSenderExternal Word32 + | TaggedSenderNewMemberProposal + | TaggedSenderNewMemberCommit + +taggedSenderToSender :: TaggedSender -> Sender +taggedSenderToSender (TaggedSenderMember i _) = SenderMember i +taggedSenderToSender (TaggedSenderExternal n) = SenderExternal n +taggedSenderToSender TaggedSenderNewMemberProposal = SenderNewMemberProposal +taggedSenderToSender TaggedSenderNewMemberCommit = SenderNewMemberCommit + +taggedSenderMembershipTag :: TaggedSender -> Maybe ByteString +taggedSenderMembershipTag (TaggedSenderMember _ t) = Just t +taggedSenderMembershipTag _ = Nothing + +-- | Craft a message with the backend itself as a sender. Return the message and its ref. +mkSignedPublicMessage :: + SecretKey -> PublicKey -> GroupId -> Epoch -> TaggedSender -> FramedContentData -> PublicMessage +mkSignedPublicMessage priv pub gid epoch sender payload = + let framedContent = + mkRawMLS + FramedContent + { groupId = gid, + epoch = epoch, + sender = taggedSenderToSender sender, + content = payload, + authenticatedData = mempty + } + tbs = + FramedContentTBS + { protocolVersion = defaultProtocolVersion, + wireFormat = WireFormatPublicTag, + content = framedContent, + groupContext = Nothing + } + sig = signWithLabel "FramedContentTBS" priv pub (mkRawMLS tbs) + in PublicMessage + { content = framedContent, + authData = mkRawMLS (FramedContentAuthData sig Nothing), + membershipTag = taggedSenderMembershipTag sender + } diff --git a/libs/wire-api/src/Wire/API/MLS/Capabilities.hs b/libs/wire-api/src/Wire/API/MLS/Capabilities.hs new file mode 100644 index 00000000000..64386ef72e3 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/Capabilities.hs @@ -0,0 +1,55 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.MLS.Capabilities where + +import Imports +import Test.QuickCheck +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Credential +import Wire.API.MLS.ProposalTag +import Wire.API.MLS.ProtocolVersion +import Wire.API.MLS.Serialisation +import Wire.Arbitrary + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.2-2 +data Capabilities = Capabilities + { versions :: [ProtocolVersion], + ciphersuites :: [CipherSuite], + extensions :: [Word16], + proposals :: [ProposalTag], + credentials :: [CredentialTag] + } + deriving (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform Capabilities) + +instance ParseMLS Capabilities where + parseMLS = + Capabilities + <$> parseMLSVector @VarInt parseMLS + <*> parseMLSVector @VarInt parseMLS + <*> parseMLSVector @VarInt parseMLS + <*> parseMLSVector @VarInt parseMLS + <*> parseMLSVector @VarInt parseMLS + +instance SerialiseMLS Capabilities where + serialiseMLS caps = do + serialiseMLSVector @VarInt serialiseMLS caps.versions + serialiseMLSVector @VarInt serialiseMLS caps.ciphersuites + serialiseMLSVector @VarInt serialiseMLS caps.extensions + serialiseMLSVector @VarInt serialiseMLS caps.proposals + serialiseMLSVector @VarInt serialiseMLS caps.credentials diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index aaf42cd5af6..c4fc0376486 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -17,21 +17,49 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.MLS.CipherSuite where +module Wire.API.MLS.CipherSuite + ( -- * MLS ciphersuites + CipherSuite (..), + CipherSuiteTag (..), + cipherSuiteTag, + tagCipherSuite, + -- * MLS signature schemes + SignatureScheme (..), + SignatureSchemeTag (..), + signatureScheme, + signatureSchemeName, + signatureSchemeTag, + csSignatureScheme, + + -- * Utilities + csHash, + csVerifySignatureWithLabel, + csVerifySignature, + signWithLabel, + ) +where + +import Cassandra.CQL +import Control.Error (note) import Control.Lens ((?~)) import Crypto.Error +import Crypto.Hash (hashWith) import Crypto.Hash.Algorithms -import qualified Crypto.KDF.HKDF as HKDF import qualified Crypto.PubKey.Ed25519 as Ed25519 -import Data.Aeson (parseJSON, toJSON) +import qualified Data.Aeson as Aeson +import Data.Aeson.Types (FromJSON (..), FromJSONKey (..), ToJSON (..), ToJSONKey (..)) +import qualified Data.Aeson.Types as Aeson +import Data.ByteArray hiding (index) +import qualified Data.ByteArray as BA import Data.Proxy import Data.Schema import qualified Data.Swagger as S import qualified Data.Swagger.Internal.Schema as S +import qualified Data.Text as T import Data.Word import Imports -import Wire.API.MLS.Credential +import Servant (FromHttpApiData (parseQueryParam)) import Wire.API.MLS.Serialisation import Wire.Arbitrary @@ -79,16 +107,117 @@ cipherSuiteTag (CipherSuite n) = case n of tagCipherSuite :: CipherSuiteTag -> CipherSuite tagCipherSuite MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = CipherSuite 1 -csHash :: CipherSuiteTag -> ByteString -> ByteString -> ByteString +csHash :: CipherSuiteTag -> ByteString -> RawMLS a -> ByteString csHash MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 ctx value = - HKDF.expand (HKDF.extract @SHA256 (mempty :: ByteString) value) ctx 16 + convert . hashWith SHA256 . encodeMLS' $ RefHashInput ctx value -csVerifySignature :: CipherSuiteTag -> ByteString -> ByteString -> ByteString -> Bool +csVerifySignature :: CipherSuiteTag -> ByteString -> RawMLS a -> ByteString -> Bool csVerifySignature MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 pub x sig = fromMaybe False . maybeCryptoError $ do pub' <- Ed25519.publicKey pub sig' <- Ed25519.signature sig - pure $ Ed25519.verify pub' x sig' + pure $ Ed25519.verify pub' x.raw sig' + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-5.2-5 +type RefHashInput = SignContent + +pattern RefHashInput :: ByteString -> RawMLS a -> RefHashInput a +pattern RefHashInput label content = SignContent label content + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-5.1.2-6 +data SignContent a = SignContent + { sigLabel :: ByteString, + content :: RawMLS a + } + +instance SerialiseMLS (SignContent a) where + serialiseMLS c = do + serialiseMLSBytes @VarInt c.sigLabel + serialiseMLSBytes @VarInt c.content.raw + +mkSignContent :: ByteString -> RawMLS a -> SignContent a +mkSignContent sigLabel content = + SignContent + { sigLabel = "MLS 1.0 " <> sigLabel, + content = content + } + +csVerifySignatureWithLabel :: + CipherSuiteTag -> + ByteString -> + ByteString -> + RawMLS a -> + ByteString -> + Bool +csVerifySignatureWithLabel cs pub label x sig = + csVerifySignature cs pub (mkRawMLS (mkSignContent label x)) sig + +-- FUTUREWORK: generalise to arbitrary ciphersuites +signWithLabel :: ByteString -> Ed25519.SecretKey -> Ed25519.PublicKey -> RawMLS a -> ByteString +signWithLabel sigLabel priv pub x = BA.convert $ Ed25519.sign priv pub (encodeMLS' (mkSignContent sigLabel x)) csSignatureScheme :: CipherSuiteTag -> SignatureSchemeTag csSignatureScheme MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = Ed25519 + +-- | A TLS signature scheme. +-- +-- See . +newtype SignatureScheme = SignatureScheme {unSignatureScheme :: Word16} + deriving stock (Eq, Show) + deriving newtype (ParseMLS, Arbitrary) + +signatureScheme :: SignatureSchemeTag -> SignatureScheme +signatureScheme = SignatureScheme . signatureSchemeNumber + +data SignatureSchemeTag = Ed25519 + deriving stock (Bounded, Enum, Eq, Ord, Show, Generic) + deriving (Arbitrary) via GenericUniform SignatureSchemeTag + +instance Cql SignatureSchemeTag where + ctype = Tagged TextColumn + toCql = CqlText . signatureSchemeName + fromCql (CqlText name) = + note ("Unexpected signature scheme: " <> T.unpack name) $ + signatureSchemeFromName name + fromCql _ = Left "SignatureScheme: Text expected" + +signatureSchemeNumber :: SignatureSchemeTag -> Word16 +signatureSchemeNumber Ed25519 = 0x807 + +signatureSchemeName :: SignatureSchemeTag -> Text +signatureSchemeName Ed25519 = "ed25519" + +signatureSchemeTag :: SignatureScheme -> Maybe SignatureSchemeTag +signatureSchemeTag (SignatureScheme n) = getAlt $ + flip foldMap [minBound .. maxBound] $ \s -> + guard (signatureSchemeNumber s == n) $> s + +signatureSchemeFromName :: Text -> Maybe SignatureSchemeTag +signatureSchemeFromName name = getAlt $ + flip foldMap [minBound .. maxBound] $ \s -> + guard (signatureSchemeName s == name) $> s + +parseSignatureScheme :: MonadFail f => Text -> f SignatureSchemeTag +parseSignatureScheme name = + maybe + (fail ("Unsupported signature scheme " <> T.unpack name)) + pure + (signatureSchemeFromName name) + +instance FromJSON SignatureSchemeTag where + parseJSON = Aeson.withText "SignatureScheme" parseSignatureScheme + +instance FromJSONKey SignatureSchemeTag where + fromJSONKey = Aeson.FromJSONKeyTextParser parseSignatureScheme + +instance S.ToParamSchema SignatureSchemeTag where + toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + +instance FromHttpApiData SignatureSchemeTag where + parseQueryParam = note "Unknown signature scheme" . signatureSchemeFromName + +instance ToJSON SignatureSchemeTag where + toJSON = Aeson.String . signatureSchemeName + +instance ToJSONKey SignatureSchemeTag where + toJSONKey = Aeson.toJSONKeyText signatureSchemeName diff --git a/libs/wire-api/src/Wire/API/MLS/Commit.hs b/libs/wire-api/src/Wire/API/MLS/Commit.hs index 8f1a17c8ce6..81223db5504 100644 --- a/libs/wire-api/src/Wire/API/MLS/Commit.hs +++ b/libs/wire-api/src/Wire/API/MLS/Commit.hs @@ -18,49 +18,74 @@ module Wire.API.MLS.Commit where import Imports -import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.Arbitrary +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-12.4-3 data Commit = Commit - { cProposals :: [ProposalOrRef], - cPath :: Maybe UpdatePath + { proposals :: [ProposalOrRef], + path :: Maybe UpdatePath } - deriving (Eq, Show) + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform Commit) instance ParseMLS Commit where - parseMLS = Commit <$> parseMLSVector @Word32 parseMLS <*> parseMLSOptional parseMLS + parseMLS = + Commit + <$> parseMLSVector @VarInt parseMLS + <*> parseMLSOptional parseMLS + +instance SerialiseMLS Commit where + serialiseMLS c = do + serialiseMLSVector @VarInt serialiseMLS c.proposals + serialiseMLSOptional serialiseMLS c.path +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.6-2 data UpdatePath = UpdatePath - { upLeaf :: RawMLS KeyPackage, - upNodes :: [UpdatePathNode] + { leaf :: RawMLS LeafNode, + nodes :: [UpdatePathNode] } - deriving (Eq, Show) + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform UpdatePath) instance ParseMLS UpdatePath where - parseMLS = UpdatePath <$> parseMLS <*> parseMLSVector @Word32 parseMLS + parseMLS = UpdatePath <$> parseMLS <*> parseMLSVector @VarInt parseMLS +instance SerialiseMLS UpdatePath where + serialiseMLS up = do + serialiseMLS up.leaf + serialiseMLSVector @VarInt serialiseMLS up.nodes + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.6-2 data UpdatePathNode = UpdatePathNode - { upnPublicKey :: ByteString, - upnSecret :: [HPKECiphertext] + { publicKey :: ByteString, + secret :: [HPKECiphertext] } - deriving (Eq, Show) + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform UpdatePathNode) instance ParseMLS UpdatePathNode where - parseMLS = UpdatePathNode <$> parseMLSBytes @Word16 <*> parseMLSVector @Word32 parseMLS + parseMLS = UpdatePathNode <$> parseMLSBytes @VarInt <*> parseMLSVector @VarInt parseMLS + +instance SerialiseMLS UpdatePathNode where + serialiseMLS upn = do + serialiseMLSBytes @VarInt upn.publicKey + serialiseMLSVector @VarInt serialiseMLS upn.secret +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.6-2 data HPKECiphertext = HPKECiphertext - { hcOutput :: ByteString, - hcCiphertext :: ByteString + { output :: ByteString, + ciphertext :: ByteString } deriving (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform HPKECiphertext) instance ParseMLS HPKECiphertext where - parseMLS = HPKECiphertext <$> parseMLSBytes @Word16 <*> parseMLSBytes @Word16 + parseMLS = HPKECiphertext <$> parseMLSBytes @VarInt <*> parseMLSBytes @VarInt instance SerialiseMLS HPKECiphertext where serialiseMLS (HPKECiphertext out ct) = do - serialiseMLSBytes @Word16 out - serialiseMLSBytes @Word16 ct + serialiseMLSBytes @VarInt out + serialiseMLSBytes @VarInt ct diff --git a/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs b/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs index e04902d9691..1ca590e04ee 100644 --- a/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs +++ b/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs @@ -15,65 +15,82 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.MLS.CommitBundle where +module Wire.API.MLS.CommitBundle (CommitBundle (..)) where -import Control.Lens (view, (.~), (?~)) -import Data.Bifunctor (first) -import qualified Data.ByteString as BS -import Data.ProtoLens (decodeMessage, encodeMessage) -import qualified Data.ProtoLens (Message (defMessage)) +import Control.Applicative import qualified Data.Swagger as S import qualified Data.Text as T import Imports -import qualified Proto.Mls -import qualified Proto.Mls_Fields as Proto.Mls -import Wire.API.ConverProtoLens -import Wire.API.MLS.GroupInfoBundle +import Wire.API.MLS.GroupInfo import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.Welcome data CommitBundle = CommitBundle - { cbCommitMsg :: RawMLS (Message 'MLSPlainText), - cbWelcome :: Maybe (RawMLS Welcome), - cbGroupInfoBundle :: GroupInfoBundle + { commitMsg :: RawMLS Message, + welcome :: Maybe (RawMLS Welcome), + groupInfo :: RawMLS GroupInfo } - deriving (Eq, Show) + deriving stock (Eq, Show, Generic) -instance ConvertProtoLens Proto.Mls.CommitBundle CommitBundle where - fromProtolens protoBundle = protoLabel "CommitBundle" $ do - CommitBundle - <$> protoLabel "commit" (decodeMLS' (view Proto.Mls.commit protoBundle)) - <*> protoLabel - "welcome" - ( let bs = view Proto.Mls.welcome protoBundle - in if BS.length bs == 0 - then pure Nothing - else Just <$> decodeMLS' bs - ) - <*> protoLabel "group_info_bundle" (fromProtolens (view Proto.Mls.groupInfoBundle protoBundle)) - toProtolens bundle = - let commitData = rmRaw (cbCommitMsg bundle) - welcomeData = foldMap rmRaw (cbWelcome bundle) - groupInfoData = toProtolens (cbGroupInfoBundle bundle) - in ( Data.ProtoLens.defMessage - & Proto.Mls.commit .~ commitData - & Proto.Mls.welcome .~ welcomeData - & Proto.Mls.groupInfoBundle .~ groupInfoData - ) +data CommitBundleF f = CommitBundleF + { commitMsg :: f (RawMLS Message), + welcome :: f (RawMLS Welcome), + groupInfo :: f (RawMLS GroupInfo) + } -instance S.ToSchema CommitBundle where - declareNamedSchema _ = - pure $ - S.NamedSchema (Just "CommitBundle") $ - mempty - & S.description - ?~ "A protobuf-serialized object. See wireapp/generic-message-proto for the definition." +deriving instance Show (CommitBundleF []) + +instance Alternative f => Semigroup (CommitBundleF f) where + cb1 <> cb2 = + CommitBundleF + (cb1.commitMsg <|> cb2.commitMsg) + (cb1.welcome <|> cb2.welcome) + (cb1.groupInfo <|> cb2.groupInfo) + +instance Alternative f => Monoid (CommitBundleF f) where + mempty = CommitBundleF empty empty empty + +checkCommitBundleF :: CommitBundleF [] -> Either Text CommitBundle +checkCommitBundleF cb = + CommitBundle + <$> check "commit" cb.commitMsg + <*> checkOpt "welcome" cb.welcome + <*> check "group info" cb.groupInfo + where + check :: Text -> [a] -> Either Text a + check _ [x] = pure x + check name [] = Left ("Missing " <> name) + check name _ = Left ("Redundant occurrence of " <> name) -deserializeCommitBundle :: ByteString -> Either Text CommitBundle -deserializeCommitBundle b = do - protoCommitBundle :: Proto.Mls.CommitBundle <- first (("Parsing protobuf failed: " <>) . T.pack) (decodeMessage b) - first ("Converting from protobuf failed: " <>) (fromProtolens protoCommitBundle) + checkOpt :: Text -> [a] -> Either Text (Maybe a) + checkOpt _ [] = pure Nothing + checkOpt _ [x] = pure (Just x) + checkOpt name _ = Left ("Redundant occurrence of " <> name) -serializeCommitBundle :: CommitBundle -> ByteString -serializeCommitBundle = encodeMessage . (toProtolens @Proto.Mls.CommitBundle @CommitBundle) +findMessageInStream :: Alternative f => RawMLS Message -> Either Text (CommitBundleF f) +findMessageInStream msg = case msg.value.content of + MessagePublic mp -> case mp.content.value.content of + FramedContentCommit _ -> pure (CommitBundleF (pure msg) empty empty) + _ -> Left "unexpected public message" + MessageWelcome w -> pure (CommitBundleF empty (pure w) empty) + MessageGroupInfo gi -> pure (CommitBundleF empty empty (pure gi)) + _ -> Left "unexpected message type" + +findMessagesInStream :: Alternative f => [RawMLS Message] -> Either Text (CommitBundleF f) +findMessagesInStream = getAp . foldMap (Ap . findMessageInStream) + +instance ParseMLS CommitBundle where + parseMLS = do + msgs <- parseMLSStream parseMLS + either (fail . T.unpack) pure $ + findMessagesInStream msgs >>= checkCommitBundleF + +instance SerialiseMLS CommitBundle where + serialiseMLS cb = do + serialiseMLS cb.commitMsg + traverse_ (serialiseMLS . mkMessage . MessageWelcome) cb.welcome + serialiseMLS $ mkMessage (MessageGroupInfo cb.groupInfo) + +instance S.ToSchema CommitBundle where + declareNamedSchema _ = pure (mlsSwagger "CommitBundle") diff --git a/libs/wire-api/src/Wire/API/MLS/Context.hs b/libs/wire-api/src/Wire/API/MLS/Context.hs index 661b7ce6322..4324b61d7ae 100644 --- a/libs/wire-api/src/Wire/API/MLS/Context.hs +++ b/libs/wire-api/src/Wire/API/MLS/Context.hs @@ -19,15 +19,6 @@ module Wire.API.MLS.Context where import Imports --- Warning: the "context" string here is different from the one mandated by --- the spec, but it is the one that happens to be used by openmls. Until --- openmls is patched and we switch to a fixed version, we will have to use --- the "wrong" string here as well. --- --- This is used when invoking 'csHash'. -context :: ByteString -context = "MLS 1.0 ref" - proposalContext, keyPackageContext :: ByteString -proposalContext = context -keyPackageContext = context +proposalContext = "MLS 1.0 Proposal Reference" +keyPackageContext = "MLS 1.0 KeyPackage Reference" diff --git a/libs/wire-api/src/Wire/API/MLS/Credential.hs b/libs/wire-api/src/Wire/API/MLS/Credential.hs index e695eba1d98..f614269b836 100644 --- a/libs/wire-api/src/Wire/API/MLS/Credential.hs +++ b/libs/wire-api/src/Wire/API/MLS/Credential.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -19,7 +17,6 @@ module Wire.API.MLS.Credential where -import Cassandra.CQL import Control.Error.Util import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), FromJSONKey (..), ToJSON (..), ToJSONKey (..)) @@ -30,13 +27,16 @@ import Data.Binary import Data.Binary.Get import Data.Binary.Parser import Data.Binary.Parser.Char8 +import Data.Binary.Put import Data.Domain import Data.Id import Data.Qualified import Data.Schema import qualified Data.Swagger as S import qualified Data.Text as T +import qualified Data.Text.Encoding as T import Data.UUID +import GHC.Records import Imports import Web.HttpApiData import Wire.API.MLS.Serialisation @@ -45,94 +45,39 @@ import Wire.Arbitrary -- | An MLS credential. -- -- Only the @BasicCredential@ type is supported. -data Credential = BasicCredential - { bcIdentity :: ByteString, - bcSignatureScheme :: SignatureScheme, - bcSignatureKey :: ByteString - } +-- https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-5.3-3 +data Credential = BasicCredential ByteString deriving stock (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform Credential -data CredentialTag = BasicCredentialTag - deriving stock (Enum, Bounded, Eq, Show) +data CredentialTag where + BasicCredentialTag :: CredentialTag + deriving stock (Enum, Bounded, Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform CredentialTag) instance ParseMLS CredentialTag where parseMLS = parseMLSEnum @Word16 "credential type" +instance SerialiseMLS CredentialTag where + serialiseMLS = serialiseMLSEnum @Word16 + instance ParseMLS Credential where parseMLS = parseMLS >>= \case BasicCredentialTag -> BasicCredential - <$> parseMLSBytes @Word16 - <*> parseMLS - <*> parseMLSBytes @Word16 + <$> parseMLSBytes @VarInt + +instance SerialiseMLS Credential where + serialiseMLS (BasicCredential i) = do + serialiseMLS BasicCredentialTag + serialiseMLSBytes @VarInt i credentialTag :: Credential -> CredentialTag credentialTag BasicCredential {} = BasicCredentialTag --- | A TLS signature scheme. --- --- See . -newtype SignatureScheme = SignatureScheme {unSignatureScheme :: Word16} - deriving stock (Eq, Show) - deriving newtype (ParseMLS, Arbitrary) - -signatureScheme :: SignatureSchemeTag -> SignatureScheme -signatureScheme = SignatureScheme . signatureSchemeNumber - -data SignatureSchemeTag = Ed25519 - deriving stock (Bounded, Enum, Eq, Ord, Show, Generic) - deriving (Arbitrary) via GenericUniform SignatureSchemeTag - -instance Cql SignatureSchemeTag where - ctype = Tagged TextColumn - toCql = CqlText . signatureSchemeName - fromCql (CqlText name) = - note ("Unexpected signature scheme: " <> T.unpack name) $ - signatureSchemeFromName name - fromCql _ = Left "SignatureScheme: Text expected" - -signatureSchemeNumber :: SignatureSchemeTag -> Word16 -signatureSchemeNumber Ed25519 = 0x807 - -signatureSchemeName :: SignatureSchemeTag -> Text -signatureSchemeName Ed25519 = "ed25519" - -signatureSchemeTag :: SignatureScheme -> Maybe SignatureSchemeTag -signatureSchemeTag (SignatureScheme n) = getAlt $ - flip foldMap [minBound .. maxBound] $ \s -> - guard (signatureSchemeNumber s == n) $> s - -signatureSchemeFromName :: Text -> Maybe SignatureSchemeTag -signatureSchemeFromName name = getAlt $ - flip foldMap [minBound .. maxBound] $ \s -> - guard (signatureSchemeName s == name) $> s - -parseSignatureScheme :: MonadFail f => Text -> f SignatureSchemeTag -parseSignatureScheme name = - maybe - (fail ("Unsupported signature scheme " <> T.unpack name)) - pure - (signatureSchemeFromName name) - -instance FromJSON SignatureSchemeTag where - parseJSON = Aeson.withText "SignatureScheme" parseSignatureScheme - -instance FromJSONKey SignatureSchemeTag where - fromJSONKey = Aeson.FromJSONKeyTextParser parseSignatureScheme - -instance S.ToParamSchema SignatureSchemeTag where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString - -instance FromHttpApiData SignatureSchemeTag where - parseQueryParam = note "Unknown signature scheme" . signatureSchemeFromName - -instance ToJSON SignatureSchemeTag where - toJSON = Aeson.String . signatureSchemeName - -instance ToJSONKey SignatureSchemeTag where - toJSONKey = Aeson.toJSONKeyText signatureSchemeName +instance HasField "identityData" Credential ByteString where + getField (BasicCredential i) = i data ClientIdentity = ClientIdentity { ciDomain :: Domain, @@ -141,6 +86,7 @@ data ClientIdentity = ClientIdentity } deriving stock (Eq, Ord, Generic) deriving (FromJSON, ToJSON, S.ToSchema) via Schema ClientIdentity + deriving (Arbitrary) via (GenericUniform ClientIdentity) instance Show ClientIdentity where show (ClientIdentity dom u c) = @@ -164,6 +110,17 @@ instance ToSchema ClientIdentity where <*> ciUser .= field "user_id" schema <*> ciClient .= field "client_id" schema +instance S.ToParamSchema ClientIdentity where + toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + +instance FromHttpApiData ClientIdentity where + parseHeader = decodeMLS' + parseUrlPiece = decodeMLS' . T.encodeUtf8 + +instance ToHttpApiData ClientIdentity where + toHeader = encodeMLS' + toUrlPiece = T.decodeUtf8 . encodeMLS' + instance ParseMLS ClientIdentity where parseMLS = do uid <- @@ -175,6 +132,14 @@ instance ParseMLS ClientIdentity where either fail pure . (mkDomain . T.pack) =<< many' anyChar pure $ ClientIdentity dom uid cid +instance SerialiseMLS ClientIdentity where + serialiseMLS cid = do + putByteString $ toASCIIBytes (toUUID (ciUser cid)) + putCharUtf8 ':' + putStringUtf8 $ T.unpack (client (ciClient cid)) + putCharUtf8 '@' + putStringUtf8 $ T.unpack (domainText (ciDomain cid)) + mkClientIdentity :: Qualified UserId -> ClientId -> ClientIdentity mkClientIdentity (Qualified uid domain) = ClientIdentity domain uid diff --git a/libs/wire-api/src/Wire/API/MLS/Extension.hs b/libs/wire-api/src/Wire/API/MLS/Extension.hs index 5093398adf9..eab027e7158 100644 --- a/libs/wire-api/src/Wire/API/MLS/Extension.hs +++ b/libs/wire-api/src/Wire/API/MLS/Extension.hs @@ -1,7 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE StandaloneKindSignatures #-} -{-# LANGUAGE TemplateHaskell #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -19,52 +15,14 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.MLS.Extension - ( -- * Extensions - Extension (..), - decodeExtension, - parseExtension, - ExtensionTag (..), - CapabilitiesExtensionTagSym0, - LifetimeExtensionTagSym0, - SExtensionTag (..), - SomeExtension (..), - Capabilities (..), - Lifetime (..), - - -- * Other types - Timestamp (..), - ProtocolVersion (..), - ProtocolVersionTag (..), - - -- * Utilities - pvTag, - tsPOSIX, - ) -where +module Wire.API.MLS.Extension where import Data.Binary -import Data.Kind -import Data.Singletons.TH -import Data.Time.Clock.POSIX import Imports -import Wire.API.MLS.CipherSuite import Wire.API.MLS.Serialisation import Wire.Arbitrary -newtype ProtocolVersion = ProtocolVersion {pvNumber :: Word8} - deriving newtype (Eq, Ord, Show, Binary, Arbitrary, ParseMLS, SerialiseMLS) - -data ProtocolVersionTag = ProtocolMLS10 | ProtocolMLSDraft11 - deriving stock (Bounded, Enum, Eq, Show, Generic) - deriving (Arbitrary) via GenericUniform ProtocolVersionTag - -pvTag :: ProtocolVersion -> Maybe ProtocolVersionTag -pvTag (ProtocolVersion v) = case v of - 1 -> pure ProtocolMLS10 - 200 -> pure ProtocolMLSDraft11 - _ -> Nothing - +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.2-2 data Extension = Extension { extType :: Word16, extData :: ByteString @@ -73,78 +31,9 @@ data Extension = Extension deriving (Arbitrary) via GenericUniform Extension instance ParseMLS Extension where - parseMLS = Extension <$> parseMLS <*> parseMLSBytes @Word32 + parseMLS = Extension <$> parseMLS <*> parseMLSBytes @VarInt instance SerialiseMLS Extension where serialiseMLS (Extension ty d) = do serialiseMLS ty - serialiseMLSBytes @Word32 d - -data ExtensionTag - = CapabilitiesExtensionTag - | LifetimeExtensionTag - deriving (Bounded, Enum) - -$(genSingletons [''ExtensionTag]) - -type family ExtensionType (t :: ExtensionTag) :: Type where - ExtensionType 'CapabilitiesExtensionTag = Capabilities - ExtensionType 'LifetimeExtensionTag = Lifetime - -parseExtension :: Sing t -> Get (ExtensionType t) -parseExtension SCapabilitiesExtensionTag = parseMLS -parseExtension SLifetimeExtensionTag = parseMLS - -data SomeExtension where - SomeExtension :: Sing t -> ExtensionType t -> SomeExtension - -instance Eq SomeExtension where - SomeExtension SCapabilitiesExtensionTag caps1 == SomeExtension SCapabilitiesExtensionTag caps2 = caps1 == caps2 - SomeExtension SLifetimeExtensionTag lt1 == SomeExtension SLifetimeExtensionTag lt2 = lt1 == lt2 - _ == _ = False - -instance Show SomeExtension where - show (SomeExtension SCapabilitiesExtensionTag caps) = show caps - show (SomeExtension SLifetimeExtensionTag lt) = show lt - -decodeExtension :: Extension -> Either Text (Maybe SomeExtension) -decodeExtension e = do - case toMLSEnum' (extType e) of - Left MLSEnumUnknown -> pure Nothing - Left MLSEnumInvalid -> Left "Invalid extension type" - Right t -> withSomeSing t $ \st -> - Just <$> decodeMLSWith' (SomeExtension st <$> parseExtension st) (extData e) - -data Capabilities = Capabilities - { capVersions :: [ProtocolVersion], - capCiphersuites :: [CipherSuite], - capExtensions :: [Word16], - capProposals :: [Word16] - } - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform Capabilities) - -instance ParseMLS Capabilities where - parseMLS = - Capabilities - <$> parseMLSVector @Word8 parseMLS - <*> parseMLSVector @Word8 parseMLS - <*> parseMLSVector @Word8 parseMLS - <*> parseMLSVector @Word8 parseMLS - --- | Seconds since the UNIX epoch. -newtype Timestamp = Timestamp {timestampSeconds :: Word64} - deriving newtype (Eq, Show, Arbitrary, ParseMLS) - -tsPOSIX :: Timestamp -> POSIXTime -tsPOSIX = fromIntegral . timestampSeconds - -data Lifetime = Lifetime - { ltNotBefore :: Timestamp, - ltNotAfter :: Timestamp - } - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via GenericUniform Lifetime - -instance ParseMLS Lifetime where - parseMLS = Lifetime <$> parseMLS <*> parseMLS + serialiseMLSBytes @VarInt d diff --git a/libs/wire-api/src/Wire/API/MLS/Group.hs b/libs/wire-api/src/Wire/API/MLS/Group.hs index c693ddd2a21..31105520009 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group.hs @@ -39,10 +39,10 @@ instance IsString GroupId where fromString = GroupId . fromString instance ParseMLS GroupId where - parseMLS = GroupId <$> parseMLSBytes @Word8 + parseMLS = GroupId <$> parseMLSBytes @VarInt instance SerialiseMLS GroupId where - serialiseMLS (GroupId gid) = serialiseMLSBytes @Word8 gid + serialiseMLS (GroupId gid) = serialiseMLSBytes @VarInt gid instance ToSchema GroupId where schema = diff --git a/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs b/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs new file mode 100644 index 00000000000..77cf2036627 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs @@ -0,0 +1,140 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.MLS.GroupInfo + ( GroupContext (..), + GroupInfo (..), + GroupInfoData (..), + ) +where + +import Data.Binary.Get +import Data.Binary.Put +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Swagger as S +import GHC.Records +import Imports +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Epoch +import Wire.API.MLS.Extension +import Wire.API.MLS.Group +import Wire.API.MLS.ProtocolVersion +import Wire.API.MLS.Serialisation +import Wire.Arbitrary + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-8.1-2 +data GroupContext = GroupContext + { protocolVersion :: ProtocolVersion, + cipherSuite :: CipherSuite, + groupId :: GroupId, + epoch :: Epoch, + treeHash :: ByteString, + confirmedTranscriptHash :: ByteString, + extensions :: [Extension] + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform GroupContext) + +instance ParseMLS GroupContext where + parseMLS = + GroupContext + <$> parseMLS + <*> parseMLS + <*> parseMLS + <*> parseMLS + <*> parseMLSBytes @VarInt + <*> parseMLSBytes @VarInt + <*> parseMLSVector @VarInt parseMLS + +instance SerialiseMLS GroupContext where + serialiseMLS gc = do + serialiseMLS gc.protocolVersion + serialiseMLS gc.cipherSuite + serialiseMLS gc.groupId + serialiseMLS gc.epoch + serialiseMLSBytes @VarInt gc.treeHash + serialiseMLSBytes @VarInt gc.confirmedTranscriptHash + serialiseMLSVector @VarInt serialiseMLS gc.extensions + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-12.4.3-7 +data GroupInfoTBS = GroupInfoTBS + { groupContext :: GroupContext, + extensions :: [Extension], + confirmationTag :: ByteString, + signer :: Word32 + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform GroupInfoTBS) + +instance ParseMLS GroupInfoTBS where + parseMLS = + GroupInfoTBS + <$> parseMLS + <*> parseMLSVector @VarInt parseMLS + <*> parseMLSBytes @VarInt + <*> parseMLS + +instance SerialiseMLS GroupInfoTBS where + serialiseMLS tbs = do + serialiseMLS tbs.groupContext + serialiseMLSVector @VarInt serialiseMLS tbs.extensions + serialiseMLSBytes @VarInt tbs.confirmationTag + serialiseMLS tbs.signer + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-12.4.3-2 +data GroupInfo = GroupInfo + { tbs :: GroupInfoTBS, + signature_ :: ByteString + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform GroupInfo) + +instance ParseMLS GroupInfo where + parseMLS = + GroupInfo + <$> parseMLS + <*> parseMLSBytes @VarInt + +instance SerialiseMLS GroupInfo where + serialiseMLS gi = do + serialiseMLS gi.tbs + serialiseMLSBytes @VarInt gi.signature_ + +instance HasField "groupContext" GroupInfo GroupContext where + getField = (.tbs.groupContext) + +instance HasField "extensions" GroupInfo [Extension] where + getField = (.tbs.extensions) + +instance HasField "confirmationTag" GroupInfo ByteString where + getField = (.tbs.confirmationTag) + +instance HasField "signer" GroupInfo Word32 where + getField = (.tbs.signer) + +newtype GroupInfoData = GroupInfoData {unGroupInfoData :: ByteString} + deriving stock (Eq, Ord, Show) + deriving newtype (Arbitrary) + +instance ParseMLS GroupInfoData where + parseMLS = GroupInfoData . LBS.toStrict <$> getRemainingLazyByteString + +instance SerialiseMLS GroupInfoData where + serialiseMLS (GroupInfoData bs) = putByteString bs + +instance S.ToSchema GroupInfoData where + declareNamedSchema _ = pure (mlsSwagger "GroupInfoData") diff --git a/libs/wire-api/src/Wire/API/MLS/GroupInfoBundle.hs b/libs/wire-api/src/Wire/API/MLS/GroupInfoBundle.hs deleted file mode 100644 index 93cc706e98c..00000000000 --- a/libs/wire-api/src/Wire/API/MLS/GroupInfoBundle.hs +++ /dev/null @@ -1,98 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 Wire.API.MLS.GroupInfoBundle where - -import Control.Lens (view, (.~)) -import Data.ProtoLens (Message (defMessage)) -import Imports -import qualified Proto.Mls -import qualified Proto.Mls_Fields as Proto.Mls -import Test.QuickCheck -import Wire.API.ConverProtoLens -import Wire.API.MLS.PublicGroupState -import Wire.API.MLS.Serialisation -import Wire.Arbitrary - -data GroupInfoType = GroupInfoTypePublicGroupState | UnencryptedGroupInfo | JweEncryptedGroupInfo - deriving stock (Eq, Show, Generic, Enum, Bounded) - deriving (Arbitrary) via (GenericUniform GroupInfoType) - -instance ConvertProtoLens Proto.Mls.GroupInfoType GroupInfoType where - fromProtolens Proto.Mls.PUBLIC_GROUP_STATE = pure GroupInfoTypePublicGroupState - fromProtolens Proto.Mls.GROUP_INFO = pure UnencryptedGroupInfo - fromProtolens Proto.Mls.GROUP_INFO_JWE = pure JweEncryptedGroupInfo - - toProtolens GroupInfoTypePublicGroupState = Proto.Mls.PUBLIC_GROUP_STATE - toProtolens UnencryptedGroupInfo = Proto.Mls.GROUP_INFO - toProtolens JweEncryptedGroupInfo = Proto.Mls.GROUP_INFO_JWE - -data RatchetTreeType = TreeFull | TreeDelta | TreeByRef - deriving stock (Eq, Show, Generic, Bounded, Enum) - deriving (Arbitrary) via (GenericUniform RatchetTreeType) - -instance ConvertProtoLens Proto.Mls.RatchetTreeType RatchetTreeType where - fromProtolens Proto.Mls.FULL = pure TreeFull - fromProtolens Proto.Mls.DELTA = pure TreeDelta - fromProtolens Proto.Mls.REFERENCE = pure TreeByRef - - toProtolens TreeFull = Proto.Mls.FULL - toProtolens TreeDelta = Proto.Mls.DELTA - toProtolens TreeByRef = Proto.Mls.REFERENCE - -data GroupInfoBundle = GroupInfoBundle - { gipGroupInfoType :: GroupInfoType, - gipRatchetTreeType :: RatchetTreeType, - gipGroupState :: RawMLS PublicGroupState - } - deriving stock (Eq, Show, Generic) - -instance ConvertProtoLens Proto.Mls.GroupInfoBundle GroupInfoBundle where - fromProtolens protoBundle = - protoLabel "GroupInfoBundle" $ - GroupInfoBundle - <$> protoLabel "field group_info_type" (fromProtolens (view Proto.Mls.groupInfoType protoBundle)) - <*> protoLabel "field ratchet_tree_type" (fromProtolens (view Proto.Mls.ratchetTreeType protoBundle)) - <*> protoLabel "field group_info" (decodeMLS' (view Proto.Mls.groupInfo protoBundle)) - toProtolens bundle = - let encryptionType = toProtolens (gipGroupInfoType bundle) - treeType = toProtolens (gipRatchetTreeType bundle) - in ( defMessage - & Proto.Mls.groupInfoType .~ encryptionType - & Proto.Mls.ratchetTreeType .~ treeType - & Proto.Mls.groupInfo .~ rmRaw (gipGroupState bundle) - ) - -instance Arbitrary GroupInfoBundle where - arbitrary = - GroupInfoBundle - <$> arbitrary - <*> arbitrary - <*> (mkRawMLS <$> arbitrary) - -instance ParseMLS GroupInfoBundle where - parseMLS = - GroupInfoBundle - <$> parseMLSEnum @Word8 "GroupInfoTypeEnum" - <*> parseMLSEnum @Word8 "RatchetTreeEnum" - <*> parseMLS - -instance SerialiseMLS GroupInfoBundle where - serialiseMLS (GroupInfoBundle e t pgs) = do - serialiseMLSEnum @Word8 e - serialiseMLSEnum @Word8 t - serialiseMLS pgs diff --git a/libs/wire-api/src/Wire/API/MLS/HPKEPublicKey.hs b/libs/wire-api/src/Wire/API/MLS/HPKEPublicKey.hs new file mode 100644 index 00000000000..3d0d947f083 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/HPKEPublicKey.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.MLS.HPKEPublicKey where + +import Imports +import Test.QuickCheck +import Wire.API.MLS.Serialisation + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-5.1.1-2 +newtype HPKEPublicKey = HPKEPublicKey {unHPKEPublicKey :: ByteString} + deriving (Show, Eq, Arbitrary) + +instance ParseMLS HPKEPublicKey where + parseMLS = HPKEPublicKey <$> parseMLSBytes @VarInt + +instance SerialiseMLS HPKEPublicKey where + serialiseMLS = serialiseMLSBytes @VarInt . unHPKEPublicKey diff --git a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs index 4d213c71b06..19e9490993b 100644 --- a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs +++ b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -24,17 +22,11 @@ module Wire.API.MLS.KeyPackage KeyPackageCount (..), KeyPackageData (..), KeyPackage (..), - kpProtocolVersion, - kpCipherSuite, - kpInitKey, - kpCredential, - kpExtensions, - kpIdentity, + keyPackageIdentity, kpRef, kpRef', KeyPackageTBS (..), KeyPackageRef (..), - KeyPackageUpdate (..), ) where @@ -42,16 +34,13 @@ import Cassandra.CQL hiding (Set) import Control.Applicative import Control.Lens hiding (set, (.=)) import Data.Aeson (FromJSON, ToJSON) -import Data.Binary -import Data.Binary.Get -import Data.Binary.Put -import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as LBS import Data.Id import Data.Json.Util import Data.Qualified import Data.Schema import qualified Data.Swagger as S +import GHC.Records import Imports import Test.QuickCheck import Web.HttpApiData @@ -59,18 +48,21 @@ import Wire.API.MLS.CipherSuite import Wire.API.MLS.Context import Wire.API.MLS.Credential import Wire.API.MLS.Extension +import Wire.API.MLS.HPKEPublicKey +import Wire.API.MLS.LeafNode +import Wire.API.MLS.ProtocolVersion import Wire.API.MLS.Serialisation import Wire.Arbitrary data KeyPackageUpload = KeyPackageUpload - {kpuKeyPackages :: [RawMLS KeyPackage]} + {keyPackages :: [RawMLS KeyPackage]} deriving (FromJSON, ToJSON, S.ToSchema) via Schema KeyPackageUpload instance ToSchema KeyPackageUpload where schema = object "KeyPackageUpload" $ KeyPackageUpload - <$> kpuKeyPackages .= field "key_packages" (array rawKeyPackageSchema) + <$> keyPackages .= field "key_packages" (array rawKeyPackageSchema) newtype KeyPackageData = KeyPackageData {kpData :: ByteString} deriving stock (Eq, Ord, Show) @@ -90,10 +82,10 @@ instance Cql KeyPackageData where fromCql _ = Left "Expected CqlBlob" data KeyPackageBundleEntry = KeyPackageBundleEntry - { kpbeUser :: Qualified UserId, - kpbeClient :: ClientId, - kpbeRef :: KeyPackageRef, - kpbeKeyPackage :: KeyPackageData + { user :: Qualified UserId, + client :: ClientId, + ref :: KeyPackageRef, + keyPackage :: KeyPackageData } deriving stock (Eq, Ord, Show) @@ -101,12 +93,12 @@ instance ToSchema KeyPackageBundleEntry where schema = object "KeyPackageBundleEntry" $ KeyPackageBundleEntry - <$> kpbeUser .= qualifiedObjectSchema "user" schema - <*> kpbeClient .= field "client" schema - <*> kpbeRef .= field "key_package_ref" schema - <*> kpbeKeyPackage .= field "key_package" schema + <$> (.user) .= qualifiedObjectSchema "user" schema + <*> (.client) .= field "client" schema + <*> (.ref) .= field "key_package_ref" schema + <*> (.keyPackage) .= field "key_package" schema -newtype KeyPackageBundle = KeyPackageBundle {kpbEntries :: Set KeyPackageBundleEntry} +newtype KeyPackageBundle = KeyPackageBundle {entries :: Set KeyPackageBundleEntry} deriving stock (Eq, Show) deriving (FromJSON, ToJSON, S.ToSchema) via Schema KeyPackageBundle @@ -114,7 +106,7 @@ instance ToSchema KeyPackageBundle where schema = object "KeyPackageBundle" $ KeyPackageBundle - <$> kpbEntries .= field "key_packages" (set schema) + <$> (.entries) .= field "key_packages" (set schema) newtype KeyPackageCount = KeyPackageCount {unKeyPackageCount :: Int} deriving newtype (Eq, Ord, Num, Show) @@ -129,18 +121,16 @@ newtype KeyPackageRef = KeyPackageRef {unKeyPackageRef :: ByteString} deriving stock (Eq, Ord, Show) deriving (FromHttpApiData, ToHttpApiData, S.ToParamSchema) via Base64ByteString deriving (ToJSON, FromJSON, S.ToSchema) via (Schema KeyPackageRef) - -instance Arbitrary KeyPackageRef where - arbitrary = KeyPackageRef . B.pack <$> vectorOf 16 arbitrary + deriving newtype (Arbitrary) instance ToSchema KeyPackageRef where schema = named "KeyPackageRef" $ unKeyPackageRef .= fmap KeyPackageRef base64Schema instance ParseMLS KeyPackageRef where - parseMLS = KeyPackageRef <$> getByteString 16 + parseMLS = KeyPackageRef <$> parseMLSBytes @VarInt instance SerialiseMLS KeyPackageRef where - serialiseMLS = putByteString . unKeyPackageRef + serialiseMLS = serialiseMLSBytes @VarInt . unKeyPackageRef instance Cql KeyPackageRef where ctype = Tagged BlobColumn @@ -153,6 +143,7 @@ kpRef :: CipherSuiteTag -> KeyPackageData -> KeyPackageRef kpRef cs = KeyPackageRef . csHash cs keyPackageContext + . flip RawMLS () . kpData -- | Compute ref of a key package. Return 'Nothing' if the key package cipher @@ -160,17 +151,18 @@ kpRef cs = kpRef' :: RawMLS KeyPackage -> Maybe KeyPackageRef kpRef' kp = kpRef - <$> cipherSuiteTag (kpCipherSuite (rmValue kp)) - <*> pure (KeyPackageData (rmRaw kp)) + <$> cipherSuiteTag (kp.value.cipherSuite) + <*> pure (KeyPackageData (raw kp)) -------------------------------------------------------------------------------- +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-10-6 data KeyPackageTBS = KeyPackageTBS - { kpuProtocolVersion :: ProtocolVersion, - kpuCipherSuite :: CipherSuite, - kpuInitKey :: ByteString, - kpuCredential :: Credential, - kpuExtensions :: [Extension] + { protocolVersion :: ProtocolVersion, + cipherSuite :: CipherSuite, + initKey :: HPKEPublicKey, + leafNode :: LeafNode, + extensions :: [Extension] } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform KeyPackageTBS @@ -180,36 +172,46 @@ instance ParseMLS KeyPackageTBS where KeyPackageTBS <$> parseMLS <*> parseMLS - <*> parseMLSBytes @Word16 <*> parseMLS - <*> parseMLSVector @Word32 parseMLS + <*> parseMLS + <*> parseMLSVector @VarInt parseMLS +instance SerialiseMLS KeyPackageTBS where + serialiseMLS tbs = do + serialiseMLS tbs.protocolVersion + serialiseMLS tbs.cipherSuite + serialiseMLS tbs.initKey + serialiseMLS tbs.leafNode + serialiseMLSVector @VarInt serialiseMLS tbs.extensions + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-10-6 data KeyPackage = KeyPackage - { kpTBS :: RawMLS KeyPackageTBS, - kpSignature :: ByteString + { tbs :: RawMLS KeyPackageTBS, + signature_ :: ByteString } - deriving stock (Eq, Show) + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform KeyPackage) instance S.ToSchema KeyPackage where declareNamedSchema _ = pure (mlsSwagger "KeyPackage") -kpProtocolVersion :: KeyPackage -> ProtocolVersion -kpProtocolVersion = kpuProtocolVersion . rmValue . kpTBS +instance HasField "protocolVersion" KeyPackage ProtocolVersion where + getField = (.tbs.value.protocolVersion) -kpCipherSuite :: KeyPackage -> CipherSuite -kpCipherSuite = kpuCipherSuite . rmValue . kpTBS +instance HasField "cipherSuite" KeyPackage CipherSuite where + getField = (.tbs.value.cipherSuite) -kpInitKey :: KeyPackage -> ByteString -kpInitKey = kpuInitKey . rmValue . kpTBS +instance HasField "initKey" KeyPackage HPKEPublicKey where + getField = (.tbs.value.initKey) -kpCredential :: KeyPackage -> Credential -kpCredential = kpuCredential . rmValue . kpTBS +instance HasField "extensions" KeyPackage [Extension] where + getField = (.tbs.value.extensions) -kpExtensions :: KeyPackage -> [Extension] -kpExtensions = kpuExtensions . rmValue . kpTBS +instance HasField "leafNode" KeyPackage LeafNode where + getField = (.tbs.value.leafNode) -kpIdentity :: KeyPackage -> Either Text ClientIdentity -kpIdentity = decodeMLS' @ClientIdentity . bcIdentity . kpCredential +keyPackageIdentity :: KeyPackage -> Either Text ClientIdentity +keyPackageIdentity = decodeMLS' @ClientIdentity . (.leafNode.credential.identityData) rawKeyPackageSchema :: ValueSchema NamedSwaggerDoc (RawMLS KeyPackage) rawKeyPackageSchema = @@ -223,11 +225,9 @@ instance ParseMLS KeyPackage where parseMLS = KeyPackage <$> parseRawMLS parseMLS - <*> parseMLSBytes @Word16 - --------------------------------------------------------------------------------- + <*> parseMLSBytes @VarInt -data KeyPackageUpdate = KeyPackageUpdate - { kpupPrevious :: KeyPackageRef, - kpupNext :: KeyPackageRef - } +instance SerialiseMLS KeyPackage where + serialiseMLS kp = do + serialiseMLS kp.tbs + serialiseMLSBytes @VarInt kp.signature_ diff --git a/libs/wire-api/src/Wire/API/MLS/Keys.hs b/libs/wire-api/src/Wire/API/MLS/Keys.hs index 96841a46868..8a47539e8b9 100644 --- a/libs/wire-api/src/Wire/API/MLS/Keys.hs +++ b/libs/wire-api/src/Wire/API/MLS/Keys.hs @@ -32,6 +32,7 @@ import qualified Data.Map as Map import Data.Schema import qualified Data.Swagger as S import Imports +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential data MLSKeys = MLSKeys diff --git a/libs/wire-api/src/Wire/API/MLS/LeafNode.hs b/libs/wire-api/src/Wire/API/MLS/LeafNode.hs new file mode 100644 index 00000000000..9e362bd6c72 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/LeafNode.hs @@ -0,0 +1,201 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.MLS.LeafNode + ( LeafIndex, + LeafNode (..), + LeafNodeCore (..), + LeafNodeTBS (..), + LeafNodeTBSExtra (..), + LeafNodeSource (..), + LeafNodeSourceTag (..), + leafNodeSourceTag, + ) +where + +import Data.Binary +import qualified Data.Swagger as S +import GHC.Records +import Imports +import Test.QuickCheck +import Wire.API.MLS.Capabilities +import Wire.API.MLS.Credential +import Wire.API.MLS.Extension +import Wire.API.MLS.Group +import Wire.API.MLS.HPKEPublicKey +import Wire.API.MLS.Lifetime +import Wire.API.MLS.Serialisation +import Wire.Arbitrary + +type LeafIndex = Word32 + +-- LeafNodeCore contains fields in the intersection of LeafNode and LeafNodeTBS +-- +-- https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.2-2 +data LeafNodeCore = LeafNodeCore + { encryptionKey :: HPKEPublicKey, + signatureKey :: ByteString, + credential :: Credential, + capabilities :: Capabilities, + source :: LeafNodeSource, + extensions :: [Extension] + } + deriving (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform LeafNodeCore) + +-- extra fields in LeafNodeTBS, but not in LeafNode +-- +-- https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.2-2 +data LeafNodeTBSExtra + = LeafNodeTBSExtraKeyPackage + | LeafNodeTBSExtraUpdate GroupId LeafIndex + | LeafNodeTBSExtraCommit GroupId LeafIndex + +serialiseUntaggedLeafNodeTBSExtra :: LeafNodeTBSExtra -> Put +serialiseUntaggedLeafNodeTBSExtra LeafNodeTBSExtraKeyPackage = pure () +serialiseUntaggedLeafNodeTBSExtra (LeafNodeTBSExtraUpdate gid idx) = do + serialiseMLS gid + serialiseMLS idx +serialiseUntaggedLeafNodeTBSExtra (LeafNodeTBSExtraCommit gid idx) = do + serialiseMLS gid + serialiseMLS idx + +instance HasField "tag" LeafNodeTBSExtra LeafNodeSourceTag where + getField = \case + LeafNodeTBSExtraKeyPackage -> LeafNodeSourceKeyPackageTag + LeafNodeTBSExtraCommit _ _ -> LeafNodeSourceCommitTag + LeafNodeTBSExtraUpdate _ _ -> LeafNodeSourceUpdateTag + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.2-2 +data LeafNodeTBS = LeafNodeTBS + { core :: RawMLS LeafNodeCore, + extra :: LeafNodeTBSExtra + } + +instance SerialiseMLS LeafNodeTBS where + serialiseMLS tbs = do + serialiseMLS tbs.core + serialiseUntaggedLeafNodeTBSExtra tbs.extra + +instance ParseMLS LeafNodeCore where + parseMLS = + LeafNodeCore + <$> parseMLS + <*> parseMLSBytes @VarInt + <*> parseMLS + <*> parseMLS + <*> parseMLS + <*> parseMLSVector @VarInt parseMLS + +instance SerialiseMLS LeafNodeCore where + serialiseMLS core = do + serialiseMLS core.encryptionKey + serialiseMLSBytes @VarInt core.signatureKey + serialiseMLS core.credential + serialiseMLS core.capabilities + serialiseMLS core.source + serialiseMLSVector @VarInt serialiseMLS core.extensions + +-- | This type can only verify the signature when the LeafNodeSource is +-- LeafNodeSourceKeyPackage +-- +-- https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.2-2 +data LeafNode = LeafNode + { core :: RawMLS LeafNodeCore, + signature_ :: ByteString + } + deriving (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform LeafNode) + +instance ParseMLS LeafNode where + parseMLS = + LeafNode + <$> parseMLS + <*> parseMLSBytes @VarInt + +instance SerialiseMLS LeafNode where + serialiseMLS ln = do + serialiseMLS ln.core + serialiseMLSBytes @VarInt ln.signature_ + +instance S.ToSchema LeafNode where + declareNamedSchema _ = pure (mlsSwagger "LeafNode") + +instance HasField "encryptionKey" LeafNode HPKEPublicKey where + getField = (.core.value.encryptionKey) + +instance HasField "signatureKey" LeafNode ByteString where + getField = (.core.value.signatureKey) + +instance HasField "credential" LeafNode Credential where + getField = (.core.value.credential) + +instance HasField "capabilities" LeafNode Capabilities where + getField = (.core.value.capabilities) + +instance HasField "source" LeafNode LeafNodeSource where + getField = (.core.value.source) + +instance HasField "extensions" LeafNode [Extension] where + getField = (.core.value.extensions) + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.2-2 +data LeafNodeSource + = LeafNodeSourceKeyPackage Lifetime + | LeafNodeSourceUpdate + | LeafNodeSourceCommit ByteString + deriving (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform LeafNodeSource) + +instance ParseMLS LeafNodeSource where + parseMLS = + parseMLS >>= \case + LeafNodeSourceKeyPackageTag -> LeafNodeSourceKeyPackage <$> parseMLS + LeafNodeSourceUpdateTag -> pure LeafNodeSourceUpdate + LeafNodeSourceCommitTag -> LeafNodeSourceCommit <$> parseMLSBytes @VarInt + +instance SerialiseMLS LeafNodeSource where + serialiseMLS (LeafNodeSourceKeyPackage lt) = do + serialiseMLS LeafNodeSourceKeyPackageTag + serialiseMLS lt + serialiseMLS LeafNodeSourceUpdate = + serialiseMLS LeafNodeSourceUpdateTag + serialiseMLS (LeafNodeSourceCommit bs) = do + serialiseMLS LeafNodeSourceCommitTag + serialiseMLSBytes @VarInt bs + +data LeafNodeSourceTag + = LeafNodeSourceKeyPackageTag + | LeafNodeSourceUpdateTag + | LeafNodeSourceCommitTag + deriving (Show, Eq, Ord, Enum, Bounded) + +instance ParseMLS LeafNodeSourceTag where + parseMLS = parseMLSEnum @Word8 "leaf node source" + +instance SerialiseMLS LeafNodeSourceTag where + serialiseMLS = serialiseMLSEnum @Word8 + +instance HasField "name" LeafNodeSourceTag Text where + getField LeafNodeSourceKeyPackageTag = "key_package" + getField LeafNodeSourceUpdateTag = "update" + getField LeafNodeSourceCommitTag = "commit" + +leafNodeSourceTag :: LeafNodeSource -> LeafNodeSourceTag +leafNodeSourceTag (LeafNodeSourceKeyPackage _) = LeafNodeSourceKeyPackageTag +leafNodeSourceTag LeafNodeSourceUpdate = LeafNodeSourceUpdateTag +leafNodeSourceTag (LeafNodeSourceCommit _) = LeafNodeSourceCommitTag diff --git a/libs/wire-api/src/Wire/API/MLS/Lifetime.hs b/libs/wire-api/src/Wire/API/MLS/Lifetime.hs new file mode 100644 index 00000000000..0f17c2978d4 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/Lifetime.hs @@ -0,0 +1,48 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 . +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +module Wire.API.MLS.Lifetime where + +import Data.Time.Clock.POSIX +import Imports +import Test.QuickCheck +import Wire.API.MLS.Serialisation +import Wire.Arbitrary + +-- | Seconds since the UNIX epoch. +newtype Timestamp = Timestamp {timestampSeconds :: Word64} + deriving newtype (Eq, Show, Arbitrary, ParseMLS, SerialiseMLS) + +tsPOSIX :: Timestamp -> POSIXTime +tsPOSIX = fromIntegral . timestampSeconds + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.2-2 +data Lifetime = Lifetime + { ltNotBefore :: Timestamp, + ltNotAfter :: Timestamp + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via GenericUniform Lifetime + +instance ParseMLS Lifetime where + parseMLS = Lifetime <$> parseMLS <*> parseMLS + +instance SerialiseMLS Lifetime where + serialiseMLS lt = do + serialiseMLS lt.ltNotBefore + serialiseMLS lt.ltNotAfter diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs index 1787ceab4bf..df56293ccfb 100644 --- a/libs/wire-api/src/Wire/API/MLS/Message.hs +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -1,7 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE StandaloneKindSignatures #-} -{-# LANGUAGE TemplateHaskell #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -20,224 +16,193 @@ -- with this program. If not, see . module Wire.API.MLS.Message - ( Message (..), - msgGroupId, - msgEpoch, - msgSender, - msgPayload, - MessageTBS (..), - MessageExtraFields (..), + ( -- * MLS Message types WireFormatTag (..), - SWireFormatTag (..), - SomeMessage (..), - ContentType (..), - MessagePayload (..), + Message (..), + mkMessage, + MessageContent (..), + PublicMessage (..), + PrivateMessage (..), + FramedContent (..), + FramedContentData (..), + FramedContentDataTag (..), + FramedContentTBS (..), + FramedContentAuthData (..), Sender (..), - MLSPlainTextSym0, - MLSCipherTextSym0, - MLSMessageSendingStatus (..), - KnownFormatTag (..), UnreachableUsers (..), + + -- * Utilities verifyMessageSignature, - mkSignedMessage, + + -- * Servant types + MLSMessageSendingStatus (..), ) where import Control.Lens ((?~)) -import Crypto.PubKey.Ed25519 import qualified Data.Aeson as A import Data.Binary -import Data.Binary.Get -import Data.Binary.Put -import qualified Data.ByteArray as BA import Data.Id import Data.Json.Util -import Data.Kind import Data.Qualified import Data.Schema -import Data.Singletons.TH import qualified Data.Swagger as S +import GHC.Records import Imports -import Test.QuickCheck hiding (label) import Wire.API.Event.Conversation import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.Epoch import Wire.API.MLS.Group +import Wire.API.MLS.GroupInfo import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode import Wire.API.MLS.Proposal +import Wire.API.MLS.ProtocolVersion import Wire.API.MLS.Serialisation -import Wire.Arbitrary (GenericUniform (..)) +import Wire.API.MLS.Welcome +import Wire.Arbitrary -data WireFormatTag = MLSPlainText | MLSCipherText - deriving (Bounded, Enum, Eq, Show) - -$(genSingletons [''WireFormatTag]) +data WireFormatTag + = WireFormatPublicTag + | WireFormatPrivateTag + | WireFormatWelcomeTag + | WireFormatGroupInfoTag + | WireFormatKeyPackageTag + deriving (Enum, Bounded, Eq, Show) instance ParseMLS WireFormatTag where - parseMLS = parseMLSEnum @Word8 "wire format" - -data family MessageExtraFields (tag :: WireFormatTag) :: Type - -data instance MessageExtraFields 'MLSPlainText = MessageExtraFields - { msgSignature :: ByteString, - msgConfirmation :: Maybe ByteString, - msgMembership :: Maybe ByteString - } - deriving (Generic) - deriving (Arbitrary) via (GenericUniform (MessageExtraFields 'MLSPlainText)) + parseMLS = parseMLSEnum @Word16 "wire format" -instance ParseMLS (MessageExtraFields 'MLSPlainText) where - parseMLS = - MessageExtraFields - <$> label "msgSignature" (parseMLSBytes @Word16) - <*> label "msgConfirmation" (parseMLSOptional (parseMLSBytes @Word8)) - <*> label "msgMembership" (parseMLSOptional (parseMLSBytes @Word8)) - -instance SerialiseMLS (MessageExtraFields 'MLSPlainText) where - serialiseMLS (MessageExtraFields sig mconf mmemb) = do - serialiseMLSBytes @Word16 sig - serialiseMLSOptional (serialiseMLSBytes @Word8) mconf - serialiseMLSOptional (serialiseMLSBytes @Word8) mmemb - -data instance MessageExtraFields 'MLSCipherText = NoExtraFields - -instance ParseMLS (MessageExtraFields 'MLSCipherText) where - parseMLS = pure NoExtraFields - -deriving instance Eq (MessageExtraFields 'MLSPlainText) +instance SerialiseMLS WireFormatTag where + serialiseMLS = serialiseMLSEnum @Word16 -deriving instance Eq (MessageExtraFields 'MLSCipherText) - -deriving instance Show (MessageExtraFields 'MLSPlainText) - -deriving instance Show (MessageExtraFields 'MLSCipherText) - -data Message (tag :: WireFormatTag) = Message - { msgTBS :: RawMLS (MessageTBS tag), - msgExtraFields :: MessageExtraFields tag +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6-4 +data Message = Message + { protocolVersion :: ProtocolVersion, + content :: MessageContent } + deriving (Eq, Show) -deriving instance Eq (Message 'MLSPlainText) - -deriving instance Eq (Message 'MLSCipherText) - -deriving instance Show (Message 'MLSPlainText) - -deriving instance Show (Message 'MLSCipherText) - -instance ParseMLS (Message 'MLSPlainText) where - parseMLS = Message <$> label "tbs" parseMLS <*> label "MessageExtraFields" parseMLS - -instance SerialiseMLS (Message 'MLSPlainText) where - serialiseMLS (Message msgTBS msgExtraFields) = do - putByteString (rmRaw msgTBS) - serialiseMLS msgExtraFields - -instance ParseMLS (Message 'MLSCipherText) where - parseMLS = Message <$> parseMLS <*> parseMLS - --- | This corresponds to the format byte at the beginning of a message. --- It does not convey any information, but it needs to be present in --- order for signature verification to work. -data KnownFormatTag (tag :: WireFormatTag) = KnownFormatTag - -instance ParseMLS (KnownFormatTag tag) where - parseMLS = parseMLS @WireFormatTag $> KnownFormatTag - -instance SerialiseMLS (KnownFormatTag 'MLSPlainText) where - serialiseMLS _ = put (fromMLSEnum @Word8 MLSPlainText) - -instance SerialiseMLS (KnownFormatTag 'MLSCipherText) where - serialiseMLS _ = put (fromMLSEnum @Word8 MLSCipherText) - -deriving instance Eq (KnownFormatTag 'MLSPlainText) +mkMessage :: MessageContent -> Message +mkMessage = Message defaultProtocolVersion -deriving instance Eq (KnownFormatTag 'MLSCipherText) +instance ParseMLS Message where + parseMLS = + Message + <$> parseMLS + <*> parseMLS + +instance SerialiseMLS Message where + serialiseMLS msg = do + serialiseMLS msg.protocolVersion + serialiseMLS msg.content + +instance HasField "wireFormat" Message WireFormatTag where + getField = (.content.wireFormat) + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6-4 +data MessageContent + = MessagePrivate (RawMLS PrivateMessage) + | MessagePublic PublicMessage + | MessageWelcome (RawMLS Welcome) + | MessageGroupInfo (RawMLS GroupInfo) + | MessageKeyPackage (RawMLS KeyPackage) + deriving (Eq, Show) -deriving instance Show (KnownFormatTag 'MLSPlainText) +instance HasField "wireFormat" MessageContent WireFormatTag where + getField (MessagePrivate _) = WireFormatPrivateTag + getField (MessagePublic _) = WireFormatPublicTag + getField (MessageWelcome _) = WireFormatWelcomeTag + getField (MessageGroupInfo _) = WireFormatGroupInfoTag + getField (MessageKeyPackage _) = WireFormatKeyPackageTag -deriving instance Show (KnownFormatTag 'MLSCipherText) +instance ParseMLS MessageContent where + parseMLS = + parseMLS >>= \case + WireFormatPrivateTag -> MessagePrivate <$> parseMLS + WireFormatPublicTag -> MessagePublic <$> parseMLS + WireFormatWelcomeTag -> MessageWelcome <$> parseMLS + WireFormatGroupInfoTag -> MessageGroupInfo <$> parseMLS + WireFormatKeyPackageTag -> MessageKeyPackage <$> parseMLS + +instance SerialiseMLS MessageContent where + serialiseMLS (MessagePrivate msg) = do + serialiseMLS WireFormatPrivateTag + serialiseMLS msg + serialiseMLS (MessagePublic msg) = do + serialiseMLS WireFormatPublicTag + serialiseMLS msg + serialiseMLS (MessageWelcome welcome) = do + serialiseMLS WireFormatWelcomeTag + serialiseMLS welcome + serialiseMLS (MessageGroupInfo gi) = do + serialiseMLS WireFormatGroupInfoTag + serialiseMLS gi + serialiseMLS (MessageKeyPackage kp) = do + serialiseMLS WireFormatKeyPackageTag + serialiseMLS kp + +instance S.ToSchema Message where + declareNamedSchema _ = pure (mlsSwagger "MLSMessage") -data MessageTBS (tag :: WireFormatTag) = MessageTBS - { tbsMsgFormat :: KnownFormatTag tag, - tbsMsgGroupId :: GroupId, - tbsMsgEpoch :: Epoch, - tbsMsgAuthData :: ByteString, - tbsMsgSender :: Sender tag, - tbsMsgPayload :: MessagePayload tag +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6.2-2 +data PublicMessage = PublicMessage + { content :: RawMLS FramedContent, + authData :: RawMLS FramedContentAuthData, + -- Present iff content.value.sender is of type Member. + -- https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6.2-4 + membershipTag :: Maybe ByteString } + deriving (Eq, Show) -msgGroupId :: Message tag -> GroupId -msgGroupId = tbsMsgGroupId . rmValue . msgTBS - -msgEpoch :: Message tag -> Epoch -msgEpoch = tbsMsgEpoch . rmValue . msgTBS - -msgSender :: Message tag -> Sender tag -msgSender = tbsMsgSender . rmValue . msgTBS - -msgPayload :: Message tag -> MessagePayload tag -msgPayload = tbsMsgPayload . rmValue . msgTBS - -instance ParseMLS (MessageTBS 'MLSPlainText) where +instance ParseMLS PublicMessage where parseMLS = do - f <- parseMLS - g <- parseMLS - e <- parseMLS - s <- parseMLS - d <- parseMLSBytes @Word32 - MessageTBS f g e d s <$> parseMLS - -instance ParseMLS (MessageTBS 'MLSCipherText) where - parseMLS = do - f <- parseMLS - g <- parseMLS - e <- parseMLS - ct <- parseMLS - d <- parseMLSBytes @Word32 - s <- parseMLS - p <- parseMLSBytes @Word32 - pure $ MessageTBS f g e d s (CipherText ct p) - -instance SerialiseMLS (MessageTBS 'MLSPlainText) where - serialiseMLS (MessageTBS f g e d s p) = do - serialiseMLS f - serialiseMLS g - serialiseMLS e - serialiseMLS s - serialiseMLSBytes @Word32 d - serialiseMLS p - -deriving instance Eq (MessageTBS 'MLSPlainText) - -deriving instance Eq (MessageTBS 'MLSCipherText) - -deriving instance Show (MessageTBS 'MLSPlainText) - -deriving instance Show (MessageTBS 'MLSCipherText) - -data SomeMessage where - SomeMessage :: Sing tag -> Message tag -> SomeMessage - -instance S.ToSchema SomeMessage where - declareNamedSchema _ = pure (mlsSwagger "MLSMessage") - -instance ParseMLS SomeMessage where - parseMLS = - lookAhead parseMLS >>= \case - MLSPlainText -> SomeMessage SMLSPlainText <$> parseMLS - MLSCipherText -> SomeMessage SMLSCipherText <$> parseMLS - -data family Sender (tag :: WireFormatTag) :: Type - -data instance Sender 'MLSCipherText = EncryptedSender {esData :: ByteString} + content <- parseMLS + authData <- parseRawMLS (parseFramedContentAuthData (framedContentDataTag (content.value.content))) + membershipTag <- case content.value.sender of + SenderMember _ -> Just <$> parseMLSBytes @VarInt + _ -> pure Nothing + pure + PublicMessage + { content = content, + authData = authData, + membershipTag = membershipTag + } + +instance SerialiseMLS PublicMessage where + serialiseMLS msg = do + serialiseMLS msg.content + serialiseMLS msg.authData + traverse_ (serialiseMLSBytes @VarInt) msg.membershipTag + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6.3.1-2 +data PrivateMessage = PrivateMessage + { groupId :: GroupId, + epoch :: Epoch, + tag :: FramedContentDataTag, + authenticatedData :: ByteString, + encryptedSenderData :: ByteString, + ciphertext :: ByteString + } deriving (Eq, Show) -instance ParseMLS (Sender 'MLSCipherText) where - parseMLS = EncryptedSender <$> parseMLSBytes @Word8 - -data SenderTag = MemberSenderTag | PreconfiguredSenderTag | NewMemberSenderTag +instance ParseMLS PrivateMessage where + parseMLS = + PrivateMessage + <$> parseMLS + <*> parseMLS + <*> parseMLS + <*> parseMLSBytes @VarInt + <*> parseMLSBytes @VarInt + <*> parseMLSBytes @VarInt + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6-4 +data SenderTag + = SenderMemberTag + | SenderExternalTag + | SenderNewMemberProposalTag + | SenderNewMemberCommitTag deriving (Bounded, Enum, Show, Eq) instance ParseMLS SenderTag where @@ -246,77 +211,170 @@ instance ParseMLS SenderTag where instance SerialiseMLS SenderTag where serialiseMLS = serialiseMLSEnum @Word8 --- NOTE: according to the spec, the preconfigured sender case contains a --- bytestring, not a u32. However, as of 2022-08-02, the openmls fork used by --- the clients is using a u32 here. -data instance Sender 'MLSPlainText - = MemberSender KeyPackageRef - | PreconfiguredSender Word32 - | NewMemberSender +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6-4 +data Sender + = SenderMember LeafIndex + | SenderExternal Word32 + | SenderNewMemberProposal + | SenderNewMemberCommit deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform Sender) -instance ParseMLS (Sender 'MLSPlainText) where +instance ParseMLS Sender where parseMLS = parseMLS >>= \case - MemberSenderTag -> MemberSender <$> parseMLS - PreconfiguredSenderTag -> PreconfiguredSender <$> get - NewMemberSenderTag -> pure NewMemberSender - -instance SerialiseMLS (Sender 'MLSPlainText) where - serialiseMLS (MemberSender r) = do - serialiseMLS MemberSenderTag - serialiseMLS r - serialiseMLS (PreconfiguredSender x) = do - serialiseMLS PreconfiguredSenderTag - put x - serialiseMLS NewMemberSender = serialiseMLS NewMemberSenderTag - -data family MessagePayload (tag :: WireFormatTag) :: Type - -deriving instance Eq (MessagePayload 'MLSPlainText) - -deriving instance Eq (MessagePayload 'MLSCipherText) - -deriving instance Show (MessagePayload 'MLSPlainText) - -deriving instance Show (MessagePayload 'MLSCipherText) - -data instance MessagePayload 'MLSCipherText = CipherText - { msgContentType :: Word8, - msgCipherText :: ByteString + SenderMemberTag -> SenderMember <$> parseMLS + SenderExternalTag -> SenderExternal <$> parseMLS + SenderNewMemberProposalTag -> pure SenderNewMemberProposal + SenderNewMemberCommitTag -> pure SenderNewMemberCommit + +instance SerialiseMLS Sender where + serialiseMLS (SenderMember i) = do + serialiseMLS SenderMemberTag + serialiseMLS i + serialiseMLS (SenderExternal w) = do + serialiseMLS SenderExternalTag + serialiseMLS w + serialiseMLS SenderNewMemberProposal = + serialiseMLS SenderNewMemberProposalTag + serialiseMLS SenderNewMemberCommit = + serialiseMLS SenderNewMemberCommitTag + +needsGroupContext :: Sender -> Bool +needsGroupContext (SenderMember _) = True +needsGroupContext (SenderExternal _) = True +needsGroupContext _ = False + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6-4 +data FramedContent = FramedContent + { groupId :: GroupId, + epoch :: Epoch, + sender :: Sender, + authenticatedData :: ByteString, + content :: FramedContentData } + deriving (Eq, Show) -data ContentType - = ApplicationMessageTag - | ProposalMessageTag - | CommitMessageTag - deriving (Bounded, Enum, Eq, Show) +instance ParseMLS FramedContent where + parseMLS = + FramedContent + <$> parseMLS + <*> parseMLS + <*> parseMLS + <*> parseMLSBytes @VarInt + <*> parseMLS + +instance SerialiseMLS FramedContent where + serialiseMLS fc = do + serialiseMLS fc.groupId + serialiseMLS fc.epoch + serialiseMLS fc.sender + serialiseMLSBytes @VarInt fc.authenticatedData + serialiseMLS fc.content + +data FramedContentDataTag + = FramedContentApplicationDataTag + | FramedContentProposalTag + | FramedContentCommitTag + deriving (Enum, Bounded, Eq, Ord, Show) + +instance ParseMLS FramedContentDataTag where + parseMLS = parseMLSEnum @Word8 "ContentType" + +instance SerialiseMLS FramedContentDataTag where + serialiseMLS = serialiseMLSEnum @Word8 -instance ParseMLS ContentType where - parseMLS = parseMLSEnum @Word8 "content type" +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6-4 +data FramedContentData + = FramedContentApplicationData ByteString + | FramedContentProposal (RawMLS Proposal) + | FramedContentCommit (RawMLS Commit) + deriving (Eq, Show) -data instance MessagePayload 'MLSPlainText - = ApplicationMessage ByteString - | ProposalMessage (RawMLS Proposal) - | CommitMessage Commit +framedContentDataTag :: FramedContentData -> FramedContentDataTag +framedContentDataTag (FramedContentApplicationData _) = FramedContentApplicationDataTag +framedContentDataTag (FramedContentProposal _) = FramedContentProposalTag +framedContentDataTag (FramedContentCommit _) = FramedContentCommitTag -instance ParseMLS (MessagePayload 'MLSPlainText) where +instance ParseMLS FramedContentData where parseMLS = parseMLS >>= \case - ApplicationMessageTag -> ApplicationMessage <$> parseMLSBytes @Word32 - ProposalMessageTag -> ProposalMessage <$> parseMLS - CommitMessageTag -> CommitMessage <$> parseMLS + FramedContentApplicationDataTag -> + FramedContentApplicationData <$> parseMLSBytes @VarInt + FramedContentProposalTag -> FramedContentProposal <$> parseMLS + FramedContentCommitTag -> FramedContentCommit <$> parseMLS + +instance SerialiseMLS FramedContentData where + serialiseMLS (FramedContentApplicationData bs) = do + serialiseMLS FramedContentApplicationDataTag + serialiseMLSBytes @VarInt bs + serialiseMLS (FramedContentProposal prop) = do + serialiseMLS FramedContentProposalTag + serialiseMLS prop + serialiseMLS (FramedContentCommit commit) = do + serialiseMLS FramedContentCommitTag + serialiseMLS commit + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6.1-2 +data FramedContentTBS = FramedContentTBS + { protocolVersion :: ProtocolVersion, + wireFormat :: WireFormatTag, + content :: RawMLS FramedContent, + groupContext :: Maybe (RawMLS GroupContext) + } + deriving (Eq, Show) -instance SerialiseMLS ContentType where - serialiseMLS = serialiseMLSEnum @Word8 +instance SerialiseMLS FramedContentTBS where + serialiseMLS tbs = do + serialiseMLS tbs.protocolVersion + serialiseMLS tbs.wireFormat + serialiseMLS tbs.content + traverse_ serialiseMLS tbs.groupContext + +framedContentTBS :: RawMLS GroupContext -> RawMLS FramedContent -> FramedContentTBS +framedContentTBS ctx msgContent = + FramedContentTBS + { protocolVersion = defaultProtocolVersion, + wireFormat = WireFormatPublicTag, + content = msgContent, + groupContext = guard (needsGroupContext msgContent.value.sender) $> ctx + } + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6.1-2 +data FramedContentAuthData = FramedContentAuthData + { signature_ :: ByteString, + -- Present iff it is part of a commit. + confirmationTag :: Maybe ByteString + } + deriving (Eq, Show) -instance SerialiseMLS (MessagePayload 'MLSPlainText) where - serialiseMLS (ProposalMessage raw) = do - serialiseMLS ProposalMessageTag - putByteString (rmRaw raw) - -- We do not need to serialise Commit and Application messages, - -- so the next case is left as a stub - serialiseMLS _ = pure () +parseFramedContentAuthData :: FramedContentDataTag -> Get FramedContentAuthData +parseFramedContentAuthData t = do + sig <- parseMLSBytes @VarInt + confirmationTag <- case t of + FramedContentCommitTag -> Just <$> parseMLSBytes @VarInt + _ -> pure Nothing + pure (FramedContentAuthData sig confirmationTag) + +instance SerialiseMLS FramedContentAuthData where + serialiseMLS ad = do + serialiseMLSBytes @VarInt ad.signature_ + traverse_ (serialiseMLSBytes @VarInt) ad.confirmationTag + +verifyMessageSignature :: + RawMLS GroupContext -> + RawMLS FramedContent -> + RawMLS FramedContentAuthData -> + ByteString -> + Bool +verifyMessageSignature ctx msgContent authData pubkey = isJust $ do + let tbs = mkRawMLS (framedContentTBS ctx msgContent) + sig = authData.value.signature_ + cs <- cipherSuiteTag ctx.value.cipherSuite + guard $ csVerifySignature cs pubkey tbs sig + +-------------------------------------------------------------------------------- +-- Servant newtype UnreachableUsers = UnreachableUsers {unreachableUsers :: [Qualified UserId]} deriving stock (Eq, Show) @@ -357,28 +415,3 @@ instance ToSchema MLSMessageSendingStatus where "failed_to_send" (description ?~ "List of federated users who could not be reached and did not receive the message") schema - -verifyMessageSignature :: CipherSuiteTag -> Message 'MLSPlainText -> ByteString -> Bool -verifyMessageSignature cs msg pubkey = - csVerifySignature cs pubkey (rmRaw (msgTBS msg)) (msgSignature (msgExtraFields msg)) - -mkSignedMessage :: - SecretKey -> - PublicKey -> - GroupId -> - Epoch -> - MessagePayload 'MLSPlainText -> - Message 'MLSPlainText -mkSignedMessage priv pub gid epoch payload = - let tbs = - mkRawMLS $ - MessageTBS - { tbsMsgFormat = KnownFormatTag, - tbsMsgGroupId = gid, - tbsMsgEpoch = epoch, - tbsMsgAuthData = mempty, - tbsMsgSender = PreconfiguredSender 0, - tbsMsgPayload = payload - } - sig = BA.convert $ sign priv pub (rmRaw tbs) - in Message tbs (MessageExtraFields sig Nothing Nothing) diff --git a/libs/wire-api/src/Wire/API/MLS/Proposal.hs b/libs/wire-api/src/Wire/API/MLS/Proposal.hs index 1226811c6e8..1ae2ef989ac 100644 --- a/libs/wire-api/src/Wire/API/MLS/Proposal.hs +++ b/libs/wire-api/src/Wire/API/MLS/Proposal.hs @@ -1,4 +1,6 @@ {-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -15,54 +17,46 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# LANGUAGE TemplateHaskell #-} module Wire.API.MLS.Proposal where import Cassandra -import Control.Arrow import Control.Lens (makePrisms) import Data.Binary -import Data.Binary.Get -import Data.Binary.Put -import qualified Data.ByteString.Lazy as LBS +import Data.ByteString as B +import GHC.Records import Imports +import Test.QuickCheck import Wire.API.MLS.CipherSuite -import Wire.API.MLS.Context import Wire.API.MLS.Extension import Wire.API.MLS.Group import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode +import Wire.API.MLS.ProposalTag +import Wire.API.MLS.ProtocolVersion import Wire.API.MLS.Serialisation import Wire.Arbitrary -data ProposalTag - = AddProposalTag - | UpdateProposalTag - | RemoveProposalTag - | PreSharedKeyProposalTag - | ReInitProposalTag - | ExternalInitProposalTag - | AppAckProposalTag - | GroupContextExtensionsProposalTag - deriving stock (Bounded, Enum, Eq, Generic, Show) - deriving (Arbitrary) via GenericUniform ProposalTag - -instance ParseMLS ProposalTag where - parseMLS = parseMLSEnum @Word16 "proposal type" - -instance SerialiseMLS ProposalTag where - serialiseMLS = serialiseMLSEnum @Word16 - +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-12.1-2 data Proposal = AddProposal (RawMLS KeyPackage) - | UpdateProposal KeyPackage - | RemoveProposal KeyPackageRef - | PreSharedKeyProposal PreSharedKeyID - | ReInitProposal ReInit + | UpdateProposal (RawMLS LeafNode) + | RemoveProposal LeafIndex + | PreSharedKeyProposal (RawMLS PreSharedKeyID) + | ReInitProposal (RawMLS ReInit) | ExternalInitProposal ByteString - | AppAckProposal [MessageRange] | GroupContextExtensionsProposal [Extension] - deriving stock (Eq, Show) + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform Proposal) + +instance HasField "tag" Proposal ProposalTag where + getField (AddProposal _) = AddProposalTag + getField (UpdateProposal _) = UpdateProposalTag + getField (RemoveProposal _) = RemoveProposalTag + getField (PreSharedKeyProposal _) = PreSharedKeyProposalTag + getField (ReInitProposal _) = ReInitProposalTag + getField (ExternalInitProposal _) = ExternalInitProposalTag + getField (GroupContextExtensionsProposal _) = GroupContextExtensionsProposalTag instance ParseMLS Proposal where parseMLS = @@ -72,57 +66,86 @@ instance ParseMLS Proposal where RemoveProposalTag -> RemoveProposal <$> parseMLS PreSharedKeyProposalTag -> PreSharedKeyProposal <$> parseMLS ReInitProposalTag -> ReInitProposal <$> parseMLS - ExternalInitProposalTag -> ExternalInitProposal <$> parseMLSBytes @Word16 - AppAckProposalTag -> AppAckProposal <$> parseMLSVector @Word32 parseMLS + ExternalInitProposalTag -> ExternalInitProposal <$> parseMLSBytes @VarInt GroupContextExtensionsProposalTag -> - GroupContextExtensionsProposal <$> parseMLSVector @Word32 parseMLS - -mkRemoveProposal :: KeyPackageRef -> RawMLS Proposal -mkRemoveProposal ref = RawMLS bytes (RemoveProposal ref) - where - bytes = LBS.toStrict . runPut $ do - serialiseMLS RemoveProposalTag - serialiseMLS ref - -serialiseAppAckProposal :: [MessageRange] -> Put -serialiseAppAckProposal mrs = do - serialiseMLS AppAckProposalTag - serialiseMLSVector @Word32 serialiseMLS mrs - -mkAppAckProposal :: [MessageRange] -> RawMLS Proposal -mkAppAckProposal = uncurry RawMLS . (bytes &&& AppAckProposal) - where - bytes = LBS.toStrict . runPut . serialiseAppAckProposal - --- | Compute the proposal ref given a ciphersuite and the raw proposal data. -proposalRef :: CipherSuiteTag -> RawMLS Proposal -> ProposalRef -proposalRef cs = - ProposalRef - . csHash cs proposalContext - . rmRaw - + GroupContextExtensionsProposal <$> parseMLSVector @VarInt parseMLS + +instance SerialiseMLS Proposal where + serialiseMLS (AddProposal kp) = do + serialiseMLS AddProposalTag + serialiseMLS kp + serialiseMLS (UpdateProposal ln) = do + serialiseMLS UpdateProposalTag + serialiseMLS ln + serialiseMLS (RemoveProposal i) = do + serialiseMLS RemoveProposalTag + serialiseMLS i + serialiseMLS (PreSharedKeyProposal k) = do + serialiseMLS PreSharedKeyProposalTag + serialiseMLS k + serialiseMLS (ReInitProposal ri) = do + serialiseMLS ReInitProposalTag + serialiseMLS ri + serialiseMLS (ExternalInitProposal ko) = do + serialiseMLS ExternalInitProposalTag + serialiseMLSBytes @VarInt ko + serialiseMLS (GroupContextExtensionsProposal es) = do + serialiseMLS GroupContextExtensionsProposalTag + serialiseMLSVector @VarInt serialiseMLS es + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-8.4-6 data PreSharedKeyTag = ExternalKeyTag | ResumptionKeyTag deriving (Bounded, Enum, Eq, Show) instance ParseMLS PreSharedKeyTag where - parseMLS = parseMLSEnum @Word16 "PreSharedKeyID type" + parseMLS = parseMLSEnum @Word8 "PreSharedKeyID type" -data PreSharedKeyID = ExternalKeyID ByteString | ResumptionKeyID Resumption - deriving stock (Eq, Show) +instance SerialiseMLS PreSharedKeyTag where + serialiseMLS = serialiseMLSEnum @Word8 -instance ParseMLS PreSharedKeyID where +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-8.4-6 +data PreSharedKeyIDCore = ExternalKeyID ByteString | ResumptionKeyID Resumption + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform PreSharedKeyIDCore) + +instance ParseMLS PreSharedKeyIDCore where parseMLS = do t <- parseMLS case t of - ExternalKeyTag -> ExternalKeyID <$> parseMLSBytes @Word8 + ExternalKeyTag -> ExternalKeyID <$> parseMLSBytes @VarInt ResumptionKeyTag -> ResumptionKeyID <$> parseMLS +instance SerialiseMLS PreSharedKeyIDCore where + serialiseMLS (ExternalKeyID bs) = do + serialiseMLS ExternalKeyTag + serialiseMLSBytes @VarInt bs + serialiseMLS (ResumptionKeyID r) = do + serialiseMLS ResumptionKeyTag + serialiseMLS r + +data PreSharedKeyID = PreSharedKeyID + { core :: PreSharedKeyIDCore, + nonce :: ByteString + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform PreSharedKeyID) + +instance ParseMLS PreSharedKeyID where + parseMLS = PreSharedKeyID <$> parseMLS <*> parseMLSBytes @VarInt + +instance SerialiseMLS PreSharedKeyID where + serialiseMLS psk = do + serialiseMLS psk.core + serialiseMLSBytes @VarInt psk.nonce + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-8.4-6 data Resumption = Resumption - { resUsage :: Word8, - resGroupId :: GroupId, - resEpoch :: Word64 + { usage :: Word8, + groupId :: GroupId, + epoch :: Word64 } - deriving stock (Eq, Show) + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform Resumption) instance ParseMLS Resumption where parseMLS = @@ -131,13 +154,21 @@ instance ParseMLS Resumption where <*> parseMLS <*> parseMLS +instance SerialiseMLS Resumption where + serialiseMLS r = do + serialiseMLS r.usage + serialiseMLS r.groupId + serialiseMLS r.epoch + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-12.1.5-2 data ReInit = ReInit - { riGroupId :: GroupId, - riProtocolVersion :: ProtocolVersion, - riCipherSuite :: CipherSuite, - riExtensions :: [Extension] + { groupId :: GroupId, + protocolVersion :: ProtocolVersion, + cipherSuite :: CipherSuite, + extensions :: [Extension] } - deriving stock (Eq, Show) + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform ReInit) instance ParseMLS ReInit where parseMLS = @@ -145,12 +176,19 @@ instance ParseMLS ReInit where <$> parseMLS <*> parseMLS <*> parseMLS - <*> parseMLSVector @Word32 parseMLS + <*> parseMLSVector @VarInt parseMLS + +instance SerialiseMLS ReInit where + serialiseMLS ri = do + serialiseMLS ri.groupId + serialiseMLS ri.protocolVersion + serialiseMLS ri.cipherSuite + serialiseMLSVector @VarInt serialiseMLS ri.extensions data MessageRange = MessageRange - { mrSender :: KeyPackageRef, - mrFirstGeneration :: Word32, - mrLastGeneration :: Word32 + { sender :: KeyPackageRef, + firstGeneration :: Word32, + lastGeneration :: Word32 } deriving stock (Eq, Show) @@ -166,18 +204,24 @@ instance ParseMLS MessageRange where instance SerialiseMLS MessageRange where serialiseMLS MessageRange {..} = do - serialiseMLS mrSender - serialiseMLS mrFirstGeneration - serialiseMLS mrLastGeneration + serialiseMLS sender + serialiseMLS firstGeneration + serialiseMLS lastGeneration +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-12.4-3 data ProposalOrRefTag = InlineTag | RefTag deriving stock (Bounded, Enum, Eq, Show) instance ParseMLS ProposalOrRefTag where parseMLS = parseMLSEnum @Word8 "ProposalOrRef type" +instance SerialiseMLS ProposalOrRefTag where + serialiseMLS = serialiseMLSEnum @Word8 + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-12.4-3 data ProposalOrRef = Inline Proposal | Ref ProposalRef - deriving stock (Eq, Show) + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform ProposalOrRef) instance ParseMLS ProposalOrRef where parseMLS = @@ -185,11 +229,23 @@ instance ParseMLS ProposalOrRef where InlineTag -> Inline <$> parseMLS RefTag -> Ref <$> parseMLS +instance SerialiseMLS ProposalOrRef where + serialiseMLS (Inline p) = do + serialiseMLS InlineTag + serialiseMLS p + serialiseMLS (Ref r) = do + serialiseMLS RefTag + serialiseMLS r + newtype ProposalRef = ProposalRef {unProposalRef :: ByteString} - deriving stock (Eq, Show, Ord) + deriving stock (Eq, Show, Ord, Generic) + deriving newtype (Arbitrary) instance ParseMLS ProposalRef where - parseMLS = ProposalRef <$> getByteString 16 + parseMLS = ProposalRef <$> parseMLSBytes @VarInt + +instance SerialiseMLS ProposalRef where + serialiseMLS = serialiseMLSBytes @VarInt . unProposalRef makePrisms ''ProposalOrRef diff --git a/libs/wire-api/src/Wire/API/MLS/ProposalTag.hs b/libs/wire-api/src/Wire/API/MLS/ProposalTag.hs new file mode 100644 index 00000000000..8e7d8b36705 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/ProposalTag.hs @@ -0,0 +1,40 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.MLS.ProposalTag where + +import Data.Binary +import Imports +import Wire.API.MLS.Serialisation +import Wire.Arbitrary + +data ProposalTag + = AddProposalTag + | UpdateProposalTag + | RemoveProposalTag + | PreSharedKeyProposalTag + | ReInitProposalTag + | ExternalInitProposalTag + | GroupContextExtensionsProposalTag + deriving stock (Bounded, Enum, Eq, Ord, Generic, Show) + deriving (Arbitrary) via GenericUniform ProposalTag + +instance ParseMLS ProposalTag where + parseMLS = parseMLSEnum @Word16 "proposal type" + +instance SerialiseMLS ProposalTag where + serialiseMLS = serialiseMLSEnum @Word16 diff --git a/libs/wire-api/src/Wire/API/MLS/ProtocolVersion.hs b/libs/wire-api/src/Wire/API/MLS/ProtocolVersion.hs new file mode 100644 index 00000000000..9d8a0220682 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/ProtocolVersion.hs @@ -0,0 +1,53 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 . +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +module Wire.API.MLS.ProtocolVersion + ( ProtocolVersion (..), + ProtocolVersionTag (..), + pvTag, + protocolVersionFromTag, + defaultProtocolVersion, + ) +where + +import Data.Binary +import Imports +import Wire.API.MLS.Serialisation +import Wire.Arbitrary + +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6-4 +newtype ProtocolVersion = ProtocolVersion {pvNumber :: Word16} + deriving newtype (Eq, Ord, Show, Binary, Arbitrary, ParseMLS, SerialiseMLS) + +data ProtocolVersionTag = ProtocolMLS10 | ProtocolMLSDraft11 + deriving stock (Bounded, Enum, Eq, Show, Generic) + deriving (Arbitrary) via GenericUniform ProtocolVersionTag + +pvTag :: ProtocolVersion -> Maybe ProtocolVersionTag +pvTag (ProtocolVersion v) = case v of + 1 -> pure ProtocolMLS10 + -- used by openmls + 200 -> pure ProtocolMLSDraft11 + _ -> Nothing + +protocolVersionFromTag :: ProtocolVersionTag -> ProtocolVersion +protocolVersionFromTag ProtocolMLS10 = ProtocolVersion 1 +protocolVersionFromTag ProtocolMLSDraft11 = ProtocolVersion 200 + +defaultProtocolVersion :: ProtocolVersion +defaultProtocolVersion = protocolVersionFromTag ProtocolMLS10 diff --git a/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs b/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs deleted file mode 100644 index 38772d5b00c..00000000000 --- a/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs +++ /dev/null @@ -1,121 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 . -{-# LANGUAGE RecordWildCards #-} - -module Wire.API.MLS.PublicGroupState where - -import Data.Binary -import Data.Binary.Get -import Data.Binary.Put -import qualified Data.ByteString.Lazy as LBS -import qualified Data.Swagger as S -import Imports -import Test.QuickCheck hiding (label) -import Wire.API.MLS.CipherSuite -import Wire.API.MLS.Epoch -import Wire.API.MLS.Extension -import Wire.API.MLS.Group -import Wire.API.MLS.KeyPackage -import Wire.API.MLS.Serialisation -import Wire.Arbitrary - -data PublicGroupStateTBS = PublicGroupStateTBS - { pgsVersion :: ProtocolVersion, - pgsCipherSuite :: CipherSuite, - pgsGroupId :: GroupId, - pgsEpoch :: Epoch, - pgsTreeHash :: ByteString, - pgsInterimTranscriptHash :: ByteString, - pgsConfirmedInterimTranscriptHash :: ByteString, - pgsGroupContextExtensions :: ByteString, - pgsOtherExtensions :: ByteString, - pgsExternalPub :: ByteString, - pgsSigner :: KeyPackageRef - } - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform PublicGroupStateTBS) - -instance ParseMLS PublicGroupStateTBS where - parseMLS = - PublicGroupStateTBS - <$> label "pgsVersion" parseMLS - <*> label "pgsCipherSuite" parseMLS - <*> label "pgsGroupId" parseMLS - <*> label "pgsEpoch" parseMLS - <*> label "pgsTreeHash" (parseMLSBytes @Word8) - <*> label "pgsInterimTranscriptHash" (parseMLSBytes @Word8) - <*> label "pgsConfirmedInterimTranscriptHash" (parseMLSBytes @Word8) - <*> label "pgsGroupContextExtensions" (parseMLSBytes @Word32) - <*> label "pgsOtherExtensions" (parseMLSBytes @Word32) - <*> label "pgsExternalPub" (parseMLSBytes @Word16) - <*> label "pgsSigner" parseMLS - -instance SerialiseMLS PublicGroupStateTBS where - serialiseMLS (PublicGroupStateTBS {..}) = do - serialiseMLS pgsVersion - serialiseMLS pgsCipherSuite - serialiseMLS pgsGroupId - serialiseMLS pgsEpoch - serialiseMLSBytes @Word8 pgsTreeHash - serialiseMLSBytes @Word8 pgsInterimTranscriptHash - serialiseMLSBytes @Word8 pgsConfirmedInterimTranscriptHash - serialiseMLSBytes @Word32 pgsGroupContextExtensions - serialiseMLSBytes @Word32 pgsOtherExtensions - serialiseMLSBytes @Word16 pgsExternalPub - serialiseMLS pgsSigner - -data PublicGroupState = PublicGroupState - { pgTBS :: RawMLS PublicGroupStateTBS, - pgSignature :: ByteString - } - deriving stock (Eq, Show, Generic) - --- | A type that holds an MLS-encoded 'PublicGroupState' value via --- 'serialiseMLS'. -newtype OpaquePublicGroupState = OpaquePublicGroupState - {unOpaquePublicGroupState :: ByteString} - deriving (Generic, Eq, Show) - deriving (Arbitrary) via (GenericUniform OpaquePublicGroupState) - -instance ParseMLS OpaquePublicGroupState where - parseMLS = OpaquePublicGroupState . LBS.toStrict <$> getRemainingLazyByteString - -instance SerialiseMLS OpaquePublicGroupState where - serialiseMLS (OpaquePublicGroupState bs) = putByteString bs - -instance S.ToSchema OpaquePublicGroupState where - declareNamedSchema _ = pure (mlsSwagger "OpaquePublicGroupState") - -toOpaquePublicGroupState :: RawMLS PublicGroupState -> OpaquePublicGroupState -toOpaquePublicGroupState = OpaquePublicGroupState . rmRaw - -instance Arbitrary PublicGroupState where - arbitrary = - PublicGroupState - <$> (mkRawMLS <$> arbitrary) - <*> arbitrary - -instance ParseMLS PublicGroupState where - parseMLS = - PublicGroupState - <$> label "pgTBS" parseMLS - <*> label "pgSignature" (parseMLSBytes @Word16) - -instance SerialiseMLS PublicGroupState where - serialiseMLS PublicGroupState {..} = do - serialiseMLS pgTBS - serialiseMLSBytes @Word16 pgSignature diff --git a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs index 0881c317739..ebb4b4307a8 100644 --- a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE BinaryLiterals #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -18,6 +21,9 @@ module Wire.API.MLS.Serialisation ( ParseMLS (..), SerialiseMLS (..), + VarInt (..), + parseMLSStream, + serialiseMLSStream, parseMLSVector, serialiseMLSVector, parseMLSBytes, @@ -42,6 +48,7 @@ module Wire.API.MLS.Serialisation mlsSwagger, parseRawMLS, mkRawMLS, + traceMLS, ) where @@ -52,9 +59,10 @@ import Data.Aeson (FromJSON (..)) import qualified Data.Aeson as Aeson import Data.Bifunctor import Data.Binary -import Data.Binary.Builder +import Data.Binary.Builder (toLazyByteString) import Data.Binary.Get import Data.Binary.Put +import Data.Bits import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import Data.Json.Util @@ -63,7 +71,9 @@ import Data.Proxy import Data.Schema import qualified Data.Swagger as S import qualified Data.Text as Text +import Debug.Trace import Imports +import Test.QuickCheck (Arbitrary (..), chooseInt) -- | Parse a value encoded using the "TLS presentation" format. class ParseMLS a where @@ -73,6 +83,55 @@ class ParseMLS a where class SerialiseMLS a where serialiseMLS :: a -> Put +-- | An integer value serialised with a variable-size encoding. +-- +-- The underlying Word32 must be strictly less than 2^30. +newtype VarInt = VarInt {unVarInt :: Word32} + deriving newtype (Eq, Ord, Num, Enum, Integral, Real, Show) + +instance Arbitrary VarInt where + arbitrary = fromIntegral <$> chooseInt (0, 1073741823) + +-- From the MLS spec: +-- +-- Prefix | Length | Usable Bits | Min | Max +-- -------+--------+-------------+-----+--------- +-- 00 1 6 0 63 +-- 01 2 14 64 16383 +-- 10 4 30 16384 1073741823 +-- 11 invalid - - - +-- +instance Binary VarInt where + put :: VarInt -> Put + put (VarInt w) + | w < 64 = putWord8 (fromIntegral w) + | w < 16384 = putWord16be (0x4000 .|. fromIntegral w) + | w < 1073741824 = putWord32be (0x80000000 .|. w) + | otherwise = error "invalid VarInt" + + get :: Get VarInt + get = do + w <- lookAhead getWord8 + case shiftR (w .&. 0xc0) 6 of + 0b00 -> VarInt . fromIntegral <$> getWord8 + 0b01 -> VarInt . (.&. 0x3fff) . fromIntegral <$> getWord16be + 0b10 -> VarInt . (.&. 0x3fffffff) . fromIntegral <$> getWord32be + _ -> fail "invalid VarInt prefix" + +instance SerialiseMLS VarInt where serialiseMLS = put + +instance ParseMLS VarInt where parseMLS = get + +parseMLSStream :: Get a -> Get [a] +parseMLSStream p = do + e <- isEmpty + if e + then pure [] + else (:) <$> p <*> parseMLSStream p + +serialiseMLSStream :: (a -> Put) -> [a] -> Put +serialiseMLSStream = traverse_ + parseMLSVector :: forall w a. (Binary w, Integral w) => Get a -> Get [a] parseMLSVector getItem = do len <- get @w @@ -139,19 +198,19 @@ serialiseMLSEnum :: Put serialiseMLSEnum = put . fromMLSEnum @w -data MLSEnumError = MLSEnumUnknown | MLSEnumInvalid +data MLSEnumError = MLSEnumUnknown Int | MLSEnumInvalid toMLSEnum' :: forall a w. (Bounded a, Enum a, Integral w) => w -> Either MLSEnumError a toMLSEnum' w = case fromIntegral w - 1 of n | n < 0 -> Left MLSEnumInvalid - | n < fromEnum @a minBound || n > fromEnum @a maxBound -> Left MLSEnumUnknown + | n < fromEnum @a minBound || n > fromEnum @a maxBound -> Left (MLSEnumUnknown n) | otherwise -> pure (toEnum n) toMLSEnum :: forall a w f. (Bounded a, Enum a, MonadFail f, Integral w) => String -> w -> f a toMLSEnum name = either err pure . toMLSEnum' where - err MLSEnumUnknown = fail $ "Unknown " <> name + err (MLSEnumUnknown value) = fail $ "Unknown " <> name <> ": " <> show value err MLSEnumInvalid = fail $ "Invalid " <> name fromMLSEnum :: (Integral w, Enum a) => a -> w @@ -205,11 +264,14 @@ decodeMLSWith' p = decodeMLSWith p . LBS.fromStrict -- retain the original serialised bytes (e.g. for signature verification, or to -- forward them verbatim). data RawMLS a = RawMLS - { rmRaw :: ByteString, - rmValue :: a + { raw :: ByteString, + value :: a } deriving stock (Eq, Show, Foldable) +instance (Arbitrary a, SerialiseMLS a) => Arbitrary (RawMLS a) where + arbitrary = mkRawMLS <$> arbitrary + -- | A schema for a raw MLS object. -- -- This can be used for embedding MLS objects into JSON. It expresses the @@ -219,7 +281,7 @@ data RawMLS a = RawMLS -- Note that a 'ValueSchema' for the underlying type @a@ is /not/ required. rawMLSSchema :: Text -> (ByteString -> Either Text a) -> ValueSchema NamedSwaggerDoc (RawMLS a) rawMLSSchema name p = - (toBase64Text . rmRaw) + (toBase64Text . raw) .= parsedText name (rawMLSFromText p) mlsSwagger :: Text -> S.NamedSchema @@ -260,7 +322,15 @@ instance ParseMLS a => ParseMLS (RawMLS a) where parseMLS = parseRawMLS parseMLS instance SerialiseMLS (RawMLS a) where - serialiseMLS = putByteString . rmRaw + serialiseMLS = putByteString . raw mkRawMLS :: SerialiseMLS a => a -> RawMLS a mkRawMLS x = RawMLS (LBS.toStrict (runPut (serialiseMLS x))) x + +traceMLS :: Show a => String -> Get a -> Get a +traceMLS l g = do + begin <- bytesRead + r <- g + end <- bytesRead + traceM $ l <> " " <> show begin <> ":" <> show end <> " " <> show r + pure r diff --git a/libs/wire-api/src/Wire/API/MLS/Servant.hs b/libs/wire-api/src/Wire/API/MLS/Servant.hs index 33831f241b9..9a34c399de7 100644 --- a/libs/wire-api/src/Wire/API/MLS/Servant.hs +++ b/libs/wire-api/src/Wire/API/MLS/Servant.hs @@ -15,17 +15,14 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.MLS.Servant (MLS, mimeUnrenderMLSWith, CommitBundleMimeType) where +module Wire.API.MLS.Servant (MLS, mimeUnrenderMLSWith) where import Data.Bifunctor import Data.Binary -import qualified Data.ByteString.Lazy as LBS import qualified Data.Text as T import Imports import Network.HTTP.Media ((//)) import Servant.API hiding (Get) -import Wire.API.MLS.CommitBundle -import Wire.API.MLS.PublicGroupState (OpaquePublicGroupState, unOpaquePublicGroupState) import Wire.API.MLS.Serialisation data MLS @@ -36,19 +33,8 @@ instance Accept MLS where instance {-# OVERLAPPABLE #-} ParseMLS a => MimeUnrender MLS a where mimeUnrender _ = mimeUnrenderMLSWith parseMLS -instance MimeRender MLS OpaquePublicGroupState where - mimeRender _ = LBS.fromStrict . unOpaquePublicGroupState +instance {-# OVERLAPPABLE #-} SerialiseMLS a => MimeRender MLS a where + mimeRender _ = encodeMLS mimeUnrenderMLSWith :: Get a -> LByteString -> Either String a mimeUnrenderMLSWith p = first T.unpack . decodeMLSWith p - -data CommitBundleMimeType - -instance Accept CommitBundleMimeType where - contentType _ = "application" // "x-protobuf" - -instance MimeUnrender CommitBundleMimeType CommitBundle where - mimeUnrender _ = first T.unpack . deserializeCommitBundle . LBS.toStrict - -instance MimeRender CommitBundleMimeType CommitBundle where - mimeRender _ = LBS.fromStrict . serializeCommitBundle diff --git a/libs/wire-api/src/Wire/API/MLS/Validation.hs b/libs/wire-api/src/Wire/API/MLS/Validation.hs new file mode 100644 index 00000000000..bb13cd4cd76 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/Validation.hs @@ -0,0 +1,125 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.MLS.Validation + ( -- * Main key package validation function + validateKeyPackage, + validateLeafNode, + ) +where + +import Control.Applicative +import Imports +import Wire.API.MLS.Capabilities +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Credential +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode +import Wire.API.MLS.Lifetime +import Wire.API.MLS.ProtocolVersion +import Wire.API.MLS.Serialisation + +validateKeyPackage :: + Maybe ClientIdentity -> + KeyPackage -> + Either Text (CipherSuiteTag, Lifetime) +validateKeyPackage mIdentity kp = do + -- get ciphersuite + cs <- + maybe + (Left "Unsupported ciphersuite") + pure + $ cipherSuiteTag kp.cipherSuite + + -- validate signature + unless + ( csVerifySignatureWithLabel + cs + kp.leafNode.signatureKey + "KeyPackageTBS" + kp.tbs + kp.signature_ + ) + $ Left "Invalid KeyPackage signature" + + -- validate protocol version + maybe + (Left "Unsupported protocol version") + pure + (pvTag (kp.protocolVersion) >>= guard . (== ProtocolMLS10)) + + -- validate credential, lifetime and capabilities + validateLeafNode cs mIdentity LeafNodeTBSExtraKeyPackage kp.leafNode + + lt <- case kp.leafNode.source of + LeafNodeSourceKeyPackage lt -> pure lt + -- unreachable + _ -> Left "Unexpected leaf node source" + + pure (cs, lt) + +validateLeafNode :: + CipherSuiteTag -> + Maybe ClientIdentity -> + LeafNodeTBSExtra -> + LeafNode -> + Either Text () +validateLeafNode cs mIdentity extra leafNode = do + let tbs = LeafNodeTBS leafNode.core extra + unless + ( csVerifySignatureWithLabel + cs + leafNode.signatureKey + "LeafNodeTBS" + (mkRawMLS tbs) + leafNode.signature_ + ) + $ Left "Invalid LeafNode signature" + + validateCredential mIdentity leafNode.credential + validateSource extra.tag leafNode.source + validateCapabilities leafNode.capabilities + +validateCredential :: Maybe ClientIdentity -> Credential -> Either Text () +validateCredential mIdentity (BasicCredential cred) = do + identity <- + either credentialError pure $ + decodeMLS' cred + unless (maybe True (identity ==) mIdentity) $ + Left "client identity does not match credential identity" + where + credentialError e = + Left $ + "Failed to parse identity: " <> e + +validateSource :: LeafNodeSourceTag -> LeafNodeSource -> Either Text () +validateSource t s = do + let t' = leafNodeSourceTag s + if t == t' + then pure () + else + Left $ + "Expected '" + <> t.name + <> "' source, got '" + <> t'.name + <> "'" + +validateCapabilities :: Capabilities -> Either Text () +validateCapabilities caps = + unless (BasicCredentialTag `elem` caps.credentials) $ + Left "missing BasicCredential capability" diff --git a/libs/wire-api/src/Wire/API/MLS/Welcome.hs b/libs/wire-api/src/Wire/API/MLS/Welcome.hs index 929dc78af52..17dc605d8c5 100644 --- a/libs/wire-api/src/Wire/API/MLS/Welcome.hs +++ b/libs/wire-api/src/Wire/API/MLS/Welcome.hs @@ -21,14 +21,13 @@ import qualified Data.Swagger as S import Imports import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit -import Wire.API.MLS.Extension import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation import Wire.Arbitrary +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-12.4.3.1-5 data Welcome = Welcome - { welProtocolVersion :: ProtocolVersion, - welCipherSuite :: CipherSuite, + { welCipherSuite :: CipherSuite, welSecrets :: [GroupSecrets], welGroupInfo :: ByteString } @@ -41,18 +40,17 @@ instance S.ToSchema Welcome where instance ParseMLS Welcome where parseMLS = Welcome - <$> parseMLS @ProtocolVersion - <*> parseMLS - <*> parseMLSVector @Word32 parseMLS - <*> parseMLSBytes @Word32 + <$> parseMLS + <*> parseMLSVector @VarInt parseMLS + <*> parseMLSBytes @VarInt instance SerialiseMLS Welcome where - serialiseMLS (Welcome pv cs ss gi) = do - serialiseMLS pv + serialiseMLS (Welcome cs ss gi) = do serialiseMLS cs - serialiseMLSVector @Word32 serialiseMLS ss - serialiseMLSBytes @Word32 gi + serialiseMLSVector @VarInt serialiseMLS ss + serialiseMLSBytes @VarInt gi +-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-12.4.3.1-5 data GroupSecrets = GroupSecrets { gsNewMember :: KeyPackageRef, gsSecrets :: HPKECiphertext diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 21d8b3a85ec..b9ca600b367 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -14,7 +14,6 @@ -- -- 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.OAuth where 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 8acf7709411..50285d680b4 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -29,10 +29,6 @@ module Wire.API.Routes.Internal.Brig DeleteAccountConferenceCallingConfig, swaggerDoc, module Wire.API.Routes.Internal.Brig.EJPD, - NewKeyPackageRef (..), - NewKeyPackage (..), - NewKeyPackageResult (..), - DeleteKeyPackageRefsRequest (..), ) where @@ -51,8 +47,7 @@ import Servant.Swagger.Internal.Orphans () import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig -import Wire.API.MLS.Credential -import Wire.API.MLS.KeyPackage +import Wire.API.MLS.CipherSuite (SignatureSchemeTag) import Wire.API.MakesFederatedCall import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Brig.EJPD @@ -182,138 +177,7 @@ instance ToSchema NewKeyPackageRef where <*> nkprClientId .= field "client_id" schema <*> nkprConversation .= field "conversation" schema -data NewKeyPackage = NewKeyPackage - { nkpConversation :: Qualified ConvId, - nkpKeyPackage :: KeyPackageData - } - deriving stock (Eq, Show, Generic) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema NewKeyPackage) - -instance ToSchema NewKeyPackage where - schema = - object "NewKeyPackage" $ - NewKeyPackage - <$> nkpConversation .= field "conversation" schema - <*> nkpKeyPackage .= field "key_package" schema - -data NewKeyPackageResult = NewKeyPackageResult - { nkpresClientIdentity :: ClientIdentity, - nkpresKeyPackageRef :: KeyPackageRef - } - deriving stock (Eq, Show, Generic) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema NewKeyPackageResult) - -instance ToSchema NewKeyPackageResult where - schema = - object "NewKeyPackageResult" $ - NewKeyPackageResult - <$> nkpresClientIdentity .= field "client_identity" schema - <*> nkpresKeyPackageRef .= field "key_package_ref" schema - -newtype DeleteKeyPackageRefsRequest = DeleteKeyPackageRefsRequest {unDeleteKeyPackageRefsRequest :: [KeyPackageRef]} - deriving (Eq, Show) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema DeleteKeyPackageRefsRequest) - -instance ToSchema DeleteKeyPackageRefsRequest where - schema = - object "DeleteKeyPackageRefsRequest" $ - DeleteKeyPackageRefsRequest - <$> unDeleteKeyPackageRefsRequest .= field "key_package_refs" (array schema) - -type MLSAPI = - "mls" - :> ( ( "key-packages" - :> ( ( Capture "ref" KeyPackageRef - :> ( Named - "get-client-by-key-package-ref" - ( Summary "Resolve an MLS key package ref to a qualified client ID" - :> MultiVerb - 'GET - '[Servant.JSON] - '[ RespondEmpty 404 "Key package ref not found", - Respond 200 "Key package ref found" ClientIdentity - ] - (Maybe ClientIdentity) - ) - :<|> ( "conversation" - :> ( PutConversationByKeyPackageRef - :<|> GetConversationByKeyPackageRef - ) - ) - :<|> Named - "put-key-package-ref" - ( Summary "Create a new KeyPackageRef mapping" - :> ReqBody '[Servant.JSON] NewKeyPackageRef - :> MultiVerb - 'PUT - '[Servant.JSON] - '[RespondEmpty 201 "Key package ref mapping created"] - () - ) - :<|> Named - "post-key-package-ref" - ( Summary "Update a KeyPackageRef in mapping" - :> ReqBody '[Servant.JSON] KeyPackageRef - :> MultiVerb - 'POST - '[Servant.JSON] - '[RespondEmpty 201 "Key package ref mapping updated"] - () - ) - ) - ) - :<|> Named - "delete-key-package-refs" - ( Summary "Delete a batch of KeyPackageRef mappings" - :> ReqBody '[Servant.JSON] DeleteKeyPackageRefsRequest - :> MultiVerb - 'DELETE - '[Servant.JSON] - '[RespondEmpty 200 "Key package ref mappings deleted"] - () - ) - ) - ) - :<|> GetMLSClients - :<|> MapKeyPackageRefs - :<|> Named - "put-key-package-add" - ( "key-package-add" - :> ReqBody '[Servant.JSON] NewKeyPackage - :> MultiVerb1 - 'PUT - '[Servant.JSON] - (Respond 200 "Key package ref mapping updated" NewKeyPackageResult) - ) - ) - -type PutConversationByKeyPackageRef = - Named - "put-conversation-by-key-package-ref" - ( Summary "Associate a conversation with a key package" - :> ReqBody '[Servant.JSON] (Qualified ConvId) - :> MultiVerb - 'PUT - '[Servant.JSON] - [ RespondEmpty 404 "No key package found by reference", - RespondEmpty 204 "Converstaion associated" - ] - Bool - ) - -type GetConversationByKeyPackageRef = - Named - "get-conversation-by-key-package-ref" - ( Summary - "Retrieve the conversation associated with a key package" - :> MultiVerb - 'GET - '[Servant.JSON] - [ RespondEmpty 404 "No associated conversation or bad key package", - Respond 200 "Conversation found" (Qualified ConvId) - ] - (Maybe (Qualified ConvId)) - ) +type MLSAPI = "mls" :> GetMLSClients type GetMLSClients = Summary "Return all clients and all MLS-capable clients of a user" @@ -326,12 +190,6 @@ type GetMLSClients = '[Servant.JSON] (Respond 200 "MLS clients" (Set ClientInfo)) -type MapKeyPackageRefs = - Summary "Insert bundle into the KeyPackage ref mapping. Only for tests." - :> "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/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 7519255d916..b0da0f7509e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -33,7 +33,7 @@ import Wire.API.Conversation.Typing import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation -import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.GroupInfo import Wire.API.MLS.Servant import Wire.API.MLS.SubConversation import Wire.API.MakesFederatedCall @@ -213,7 +213,7 @@ type ConversationAPI = ( Respond 200 "The group information" - OpaquePublicGroupState + GroupInfoData ) ) :<|> Named @@ -371,7 +371,6 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-created" :> Until 'V3 :> CanThrow 'ConvAccessDenied - :> CanThrow 'MLSMissingSenderClient :> CanThrow 'MLSNonEmptyMemberList :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected @@ -380,7 +379,6 @@ type ConversationAPI = :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" :> ZLocalUser - :> ZOptClient :> ZOptConn :> "conversations" :> VersionedReqBody 'V2 '[Servant.JSON] NewConv @@ -394,7 +392,6 @@ type ConversationAPI = :> From 'V3 :> Until 'V4 :> CanThrow 'ConvAccessDenied - :> CanThrow 'MLSMissingSenderClient :> CanThrow 'MLSNonEmptyMemberList :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected @@ -403,7 +400,6 @@ type ConversationAPI = :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" :> ZLocalUser - :> ZOptClient :> ZOptConn :> "conversations" :> ReqBody '[Servant.JSON] NewConv @@ -415,7 +411,6 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-created" :> From 'V4 :> CanThrow 'ConvAccessDenied - :> CanThrow 'MLSMissingSenderClient :> CanThrow 'MLSNonEmptyMemberList :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected @@ -424,7 +419,6 @@ type ConversationAPI = :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" :> ZLocalUser - :> ZOptClient :> ZOptConn :> "conversations" :> ReqBody '[Servant.JSON] NewConv @@ -551,7 +545,7 @@ type ConversationAPI = ( Respond 200 "The group information" - OpaquePublicGroupState + GroupInfoData ) ) -- This endpoint can lead to the following events being sent: @@ -1261,7 +1255,6 @@ type ConversationAPI = :> CanThrow 'ConvInvalidProtocolTransition :> CanThrow 'ConvMemberNotFound :> ZLocalUser - :> ZClient :> ZConn :> "conversations" :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index 9d010bf6e97..b0c9dce832c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -21,109 +21,53 @@ import Servant hiding (WithStatus) import Servant.Swagger.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.Event.Conversation import Wire.API.MLS.CommitBundle import Wire.API.MLS.Keys import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.Servant -import Wire.API.MLS.Welcome import Wire.API.MakesFederatedCall import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public -import Wire.API.Routes.Version type MLSMessagingAPI = Named - "mls-welcome-message" - ( Summary "Post an MLS welcome message" - :> Until 'V3 - :> MakesFederatedCall 'Galley "mls-welcome" - :> CanThrow 'MLSKeyPackageRefNotFound + "mls-message" + ( Summary "Post an MLS message" + :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Galley "send-mls-message" + :> MakesFederatedCall 'Galley "on-conversation-updated" + :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> MakesFederatedCall 'Galley "on-new-remote-subconversation" + :> MakesFederatedCall 'Brig "get-mls-clients" + :> MakesFederatedCall 'Galley "on-delete-mls-conversation" + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvMemberNotFound + :> CanThrow 'ConvNotFound + :> CanThrow 'LegalHoldNotEnabled + :> CanThrow 'MissingLegalholdConsent + :> CanThrow 'MLSClientMismatch + :> CanThrow 'MLSClientSenderUserMismatch + :> CanThrow 'MLSCommitMissingReferences + :> CanThrow 'MLSGroupConversationMismatch + :> CanThrow 'MLSInvalidLeafNodeIndex :> CanThrow 'MLSNotEnabled - :> "welcome" + :> CanThrow 'MLSProposalNotFound + :> CanThrow 'MLSProtocolErrorTag + :> CanThrow 'MLSSelfRemovalNotAllowed + :> CanThrow 'MLSStaleMessage + :> CanThrow 'MLSSubConvClientNotInParent + :> CanThrow 'MLSUnsupportedMessage + :> CanThrow 'MLSUnsupportedProposal + :> CanThrow MLSProposalFailure + :> "messages" :> ZLocalUser + :> ZClient :> ZConn - :> ReqBody '[MLS] (RawMLS Welcome) - :> MultiVerb1 'POST '[JSON] (RespondEmpty 201 "Welcome message sent") + :> ReqBody '[MLS] (RawMLS Message) + :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" MLSMessageSendingStatus) ) - :<|> Named - "mls-message-v1" - ( Summary "Post an MLS message" - :> MakesFederatedCall 'Brig "get-mls-clients" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" - :> MakesFederatedCall 'Galley "send-mls-message" - :> Until 'V2 - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvMemberNotFound - :> CanThrow 'ConvNotFound - :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'MLSClientMismatch - :> CanThrow 'MLSCommitMissingReferences - :> CanThrow 'MLSKeyPackageRefNotFound - :> CanThrow 'MLSNotEnabled - :> CanThrow 'MLSProposalNotFound - :> CanThrow 'MLSProtocolErrorTag - :> CanThrow 'MLSSelfRemovalNotAllowed - :> CanThrow 'MLSStaleMessage - :> CanThrow 'MLSUnsupportedMessage - :> CanThrow 'MLSUnsupportedProposal - :> CanThrow 'MLSClientSenderUserMismatch - :> CanThrow 'MLSGroupConversationMismatch - :> CanThrow 'MLSMissingSenderClient - :> CanThrow 'MissingLegalholdConsent - :> CanThrow 'MLSSubConvClientNotInParent - :> CanThrow MLSProposalFailure - :> "messages" - :> ZLocalUser - :> ZOptClient - :> ZConn - :> ReqBody '[MLS] (RawMLS SomeMessage) - :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" [Event]) - ) - :<|> Named - "mls-message" - ( Summary "Post an MLS message" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "send-mls-message" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" - :> MakesFederatedCall 'Brig "get-mls-clients" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" - :> From 'V2 - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvMemberNotFound - :> CanThrow 'ConvNotFound - :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'MLSClientMismatch - :> CanThrow 'MLSCommitMissingReferences - :> CanThrow 'MLSKeyPackageRefNotFound - :> CanThrow 'MLSNotEnabled - :> CanThrow 'MLSProposalNotFound - :> CanThrow 'MLSProtocolErrorTag - :> CanThrow 'MLSSelfRemovalNotAllowed - :> CanThrow 'MLSStaleMessage - :> CanThrow 'MLSUnsupportedMessage - :> CanThrow 'MLSUnsupportedProposal - :> CanThrow 'MLSClientSenderUserMismatch - :> CanThrow 'MLSGroupConversationMismatch - :> CanThrow 'MLSMissingSenderClient - :> CanThrow 'MissingLegalholdConsent - :> CanThrow 'MLSSubConvClientNotInParent - :> CanThrow MLSProposalFailure - :> "messages" - :> ZLocalUser - :> ZOptClient - :> ZConn - :> ReqBody '[MLS] (RawMLS SomeMessage) - :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" MLSMessageSendingStatus) - ) :<|> Named "mls-commit-bundle" ( Summary "Post a MLS CommitBundle" @@ -135,39 +79,36 @@ type MLSMessagingAPI = :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Brig "get-mls-clients" :> MakesFederatedCall 'Galley "on-delete-mls-conversation" - :> From 'V4 :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvMemberNotFound :> CanThrow 'ConvNotFound :> CanThrow 'LegalHoldNotEnabled + :> CanThrow 'MissingLegalholdConsent :> CanThrow 'MLSClientMismatch + :> CanThrow 'MLSClientSenderUserMismatch :> CanThrow 'MLSCommitMissingReferences - :> CanThrow 'MLSKeyPackageRefNotFound + :> CanThrow 'MLSGroupConversationMismatch + :> CanThrow 'MLSInvalidLeafNodeIndex :> CanThrow 'MLSNotEnabled :> CanThrow 'MLSProposalNotFound :> CanThrow 'MLSProtocolErrorTag :> CanThrow 'MLSSelfRemovalNotAllowed :> CanThrow 'MLSStaleMessage + :> CanThrow 'MLSSubConvClientNotInParent :> CanThrow 'MLSUnsupportedMessage :> CanThrow 'MLSUnsupportedProposal - :> CanThrow 'MLSClientSenderUserMismatch - :> CanThrow 'MLSGroupConversationMismatch - :> CanThrow 'MLSMissingSenderClient :> CanThrow 'MLSWelcomeMismatch - :> CanThrow 'MissingLegalholdConsent - :> CanThrow 'MLSSubConvClientNotInParent :> CanThrow MLSProposalFailure :> "commit-bundles" :> ZLocalUser - :> ZOptClient + :> ZClient :> ZConn - :> ReqBody '[CommitBundleMimeType] CommitBundle + :> ReqBody '[MLS] (RawMLS CommitBundle) :> MultiVerb1 'POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus) ) :<|> Named "mls-public-keys" ( Summary "Get public keys used by the backend to sign external proposals" - :> From 'V4 :> CanThrow 'MLSNotEnabled :> "public-keys" :> ZLocalUser diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index 397ee41cd99..45053ff4a57 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -100,8 +100,8 @@ import Deriving.Swagger StripPrefix, ) import Imports -import Wire.API.MLS.Credential -import Wire.API.User.Auth (CookieLabel) +import Wire.API.MLS.CipherSuite +import Wire.API.User.Auth import Wire.API.User.Client.Prekey as Prekey import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..), generateExample, mapOf', setOf') diff --git a/libs/wire-api/test/golden.hs b/libs/wire-api/test/golden.hs new file mode 100644 index 00000000000..0ff7c7e4ca8 --- /dev/null +++ b/libs/wire-api/test/golden.hs @@ -0,0 +1,5 @@ +import Imports +import qualified Test.Wire.API.Golden.Run as Run + +main :: IO () +main = Run.main diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Client_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Client_user.hs index 65b4ad7d34a..c137ae7b691 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Client_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Client_user.hs @@ -25,7 +25,7 @@ import qualified Data.Map as Map import Data.Misc import Data.Set as Set import Imports -import Wire.API.MLS.Credential +import Wire.API.MLS.CipherSuite import Wire.API.User.Auth (CookieLabel (CookieLabel, cookieLabelText)) import Wire.API.User.Client diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs index 6af4068cdbb..fbe5d29ac01 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs @@ -26,7 +26,7 @@ import Data.Range (unsafeRange) import qualified Data.Set as Set import Data.Text.Ascii (AsciiChars (validate)) import Imports (Maybe (Just, Nothing), fromRight, mempty, undefined) -import Wire.API.MLS.Credential +import Wire.API.MLS.CipherSuite import Wire.API.User.Auth (CookieLabel (CookieLabel, cookieLabelText)) import Wire.API.User.Client import Wire.API.User.Client.Prekey diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UpdateClient_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UpdateClient_user.hs index 5f164f77cf5..655532ef6a0 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UpdateClient_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UpdateClient_user.hs @@ -21,7 +21,7 @@ module Test.Wire.API.Golden.Generated.UpdateClient_user where import qualified Data.Map as Map import Imports -import Wire.API.MLS.Credential +import Wire.API.MLS.CipherSuite import Wire.API.User.Client import Wire.API.User.Client.Prekey diff --git a/libs/wire-api/test/golden/Main.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Run.hs similarity index 96% rename from libs/wire-api/test/golden/Main.hs rename to libs/wire-api/test/golden/Test/Wire/API/Golden/Run.hs index 7ad5b57d554..e1e110783ec 100644 --- a/libs/wire-api/test/golden/Main.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Run.hs @@ -15,10 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main - ( main, - ) -where +module Test.Wire.API.Golden.Run (main) where import Imports import Test.Tasty diff --git a/libs/wire-api/test/resources/key_package1.mls b/libs/wire-api/test/resources/key_package1.mls deleted file mode 100644 index 8023c6907928ca58c8e3ff3ec2ee3ce4bc1f99eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 262 zcmZQ%U}R8Wv69?(F|yfHt1|cP`k+T!{tMT&OaFhpVnOEKk5P}TGJ$GrlT1ua6HSZ_ zbuCkp%ydmm3`}$_%u>yD%@U1MlTyu%(~=X@tc(*=O)U&fO&xM`iuF<}5_1c3QuUJa zb2-=<6eRoCUUAX8I9u(~{?F_3HlM$$+4G&jUgc-^p|73G%uh{bU|=u+wmWU{gcM_KBO8 z$-m?@jpivxotf{>a^dHN=Z{X`u=~~BsjvF0DC5uqrY-N}QZ|)ea}P8RdZ?k~_=+6> DHm_P$ diff --git a/libs/wire-api/test/unit.hs b/libs/wire-api/test/unit.hs new file mode 100644 index 00000000000..dbf3fb9acb9 --- /dev/null +++ b/libs/wire-api/test/unit.hs @@ -0,0 +1,5 @@ +import Imports +import qualified Test.Wire.API.Run as Run + +main :: IO () +main = Run.main diff --git a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs index ea608ae122a..10ec20569a4 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs @@ -19,36 +19,36 @@ module Test.Wire.API.MLS where import Control.Concurrent.Async import qualified Crypto.PubKey.Ed25519 as Ed25519 -import Data.ByteArray +import Data.ByteArray hiding (length) import qualified Data.ByteString as BS -import qualified Data.ByteString.Lazy as LBS +import qualified Data.ByteString.Char8 as B8 import Data.Domain -import Data.Either.Combinators -import Data.Hex import Data.Id import Data.Json.Util (toBase64Text) import Data.Qualified import qualified Data.Text as T import qualified Data.Text as Text -import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID import Imports import System.Exit import System.FilePath (()) import System.Process +import System.Random import Test.Tasty import Test.Tasty.HUnit import UnliftIO (withSystemTempDirectory) +import Wire.API.MLS.AuthenticatedContent import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.Credential import Wire.API.MLS.Epoch -import Wire.API.MLS.Extension import Wire.API.MLS.Group +import Wire.API.MLS.GroupInfo +import Wire.API.MLS.HPKEPublicKey import Wire.API.MLS.KeyPackage import Wire.API.MLS.Message import Wire.API.MLS.Proposal -import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.ProtocolVersion import Wire.API.MLS.Serialisation import Wire.API.MLS.Welcome @@ -58,117 +58,140 @@ tests = [ testCase "parse key package" testParseKeyPackage, testCase "parse commit message" testParseCommit, testCase "parse application message" testParseApplication, - testCase "parse welcome message" testParseWelcome, + testCase "parse welcome and groupinfo message" testParseWelcomeAndGroupInfo, testCase "key package ref" testKeyPackageRef, - testCase "validate message signature" testVerifyMLSPlainTextWithKey, - testCase "create signed remove proposal" testRemoveProposalMessageSignature, - testCase "parse GroupInfoBundle" testParseGroupInfoBundle -- TODO: remove this also + testCase "create signed remove proposal" testRemoveProposalMessageSignature ] testParseKeyPackage :: IO () testParseKeyPackage = do - kpData <- BS.readFile "test/resources/key_package1.mls" + alice <- randomIdentity + let qcid = B8.unpack (encodeMLS' alice) + kpData <- withSystemTempDirectory "mls" $ \tmp -> do + void $ spawn (cli qcid tmp ["init", qcid]) Nothing + spawn (cli qcid tmp ["key-package", "create"]) Nothing + kp <- case decodeMLS' @KeyPackage kpData of Left err -> assertFailure (T.unpack err) Right x -> pure x - pvTag (kpProtocolVersion kp) @?= Just ProtocolMLS10 - kpCipherSuite kp @?= CipherSuite 1 - BS.length (kpInitKey kp) @?= 32 + pvTag (kp.protocolVersion) @?= Just ProtocolMLS10 + kp.cipherSuite @?= CipherSuite 1 + BS.length (unHPKEPublicKey kp.initKey) @?= 32 - case decodeMLS' @ClientIdentity (bcIdentity (kpCredential kp)) of + case keyPackageIdentity kp of Left err -> assertFailure $ "Failed to parse identity: " <> T.unpack err - Right identity -> - identity - @?= ClientIdentity - { ciDomain = Domain "mls.example.com", - ciUser = Id (fromJust (UUID.fromString "b455a431-9db6-4404-86e7-6a3ebe73fcaf")), - ciClient = newClientId 0x3ae58155 - } - - -- check raw TBS package - let rawTBS = rmRaw (kpTBS kp) - rawTBS @?= BS.take 196 kpData + Right identity -> identity @?= alice testParseCommit :: IO () testParseCommit = do - msgData <- LBS.readFile "test/resources/commit1.mls" - msg :: Message 'MLSPlainText <- case decodeMLS @SomeMessage msgData of + qcid <- B8.unpack . encodeMLS' <$> randomIdentity + commitData <- withSystemTempDirectory "mls" $ \tmp -> do + void $ spawn (cli qcid tmp ["init", qcid]) Nothing + groupJSON <- spawn (cli qcid tmp ["group", "create", "Zm9v"]) Nothing + spawn (cli qcid tmp ["commit", "--group", "-"]) (Just groupJSON) + + msg <- case decodeMLS' @Message commitData of Left err -> assertFailure (T.unpack err) - Right (SomeMessage SMLSCipherText _) -> - assertFailure "Expected plain text message, found encrypted" - Right (SomeMessage SMLSPlainText msg) -> - pure msg + Right x -> pure x + + pvTag (msg.protocolVersion) @?= Just ProtocolMLS10 - msgGroupId msg @?= "test_group" - msgEpoch msg @?= Epoch 0 + pmsg <- case msg.content of + MessagePublic x -> pure x + _ -> assertFailure "expected public message" - case msgSender msg of - MemberSender kp -> kp @?= KeyPackageRef (fromRight' (unhex "24e4b0a802a2b81f00a9af7df5e91da8")) - _ -> assertFailure "Unexpected sender type" + pmsg.content.value.sender @?= SenderMember 0 - let payload = msgPayload msg - commit <- case payload of - CommitMessage c -> pure c - _ -> assertFailure "Unexpected message type" + commit <- case pmsg.content.value.content of + FramedContentCommit c -> pure c + _ -> assertFailure "expected commit" - case cProposals commit of - [Inline (AddProposal _)] -> pure () - _ -> assertFailure "Unexpected proposals" + commit.value.proposals @?= [] testParseApplication :: IO () testParseApplication = do - msgData <- LBS.readFile "test/resources/app_message1.mls" - msg :: Message 'MLSCipherText <- case decodeMLS @SomeMessage msgData of - Left err -> assertFailure (T.unpack err) - Right (SomeMessage SMLSCipherText msg) -> pure msg - Right (SomeMessage SMLSPlainText _) -> - assertFailure "Expected encrypted message, found plain text" - - msgGroupId msg @?= "test_group" - msgEpoch msg @?= Epoch 0 - msgContentType (msgPayload msg) @?= fromMLSEnum ApplicationMessageTag - -testParseWelcome :: IO () -testParseWelcome = do - welData <- LBS.readFile "test/resources/welcome1.mls" - wel <- case decodeMLS welData of + qcid <- B8.unpack . encodeMLS' <$> randomIdentity + msgData <- withSystemTempDirectory "mls" $ \tmp -> do + void $ spawn (cli qcid tmp ["init", qcid]) Nothing + groupJSON <- spawn (cli qcid tmp ["group", "create", "Zm9v"]) Nothing + spawn (cli qcid tmp ["message", "--group", "-", "hello"]) (Just groupJSON) + + msg <- case decodeMLS' @Message msgData of Left err -> assertFailure (T.unpack err) Right x -> pure x - welCipherSuite wel @?= CipherSuite 1 - map gsNewMember (welSecrets wel) @?= [KeyPackageRef (fromRight' (unhex "ab4692703ca6d50ffdeaae3096f885c2"))] + pvTag (msg.protocolVersion) @?= Just ProtocolMLS10 + + pmsg <- case msg.content of + MessagePrivate x -> pure x.value + _ -> assertFailure "expected private message" + + pmsg.groupId @?= GroupId "foo" + pmsg.epoch @?= Epoch 0 + +testParseWelcomeAndGroupInfo :: IO () +testParseWelcomeAndGroupInfo = do + qcid <- B8.unpack . encodeMLS' <$> randomIdentity + qcid2 <- B8.unpack . encodeMLS' <$> randomIdentity + (welData, giData) <- withSystemTempDirectory "mls" $ \tmp -> do + void $ spawn (cli qcid tmp ["init", qcid]) Nothing + void $ spawn (cli qcid2 tmp ["init", qcid2]) Nothing + groupJSON <- spawn (cli qcid tmp ["group", "create", "Zm9v"]) Nothing + kp <- spawn (cli qcid2 tmp ["key-package", "create"]) Nothing + BS.writeFile (tmp "kp") kp + void $ + spawn + ( cli + qcid + tmp + [ "member", + "add", + "--group", + "-", + tmp "kp", + "--welcome-out", + tmp "welcome", + "--group-info-out", + tmp "gi" + ] + ) + (Just groupJSON) + (,) + <$> BS.readFile (tmp "welcome") + <*> BS.readFile (tmp "gi") + + do + welcomeMsg <- case decodeMLS' @Message welData of + Left err -> assertFailure (T.unpack err) + Right x -> pure x + + pvTag (welcomeMsg.protocolVersion) @?= Just ProtocolMLS10 + + wel <- case welcomeMsg.content of + MessageWelcome x -> pure x.value + _ -> assertFailure "expected welcome message" + + length (wel.welSecrets) @?= 1 + + do + gi <- case decodeMLS' @GroupInfo giData of + Left err -> assertFailure (T.unpack err) + Right x -> pure x + + gi.groupContext.groupId @?= GroupId "foo" + gi.groupContext.epoch @?= Epoch 1 testKeyPackageRef :: IO () testKeyPackageRef = do - kpData <- BS.readFile "test/resources/key_package1.mls" - ref <- KeyPackageRef <$> BS.readFile "test/resources/key_package_ref1" - kpRef MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 (KeyPackageData kpData) @?= ref + let qcid = "b455a431-9db6-4404-86e7-6a3ebe73fcaf:3ae58155@mls.example.com" + (kpData, ref) <- withSystemTempDirectory "mls" $ \tmp -> do + void $ spawn (cli qcid tmp ["init", qcid]) Nothing + kpData <- spawn (cli qcid tmp ["key-package", "create"]) Nothing + ref <- spawn (cli qcid tmp ["key-package", "ref", "-"]) (Just kpData) + pure (kpData, KeyPackageRef ref) -testVerifyMLSPlainTextWithKey :: IO () -testVerifyMLSPlainTextWithKey = do - -- this file was created with openmls from the client that is in the add proposal - msgData <- BS.readFile "test/resources/external_proposal.mls" - - msg :: Message 'MLSPlainText <- case decodeMLS' @SomeMessage msgData of - Left err -> assertFailure (T.unpack err) - Right (SomeMessage SMLSCipherText _) -> - assertFailure "Expected SomeMessage SMLSCipherText" - Right (SomeMessage SMLSPlainText msg) -> - pure msg - - kp <- case msgPayload msg of - ProposalMessage prop -> - case rmValue prop of - AddProposal kp -> pure kp - _ -> error "Expected AddProposal" - _ -> error "Expected ProposalMessage" - - let pubkey = bcSignatureKey . kpCredential . rmValue $ kp - liftIO - $ assertBool - "message signature verification failed" - $ verifyMessageSignature MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 msg pubkey + kpRef MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 (KeyPackageData kpData) @?= ref testRemoveProposalMessageSignature :: IO () testRemoveProposalMessageSignature = withSystemTempDirectory "mls" $ \tmp -> do @@ -176,32 +199,42 @@ testRemoveProposalMessageSignature = withSystemTempDirectory "mls" $ \tmp -> do let c = newClientId 0x3ae58155 usr <- flip Qualified (Domain "example.com") <$> (Id <$> UUID.nextRandom) pure (userClientQid usr c) - void . liftIO $ spawn (cli qcid tmp ["init", qcid]) Nothing + void $ spawn (cli qcid tmp ["init", qcid]) Nothing qcid2 <- do let c = newClientId 0x4ae58157 usr <- flip Qualified (Domain "example.com") <$> (Id <$> UUID.nextRandom) pure (userClientQid usr c) - void . liftIO $ spawn (cli qcid2 tmp ["init", qcid2]) Nothing - kp <- liftIO $ decodeMLSError <$> spawn (cli qcid2 tmp ["key-package", "create"]) Nothing - liftIO $ BS.writeFile (tmp qcid2) (rmRaw kp) + void $ spawn (cli qcid2 tmp ["init", qcid2]) Nothing + kp :: RawMLS KeyPackage <- + decodeMLSError <$> spawn (cli qcid2 tmp ["key-package", "create"]) Nothing + BS.writeFile (tmp qcid2) (raw kp) + secretKey <- Ed25519.generateSecretKey let groupFilename = "group" - let gid = GroupId "abcd" - createGroup tmp qcid groupFilename gid + gid = GroupId "abcd" + signerKeyFilename = "signer-key.bin" + publicKey = Ed25519.toPublic secretKey + BS.writeFile (tmp signerKeyFilename) (convert publicKey) + createGroup tmp qcid groupFilename signerKeyFilename gid - void $ liftIO $ spawn (cli qcid tmp ["member", "add", "--group", tmp groupFilename, "--in-place", tmp qcid2]) Nothing + void $ spawn (cli qcid tmp ["member", "add", "--group", tmp groupFilename, "--in-place", tmp qcid2]) Nothing - secretKey <- Ed25519.generateSecretKey - let publicKey = Ed25519.toPublic secretKey - let message = mkSignedMessage secretKey publicKey gid (Epoch 1) (ProposalMessage (mkRemoveProposal (fromJust (kpRef' kp)))) + let proposal = mkRawMLS (RemoveProposal 1) + pmessage = + mkSignedPublicMessage + secretKey + publicKey + gid + (Epoch 1) + (TaggedSenderExternal 0) + (FramedContentProposal proposal) + message = mkMessage $ MessagePublic pmessage + messageFilename = "signed-message.mls" - let messageFilename = "signed-message.mls" - BS.writeFile (tmp messageFilename) (rmRaw (mkRawMLS message)) - let signerKeyFilename = "signer-key.bin" - BS.writeFile (tmp signerKeyFilename) (convert publicKey) + BS.writeFile (tmp messageFilename) (raw (mkRawMLS message)) - void . liftIO $ + void $ spawn ( cli qcid @@ -209,65 +242,25 @@ testRemoveProposalMessageSignature = withSystemTempDirectory "mls" $ \tmp -> do [ "consume", "--group", tmp groupFilename, - "--signer-key", - tmp signerKeyFilename, tmp messageFilename ] ) Nothing -testParseGroupInfoBundle :: IO () -testParseGroupInfoBundle = withSystemTempDirectory "mls" $ \tmp -> do - qcid <- do - let c = newClientId 0x3ae58155 - usr <- flip Qualified (Domain "example.com") <$> (Id <$> UUID.nextRandom) - pure (userClientQid usr c) - void . liftIO $ spawn (cli qcid tmp ["init", qcid]) Nothing - - qcid2 <- do - let c = newClientId 0x4ae58157 - usr <- flip Qualified (Domain "example.com") <$> (Id <$> UUID.nextRandom) - pure (userClientQid usr c) - void . liftIO $ spawn (cli qcid2 tmp ["init", qcid2]) Nothing - kp :: RawMLS KeyPackage <- liftIO $ decodeMLSError <$> spawn (cli qcid2 tmp ["key-package", "create"]) Nothing - liftIO $ BS.writeFile (tmp qcid2) (rmRaw kp) - - let groupFilename = "group" - let gid = GroupId "abcd" - createGroup tmp qcid groupFilename gid - - void $ - liftIO $ - spawn - ( cli - qcid - tmp - [ "member", - "add", - "--group", - tmp groupFilename, - "--in-place", - tmp qcid2, - "--group-state-out", - tmp "group-info-bundle" - ] - ) - Nothing - - bundleBS <- BS.readFile (tmp "group-info-bundle") - case decodeMLS' @PublicGroupState bundleBS of - Left err -> assertFailure ("Failed parsing PublicGroupState: " <> T.unpack err) - Right _ -> pure () - -createGroup :: FilePath -> String -> String -> GroupId -> IO () -createGroup tmp store groupName gid = do +createGroup :: FilePath -> String -> String -> String -> GroupId -> IO () +createGroup tmp store groupName removalKey gid = do groupJSON <- liftIO $ spawn ( cli store tmp - ["group", "create", T.unpack (toBase64Text (unGroupId gid))] + [ "group", + "create", + "--removal-key", + tmp removalKey, + T.unpack (toBase64Text (unGroupId gid)) + ] ) Nothing liftIO $ BS.writeFile (tmp groupName) groupJSON @@ -281,7 +274,7 @@ userClientQid :: Qualified UserId -> ClientId -> String userClientQid usr c = show (qUnqualified usr) <> ":" - <> T.unpack (client c) + <> T.unpack c.client <> "@" <> T.unpack (domainText (qDomain usr)) @@ -306,3 +299,9 @@ cli :: String -> FilePath -> [String] -> CreateProcess cli store tmp args = proc "mls-test-cli" $ ["--store", tmp (store <> ".db")] <> args + +randomIdentity :: IO ClientIdentity +randomIdentity = do + uid <- Id <$> UUID.nextRandom + c <- newClientId <$> randomIO + pure $ ClientIdentity (Domain "mls.example.com") uid c diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs index d73620945bb..df82c017e09 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs @@ -1,4 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -15,24 +16,23 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# OPTIONS_GHC -Wwarn #-} module Test.Wire.API.Roundtrip.MLS (tests) where -import Data.Binary.Put +import Data.Hex import Imports -import qualified Proto.Mls import qualified Test.Tasty as T import Test.Tasty.QuickCheck import Type.Reflection (typeRep) -import Wire.API.ConverProtoLens +import Wire.API.MLS.Commit import Wire.API.MLS.CommitBundle +import Wire.API.MLS.Credential import Wire.API.MLS.Extension -import Wire.API.MLS.GroupInfoBundle +import Wire.API.MLS.GroupInfo import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode import Wire.API.MLS.Message import Wire.API.MLS.Proposal -import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation import Wire.API.MLS.Welcome @@ -40,17 +40,21 @@ tests :: T.TestTree tests = T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "MLS roundtrip tests" $ [ testRoundTrip @KeyPackageRef, + testRoundTrip @LeafNode, + testRoundTrip @LeafNodeCore, + testRoundTrip @KeyPackageTBS, + testRoundTrip @Credential, + testRoundTrip @ClientIdentity, testRoundTrip @TestPreconfiguredSender, testRoundTrip @RemoveProposalMessage, testRoundTrip @RemoveProposalPayload, - testRoundTrip @AppAckProposalTest, testRoundTrip @ExtensionVector, - testRoundTrip @PublicGroupStateTBS, - testRoundTrip @PublicGroupState, + testRoundTrip @GroupInfoData, + testRoundTrip @TestCommitBundle, testRoundTrip @Welcome, - testRoundTrip @OpaquePublicGroupState, - testConvertProtoRoundTrip @Proto.Mls.GroupInfoBundle @GroupInfoBundle, - testConvertProtoRoundTrip @Proto.Mls.CommitBundle @TestCommitBundle + testRoundTrip @Proposal, + testRoundTrip @ProposalRef, + testRoundTrip @VarInt ] testRoundTrip :: @@ -61,138 +65,125 @@ testRoundTrip = testProperty msg trip where msg = show (typeRep @a) trip (v :: a) = - counterexample (show (runPut (serialiseMLS v))) $ - Right v === (decodeMLS . runPut . serialiseMLS) v - -testConvertProtoRoundTrip :: - forall p a. - ( Arbitrary a, - Typeable a, - Show a, - Show p, - Eq a, - ConvertProtoLens p a - ) => - T.TestTree -testConvertProtoRoundTrip = testProperty (show (typeRep @a)) trip - where - trip (v :: a) = - counterexample (show (toProtolens @p @a v)) $ - Right v === do - let pa = toProtolens @p @a v - fromProtolens @p @a pa + let serialised = encodeMLS v + parsed = decodeMLS serialised + in counterexample (show $ hex serialised) $ + Right v === parsed -------------------------------------------------------------------------------- -- auxiliary types class ArbitrarySender a where - arbitrarySender :: Gen (Sender 'MLSPlainText) + arbitrarySender :: Gen Sender -class ArbitraryMessagePayload a where - arbitraryMessagePayload :: Gen (MessagePayload 'MLSPlainText) +instance ArbitrarySender Sender where + arbitrarySender = arbitrary -class ArbitraryMessageTBS a where - arbitraryArbitraryMessageTBS :: Gen (MessageTBS 'MLSPlainText) +class ArbitraryFramedContentData a where + arbitraryFramedContentData :: Gen FramedContentData -newtype MessageGenerator tbs = MessageGenerator {unMessageGenerator :: Message 'MLSPlainText} - deriving newtype (ParseMLS, SerialiseMLS, Eq, Show) +class ArbitraryFramedContent a where + arbitraryFramedContent :: Gen FramedContent -instance (ArbitraryMessageTBS tbs) => Arbitrary (MessageGenerator tbs) where - arbitrary = do - tbs <- arbitraryArbitraryMessageTBS @tbs - MessageGenerator - <$> (Message (mkRawMLS tbs) <$> arbitrary) +newtype MessageGenerator fc = MessageGenerator {unMessageGenerator :: Message} + deriving newtype (ParseMLS, SerialiseMLS, Eq, Show) -data MessageTBSGenerator sender payload +instance ArbitraryFramedContent fc => Arbitrary (MessageGenerator fc) where + arbitrary = + fmap MessageGenerator $ do + fc <- arbitraryFramedContent @fc + mt <- case fc.sender of + SenderMember _ -> Just <$> arbitrary + _ -> pure Nothing + confirmationTag <- case fc.content of + FramedContentCommit _ -> Just <$> arbitrary + _ -> pure Nothing + Message + <$> arbitrary + <*> fmap + MessagePublic + ( PublicMessage (mkRawMLS fc) + <$> (mkRawMLS <$> (FramedContentAuthData <$> arbitrary <*> pure confirmationTag)) + <*> pure mt + ) + +data FramedContentGenerator sender payload instance ( ArbitrarySender sender, - ArbitraryMessagePayload payload + ArbitraryFramedContentData payload ) => - ArbitraryMessageTBS (MessageTBSGenerator sender payload) + ArbitraryFramedContent (FramedContentGenerator sender payload) where - arbitraryArbitraryMessageTBS = - MessageTBS KnownFormatTag + arbitraryFramedContent = + FramedContent <$> arbitrary <*> arbitrary - <*> arbitrary <*> arbitrarySender @sender - <*> arbitraryMessagePayload @payload + <*> arbitrary + <*> arbitraryFramedContentData @payload --- -newtype RemoveProposalMessage = RemoveProposalMessage {unRemoveProposalMessage :: Message 'MLSPlainText} +newtype RemoveProposalMessage = RemoveProposalMessage Message deriving newtype (ParseMLS, SerialiseMLS, Eq, Show) instance Arbitrary RemoveProposalMessage where arbitrary = RemoveProposalMessage - <$> (unMessageGenerator <$> arbitrary @(MessageGenerator (MessageTBSGenerator TestPreconfiguredSender RemoveProposalPayload))) + <$> (unMessageGenerator <$> arbitrary @(MessageGenerator (FramedContentGenerator TestPreconfiguredSender RemoveProposalPayload))) --- -newtype RemoveProposalPayload = RemoveProposalPayload {unRemoveProposalPayload :: MessagePayload 'MLSPlainText} +newtype RemoveProposalPayload = RemoveProposalPayload {unRemoveProposalPayload :: FramedContentData} deriving newtype (ParseMLS, SerialiseMLS, Eq, Show) instance Arbitrary RemoveProposalPayload where - arbitrary = RemoveProposalPayload . ProposalMessage . mkRemoveProposal <$> arbitrary + arbitrary = RemoveProposalPayload . FramedContentProposal . mkRawMLS . RemoveProposal <$> arbitrary -instance ArbitraryMessagePayload RemoveProposalPayload where - arbitraryMessagePayload = unRemoveProposalPayload <$> arbitrary +instance ArbitraryFramedContentData RemoveProposalPayload where + arbitraryFramedContentData = unRemoveProposalPayload <$> arbitrary --- newtype TestPreconfiguredSender = TestPreconfiguredSender - {unTestPreconfiguredSender :: Sender 'MLSPlainText} + {unTestPreconfiguredSender :: Sender} deriving newtype (ParseMLS, SerialiseMLS, Eq, Show) instance Arbitrary TestPreconfiguredSender where - arbitrary = TestPreconfiguredSender . PreconfiguredSender <$> arbitrary + arbitrary = TestPreconfiguredSender . SenderExternal <$> arbitrary instance ArbitrarySender TestPreconfiguredSender where arbitrarySender = unTestPreconfiguredSender <$> arbitrary --- -newtype AppAckProposalTest = AppAckProposalTest Proposal - deriving newtype (ParseMLS, Eq, Show) - -instance Arbitrary AppAckProposalTest where - arbitrary = AppAckProposalTest . AppAckProposal <$> arbitrary - -instance SerialiseMLS AppAckProposalTest where - serialiseMLS (AppAckProposalTest (AppAckProposal mrs)) = serialiseAppAckProposal mrs - serialiseMLS _ = serialiseAppAckProposal [] - ---- - newtype ExtensionVector = ExtensionVector [Extension] deriving newtype (Arbitrary, Eq, Show) instance ParseMLS ExtensionVector where - parseMLS = ExtensionVector <$> parseMLSVector @Word32 (parseMLS @Extension) + parseMLS = ExtensionVector <$> parseMLSVector @VarInt (parseMLS @Extension) instance SerialiseMLS ExtensionVector where serialiseMLS (ExtensionVector exts) = do - serialiseMLSVector @Word32 serialiseMLS exts + serialiseMLSVector @VarInt serialiseMLS exts ---- +-- -newtype TestCommitBundle = TestCommitBundle {unTestCommitBundle :: CommitBundle} - deriving (Show, Eq) +newtype TestCommitBundle = TestCommitBundle CommitBundle + deriving newtype (Eq, Show, ParseMLS, SerialiseMLS) --- | The commit bundle should contain a commit message, not a remove proposal --- message. However defining MLS serialization for Commits and all nested types --- seems overkill to test the commit bundle roundtrip instance Arbitrary TestCommitBundle where - arbitrary = do - bundle <- - CommitBundle - <$> (mkRawMLS . unRemoveProposalMessage <$> arbitrary) - <*> oneof [Just <$> (mkRawMLS <$> arbitrary), pure Nothing] - <*> arbitrary - pure (TestCommitBundle bundle) - -instance ConvertProtoLens Proto.Mls.CommitBundle TestCommitBundle where - fromProtolens = fmap TestCommitBundle . fromProtolens @Proto.Mls.CommitBundle @CommitBundle - toProtolens = toProtolens . unTestCommitBundle + arbitrary = + TestCommitBundle <$> do + commitMsg <- + mkRawMLS . unMessageGenerator @(FramedContentGenerator Sender CommitPayload) + <$> arbitrary + welcome <- arbitrary + CommitBundle commitMsg welcome <$> arbitrary + +newtype CommitPayload = CommitPayload {unCommitPayload :: RawMLS Commit} + deriving newtype (Arbitrary) + +instance ArbitraryFramedContentData CommitPayload where + arbitraryFramedContentData = FramedContentCommit . unCommitPayload <$> arbitrary diff --git a/libs/wire-api/test/unit/Main.hs b/libs/wire-api/test/unit/Test/Wire/API/Run.hs similarity index 98% rename from libs/wire-api/test/unit/Main.hs rename to libs/wire-api/test/unit/Test/Wire/API/Run.hs index a1b492c3710..14382593d05 100644 --- a/libs/wire-api/test/unit/Main.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Run.hs @@ -15,10 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main - ( main, - ) -where +module Test.Wire.API.Run (main) where import Imports import System.IO.Unsafe (unsafePerformIO) diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index f3d8f7ce9e0..f70e2c36ebb 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -1,4 +1,4 @@ -cabal-version: 1.12 +cabal-version: 3.0 name: wire-api version: 0.1.0 description: API types of the Wire collaboration platform @@ -6,11 +6,64 @@ category: Network author: Wire Swiss GmbH maintainer: Wire Swiss GmbH copyright: (c) 2020 Wire Swiss GmbH -license: AGPL-3 +license: AGPL-3.0-only license-file: LICENSE build-type: Simple +common common-all + default-language: Haskell2010 + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints + + default-extensions: + NoImplicitPrelude + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + DuplicateRecordFields + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + OverloadedLabels + OverloadedRecordDot + OverloadedStrings + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns + library + import: common-all + -- cabal-fmt: expand src exposed-modules: Wire.API.ApplyMods @@ -43,6 +96,8 @@ library Wire.API.MakesFederatedCall Wire.API.Message Wire.API.Message.Proto + Wire.API.MLS.AuthenticatedContent + Wire.API.MLS.Capabilities Wire.API.MLS.CipherSuite Wire.API.MLS.Commit Wire.API.MLS.CommitBundle @@ -51,15 +106,20 @@ library Wire.API.MLS.Epoch Wire.API.MLS.Extension Wire.API.MLS.Group - Wire.API.MLS.GroupInfoBundle + Wire.API.MLS.GroupInfo + Wire.API.MLS.HPKEPublicKey Wire.API.MLS.KeyPackage Wire.API.MLS.Keys + Wire.API.MLS.LeafNode + Wire.API.MLS.Lifetime Wire.API.MLS.Message Wire.API.MLS.Proposal - Wire.API.MLS.PublicGroupState + Wire.API.MLS.ProposalTag + Wire.API.MLS.ProtocolVersion Wire.API.MLS.Serialisation Wire.API.MLS.Servant Wire.API.MLS.SubConversation + Wire.API.MLS.Validation Wire.API.MLS.Welcome Wire.API.Notification Wire.API.OAuth @@ -162,57 +222,10 @@ library Wire.API.VersionInfo Wire.API.Wrapped - other-modules: Paths_wire_api - hs-source-dirs: src - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -Wredundant-constraints - + other-modules: Paths_wire_api + hs-source-dirs: src build-depends: - aeson >=2.0.1.0 + , aeson >=2.0.1.0 , attoparsec >=0.10 , base >=4 && <5 , base64-bytestring >=1.0 @@ -305,15 +318,13 @@ library , x509 , zauth - default-language: Haskell2010 - test-suite wire-api-golden-tests - type: exitcode-stdio-1.0 - main-is: Main.hs + import: common-all + type: exitcode-stdio-1.0 + main-is: ../golden.hs -- cabal-fmt: expand test/golden other-modules: - Main Paths_wire_api Test.Wire.API.Golden.FromJSON Test.Wire.API.Golden.Generated @@ -560,58 +571,13 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.UserClientPrekeyMap Test.Wire.API.Golden.Manual.UserIdList Test.Wire.API.Golden.Protobuf + Test.Wire.API.Golden.Run Test.Wire.API.Golden.Runner - hs-source-dirs: test/golden - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N -Wredundant-constraints - + ghc-options: -threaded -with-rtsopts=-N + hs-source-dirs: test/golden build-depends: - aeson >=2.0.1.0 + , aeson >=2.0.1.0 , aeson-pretty , aeson-qq , base @@ -654,15 +620,13 @@ test-suite wire-api-golden-tests , wire-api , wire-message-proto-lens - default-language: Haskell2010 - test-suite wire-api-tests - type: exitcode-stdio-1.0 - main-is: Main.hs + import: common-all + type: exitcode-stdio-1.0 + main-is: ../unit.hs -- cabal-fmt: expand test/unit other-modules: - Main Paths_wire_api Test.Wire.API.Call.Config Test.Wire.API.Conversation @@ -678,6 +642,7 @@ test-suite wire-api-tests Test.Wire.API.Routes Test.Wire.API.Routes.Version Test.Wire.API.Routes.Version.Wai + Test.Wire.API.Run Test.Wire.API.Swagger Test.Wire.API.Team.Export Test.Wire.API.Team.Member @@ -686,56 +651,9 @@ test-suite wire-api-tests Test.Wire.API.User.RichInfo Test.Wire.API.User.Search - hs-source-dirs: test/unit - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N -Wredundant-constraints - + hs-source-dirs: test/unit build-depends: - aeson >=2.0.1.0 + , aeson >=2.0.1.0 , aeson-pretty , aeson-qq , async @@ -769,6 +687,7 @@ test-suite wire-api-tests , process , proto-lens , QuickCheck + , random , saml2-web-sso , schema-profunctor , servant @@ -793,4 +712,4 @@ test-suite wire-api-tests , wire-api , wire-message-proto-lens - default-language: Haskell2010 + ghc-options: -threaded -with-rtsopts=-N diff --git a/nix/pkgs/mls-test-cli/default.nix b/nix/pkgs/mls-test-cli/default.nix index 968b60d9049..ddbf9b342a7 100644 --- a/nix/pkgs/mls-test-cli/default.nix +++ b/nix/pkgs/mls-test-cli/default.nix @@ -8,20 +8,29 @@ , gitMinimal }: -rustPlatform.buildRustPackage rec { - name = "mls-test-cli-${version}"; - version = "0.6.0"; - nativeBuildInputs = [ pkg-config perl gitMinimal ]; - buildInputs = [ libsodium ]; +let + version = "0.7.0"; src = fetchFromGitHub { owner = "wireapp"; repo = "mls-test-cli"; - sha256 = "sha256-FjgAcYdUr/ZWdQxbck2UEG6NEEQLuz0S4a55hrAxUs4="; - rev = "82fc148964ef5baa92a90d086fdc61adaa2b5dbf"; + rev = "29109bd32cedae64bdd9a47ef373710fad477590"; + sha256 = "sha256-1GMiEMkzcKPOd5AsQkQTSMLDkNqy3yjCC03K20vyFVY="; }; - doCheck = false; - cargoSha256 = "sha256-AlZrxa7f5JwxxrzFBgeFSaYU6QttsUpfLYfq1HzsdbE="; - cargoDepsHook = '' - mkdir -p mls-test-cli-${version}-vendor.tar.gz/ring/.git + cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/Cargo.lock"); +in rustPlatform.buildRustPackage rec { + name = "mls-test-cli-${version}"; + inherit version src; + + cargoLock = { + lockFile = cargoLockFile; + outputHashes = { + "hpke-0.10.0" = "sha256-XYkG72ZeQ3nM4JjgNU5Fe0HqNGkBGcI70rE1Kbz/6vs="; + "openmls-0.20.0" = "sha256-i5xNTYP1wPzwlnqz+yPu8apKCibRZacz4OV5VVZwY5Y="; + }; + }; + + postPatch = '' + cp ${cargoLockFile} Cargo.lock ''; + doCheck = false; } diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 00a464df5cc..c10aa3b01f9 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -1,4 +1,4 @@ -cabal-version: 1.12 +cabal-version: 3.0 name: brig version: 2.0 synopsis: User Service @@ -6,7 +6,7 @@ category: Network author: Wire Swiss GmbH maintainer: Wire Swiss GmbH copyright: (c) 2017 Wire Swiss GmbH -license: AGPL-3 +license: AGPL-3.0-only license-file: LICENSE build-type: Simple extra-source-files: @@ -14,7 +14,60 @@ extra-source-files: docs/swagger-v1.json docs/swagger.md +common common-all + default-language: Haskell2010 + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints + + default-extensions: + NoImplicitPrelude + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + DuplicateRecordFields + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + OverloadedLabels + OverloadedRecordDot + OverloadedStrings + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns + library + import: common-all + -- cabal-fmt: expand src exposed-modules: Brig.Allowlists @@ -133,58 +186,14 @@ library Brig.Version Brig.ZAuth - other-modules: Paths_brig - hs-source-dirs: src - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - + other-modules: Paths_brig + hs-source-dirs: src ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -fplugin=Polysemy.Plugin - -fplugin=TransitiveAnns.Plugin -Wredundant-constraints + -fplugin=TransitiveAnns.Plugin build-depends: - aeson >=2.0.1.0 + , aeson >=2.0.1.0 , amazonka >=2 , amazonka-core >=2 , amazonka-dynamodb >=2 @@ -317,131 +326,43 @@ library , yaml >=0.8.22 , zauth >=0.10.3 - default-language: Haskell2010 + default-language: Haskell2010 executable brig - main-is: exec/Main.hs - other-modules: Paths_brig - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - + import: common-all + main-is: exec/Main.hs + other-modules: Paths_brig ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts -Wredundant-constraints + -rtsopts build-depends: - base + , base , brig , HsOpenSSL , imports , optparse-applicative >=0.10 , types-common - default-language: Haskell2010 + default-language: Haskell2010 executable brig-index - main-is: index/src/Main.hs - other-modules: Paths_brig - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields -threaded -with-rtsopts=-N - -Wredundant-constraints - + import: common-all + main-is: index/src/Main.hs + other-modules: Paths_brig + ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N build-depends: - base + , base , brig , imports , optparse-applicative , tinylog - default-language: Haskell2010 + default-language: Haskell2010 executable brig-integration - main-is: Main.hs + import: common-all + main-is: ../integration.hs -- cabal-fmt: expand test/integration other-modules: @@ -478,62 +399,15 @@ executable brig-integration Federation.End2end Federation.Util Index.Create - Main + Run SMTP Util Util.AWS - hs-source-dirs: test/integration - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields -threaded -with-rtsopts=-N - -Wredundant-constraints - + hs-source-dirs: test/integration + ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N build-depends: - aeson + , aeson , async , attoparsec , base @@ -627,14 +501,15 @@ executable brig-integration , yaml , zauth - default-language: Haskell2010 + default-language: Haskell2010 executable brig-schema - main-is: Main.hs + import: common-all + main-is: ../main.hs -- cabal-fmt: expand schema/src other-modules: - Main + Run V43 V44 V45 @@ -671,55 +546,10 @@ executable brig-schema V_FUTUREWORK hs-source-dirs: schema/src - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields -Wredundant-constraints - + ghc-options: -funbox-strict-fields -Wredundant-constraints + default-extensions: TemplateHaskell build-depends: - base + , base , cassandra-util >=0.12 , extended , imports @@ -732,12 +562,11 @@ executable brig-schema default-language: Haskell2010 test-suite brig-tests - type: exitcode-stdio-1.0 - main-is: Main.hs - - -- cabal-fmt: expand test/unit + import: common-all + type: exitcode-stdio-1.0 + main-is: ../unit.hs other-modules: - Main + Run Test.Brig.Calling Test.Brig.Calling.Internal Test.Brig.Effects.Delay @@ -746,57 +575,10 @@ test-suite brig-tests Test.Brig.Roundtrip Test.Brig.User.Search.Index.Types - hs-source-dirs: test/unit - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields -threaded -with-rtsopts=-N - -Wredundant-constraints - + hs-source-dirs: test/unit + ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N build-depends: - aeson + , aeson , base , binary , bloodhound @@ -831,4 +613,4 @@ test-suite brig-tests , wire-api , wire-api-federation - default-language: Haskell2010 + default-language: Haskell2010 diff --git a/services/brig/schema/main.hs b/services/brig/schema/main.hs new file mode 100644 index 00000000000..d4037ab9cfa --- /dev/null +++ b/services/brig/schema/main.hs @@ -0,0 +1,5 @@ +import Imports +import qualified Run + +main :: IO () +main = Run.main diff --git a/services/brig/schema/src/Main.hs b/services/brig/schema/src/Run.hs similarity index 99% rename from services/brig/schema/src/Main.hs rename to services/brig/schema/src/Run.hs index f1f35ccd371..96bd6d1675d 100644 --- a/services/brig/schema/src/Main.hs +++ b/services/brig/schema/src/Run.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main where +module Run where import Cassandra.Schema import Control.Exception (finally) diff --git a/services/brig/schema/src/V69_MLSKeyPackageRefMapping.hs b/services/brig/schema/src/V69_MLSKeyPackageRefMapping.hs index 34c95d70e14..aae2b698ae2 100644 --- a/services/brig/schema/src/V69_MLSKeyPackageRefMapping.hs +++ b/services/brig/schema/src/V69_MLSKeyPackageRefMapping.hs @@ -26,6 +26,7 @@ import Cassandra.Schema import Imports import Text.RawString.QQ +-- FUTUREWORK: remove this table migration :: Migration migration = Migration 69 "Add key package ref mapping" $ diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index f992ff82582..55dc93cbcfe 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -27,7 +27,6 @@ import qualified Brig.API.Client as API import qualified Brig.API.Connection as API import Brig.API.Error import Brig.API.Handler -import Brig.API.MLS.KeyPackages.Validation import Brig.API.OAuth (internalOauthAPI) import Brig.API.Types import qualified Brig.API.User as API @@ -86,10 +85,7 @@ import Wire.API.Connection import Wire.API.Error import qualified Wire.API.Error.Brig as E import Wire.API.Federation.API -import Wire.API.MLS.Credential -import Wire.API.MLS.KeyPackage -import Wire.API.MLS.Serialisation -import Wire.API.Routes.Internal.Brig +import Wire.API.MLS.CipherSuite import qualified Wire.API.Routes.Internal.Brig as BrigIRoutes import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Named @@ -132,20 +128,7 @@ ejpdAPI = :<|> getConnectionsStatus mlsAPI :: ServerT BrigIRoutes.MLSAPI (Handler r) -mlsAPI = - ( ( \ref -> - Named @"get-client-by-key-package-ref" (getClientByKeyPackageRef ref) - :<|> ( Named @"put-conversation-by-key-package-ref" (putConvIdByKeyPackageRef ref) - :<|> Named @"get-conversation-by-key-package-ref" (getConvIdByKeyPackageRef ref) - ) - :<|> Named @"put-key-package-ref" (putKeyPackageRef ref) - :<|> Named @"post-key-package-ref" (postKeyPackageRef ref) - ) - :<|> Named @"delete-key-package-refs" deleteKeyPackageRefs - ) - :<|> getMLSClients - :<|> mapKeyPackageRefsInternal - :<|> Named @"put-key-package-add" upsertKeyPackage +mlsAPI = getMLSClients accountAPI :: ( Member BlacklistStore r, @@ -187,62 +170,6 @@ deleteAccountConferenceCallingConfig :: UserId -> (Handler r) NoContent deleteAccountConferenceCallingConfig uid = lift $ wrapClient $ Data.updateFeatureConferenceCalling uid Nothing $> NoContent -getClientByKeyPackageRef :: KeyPackageRef -> Handler r (Maybe ClientIdentity) -getClientByKeyPackageRef = runMaybeT . mapMaybeT wrapClientE . Data.derefKeyPackage - --- Used by galley to update conversation id in mls_key_package_ref -putConvIdByKeyPackageRef :: KeyPackageRef -> Qualified ConvId -> Handler r Bool -putConvIdByKeyPackageRef ref = lift . wrapClient . Data.keyPackageRefSetConvId ref - --- Used by galley to create a new record in mls_key_package_ref -putKeyPackageRef :: KeyPackageRef -> NewKeyPackageRef -> Handler r () -putKeyPackageRef ref = lift . wrapClient . Data.addKeyPackageRef ref - --- Used by galley to retrieve conversation id from mls_key_package_ref -getConvIdByKeyPackageRef :: KeyPackageRef -> Handler r (Maybe (Qualified ConvId)) -getConvIdByKeyPackageRef = runMaybeT . mapMaybeT wrapClientE . Data.keyPackageRefConvId - --- Used by galley to update key packages in mls_key_package_ref on commits with update_path -postKeyPackageRef :: KeyPackageRef -> KeyPackageRef -> Handler r () -postKeyPackageRef ref = lift . wrapClient . Data.updateKeyPackageRef ref - --- Used by galley to update key package refs and also validate -upsertKeyPackage :: NewKeyPackage -> Handler r NewKeyPackageResult -upsertKeyPackage nkp = do - kp <- - either - (const $ mlsProtocolError "upsertKeyPackage: Cannot decocode KeyPackage") - pure - $ decodeMLS' @(RawMLS KeyPackage) (kpData . nkpKeyPackage $ nkp) - ref <- kpRef' kp & noteH "upsertKeyPackage: Unsupported CipherSuite" - - identity <- - either - (const $ mlsProtocolError "upsertKeyPackage: Cannot decode ClientIdentity") - pure - $ kpIdentity (rmValue kp) - mp <- lift . wrapClient . runMaybeT $ Data.derefKeyPackage ref - when (isNothing mp) $ do - void $ validateKeyPackage identity kp - lift . wrapClient $ - Data.addKeyPackageRef - ref - ( NewKeyPackageRef - (fst <$> cidQualifiedClient identity) - (ciClient identity) - (nkpConversation nkp) - ) - - pure $ NewKeyPackageResult identity ref - where - noteH :: Text -> Maybe a -> Handler r a - noteH errMsg Nothing = mlsProtocolError errMsg - noteH _ (Just y) = pure y - -deleteKeyPackageRefs :: DeleteKeyPackageRefsRequest -> Handler r () -deleteKeyPackageRefs (DeleteKeyPackageRefsRequest refs) = - lift . wrapClient $ pooledForConcurrentlyN_ 16 refs Data.deleteKeyPackageRef - getMLSClients :: UserId -> SignatureSchemeTag -> Handler r (Set ClientInfo) getMLSClients usr _ss = do -- FUTUREWORK: check existence of key packages with a given ciphersuite @@ -260,12 +187,6 @@ getMLSClients usr _ss = do (cid,) . (> 0) <$> Data.countKeyPackages lusr cid -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/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index 74742fe1766..53bd3fe1647 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -48,10 +48,10 @@ import Wire.API.Team.LegalHold import Wire.API.User.Client uploadKeyPackages :: Local UserId -> ClientId -> KeyPackageUpload -> Handler r () -uploadKeyPackages lusr cid (kpuKeyPackages -> kps) = do +uploadKeyPackages lusr cid kps = do assertMLSEnabled let identity = mkClientIdentity (tUntagged lusr) cid - kps' <- traverse (validateKeyPackage identity) kps + kps' <- traverse (validateUploadedKeyPackage identity) kps.keyPackages lift . wrapClient $ Data.insertKeyPackages (tUnqualified lusr) cid kps' claimKeyPackages :: @@ -111,22 +111,20 @@ claimRemoteKeyPackages lusr target = do ckprTarget = tUnqualified target } - -- validate and set up mappings for all claimed key packages - for_ (kpbEntries bundle) $ \e -> do - let cid = mkClientIdentity (kpbeUser e) (kpbeClient e) + -- validate all claimed key packages + for_ bundle.entries $ \e -> do + let cid = mkClientIdentity e.user e.client kpRaw <- withExceptT (const . clientDataError $ KeyPackageDecodingError) . except . decodeMLS' . kpData - . kpbeKeyPackage - $ e - (refVal, _) <- validateKeyPackage cid kpRaw - unless (refVal == kpbeRef e) + $ e.keyPackage + (refVal, _) <- validateUploadedKeyPackage cid kpRaw + unless (refVal == e.ref) . throwE . clientDataError $ InvalidKeyPackageRef - wrapClientE $ Data.mapKeyPackageRef (kpbeRef e) (kpbeUser e) (kpbeClient e) pure bundle where diff --git a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs index 2ebed2e3709..26de9a143f9 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs @@ -17,13 +17,9 @@ module Brig.API.MLS.KeyPackages.Validation ( -- * Main key package validation function - validateKeyPackage, - reLifetime, - mlsProtocolError, - - -- * Exported for unit tests - findExtensions, + validateUploadedKeyPackage, validateLifetime', + mlsProtocolError, ) where @@ -32,9 +28,8 @@ import Brig.API.Handler import Brig.App import qualified Brig.Data.Client as Data import Brig.Options -import Control.Applicative -import Control.Lens (view) -import qualified Data.ByteString.Lazy as LBS +import Control.Lens +import qualified Data.ByteString as LBS import Data.Qualified import Data.Time.Clock import Data.Time.Clock.POSIX @@ -43,110 +38,46 @@ import Wire.API.Error import Wire.API.Error.Brig import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential -import Wire.API.MLS.Extension import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Lifetime import Wire.API.MLS.Serialisation +import Wire.API.MLS.Validation -validateKeyPackage :: +validateUploadedKeyPackage :: ClientIdentity -> RawMLS KeyPackage -> Handler r (KeyPackageRef, KeyPackageData) -validateKeyPackage identity (RawMLS (KeyPackageData -> kpd) kp) = do - loc <- qualifyLocal () - -- get ciphersuite - cs <- - maybe - (mlsProtocolError "Unsupported ciphersuite") - pure - $ cipherSuiteTag (kpCipherSuite kp) +validateUploadedKeyPackage identity kp = do + (cs, lt) <- either mlsProtocolError pure $ validateKeyPackage (Just identity) kp.value - -- validate signature scheme - let ss = csSignatureScheme cs - when (signatureScheme ss /= bcSignatureScheme (kpCredential kp)) $ - mlsProtocolError "Signature scheme incompatible with ciphersuite" + validateLifetime lt -- Authenticate signature key. This is performed only upon uploading a key -- package for a local client. + loc <- qualifyLocal () foldQualified loc ( \_ -> do - key <- - fmap LBS.toStrict $ - maybe - (mlsProtocolError "No key associated to the given identity and signature scheme") - pure - =<< lift (wrapClient (Data.lookupMLSPublicKey (ciUser identity) (ciClient identity) ss)) - when (key /= bcSignatureKey (kpCredential kp)) $ + mkey :: Maybe LByteString <- + lift . wrapClient $ + Data.lookupMLSPublicKey + (ciUser identity) + (ciClient identity) + (csSignatureScheme cs) + key :: LByteString <- + maybe + (mlsProtocolError "No key associated to the given identity and signature scheme") + pure + mkey + when (key /= LBS.fromStrict kp.value.leafNode.signatureKey) $ mlsProtocolError "Unrecognised signature key" ) - (pure . const ()) + (\_ -> pure ()) (cidQualifiedClient identity) - -- validate signature - unless - ( csVerifySignature - cs - (bcSignatureKey (kpCredential kp)) - (rmRaw (kpTBS kp)) - (kpSignature kp) - ) - $ mlsProtocolError "Invalid signature" - -- validate protocol version - maybe - (mlsProtocolError "Unsupported protocol version") - pure - (pvTag (kpProtocolVersion kp) >>= guard . (== ProtocolMLS10)) - -- validate credential - validateCredential identity (kpCredential kp) - -- validate extensions - validateExtensions (kpExtensions kp) + let kpd = KeyPackageData kp.raw pure (kpRef cs kpd, kpd) -validateCredential :: ClientIdentity -> Credential -> Handler r () -validateCredential identity cred = do - identity' <- - either credentialError pure $ - decodeMLS' (bcIdentity cred) - when (identity /= identity') $ - throwStd (errorToWai @'MLSIdentityMismatch) - where - credentialError e = - mlsProtocolError $ - "Failed to parse identity: " <> e - -data RequiredExtensions f = RequiredExtensions - { reLifetime :: f Lifetime, - reCapabilities :: f () - } - -deriving instance (Show (f Lifetime), Show (f ())) => Show (RequiredExtensions f) - -instance Alternative f => Semigroup (RequiredExtensions f) where - RequiredExtensions lt1 cap1 <> RequiredExtensions lt2 cap2 = - RequiredExtensions (lt1 <|> lt2) (cap1 <|> cap2) - -instance Alternative f => Monoid (RequiredExtensions f) where - mempty = RequiredExtensions empty empty - -checkRequiredExtensions :: RequiredExtensions Maybe -> Either Text (RequiredExtensions Identity) -checkRequiredExtensions re = - RequiredExtensions - <$> maybe (Left "Missing lifetime extension") (pure . Identity) (reLifetime re) - <*> maybe (Left "Missing capability extension") (pure . Identity) (reCapabilities re) - -findExtensions :: [Extension] -> Either Text (RequiredExtensions Identity) -findExtensions = checkRequiredExtensions <=< (getAp . foldMap findExtension) - -findExtension :: Extension -> Ap (Either Text) (RequiredExtensions Maybe) -findExtension ext = (Ap (decodeExtension ext) >>=) . foldMap $ \case - (SomeExtension SLifetimeExtensionTag lt) -> pure $ RequiredExtensions (Just lt) Nothing - (SomeExtension SCapabilitiesExtensionTag _) -> pure $ RequiredExtensions Nothing (Just ()) - -validateExtensions :: [Extension] -> Handler r () -validateExtensions exts = do - re <- either mlsProtocolError pure $ findExtensions exts - validateLifetime . runIdentity . reLifetime $ re - validateLifetime :: Lifetime -> Handler r () validateLifetime lt = do now <- liftIO getPOSIXTime diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 5e532d974ef..0c58480f944 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -86,7 +86,7 @@ import qualified System.CryptoBox as CryptoBox import System.Logger.Class (field, msg, val) import qualified System.Logger.Class as Log import UnliftIO (pooledMapConcurrentlyN) -import Wire.API.MLS.Credential +import Wire.API.MLS.CipherSuite import Wire.API.User.Auth import Wire.API.User.Client hiding (UpdateClient (..)) import Wire.API.User.Client.Prekey diff --git a/services/brig/src/Brig/Data/MLS/KeyPackage.hs b/services/brig/src/Brig/Data/MLS/KeyPackage.hs index 2f99e355bcb..03a69e69ab2 100644 --- a/services/brig/src/Brig/Data/MLS/KeyPackage.hs +++ b/services/brig/src/Brig/Data/MLS/KeyPackage.hs @@ -18,14 +18,7 @@ module Brig.Data.MLS.KeyPackage ( insertKeyPackages, claimKeyPackage, - mapKeyPackageRef, countKeyPackages, - derefKeyPackage, - keyPackageRefConvId, - keyPackageRefSetConvId, - addKeyPackageRef, - updateKeyPackageRef, - deleteKeyPackageRef, ) where @@ -33,24 +26,19 @@ import Brig.API.MLS.KeyPackages.Validation import Brig.App import Brig.Options hiding (Timeout) import Cassandra -import Cassandra.Settings import Control.Arrow import Control.Error -import Control.Exception import Control.Lens -import Control.Monad.Catch import Control.Monad.Random (randomRIO) -import Data.Domain import Data.Functor import Data.Id import Data.Qualified import Data.Time.Clock import Data.Time.Clock.POSIX import Imports -import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode import Wire.API.MLS.Serialisation -import Wire.API.Routes.Internal.Brig insertKeyPackages :: MonadClient m => UserId -> ClientId -> [(KeyPackageRef, KeyPackageData)] -> m () insertKeyPackages uid cid kps = retry x5 . batch $ do @@ -80,7 +68,6 @@ claimKeyPackage u c = do for mk $ \(ref, kpd) -> do retry x5 $ write deleteByRef (params LocalQuorum (tUnqualified u, c, ref)) pure (ref, kpd) - lift $ mapKeyPackageRef ref (tUntagged u) c pure (ref, kpd) where deleteByRef :: PrepQuery W (UserId, ClientId, KeyPackageRef) () @@ -127,19 +114,11 @@ getNonClaimedKeyPackages u c = do hasExpired :: POSIXTime -> Maybe NominalDiffTime -> (KeyPackage, a) -> Bool hasExpired now mMaxLifetime (kp, _) = - case findExtensions (kpExtensions kp) of - Left _ -> True -- the assumption is the key package is valid and has the - -- required extensions so we return 'True' - Right (runIdentity . reLifetime -> lt) -> + case kp.leafNode.source of + LeafNodeSourceKeyPackage lt -> either (const True) (const False) . validateLifetime' now mMaxLifetime $ lt - --- | Add key package ref to mapping table. -mapKeyPackageRef :: MonadClient m => KeyPackageRef -> Qualified UserId -> ClientId -> m () -mapKeyPackageRef ref u c = - write insertQuery (params LocalQuorum (ref, qDomain u, qUnqualified u, c)) - where - insertQuery :: PrepQuery W (KeyPackageRef, Domain, UserId, ClientId) () - insertQuery = "INSERT INTO mls_key_package_refs (ref, domain, user, client) VALUES (?, ?, ?, ?)" + _ -> True -- the assumption is the key package is valid and has the + -- required extensions so we return 'True' countKeyPackages :: ( MonadReader Env m, @@ -150,104 +129,9 @@ countKeyPackages :: m Int64 countKeyPackages u c = fromIntegral . length <$> getNonClaimedKeyPackages u c -derefKeyPackage :: MonadClient m => KeyPackageRef -> MaybeT m ClientIdentity -derefKeyPackage ref = do - (d, u, c) <- MaybeT . retry x1 $ query1 q (params LocalQuorum (Identity ref)) - pure $ ClientIdentity d u c - where - q :: PrepQuery R (Identity KeyPackageRef) (Domain, UserId, ClientId) - q = "SELECT domain, user, client from mls_key_package_refs WHERE ref = ?" - -keyPackageRefConvId :: MonadClient m => KeyPackageRef -> MaybeT m (Qualified ConvId) -keyPackageRefConvId ref = MaybeT $ do - qr <- retry x1 $ query1 q (params LocalSerial (Identity ref)) - pure $ do - (domain, cid) <- qr - Qualified <$> cid <*> domain - where - q :: PrepQuery R (Identity KeyPackageRef) (Maybe Domain, Maybe ConvId) - q = "SELECT conv_domain, conv FROM mls_key_package_refs WHERE ref = ?" - --- We want to proper update, not an upsert, to avoid "ghost" refs without user+client -keyPackageRefSetConvId :: MonadClient m => KeyPackageRef -> Qualified ConvId -> m Bool -keyPackageRefSetConvId ref convId = do - updated <- - retry x5 $ - trans - q - (params LocalQuorum (qDomain convId, qUnqualified convId, ref)) - { serialConsistency = Just LocalSerialConsistency - } - case updated of - [] -> pure False - [_] -> pure True - _ -> throwM $ ErrorCall "Primary key violation detected mls_key_package_refs.ref" - where - q :: PrepQuery W (Domain, ConvId, KeyPackageRef) x - q = "UPDATE mls_key_package_refs SET conv_domain = ?, conv = ? WHERE ref = ? IF EXISTS" - -addKeyPackageRef :: MonadClient m => KeyPackageRef -> NewKeyPackageRef -> m () -addKeyPackageRef ref nkpr = - retry x5 $ - write - q - (params LocalQuorum (nkprClientId nkpr, qUnqualified (nkprConversation nkpr), qDomain (nkprConversation nkpr), qDomain (nkprUserId nkpr), qUnqualified (nkprUserId nkpr), ref)) - where - q :: PrepQuery W (ClientId, ConvId, Domain, Domain, UserId, KeyPackageRef) x - q = "UPDATE mls_key_package_refs SET client = ?, conv = ?, conv_domain = ?, domain = ?, user = ? WHERE ref = ?" - --- | Update key package ref, used in Galley when commit reveals key package ref update for the sender. --- Nothing is changed if the previous key package ref is not found in the table. --- Updating amounts to INSERT the new key package ref, followed by DELETE the --- previous one. --- --- FUTUREWORK: this function has to be extended if a table mapping (client, --- conversation) to key package ref is added, for instance, when implementing --- external delete proposals. -updateKeyPackageRef :: MonadClient m => KeyPackageRef -> KeyPackageRef -> m () -updateKeyPackageRef prevRef newRef = - void . runMaybeT $ do - backup <- backupKeyPackageMeta prevRef - lift $ do - restoreKeyPackageMeta newRef backup - deleteKeyPackage prevRef - -deleteKeyPackageRef :: MonadClient m => KeyPackageRef -> m () -deleteKeyPackageRef ref = do - retry x5 $ - write q (params LocalQuorum (Identity ref)) - where - q :: PrepQuery W (Identity KeyPackageRef) x - q = "DELETE FROM mls_key_package_refs WHERE ref = ?" - -------------------------------------------------------------------------------- -- Utilities -backupKeyPackageMeta :: MonadClient m => KeyPackageRef -> MaybeT m (ClientId, Maybe (Qualified ConvId), Qualified UserId) -backupKeyPackageMeta ref = do - (clientId, convId, convDomain, userDomain, userId) <- MaybeT . retry x1 $ query1 q (params LocalQuorum (Identity ref)) - pure (clientId, Qualified <$> convId <*> convDomain, Qualified userId userDomain) - where - q :: PrepQuery R (Identity KeyPackageRef) (ClientId, Maybe ConvId, Maybe Domain, Domain, UserId) - q = "SELECT client, conv, conv_domain, domain, user FROM mls_key_package_refs WHERE ref = ?" - -restoreKeyPackageMeta :: MonadClient m => KeyPackageRef -> (ClientId, Maybe (Qualified ConvId), Qualified UserId) -> m () -restoreKeyPackageMeta ref (clientId, convId, userId) = do - write q (params LocalQuorum (ref, clientId, qUnqualified <$> convId, qDomain <$> convId, qDomain userId, qUnqualified userId)) - where - q :: PrepQuery W (KeyPackageRef, ClientId, Maybe ConvId, Maybe Domain, Domain, UserId) () - q = "INSERT INTO mls_key_package_refs (ref, client, conv, conv_domain, domain, user) VALUES (?, ?, ?, ?, ?, ?)" - -deleteKeyPackage :: MonadClient m => KeyPackageRef -> m () -deleteKeyPackage ref = - retry x5 $ - write - q - (params LocalQuorum (Identity ref)) - where - q :: PrepQuery W (Identity KeyPackageRef) x - q = "DELETE FROM mls_key_package_refs WHERE ref = ?" - pick :: [a] -> IO (Maybe a) pick [] = pure Nothing pick xs = do diff --git a/services/brig/test/integration.hs b/services/brig/test/integration.hs new file mode 100644 index 00000000000..d4037ab9cfa --- /dev/null +++ b/services/brig/test/integration.hs @@ -0,0 +1,5 @@ +import Imports +import qualified Run + +main :: IO () +main = Run.main diff --git a/services/brig/test/integration/API/Federation.hs b/services/brig/test/integration/API/Federation.hs index af65db356b9..36e1e42b65a 100644 --- a/services/brig/test/integration/API/Federation.hs +++ b/services/brig/test/integration/API/Federation.hs @@ -47,14 +47,12 @@ import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import UnliftIO.Temporary import Util -import Web.HttpApiData import Wire.API.Connection import Wire.API.Federation.API.Brig import qualified Wire.API.Federation.API.Brig as FedBrig import qualified Wire.API.Federation.API.Brig as S import Wire.API.Federation.Component import Wire.API.Federation.Version -import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage import Wire.API.User import Wire.API.User.Client @@ -426,7 +424,7 @@ testClaimKeyPackages brig fedBrigClient = do ClaimKeyPackageRequest (qUnqualified alice) (qUnqualified bob) liftIO $ - Set.map (\e -> (kpbeUser e, kpbeClient e)) (kpbEntries bundle) + Set.map (\e -> (e.user, e.client)) bundle.entries @?= Set.fromList [(bob, c) | c <- bobClients] -- check that we have one fewer key package now @@ -434,17 +432,6 @@ testClaimKeyPackages brig fedBrigClient = do count <- getKeyPackageCount brig bob c liftIO $ count @?= 1 - -- check that the package refs are correctly mapped - for_ (kpbEntries bundle) $ \e -> do - cid <- - responseJsonError - =<< get (brig . paths ["i", "mls", "key-packages", toHeader (kpbeRef e)]) - Opt.Opts -> Brig -> Http () testClaimKeyPackagesMLSDisabled opts brig = do alice <- fakeRemoteUser diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index 516b8934c90..b3dd9c1073b 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -36,26 +36,21 @@ import qualified Cassandra as Cass import Cassandra.Util import Control.Exception (ErrorCall (ErrorCall), throwIO) import Control.Lens ((^.), (^?!)) -import Data.Aeson (decode) import qualified Data.Aeson.Lens as Aeson import qualified Data.Aeson.Types as Aeson import Data.ByteString.Conversion (toByteString') import Data.Default import Data.Id -import Data.Qualified (Qualified (qDomain, qUnqualified)) +import Data.Qualified import qualified Data.Set as Set import GHC.TypeLits (KnownSymbol) import Imports -import Servant.API (ToHttpApiData (toUrlPiece)) -import Test.QuickCheck (Arbitrary (arbitrary), generate) import Test.Tasty import Test.Tasty.HUnit import UnliftIO (withSystemTempDirectory) import Util import Util.Options (Endpoint) import qualified Wire.API.Connection as Conn -import Wire.API.MLS.Credential -import Wire.API.MLS.KeyPackage import Wire.API.Routes.Internal.Brig import Wire.API.Team.Feature import qualified Wire.API.Team.Feature as ApiFt @@ -74,14 +69,6 @@ tests opts mgr db brig brigep gundeck galley = do test mgr "suspend non existing user and verify no db entry" $ testSuspendNonExistingUser db brig, test mgr "mls/clients" $ testGetMlsClients brig, - testGroup - "mls/key-packages" - $ [ test mgr "fresh get" $ testKpcFreshGet brig, - test mgr "put,get" $ testKpcPutGet brig, - test mgr "get,get" $ testKpcGetGet brig, - test mgr "put,put" $ testKpcPutPut brig, - test mgr "add key package ref" $ testAddKeyPackageRef brig - ], test mgr "writetimeToInt64" $ testWritetimeRepresentation opts mgr db brig brigep galley ] @@ -256,118 +243,6 @@ testGetMlsClients brig = do ) liftIO $ toList cs1 @?= [ClientInfo c True] -keyPackageCreate :: HasCallStack => Brig -> Http KeyPackageRef -keyPackageCreate brig = do - uid <- userQualifiedId <$> randomUser brig - clid <- createClient brig uid 0 - withSystemTempDirectory "mls" $ \tmp -> - uploadKeyPackages brig tmp def uid clid 2 - - uid2 <- userQualifiedId <$> randomUser brig - claimResp <- - post - ( brig - . paths - [ "mls", - "key-packages", - "claim", - toByteString' (qDomain uid), - toByteString' (qUnqualified uid) - ] - . zUser (qUnqualified uid2) - . contentJson - ) - liftIO $ - assertEqual "POST mls/key-packages/claim/:domain/:user failed" 200 (statusCode claimResp) - case responseBody claimResp >>= decode of - Nothing -> liftIO $ assertFailure "Claim response empty" - Just bundle -> case toList $ kpbEntries bundle of - [] -> liftIO $ assertFailure "Claim response held no bundles" - (h : _) -> pure $ kpbeRef h - -kpcPut :: HasCallStack => Brig -> KeyPackageRef -> Qualified ConvId -> Http () -kpcPut brig ref qConv = do - resp <- - put - ( brig - . paths ["i", "mls", "key-packages", toByteString' $ toUrlPiece ref, "conversation"] - . contentJson - . json qConv - ) - liftIO $ assertEqual "PUT i/mls/key-packages/:ref/conversation failed" 204 (statusCode resp) - -kpcGet :: HasCallStack => Brig -> KeyPackageRef -> Http (Maybe (Qualified ConvId)) -kpcGet brig ref = do - resp <- - get (brig . paths ["i", "mls", "key-packages", toByteString' $ toUrlPiece ref, "conversation"]) - liftIO $ case statusCode resp of - 404 -> pure Nothing - 200 -> pure $ responseBody resp >>= decode - _ -> assertFailure "GET i/mls/key-packages/:ref/conversation failed" - -testKpcFreshGet :: Brig -> Http () -testKpcFreshGet brig = do - ref <- keyPackageCreate brig - mqConv <- kpcGet brig ref - liftIO $ assertEqual "(fresh) Get ~= Nothing" Nothing mqConv - -testKpcPutGet :: Brig -> Http () -testKpcPutGet brig = do - ref <- keyPackageCreate brig - qConv <- liftIO $ generate arbitrary - kpcPut brig ref qConv - mqConv <- kpcGet brig ref - liftIO $ assertEqual "Put x; Get ~= x" (Just qConv) mqConv - -testKpcGetGet :: Brig -> Http () -testKpcGetGet brig = do - ref <- keyPackageCreate brig - liftIO (generate arbitrary) >>= kpcPut brig ref - mqConv1 <- kpcGet brig ref - mqConv2 <- kpcGet brig ref - liftIO $ assertEqual "Get; Get ~= Get" mqConv1 mqConv2 - -testKpcPutPut :: Brig -> Http () -testKpcPutPut brig = do - ref <- keyPackageCreate brig - qConv <- liftIO $ generate arbitrary - qConv2 <- liftIO $ generate arbitrary - kpcPut brig ref qConv - kpcPut brig ref qConv2 - mqConv <- kpcGet brig ref - liftIO $ assertEqual "Put x; Put y ~= Put y" (Just qConv2) mqConv - -testAddKeyPackageRef :: Brig -> Http () -testAddKeyPackageRef brig = do - ref <- keyPackageCreate brig - qcnv <- liftIO $ generate arbitrary - qusr <- liftIO $ generate arbitrary - c <- liftIO $ generate arbitrary - put - ( brig - . paths ["i", "mls", "key-packages", toByteString' $ toUrlPiece ref] - . json - NewKeyPackageRef - { nkprUserId = qusr, - nkprClientId = c, - nkprConversation = qcnv - } - ) - !!! const 201 === statusCode - ci <- - responseJsonError - =<< get (brig . paths ["i", "mls", "key-packages", toByteString' $ toUrlPiece ref]) - (Request -> Request) -> UserId -> m ResponseLBS getFeatureConfig galley uid = do get $ apiVersion "v1" . galley . paths ["feature-configs", featureNameBS @cfg] . zUser uid diff --git a/services/brig/test/integration/API/MLS.hs b/services/brig/test/integration/API/MLS.hs index 440da8e28d4..e236f3f54d3 100644 --- a/services/brig/test/integration/API/MLS.hs +++ b/services/brig/test/integration/API/MLS.hs @@ -28,13 +28,13 @@ import Data.Id import Data.Qualified import qualified Data.Set as Set import Data.Timeout +import Debug.Trace (traceM) import Federation.Util import Imports import Test.Tasty import Test.Tasty.HUnit import UnliftIO.Temporary import Util -import Web.HttpApiData import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation @@ -48,7 +48,7 @@ tests m b opts = [ test m "POST /mls/key-packages/self/:client" (testKeyPackageUpload b), test m "POST /mls/key-packages/self/:client (no public keys)" (testKeyPackageUploadNoKey b), test m "GET /mls/key-packages/self/:client/count" (testKeyPackageZeroCount b), - test m "GET /mls/key-packages/self/:client/count (expired package)" (testKeyPackageExpired b), + -- FUTUREWORK test m "GET /mls/key-packages/self/:client/count (expired package)" (testKeyPackageExpired b), test m "GET /mls/key-packages/claim/local/:user" (testKeyPackageClaim b), test m "GET /mls/key-packages/claim/local/:user - self claim" (testKeyPackageSelfClaim b), test m "GET /mls/key-packages/claim/remote/:user" (testKeyPackageRemoteClaim opts b) @@ -115,7 +115,7 @@ testKeyPackageClaim brig = do -- claim packages for both clients of u u' <- userQualifiedId <$> randomUser brig - bundle <- + bundle :: KeyPackageBundle <- responseJsonError =<< post ( brig @@ -124,8 +124,7 @@ testKeyPackageClaim brig = do ) (kpbeUser e, kpbeClient e)) (kpbEntries bundle) @?= Set.fromList [(u, c1), (u, c2)] - checkMapping brig u bundle + liftIO $ Set.map (\e -> (e.user, e.client)) bundle.entries @?= Set.fromList [(u, c1), (u, c2)] -- check that we have one fewer key package now for_ [c1, c2] $ \c -> do @@ -145,7 +144,7 @@ testKeyPackageSelfClaim brig = do -- claim own packages but skip the first do - bundle <- + bundle :: KeyPackageBundle <- responseJsonError =<< post ( brig @@ -154,7 +153,7 @@ testKeyPackageSelfClaim brig = do . zUser (qUnqualified u) ) (kpbeUser e, kpbeClient e)) (kpbEntries bundle) @?= Set.fromList [(u, c2)] + liftIO $ Set.map (\e -> (e.user, e.client)) bundle.entries @?= Set.fromList [(u, c2)] -- check that we still have all keypackages for client c1 count <- getKeyPackageCount brig u c1 @@ -163,7 +162,7 @@ testKeyPackageSelfClaim brig = do -- if another user sets skip_own, nothing is skipped do u' <- userQualifiedId <$> randomUser brig - bundle <- + bundle :: KeyPackageBundle <- responseJsonError =<< post ( brig @@ -172,7 +171,7 @@ testKeyPackageSelfClaim brig = do . zUser (qUnqualified u') ) (kpbeUser e, kpbeClient e)) (kpbEntries bundle) @?= Set.fromList [(u, c1), (u, c2)] + liftIO $ Set.map (\e -> (e.user, e.client)) bundle.entries @?= Set.fromList [(u, c1), (u, c2)] -- check package counts again for_ [(c1, 2), (c2, 1)] $ \(c, n) -> do @@ -181,6 +180,7 @@ testKeyPackageSelfClaim brig = do testKeyPackageRemoteClaim :: Opts -> Brig -> Http () testKeyPackageRemoteClaim opts brig = do + traceM "sun" u <- fakeRemoteUser u' <- userQualifiedId <$> randomUser brig @@ -192,12 +192,13 @@ testKeyPackageRemoteClaim opts brig = do (r, kp) <- generateKeyPackage tmp qcid Nothing pure $ KeyPackageBundleEntry - { kpbeUser = u, - kpbeClient = ciClient qcid, - kpbeRef = kp, - kpbeKeyPackage = KeyPackageData . rmRaw $ r + { user = u, + client = ciClient qcid, + ref = kp, + keyPackage = KeyPackageData . raw $ r } let mockBundle = KeyPackageBundle (Set.fromList entries) + traceM "gun" (bundle :: KeyPackageBundle, _reqs) <- liftIO . withTempMockFederator opts (Aeson.encode mockBundle) $ responseJsonError @@ -209,23 +210,10 @@ testKeyPackageRemoteClaim opts brig = do Qualified UserId -> KeyPackageBundle -> Http () -checkMapping brig u bundle = - for_ (kpbEntries bundle) $ \e -> do - cid <- - responseJsonError - =<< get (brig . paths ["i", "mls", "key-packages", toHeader (kpbeRef e)]) - Qualified UserId -> Int -> Http ClientId createClient brig u i = fmap clientId $ diff --git a/services/brig/test/integration/API/MLS/Util.hs b/services/brig/test/integration/API/MLS/Util.hs index 02af682b7c7..e22bd405688 100644 --- a/services/brig/test/integration/API/MLS/Util.hs +++ b/services/brig/test/integration/API/MLS/Util.hs @@ -34,6 +34,7 @@ import System.FilePath import System.Process import Test.Tasty.HUnit import Util +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation @@ -109,7 +110,7 @@ uploadKeyPackages brig tmp KeyingInfo {..} u c n = do . json defUpdateClient {updateClientMLSPublicKeys = Map.fromList [(Ed25519, pk)]} ) !!! const 200 === statusCode - let upload = object ["key_packages" .= toJSON (map (Base64ByteString . rmRaw) kps)] + let upload = object ["key_packages" .= toJSON (map (Base64ByteString . raw) kps)] post ( brig . paths ["mls", "key-packages", "self", toByteString' c] diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index b7bfeac716b..01bcca00c71 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -61,7 +61,7 @@ import Test.Tasty.HUnit import UnliftIO (mapConcurrently) import Util import Wire.API.Internal.Notification -import Wire.API.MLS.Credential +import Wire.API.MLS.CipherSuite import qualified Wire.API.Team.Feature as Public import Wire.API.User import qualified Wire.API.User as Public diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index 353516b10e4..1c8ba92eab2 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -61,7 +61,7 @@ import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Event.Conversation import Wire.API.Internal.Notification (ntfTransient) -import Wire.API.MLS.Credential +import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation @@ -686,7 +686,7 @@ claimRemoteKeyPackages brig1 brig2 = do for_ bobClients $ \c -> uploadKeyPackages brig2 tmp def bob c 5 - bundle <- + bundle :: KeyPackageBundle <- responseJsonError =<< post ( brig1 @@ -696,7 +696,7 @@ claimRemoteKeyPackages brig1 brig2 = do (kpbeUser e, kpbeClient e)) (kpbEntries bundle) + Set.map (\e -> (e.user, e.client)) bundle.entries @?= Set.fromList [(bob, c) | c <- bobClients] -- bob creates an MLS conversation on domain 2 with alice on domain 1, then sends a @@ -719,7 +719,7 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do let aliceClientId = show (userId alice) <> ":" - <> T.unpack (client aliceClient) + <> T.unpack aliceClient.client <> "@" <> T.unpack (domainText (qDomain (userQualifiedId alice))) @@ -737,7 +737,7 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do { updateClientMLSPublicKeys = Map.singleton Ed25519 - (bcSignatureKey (kpCredential (rmValue aliceKP))) + aliceKP.value.leafNode.signatureKey } put ( brig1 @@ -769,7 +769,7 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do let bobClientId = show (userId bob) <> ":" - <> T.unpack (client bobClient) + <> T.unpack bobClient.client <> "@" <> T.unpack (domainText (qDomain (userQualifiedId bob))) void . liftIO $ spawn (cli bobClientId tmp ["init", bobClientId]) Nothing @@ -820,7 +820,7 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do liftIO $ BS.writeFile (tmp "group.json") groupJSON -- invite alice - liftIO $ BS.writeFile (tmp aliceClientId) (rmRaw aliceKP) + liftIO $ BS.writeFile (tmp aliceClientId) (raw aliceKP) commit <- liftIO $ spawn @@ -834,6 +834,8 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do tmp "group.json", "--welcome-out", tmp "welcome", + "--group-info-out", + tmp "groupinfo.mls", tmp aliceClientId ] ) @@ -873,31 +875,14 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do -- send welcome, commit and dove WS.bracketR cannon1 (userId alice) $ \wsAlice -> do - post - ( galley2 - . paths - ["mls", "messages"] - . zUser (userId bob) - . zClient bobClient - . zConn "conn" - . header "Z-Type" "access" - . content "message/mls" - . bytes commit - ) - !!! const 201 === statusCode - - post - ( unversioned - . galley2 - . paths ["v2", "mls", "welcome"] - . zUser (userId bob) - . zClient bobClient - . zConn "conn" - . header "Z-Type" "access" - . content "message/mls" - . bytes welcome - ) - !!! const 201 === statusCode + sendCommitBundle + tmp + "groupinfo.mls" + (Just "welcome") + galley2 + (userId bob) + bobClient + commit post ( galley2 @@ -982,7 +967,7 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 let aliceClientId = show (userId alice) <> ":" - <> T.unpack (client aliceClient) + <> T.unpack aliceClient.client <> "@" <> T.unpack (domainText (qDomain (userQualifiedId alice))) @@ -997,7 +982,7 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 let bobClientId = show (userId bob) <> ":" - <> T.unpack (client bobClient) + <> T.unpack (bobClient.client) <> "@" <> T.unpack (domainText (qDomain (userQualifiedId bob))) @@ -1015,7 +1000,7 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 { updateClientMLSPublicKeys = Map.singleton Ed25519 - (bcSignatureKey (kpCredential (rmValue aliceKP))) + aliceKP.value.leafNode.signatureKey } put ( brig1 @@ -1084,7 +1069,7 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 liftIO $ BS.writeFile (tmp "group.json") groupJSON -- invite alice - liftIO $ BS.writeFile (tmp aliceClientId) (rmRaw aliceKP) + liftIO $ BS.writeFile (tmp aliceClientId) (raw aliceKP) commit <- liftIO $ spawn @@ -1098,6 +1083,8 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 tmp "group.json", "--welcome-out", tmp "welcome", + "--group-info-out", + tmp "groupinfo.mls", tmp aliceClientId ] ) @@ -1106,32 +1093,14 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 -- send welcome and commit WS.bracketR cannon1 (userId alice) $ \wsAlice -> do - post - ( galley2 - . paths - ["mls", "messages"] - . zUser (userId bob) - . zClient bobClient - . zConn "conn" - . header "Z-Type" "access" - . content "message/mls" - . bytes commit - ) - !!! const 201 === statusCode - - post - ( unversioned - . galley2 - . paths - ["v2", "mls", "welcome"] - . zUser (userId bob) - . zClient bobClient - . zConn "conn" - . header "Z-Type" "access" - . content "message/mls" - . bytes welcome - ) - !!! const 201 === statusCode + sendCommitBundle + tmp + "groupinfo.mls" + (Just "welcome") + galley2 + (userId bob) + bobClient + commit -- verify that alice receives the welcome message WS.assertMatch_ (5 # Second) wsAlice $ \n -> do @@ -1198,7 +1167,7 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 "--in-place", "--group", tmp "subgroup.json", - "--group-state-out", + "--group-info-out", tmp "subgroupstate.mls" ] ) @@ -1206,6 +1175,7 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 sendCommitBundle tmp "subgroupstate.mls" + Nothing galley2 (userId bob) bobClient @@ -1222,9 +1192,9 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 [ "external-commit", "--group-out", tmp "subgroupA.json", - "--group-state-in", + "--group-info-in", tmp "subgroupstate.mls", - "--group-state-out", + "--group-info-out", tmp "subgroupstateA.mls" ] ) @@ -1232,6 +1202,7 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 sendCommitBundle tmp "subgroupstateA.mls" + Nothing galley1 (userId alice) aliceClient diff --git a/services/brig/test/integration/Federation/Util.hs b/services/brig/test/integration/Federation/Util.hs index 8d56f4f4b06..399f17124a9 100644 --- a/services/brig/test/integration/Federation/Util.hs +++ b/services/brig/test/integration/Federation/Util.hs @@ -67,7 +67,7 @@ import Wire.API.Conversation (Conversation (cnvMembers)) import Wire.API.Conversation.Member (OtherMember (OtherMember), cmOthers) import Wire.API.Conversation.Role (roleNameWireAdmin) import Wire.API.MLS.CommitBundle -import Wire.API.MLS.GroupInfoBundle +import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.Team.Feature (FeatureStatus (..)) import Wire.API.User @@ -118,13 +118,22 @@ connectUsersEnd2End brig1 brig2 quid1 quid2 = do putConnectionQualified brig2 (qUnqualified quid2) quid1 Accepted !!! const 200 === statusCode -sendCommitBundle :: FilePath -> FilePath -> Galley -> UserId -> ClientId -> ByteString -> Http () -sendCommitBundle tmp subGroupStateFn galley uid cid commit = do +sendCommitBundle :: HasCallStack => FilePath -> FilePath -> Maybe FilePath -> Galley -> UserId -> ClientId -> ByteString -> Http () +sendCommitBundle tmp subGroupStateFn welcomeFn galley uid cid commit = do subGroupStateRaw <- liftIO $ BS.readFile $ tmp subGroupStateFn subGroupState <- either (liftIO . assertFailure . T.unpack) pure . decodeMLS' $ subGroupStateRaw subCommit <- either (liftIO . assertFailure . T.unpack) pure . decodeMLS' $ commit - let subGroupBundle = CommitBundle subCommit Nothing (GroupInfoBundle UnencryptedGroupInfo TreeFull subGroupState) - let subGroupBundleRaw = serializeCommitBundle subGroupBundle + mbWelcome <- + for + welcomeFn + $ \fn -> do + bs <- liftIO $ BS.readFile $ tmp fn + msg :: Message <- either (liftIO . assertFailure . T.unpack) pure . decodeMLS' $ bs + case msg.content of + MessageWelcome welcome -> pure welcome + _ -> liftIO . assertFailure $ "Expected a welcome" + + let subGroupBundle = CommitBundle subCommit mbWelcome subGroupState post ( galley . paths @@ -133,7 +142,7 @@ sendCommitBundle tmp subGroupStateFn galley uid cid commit = do . zClient cid . zConn "conn" . header "Z-Type" "access" - . content "application/x-protobuf" - . bytes subGroupBundleRaw + . Bilge.content "message/mls" + . lbytes (encodeMLS subGroupBundle) ) !!! const 201 === statusCode diff --git a/services/brig/test/integration/Main.hs b/services/brig/test/integration/Run.hs similarity index 99% rename from services/brig/test/integration/Main.hs rename to services/brig/test/integration/Run.hs index dee2c47caa7..4c71bcba7ad 100644 --- a/services/brig/test/integration/Main.hs +++ b/services/brig/test/integration/Run.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main +module Run ( main, ) where diff --git a/services/brig/test/unit.hs b/services/brig/test/unit.hs new file mode 100644 index 00000000000..a26473d24ee --- /dev/null +++ b/services/brig/test/unit.hs @@ -0,0 +1 @@ +import Run diff --git a/services/brig/test/unit/Main.hs b/services/brig/test/unit/Main.hs index 8cc53f5f81d..6ab5658fca1 100644 --- a/services/brig/test/unit/Main.hs +++ b/services/brig/test/unit/Main.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main +module Run ( main, ) where diff --git a/services/brig/test/unit/Run.hs b/services/brig/test/unit/Run.hs new file mode 100644 index 00000000000..6ab5658fca1 --- /dev/null +++ b/services/brig/test/unit/Run.hs @@ -0,0 +1,43 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Run + ( main, + ) +where + +import Imports +import qualified Test.Brig.Calling +import qualified Test.Brig.Calling.Internal +import qualified Test.Brig.InternalNotification +import qualified Test.Brig.MLS +import qualified Test.Brig.Roundtrip +import qualified Test.Brig.User.Search.Index.Types +import Test.Tasty + +main :: IO () +main = + defaultMain $ + testGroup + "Tests" + [ Test.Brig.User.Search.Index.Types.tests, + Test.Brig.Calling.tests, + Test.Brig.Calling.Internal.tests, + Test.Brig.Roundtrip.tests, + Test.Brig.MLS.tests, + Test.Brig.InternalNotification.tests + ] diff --git a/services/brig/test/unit/Test/Brig/MLS.hs b/services/brig/test/unit/Test/Brig/MLS.hs index e4c4f8d2588..92e2b5eb526 100644 --- a/services/brig/test/unit/Test/Brig/MLS.hs +++ b/services/brig/test/unit/Test/Brig/MLS.hs @@ -19,16 +19,12 @@ module Test.Brig.MLS where import Brig.API.MLS.KeyPackages.Validation import Data.Binary -import Data.Binary.Put -import qualified Data.ByteString.Lazy as LBS import Data.Either import Data.Time.Clock import Imports import Test.Tasty import Test.Tasty.QuickCheck -import Wire.API.MLS.CipherSuite -import Wire.API.MLS.Extension -import Wire.API.MLS.Serialisation +import Wire.API.MLS.Lifetime -- | A lifetime with a length of at least 1 day. newtype ValidLifetime = ValidLifetime Lifetime @@ -57,69 +53,6 @@ midpoint lt = ) ) -newtype ValidExtensions = ValidExtensions [Extension] - -instance Show ValidExtensions where - show (ValidExtensions exts) = "ValidExtensions (length " <> show (length exts) <> ")" - -unknownExt :: Gen Extension -unknownExt = do - Positive t0 <- arbitrary - let t = t0 + fromEnum (maxBound :: ExtensionTag) + 1 - Extension (fromIntegral t) <$> arbitrary - --- | Generate a list of extensions containing all the required ones. -instance Arbitrary ValidExtensions where - arbitrary = do - exts0 <- listOf unknownExt - LifetimeAndExtension ext1 _ <- arbitrary - exts2 <- listOf unknownExt - CapabilitiesAndExtension ext3 _ <- arbitrary - exts4 <- listOf unknownExt - pure . ValidExtensions $ exts0 <> [ext1] <> exts2 <> [ext3] <> exts4 - -newtype InvalidExtensions = InvalidExtensions [Extension] - --- | Generate a list of extensions which does not contain one of the required extensions. -instance Show InvalidExtensions where - show (InvalidExtensions exts) = "InvalidExtensions (length " <> show (length exts) <> ")" - -instance Arbitrary InvalidExtensions where - arbitrary = do - req <- fromMLSEnum <$> elements [LifetimeExtensionTag, CapabilitiesExtensionTag] - InvalidExtensions <$> listOf (arbitrary `suchThat` ((/= req) . extType)) - -data LifetimeAndExtension = LifetimeAndExtension Extension Lifetime - deriving (Show) - -instance Arbitrary LifetimeAndExtension where - arbitrary = do - lt <- arbitrary - let ext = Extension (fromIntegral (fromEnum LifetimeExtensionTag + 1)) . LBS.toStrict . runPut $ do - put (timestampSeconds (ltNotBefore lt)) - put (timestampSeconds (ltNotAfter lt)) - pure $ LifetimeAndExtension ext lt - -data CapabilitiesAndExtension = CapabilitiesAndExtension Extension Capabilities - deriving (Show) - -instance Arbitrary CapabilitiesAndExtension where - arbitrary = do - caps <- arbitrary - let ext = Extension (fromIntegral (fromEnum CapabilitiesExtensionTag + 1)) . LBS.toStrict . runPut $ do - putWord8 (fromIntegral (length (capVersions caps))) - traverse_ (putWord8 . pvNumber) (capVersions caps) - - putWord8 (fromIntegral (length (capCiphersuites caps) * 2)) - traverse_ (put . cipherSuiteNumber) (capCiphersuites caps) - - putWord8 (fromIntegral (length (capExtensions caps) * 2)) - traverse_ put (capExtensions caps) - - putWord8 (fromIntegral (length (capProposals caps) * 2)) - traverse_ put (capProposals caps) - pure $ CapabilitiesAndExtension ext caps - tests :: TestTree tests = testGroup @@ -142,16 +75,5 @@ tests = isRight $ validateLifetime' (midpoint lt) Nothing lt, testProperty "expiration too far" $ \(ValidLifetime lt) -> isLeft $ validateLifetime' (midpoint lt) (Just 10) lt - ], - testGroup - "Extensions" - [ testProperty "required extensions are found" $ \(ValidExtensions exts) -> - isRight (findExtensions exts), - testProperty "missing required extensions" $ \(InvalidExtensions exts) -> - isLeft (findExtensions exts), - testProperty "lifetime extension" $ \(LifetimeAndExtension ext lt) -> - decodeExtension ext == Right (Just (SomeExtension SLifetimeExtensionTag lt)), - testProperty "capabilities extension" $ \(CapabilitiesAndExtension ext caps) -> - decodeExtension ext == Right (Just (SomeExtension SCapabilitiesExtensionTag caps)) ] ] diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index aed44b951a7..20711d9312e 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -1,4 +1,4 @@ -cabal-version: 1.12 +cabal-version: 3.0 name: galley version: 0.83.0 synopsis: Conversations @@ -6,7 +6,7 @@ category: Network author: Wire Swiss GmbH maintainer: Wire Swiss GmbH copyright: (c) 2017 Wire Swiss GmbH -license: AGPL-3 +license: AGPL-3.0-only license-file: LICENSE build-type: Simple @@ -15,7 +15,60 @@ flag static manual: True default: False +common common-all + default-language: Haskell2010 + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints + + default-extensions: + NoImplicitPrelude + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + DuplicateRecordFields + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + OverloadedLabels + OverloadedRecordDot + OverloadedStrings + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns + library + import: common-all + -- cabal-fmt: expand src exposed-modules: Galley.API @@ -31,13 +84,18 @@ library Galley.API.Mapping Galley.API.Message Galley.API.MLS + Galley.API.MLS.Commit + Galley.API.MLS.Commit.Core + Galley.API.MLS.Commit.ExternalCommit + Galley.API.MLS.Commit.InternalCommit Galley.API.MLS.Conversation Galley.API.MLS.Enabled Galley.API.MLS.GroupInfo - Galley.API.MLS.KeyPackage + Galley.API.MLS.IncomingMessage Galley.API.MLS.Keys Galley.API.MLS.Message Galley.API.MLS.Propagate + Galley.API.MLS.Proposal Galley.API.MLS.Removal Galley.API.MLS.SubConversation Galley.API.MLS.Types @@ -145,57 +203,11 @@ library Galley.Types.UserList Galley.Validation - other-modules: Paths_galley - hs-source-dirs: src - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -fplugin=TransitiveAnns.Plugin -Wredundant-constraints - + ghc-options: -fplugin=TransitiveAnns.Plugin + other-modules: Paths_galley + hs-source-dirs: src build-depends: - aeson >=2.0.1.0 + , aeson >=2.0.1.0 , amazonka >=1.4.5 , amazonka-sqs >=1.4.5 , asn1-encoding @@ -300,60 +312,15 @@ library , wire-api-federation , x509 - default-language: Haskell2010 + default-language: Haskell2010 executable galley - main-is: exec/Main.hs - other-modules: Paths_galley - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-T -rtsopts -Wredundant-constraints - + import: common-all + main-is: exec/Main.hs + other-modules: Paths_galley + ghc-options: -threaded -with-rtsopts=-T -rtsopts build-depends: - base + , base , case-insensitive , extended , extra >=1.3 @@ -375,10 +342,11 @@ executable galley if flag(static) ld-options: -static - default-language: Haskell2010 + default-language: Haskell2010 executable galley-integration - main-is: Main.hs + import: common-all + main-is: ../integration.hs -- cabal-fmt: expand test/integration other-modules: @@ -399,60 +367,14 @@ executable galley-integration API.Teams.LegalHold.Util API.Util API.Util.TeamFeature - Main + Run TestHelpers TestSetup - hs-source-dirs: test/integration - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N -Wredundant-constraints - + ghc-options: -threaded -with-rtsopts=-N -rtsopts + hs-source-dirs: test/integration build-depends: - aeson + , aeson , aeson-qq , amazonka , amazonka-sqs @@ -559,70 +481,22 @@ executable galley-integration , wire-message-proto-lens , yaml - default-language: Haskell2010 - executable galley-migrate-data - main-is: Main.hs + import: common-all + main-is: ../main.hs -- cabal-fmt: expand migrate-data/src other-modules: Galley.DataMigration Galley.DataMigration.Types - Main Paths_galley + Run V1_BackfillBillingTeamMembers V2_MigrateMLSMembers - hs-source-dirs: migrate-data/src - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -Wredundant-constraints - + hs-source-dirs: migrate-data/src build-depends: - base + , base , case-insensitive , cassandra-util , conduit @@ -653,14 +527,15 @@ executable galley-migrate-data if flag(static) ld-options: -static - default-language: Haskell2010 + default-language: Haskell2010 executable galley-schema - main-is: Main.hs + import: common-all + main-is: ../main.hs -- cabal-fmt: expand schema/src other-modules: - Main + Run V20 V21 V22 @@ -723,57 +598,12 @@ executable galley-schema V79_TeamFeatureMlsE2EId V80_AddConversationCodePassword V81_MLSSubconversation + V82_MLSDraft17 hs-source-dirs: schema/src - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -Wredundant-constraints - + default-extensions: TemplateHaskell build-depends: - base + , base , case-insensitive , cassandra-util , extended @@ -798,66 +628,22 @@ executable galley-schema default-language: Haskell2010 test-suite galley-tests - type: exitcode-stdio-1.0 - main-is: Main.hs + import: common-all + type: exitcode-stdio-1.0 + main-is: ../unit.hs other-modules: Paths_galley + Run Test.Galley.API Test.Galley.API.Message Test.Galley.API.One2One Test.Galley.Intra.User Test.Galley.Mapping - hs-source-dirs: test/unit - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N -Wredundant-constraints - + ghc-options: -threaded -with-rtsopts=-N + hs-source-dirs: test/unit build-depends: - base + , base , case-insensitive , containers , extended @@ -889,4 +675,4 @@ test-suite galley-tests , wire-api , wire-api-federation - default-language: Haskell2010 + default-language: Haskell2010 diff --git a/services/galley/migrate-data/main.hs b/services/galley/migrate-data/main.hs new file mode 100644 index 00000000000..a26473d24ee --- /dev/null +++ b/services/galley/migrate-data/main.hs @@ -0,0 +1 @@ +import Run diff --git a/services/galley/migrate-data/src/Main.hs b/services/galley/migrate-data/src/Run.hs similarity index 98% rename from services/galley/migrate-data/src/Main.hs rename to services/galley/migrate-data/src/Run.hs index f6a051b8d57..cb1288bafb1 100644 --- a/services/galley/migrate-data/src/Main.hs +++ b/services/galley/migrate-data/src/Run.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main where +module Run where import Galley.DataMigration import Imports diff --git a/services/galley/schema/main.hs b/services/galley/schema/main.hs new file mode 100644 index 00000000000..d4037ab9cfa --- /dev/null +++ b/services/galley/schema/main.hs @@ -0,0 +1,5 @@ +import Imports +import qualified Run + +main :: IO () +main = Run.main diff --git a/services/galley/schema/src/Main.hs b/services/galley/schema/src/Run.hs similarity index 98% rename from services/galley/schema/src/Main.hs rename to services/galley/schema/src/Run.hs index 4805a7470ba..447b203f4e8 100644 --- a/services/galley/schema/src/Main.hs +++ b/services/galley/schema/src/Run.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main where +module Run where import Cassandra.Schema import Control.Exception (finally) @@ -84,6 +84,7 @@ import qualified V78_TeamFeatureOutlookCalIntegration import qualified V79_TeamFeatureMlsE2EId import qualified V80_AddConversationCodePassword import qualified V81_MLSSubconversation +import qualified V82_MLSDraft17 main :: IO () main = do @@ -153,7 +154,8 @@ main = do V78_TeamFeatureOutlookCalIntegration.migration, V79_TeamFeatureMlsE2EId.migration, V80_AddConversationCodePassword.migration, - V81_MLSSubconversation.migration + V81_MLSSubconversation.migration, + V82_MLSDraft17.migration -- When adding migrations here, don't forget to update -- 'schemaVersion' in Galley.Cassandra -- (see also docs/developer/cassandra-interaction.md) diff --git a/services/galley/src/Galley/API/MLS/KeyPackage.hs b/services/galley/schema/src/V82_MLSDraft17.hs similarity index 59% rename from services/galley/src/Galley/API/MLS/KeyPackage.hs rename to services/galley/schema/src/V82_MLSDraft17.hs index 23fe2760c0d..b277d89cf29 100644 --- a/services/galley/src/Galley/API/MLS/KeyPackage.hs +++ b/services/galley/schema/src/V82_MLSDraft17.hs @@ -15,24 +15,18 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.API.MLS.KeyPackage where +module V82_MLSDraft17 (migration) where -import qualified Data.ByteString as BS -import Galley.Effects.BrigAccess +import Cassandra.Schema import Imports -import Polysemy -import Wire.API.Error -import Wire.API.Error.Galley -import Wire.API.MLS.Credential -import Wire.API.MLS.KeyPackage +import Text.RawString.QQ -nullKeyPackageRef :: KeyPackageRef -nullKeyPackageRef = KeyPackageRef (BS.replicate 16 0) - -derefKeyPackage :: - ( Member BrigAccess r, - Member (ErrorS 'MLSKeyPackageRefNotFound) r - ) => - KeyPackageRef -> - Sem r ClientIdentity -derefKeyPackage = noteS @'MLSKeyPackageRefNotFound <=< getClientByKeyPackageRef +migration :: Migration +migration = + Migration 82 "Upgrade to MLS draft 17 structures" $ do + schema' + [r| ALTER TABLE mls_group_member_client + ADD (leaf_node_index int, + removal_pending boolean + ); + |] diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 3916bbb914f..2f9b52acc92 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -55,7 +55,6 @@ import Data.Singletons import Data.Time.Clock import Galley.API.Error import Galley.API.MLS.Removal -import Galley.API.MLS.Types (cmAssocs) import Galley.API.Util import Galley.App import Galley.Data.Conversation @@ -342,9 +341,6 @@ performAction tag origUser lconv action = do pure (mempty, action) SConversationDeleteTag -> do let deleteGroup groupId = do - cm <- E.lookupMLSClients groupId - let refs = cm & cmAssocs & map (snd . snd) - E.deleteKeyPackageRefs refs E.removeAllMLSClients groupId E.deleteAllProposals groupId diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 58ed1273dd6..80382af9ed7 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -43,7 +43,6 @@ import Data.Time import qualified Data.UUID.Tagged as U import Galley.API.Error import Galley.API.MLS -import Galley.API.MLS.KeyPackage (nullKeyPackageRef) import Galley.API.MLS.Keys (getMLSRemovalKey) import Galley.API.Mapping import Galley.API.One2One @@ -70,7 +69,6 @@ import Polysemy.Error import Polysemy.Input import qualified Polysemy.TinyLog as P import Wire.API.Conversation hiding (Conversation, Member) -import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation @@ -90,7 +88,6 @@ import Wire.API.Team.Permission hiding (self) createGroupConversationUpToV3 :: ( Member BrigAccess r, Member ConversationStore r, - Member MemberStore r, Member (ErrorS 'ConvAccessDenied) r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -100,7 +97,6 @@ createGroupConversationUpToV3 :: Member (ErrorS 'NotConnected) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'MLSNonEmptyMemberList) r, - Member (ErrorS 'MLSMissingSenderClient) r, Member (ErrorS 'MissingLegalholdConsent) r, Member FederatorAccess r, Member GundeckAccess r, @@ -108,18 +104,17 @@ createGroupConversationUpToV3 :: Member (Input Opts) r, Member (Input UTCTime) r, Member LegalHoldStore r, + Member MemberStore r, Member TeamStore r, Member P.TinyLog r ) => Local UserId -> - Maybe ClientId -> Maybe ConnId -> NewConv -> Sem r ConversationResponse -createGroupConversationUpToV3 lusr mCreatorClient conn newConv = +createGroupConversationUpToV3 lusr conn newConv = createGroupConversationGeneric lusr - mCreatorClient conn newConv (const conversationCreated) @@ -129,7 +124,6 @@ createGroupConversationUpToV3 lusr mCreatorClient conn newConv = createGroupConversation :: ( Member BrigAccess r, Member ConversationStore r, - Member MemberStore r, Member (ErrorS 'ConvAccessDenied) r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -139,7 +133,6 @@ createGroupConversation :: Member (ErrorS 'NotConnected) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'MLSNonEmptyMemberList) r, - Member (ErrorS 'MLSMissingSenderClient) r, Member (ErrorS 'MissingLegalholdConsent) r, Member FederatorAccess r, Member GundeckAccess r, @@ -147,18 +140,17 @@ createGroupConversation :: Member (Input Opts) r, Member (Input UTCTime) r, Member LegalHoldStore r, + Member MemberStore r, Member TeamStore r, Member P.TinyLog r ) => Local UserId -> - Maybe ClientId -> Maybe ConnId -> NewConv -> Sem r CreateGroupConversationResponse -createGroupConversation lusr mCreatorClient conn newConv = +createGroupConversation lusr conn newConv = createGroupConversationGeneric lusr - mCreatorClient conn newConv groupConversationCreated @@ -176,7 +168,6 @@ createGroupConversationGeneric :: Member (ErrorS 'NotConnected) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'MLSNonEmptyMemberList) r, - Member (ErrorS 'MLSMissingSenderClient) r, Member (ErrorS 'MissingLegalholdConsent) r, Member FederatorAccess r, Member GundeckAccess r, @@ -188,7 +179,6 @@ createGroupConversationGeneric :: Member P.TinyLog r ) => Local UserId -> - Maybe ClientId -> Maybe ConnId -> NewConv -> -- | The function that incorporates the failed to add remote users in the @@ -196,7 +186,7 @@ createGroupConversationGeneric :: -- ignores the first argument. (Set (Remote UserId) -> Local UserId -> Conversation -> Sem r resp) -> Sem r resp -createGroupConversationGeneric lusr mCreatorClient conn newConv convCreated = do +createGroupConversationGeneric lusr conn newConv convCreated = do (nc, fromConvSize -> allUsers) <- newRegularConversation lusr newConv let tinfo = newConvTeam newConv checkCreateConvPermissions lusr newConv tinfo allUsers @@ -218,14 +208,6 @@ createGroupConversationGeneric lusr mCreatorClient conn newConv convCreated = do failedToNotify <- do conv <- E.createConversation lcnv nc - -- set creator client for MLS conversations - case (convProtocol conv, mCreatorClient) of - (ProtocolProteus, _) -> pure () - (ProtocolMLS mlsMeta, Just c) -> - E.addMLSClients (cnvmlsGroupId mlsMeta) (tUntagged lusr) (Set.singleton (c, nullKeyPackageRef)) - (ProtocolMLS _mlsMeta, Nothing) -> throwS @'MLSMissingSenderClient - (ProtocolMixed _mlsMeta, _) -> pure () - -- NOTE: We only send (conversation) events to members of the conversation failedToNotify <- notifyCreatedConversation lusr conn conv -- We already added all the invitees, but now remove from the conversation diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 92ccd968741..389b9a4c225 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -46,7 +46,6 @@ import Galley.API.Action import Galley.API.Error import Galley.API.MLS.Enabled import Galley.API.MLS.GroupInfo -import Galley.API.MLS.KeyPackage import Galley.API.MLS.Message import Galley.API.MLS.Removal import Galley.API.MLS.SubConversation hiding (leaveSubConversation) @@ -93,13 +92,11 @@ import Wire.API.Federation.API.Common (EmptyResponse (..)) import Wire.API.Federation.API.Galley import qualified Wire.API.Federation.API.Galley as F import Wire.API.Federation.Error -import Wire.API.MLS.CommitBundle import Wire.API.MLS.Credential -import Wire.API.MLS.Message -import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.GroupInfo import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation -import Wire.API.MLS.Welcome +-- import Wire.API.MLS.Welcome import Wire.API.Message import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Named (Named (Named)) @@ -667,18 +664,21 @@ sendMLSCommitBundle remoteDomain msr = assertMLSEnabled loc <- qualifyLocal () let sender = toRemoteUnsafe remoteDomain (F.mmsrSender msr) - bundle <- either (throw . mlsProtocolError) pure $ deserializeCommitBundle (fromBase64ByteString (F.mmsrRawMessage msr)) - let msg = rmValue (cbCommitMsg bundle) - qConvOrSub <- E.lookupConvByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound + bundle <- + either (throw . mlsProtocolError) pure $ + decodeMLS' (fromBase64ByteString (F.mmsrRawMessage msr)) + + ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle + qConvOrSub <- E.lookupConvByGroupId ibundle.groupId >>= noteS @'ConvNotFound when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch uncurry F.MLSMessageResponseUpdates . first (map lcuUpdate) <$> postMLSCommitBundle loc (tUntagged sender) - (Just (mmsrSenderClient msr)) + (mmsrSenderClient msr) qConvOrSub Nothing - bundle + ibundle sendMLSMessage :: ( Member BrigAccess r, @@ -694,7 +694,6 @@ sendMLSMessage :: Member (Input UTCTime) r, Member LegalHoldStore r, Member MemberStore r, - Member Resource r, Member TeamStore r, Member P.TinyLog r, Member ProposalStore r, @@ -716,22 +715,20 @@ sendMLSMessage remoteDomain msr = loc <- qualifyLocal () let sender = toRemoteUnsafe remoteDomain (F.mmsrSender msr) raw <- either (throw . mlsProtocolError) pure $ decodeMLS' (fromBase64ByteString (F.mmsrRawMessage msr)) - case rmValue raw of - SomeMessage _ msg -> do - qConvOrSub <- E.lookupConvByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound - when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch - uncurry F.MLSMessageResponseUpdates . first (map lcuUpdate) - <$> postMLSMessage - loc - (tUntagged sender) - (Just (mmsrSenderClient msr)) - qConvOrSub - Nothing - raw + msg <- noteS @'MLSUnsupportedMessage $ mkIncomingMessage raw + qConvOrSub <- E.lookupConvByGroupId msg.groupId >>= noteS @'ConvNotFound + when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch + uncurry F.MLSMessageResponseUpdates . first (map lcuUpdate) + <$> postMLSMessage + loc + (tUntagged sender) + (mmsrSenderClient msr) + qConvOrSub + Nothing + msg mlsSendWelcome :: - ( Member BrigAccess r, - Member (Error InternalError) r, + ( Member (Error InternalError) r, Member GundeckAccess r, Member (Input Env) r, Member (Input (Local ())) r, @@ -740,26 +737,17 @@ mlsSendWelcome :: Domain -> F.MLSWelcomeRequest -> Sem r F.MLSWelcomeResponse -mlsSendWelcome _origDomain (fromBase64ByteString . F.unMLSWelcomeRequest -> rawWelcome) = +mlsSendWelcome _origDomain req = fmap (either (const MLSWelcomeMLSNotEnabled) (const MLSWelcomeSent)) . runError @(Tagged 'MLSNotEnabled ()) $ do assertMLSEnabled loc <- qualifyLocal () now <- input - welcome <- either (throw . InternalErrorWithDescription . LT.fromStrict) pure $ decodeMLS' rawWelcome - -- Extract only recipients local to this backend - rcpts <- - fmap catMaybes - $ traverse - ( fmap (fmap cidQualifiedClient . hush) - . runError @(Tagged 'MLSKeyPackageRefNotFound ()) - . derefKeyPackage - . gsNewMember - ) - $ welSecrets welcome - let lrcpts = qualifyAs loc $ fst $ partitionQualified loc rcpts - sendLocalWelcomes Nothing now rawWelcome lrcpts + welcome <- + either (throw . InternalErrorWithDescription . LT.fromStrict) pure $ + decodeMLS' (fromBase64ByteString req.welcomeMessage) + sendLocalWelcomes Nothing now welcome (qualifyAs loc req.recipients) onMLSMessageSent :: ( Member ExternalAccess r, @@ -829,7 +817,7 @@ queryGroupInfo origDomain req = getSubConversationGroupInfoFromLocalConv (tUntagged sender) subConvId lconvId pure . Base64ByteString - . unOpaquePublicGroupState + . unGroupInfoData $ state updateTypingIndicator :: diff --git a/services/galley/src/Galley/API/MLS.hs b/services/galley/src/Galley/API/MLS.hs index cbd8307232e..2b06791739d 100644 --- a/services/galley/src/Galley/API/MLS.hs +++ b/services/galley/src/Galley/API/MLS.hs @@ -18,11 +18,9 @@ module Galley.API.MLS ( isMLSEnabled, assertMLSEnabled, - postMLSWelcomeFromLocalUser, postMLSMessage, postMLSCommitBundleFromLocalUser, postMLSMessageFromLocalUser, - postMLSMessageFromLocalUserV1, getMLSPublicKeys, ) where @@ -32,7 +30,6 @@ import Data.Id import Data.Qualified import Galley.API.MLS.Enabled import Galley.API.MLS.Message -import Galley.API.MLS.Welcome import Galley.Env import Imports import Polysemy diff --git a/services/galley/src/Galley/API/MLS/Commit.hs b/services/galley/src/Galley/API/MLS/Commit.hs new file mode 100644 index 00000000000..39088273b8b --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Commit.hs @@ -0,0 +1,28 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.API.MLS.Commit + ( getCommitData, + getExternalCommitData, + processInternalCommit, + processExternalCommit, + ) +where + +import Galley.API.MLS.Commit.Core +import Galley.API.MLS.Commit.ExternalCommit +import Galley.API.MLS.Commit.InternalCommit diff --git a/services/galley/src/Galley/API/MLS/Commit/Core.hs b/services/galley/src/Galley/API/MLS/Commit/Core.hs new file mode 100644 index 00000000000..50eca037f1b --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Commit/Core.hs @@ -0,0 +1,195 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.API.MLS.Commit.Core + ( getCommitData, + incrementEpoch, + getClientInfo, + HasProposalActionEffects, + ProposalErrors, + HandleMLSProposalFailures (..), + ) +where + +import Control.Comonad +import Data.Id +import Data.Qualified +import Data.Time +import Galley.API.Error +import Galley.API.MLS.Conversation +import Galley.API.MLS.Proposal +import Galley.API.MLS.Types +import Galley.Effects +import Galley.Effects.BrigAccess +import Galley.Effects.ConversationStore +import Galley.Effects.FederatorAccess +import Galley.Effects.SubConversationStore +import Galley.Env +import Galley.Options +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.Input +import Polysemy.Internal +import Polysemy.State +import Polysemy.TinyLog +import Wire.API.Conversation.Protocol +import Wire.API.Conversation.Role +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Federation.API +import Wire.API.Federation.API.Brig +import Wire.API.Federation.Error +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Commit +import Wire.API.MLS.Credential +import Wire.API.MLS.SubConversation +import Wire.API.User.Client + +type HasProposalActionEffects r = + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'MLSClientMismatch) r, + Member (Error MLSProposalFailure) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'MLSUnsupportedProposal) r, + Member (Error MLSProtocolError) r, + Member (ErrorS 'MLSSelfRemovalNotAllowed) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member MemberStore r, + Member ProposalStore r, + Member SubConversationStore r, + Member TeamStore r, + Member TinyLog r + ) + +getCommitData :: + ( HasProposalEffects r, + Member (ErrorS 'MLSProposalNotFound) r + ) => + ClientIdentity -> + Local ConvOrSubConv -> + Epoch -> + Commit -> + Sem r ProposalAction +getCommitData senderIdentity lConvOrSub epoch commit = do + let convOrSub = tUnqualified lConvOrSub + mlsMeta = mlsMetaConvOrSub convOrSub + groupId = cnvmlsGroupId mlsMeta + + evalState (indexMapConvOrSub convOrSub) $ do + creatorAction <- + if epoch == Epoch 0 + then addProposedClient senderIdentity + else mempty + proposals <- traverse (derefOrCheckProposal mlsMeta groupId epoch) commit.proposals + action <- applyProposals mlsMeta groupId proposals + pure (creatorAction <> action) + +incrementEpoch :: + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member MemberStore r, + Member SubConversationStore r + ) => + ConvOrSubConv -> + Sem r ConvOrSubConv +incrementEpoch (Conv c) = do + let epoch' = succ (cnvmlsEpoch (mcMLSData c)) + setConversationEpoch (mcId c) epoch' + conv <- getConversation (mcId c) >>= noteS @'ConvNotFound + fmap Conv (mkMLSConversation conv >>= noteS @'ConvNotFound) +incrementEpoch (SubConv c s) = do + let epoch' = succ (cnvmlsEpoch (scMLSData s)) + setSubConversationEpoch (scParentConvId s) (scSubConvId s) epoch' + subconv <- + getSubConversation (mcId c) (scSubConvId s) >>= noteS @'ConvNotFound + pure (SubConv c subconv) + +getClientInfo :: + ( Member BrigAccess r, + Member FederatorAccess r + ) => + Local x -> + Qualified UserId -> + SignatureSchemeTag -> + Sem r (Set ClientInfo) +getClientInfo loc = foldQualified loc getLocalMLSClients getRemoteMLSClients + +getRemoteMLSClients :: + ( Member FederatorAccess r + ) => + Remote UserId -> + SignatureSchemeTag -> + Sem r (Set ClientInfo) +getRemoteMLSClients rusr ss = do + runFederated rusr $ + fedClient @'Brig @"get-mls-clients" $ + MLSClientsRequest + { mcrUserId = tUnqualified rusr, + mcrSignatureScheme = ss + } + +-------------------------------------------------------------------------------- +-- Error handling of proposal execution + +-- The following errors are caught by 'executeProposalAction' and wrapped in a +-- 'MLSProposalFailure'. This way errors caused by the execution of proposals are +-- separated from those caused by the commit processing itself. +type ProposalErrors = + '[ Error FederationError, + Error InvalidInput, + ErrorS ('ActionDenied 'AddConversationMember), + ErrorS ('ActionDenied 'LeaveConversation), + ErrorS ('ActionDenied 'RemoveConversationMember), + ErrorS 'ConvAccessDenied, + ErrorS 'InvalidOperation, + ErrorS 'NotATeamMember, + ErrorS 'NotConnected, + ErrorS 'TooManyMembers + ] + +class HandleMLSProposalFailures effs r where + handleMLSProposalFailures :: Sem (Append effs r) a -> Sem r a + +class HandleMLSProposalFailure eff r where + handleMLSProposalFailure :: Sem (eff ': r) a -> Sem r a + +instance HandleMLSProposalFailures '[] r where + handleMLSProposalFailures = id + +instance + ( HandleMLSProposalFailures effs r, + HandleMLSProposalFailure eff (Append effs r) + ) => + HandleMLSProposalFailures (eff ': effs) r + where + handleMLSProposalFailures = handleMLSProposalFailures @effs . handleMLSProposalFailure @eff + +instance + (APIError e, Member (Error MLSProposalFailure) r) => + HandleMLSProposalFailure (Error e) r + where + handleMLSProposalFailure = mapError (MLSProposalFailure . toWai) diff --git a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs new file mode 100644 index 00000000000..edb792d9328 --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs @@ -0,0 +1,197 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.API.MLS.Commit.ExternalCommit + ( getExternalCommitData, + processExternalCommit, + ) +where + +import Control.Comonad +import Control.Lens (forOf_) +import qualified Data.Map as Map +import Data.Qualified +import qualified Data.Set as Set +import Galley.API.MLS.Commit.Core +import Galley.API.MLS.Proposal +import Galley.API.MLS.Removal +import Galley.API.MLS.Types +import Galley.API.MLS.Util +import Galley.Effects +import Galley.Effects.MemberStore +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.Resource (Resource) +import Polysemy.State +import Wire.API.Conversation.Protocol +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.MLS.Commit +import Wire.API.MLS.Credential +import Wire.API.MLS.LeafNode +import Wire.API.MLS.Proposal +import Wire.API.MLS.ProposalTag +import Wire.API.MLS.Serialisation +import Wire.API.MLS.SubConversation +import Wire.API.MLS.Validation + +data ExternalCommitAction = ExternalCommitAction + { add :: LeafIndex, + remove :: Maybe LeafIndex + } + +getExternalCommitData :: + forall r. + ( Member (Error MLSProtocolError) r, + Member (ErrorS 'MLSStaleMessage) r, + Member (ErrorS 'MLSUnsupportedProposal) r, + Member (ErrorS 'MLSInvalidLeafNodeIndex) r + ) => + ClientIdentity -> + Local ConvOrSubConv -> + Epoch -> + Commit -> + Sem r ExternalCommitAction +getExternalCommitData senderIdentity lConvOrSub epoch commit = do + let convOrSub = tUnqualified lConvOrSub + mlsMeta = mlsMetaConvOrSub convOrSub + curEpoch = cnvmlsEpoch mlsMeta + groupId = cnvmlsGroupId mlsMeta + when (epoch /= curEpoch) $ throwS @'MLSStaleMessage + proposals <- traverse getInlineProposal commit.proposals + + -- According to the spec, an external commit must contain: + -- (https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#section-12.2) + -- + -- > Exactly one ExternalInit + -- > At most one Remove proposal, with which the joiner removes an old + -- > version of themselves. + -- > Zero or more PreSharedKey proposals. + -- > No other proposals. + let counts = foldr (\x -> Map.insertWith (+) x.tag (1 :: Int)) mempty proposals + + unless (Map.lookup ExternalInitProposalTag counts == Just 1) $ + throw (mlsProtocolError "External commits must contain exactly one ExternalInit proposal") + unless (null (Map.keys counts \\ allowedProposals)) $ + throw (mlsProtocolError "Invalid proposal type in an external commit") + + evalState (indexMapConvOrSub convOrSub) $ do + -- process optional removal + propAction <- applyProposals mlsMeta groupId proposals + removedIndex <- case cmAssocs (paRemove propAction) of + [(cid, idx)] + | cid /= senderIdentity -> + throw $ mlsProtocolError "Only the self client can be removed by an external commit" + | otherwise -> pure (Just idx) + [] -> pure Nothing + _ -> throw (mlsProtocolError "External commits must contain at most one Remove proposal") + + -- add sender client + addedIndex <- gets imNextIndex + + pure + ExternalCommitAction + { add = addedIndex, + remove = removedIndex + } + where + allowedProposals = [ExternalInitProposalTag, RemoveProposalTag, PreSharedKeyProposalTag] + + getInlineProposal :: ProposalOrRef -> Sem r Proposal + getInlineProposal (Ref _) = + throw (mlsProtocolError "External commits cannot reference proposals") + getInlineProposal (Inline p) = pure p + +processExternalCommit :: + forall r. + ( Member (ErrorS 'MLSStaleMessage) r, + Member (ErrorS 'MLSSubConvClientNotInParent) r, + Member Resource r, + HasProposalActionEffects r + ) => + ClientIdentity -> + Local ConvOrSubConv -> + Epoch -> + ExternalCommitAction -> + Maybe UpdatePath -> + Sem r () +processExternalCommit senderIdentity lConvOrSub epoch action updatePath = do + let convOrSub = tUnqualified lConvOrSub + + -- only members can join a subconversation + forOf_ _SubConv convOrSub $ \(mlsConv, _) -> + unless (isClientMember senderIdentity (mcMembers mlsConv)) $ + throwS @'MLSSubConvClientNotInParent + + -- extract leaf node from update path and validate it + leafNode <- + (.leaf) + <$> note + (mlsProtocolError "External commits need an update path") + updatePath + let cs = cnvmlsCipherSuite (mlsMetaConvOrSub (tUnqualified lConvOrSub)) + let groupId = cnvmlsGroupId (mlsMetaConvOrSub convOrSub) + let extra = LeafNodeTBSExtraCommit groupId action.add + case validateLeafNode cs (Just senderIdentity) extra leafNode.value of + Left errMsg -> + throw $ + mlsProtocolError ("Tried to add invalid LeafNode: " <> errMsg) + Right _ -> pure () + + withCommitLock (fmap idForConvOrSub lConvOrSub) groupId epoch $ do + executeExternalCommitAction lConvOrSub senderIdentity action + + -- increment epoch number + lConvOrSub' <- for lConvOrSub incrementEpoch + + -- fetch backend remove proposals of the previous epoch + indicesInRemoveProposals <- + -- skip remove proposals of already removed by the external commit + (\\ toList action.remove) + <$> getPendingBackendRemoveProposals groupId epoch + + -- requeue backend remove proposals for the current epoch + let cm = membersConvOrSub (tUnqualified lConvOrSub') + createAndSendRemoveProposals + lConvOrSub' + indicesInRemoveProposals + (cidQualifiedUser senderIdentity) + cm + +executeExternalCommitAction :: + forall r. + HasProposalActionEffects r => + Local ConvOrSubConv -> + ClientIdentity -> + ExternalCommitAction -> + Sem r () +executeExternalCommitAction lconvOrSub senderIdentity action = do + let mlsMeta = mlsMetaConvOrSub $ tUnqualified lconvOrSub + + -- Remove deprecated sender client from conversation state. + for_ action.remove $ \_ -> + removeMLSClients + (cnvmlsGroupId mlsMeta) + (cidQualifiedUser senderIdentity) + (Set.singleton (ciClient senderIdentity)) + + -- Add new sender client to the conversation state. + addMLSClients + (cnvmlsGroupId mlsMeta) + (cidQualifiedUser senderIdentity) + (Set.singleton (ciClient senderIdentity, action.add)) diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs new file mode 100644 index 00000000000..24991a3d3b7 --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -0,0 +1,269 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.API.MLS.Commit.InternalCommit (processInternalCommit) where + +import Control.Comonad +import Control.Error.Util (hush) +import Control.Lens (forOf_, preview) +import Control.Lens.Extras (is) +import Data.Id +import Data.List.NonEmpty (NonEmpty, nonEmpty) +import qualified Data.Map as Map +import Data.Qualified +import qualified Data.Set as Set +import Data.Tuple.Extra +import Galley.API.Action +import Galley.API.MLS.Commit.Core +import Galley.API.MLS.Conversation +import Galley.API.MLS.Proposal +import Galley.API.MLS.Types +import Galley.API.MLS.Util +import Galley.Data.Conversation.Types hiding (Conversation) +import qualified Galley.Data.Conversation.Types as Data +import Galley.Effects +import Galley.Effects.FederatorAccess +import Galley.Effects.MemberStore +import Galley.Effects.ProposalStore +import Galley.Types.Conversations.Members +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.Resource (Resource) +import Wire.API.Conversation.Protocol +import Wire.API.Conversation.Role +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Federation.API +import Wire.API.Federation.API.Galley +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Commit +import Wire.API.MLS.Credential +import qualified Wire.API.MLS.Proposal as Proposal +import Wire.API.MLS.SubConversation +import Wire.API.User.Client + +processInternalCommit :: + forall r. + ( HasProposalEffects r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'MLSCommitMissingReferences) r, + Member (ErrorS 'MLSSelfRemovalNotAllowed) r, + Member (ErrorS 'MLSStaleMessage) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member SubConversationStore r, + Member Resource r + ) => + ClientIdentity -> + Maybe ConnId -> + Local ConvOrSubConv -> + Epoch -> + ProposalAction -> + Commit -> + Sem r [LocalConversationUpdate] +processInternalCommit senderIdentity con lConvOrSub epoch action commit = do + let convOrSub = tUnqualified lConvOrSub + mlsMeta = mlsMetaConvOrSub convOrSub + qusr = cidQualifiedUser senderIdentity + cm = membersConvOrSub convOrSub + ss = csSignatureScheme (cnvmlsCipherSuite mlsMeta) + newUserClients = Map.assocs (paAdd action) + + -- check all pending proposals are referenced in the commit + allPendingProposals <- getAllPendingProposalRefs (cnvmlsGroupId mlsMeta) epoch + let referencedProposals = Set.fromList $ mapMaybe (\x -> preview Proposal._Ref x) commit.proposals + unless (all (`Set.member` referencedProposals) allPendingProposals) $ + throwS @'MLSCommitMissingReferences + + withCommitLock (fmap idForConvOrSub lConvOrSub) (cnvmlsGroupId (mlsMetaConvOrSub convOrSub)) epoch $ do + -- FUTUREWORK: remove this check after remote admins are implemented in federation https://wearezeta.atlassian.net/browse/FS-216 + foldQualified lConvOrSub (\_ -> pure ()) (\_ -> throwS @'MLSUnsupportedProposal) qusr + + -- no client can be directly added to a subconversation + when (is _SubConv convOrSub && any ((senderIdentity /=) . fst) (cmAssocs (paAdd action))) $ + throw (mlsProtocolError "Add proposals in subconversations are not supported") + + -- Note [client removal] + -- We support two types of removals: + -- 1. when a user is removed from a group, all their clients have to be removed + -- 2. when a client is deleted, that particular client (but not necessarily + -- other clients of the same user) has to be removed. + -- + -- Type 2 requires no special processing on the backend, so here we filter + -- out all removals of that type, so that further checks and processing can + -- be applied only to type 1 removals. + -- + -- Furthermore, subconversation clients can be removed arbitrarily, so this + -- processing is only necessary for main conversations. In the + -- subconversation case, an empty list is returned. + membersToRemove <- case convOrSub of + SubConv _ _ -> pure [] + Conv _ -> mapMaybe hush <$$> for (Map.assocs (paRemove action)) $ + \(qtarget, Map.keysSet -> clients) -> runError @() $ do + let clientsInConv = Map.keysSet (Map.findWithDefault mempty qtarget cm) + let removedClients = Set.intersection clients clientsInConv + + -- ignore user if none of their clients are being removed + when (Set.null removedClients) $ throw () + + -- return error if the user is trying to remove themself + when (cidQualifiedUser senderIdentity == qtarget) $ + throwS @'MLSSelfRemovalNotAllowed + + -- FUTUREWORK: add tests against this situation for conv v subconv + when (removedClients /= clientsInConv) $ do + -- FUTUREWORK: turn this error into a proper response + throwS @'MLSClientMismatch + + pure qtarget + + -- for each user, we compare their clients with the ones being added to the conversation + for_ newUserClients $ \(qtarget, newclients) -> case Map.lookup qtarget cm of + -- user is already present, skip check in this case + Just _ -> pure () + -- new user + Nothing -> do + -- final set of clients in the conversation + let clients = Map.keysSet (newclients <> Map.findWithDefault mempty qtarget cm) + -- get list of mls clients from brig + clientInfo <- getClientInfo lConvOrSub qtarget ss + let allClients = Set.map ciId clientInfo + let allMLSClients = Set.map ciId (Set.filter ciMLS clientInfo) + -- We check the following condition: + -- allMLSClients ⊆ clients ⊆ allClients + -- i.e. + -- - if a client has at least 1 key package, it has to be added + -- - if a client is being added, it has to still exist + -- + -- The reason why we can't simply check that clients == allMLSClients is + -- that a client with no remaining key packages might be added by a user + -- who just fetched its last key package. + unless + ( Set.isSubsetOf allMLSClients clients + && Set.isSubsetOf clients allClients + ) + $ do + -- unless (Set.isSubsetOf allClients clients) $ do + -- FUTUREWORK: turn this error into a proper response + throwS @'MLSClientMismatch + + -- remove users from the conversation and send events + removeEvents <- + foldMap + (removeMembers qusr con lConvOrSub) + (nonEmpty membersToRemove) + + -- Remove clients from the conversation state. This includes client removals + -- of all types (see Note [client removal]). + for_ (Map.assocs (paRemove action)) $ \(qtarget, clients) -> do + removeMLSClients (cnvmlsGroupId mlsMeta) qtarget (Map.keysSet clients) + + -- if this is a new subconversation, call `on-new-remote-conversation` on all + -- the remote backends involved in the main conversation + forOf_ _SubConv convOrSub $ \(mlsConv, subConv) -> do + when (cnvmlsEpoch (scMLSData subConv) == Epoch 0) $ do + let remoteDomains = + Set.fromList + ( map + (void . rmId) + (mcRemoteMembers mlsConv) + ) + let nrc = + NewRemoteSubConversation + { nrscConvId = mcId mlsConv, + nrscSubConvId = scSubConvId subConv, + nrscMlsData = scMLSData subConv + } + runFederatedConcurrently_ (toList remoteDomains) $ \_ -> do + void $ fedClient @'Galley @"on-new-remote-subconversation" nrc + + -- add users to the conversation and send events + addEvents <- + foldMap (addMembers qusr con lConvOrSub) + . nonEmpty + . map fst + $ newUserClients + + -- add clients in the conversation state + for_ newUserClients $ \(qtarget, newClients) -> do + addMLSClients (cnvmlsGroupId mlsMeta) qtarget (Set.fromList (Map.assocs newClients)) + + -- increment epoch number + for_ lConvOrSub incrementEpoch + + pure (addEvents <> removeEvents) + +addMembers :: + HasProposalActionEffects r => + Qualified UserId -> + Maybe ConnId -> + Local ConvOrSubConv -> + NonEmpty (Qualified UserId) -> + Sem r [LocalConversationUpdate] +addMembers qusr con lConvOrSub users = case tUnqualified lConvOrSub of + Conv mlsConv -> do + let lconv = qualifyAs lConvOrSub (mcConv mlsConv) + -- FUTUREWORK: update key package ref mapping to reflect conversation membership + foldMap + ( handleNoChanges + . handleMLSProposalFailures @ProposalErrors + . fmap pure + . updateLocalConversationUnchecked @'ConversationJoinTag lconv qusr con + . flip ConversationJoin roleNameWireMember + ) + . nonEmpty + . filter (flip Set.notMember (existingMembers lconv)) + . toList + $ users + SubConv _ _ -> pure [] + +removeMembers :: + HasProposalActionEffects r => + Qualified UserId -> + Maybe ConnId -> + Local ConvOrSubConv -> + NonEmpty (Qualified UserId) -> + Sem r [LocalConversationUpdate] +removeMembers qusr con lConvOrSub users = case tUnqualified lConvOrSub of + Conv mlsConv -> do + let lconv = qualifyAs lConvOrSub (mcConv mlsConv) + foldMap + ( handleNoChanges + . handleMLSProposalFailures @ProposalErrors + . fmap pure + . updateLocalConversationUnchecked @'ConversationRemoveMembersTag lconv qusr con + ) + . nonEmpty + . filter (flip Set.member (existingMembers lconv)) + . toList + $ users + SubConv _ _ -> pure [] + +handleNoChanges :: Monoid a => Sem (Error NoChanges ': r) a -> Sem r a +handleNoChanges = fmap fold . runError + +existingLocalMembers :: Local Data.Conversation -> Set (Qualified UserId) +existingLocalMembers lconv = + (Set.fromList . map (fmap lmId . tUntagged)) (traverse convLocalMembers lconv) + +existingRemoteMembers :: Local Data.Conversation -> Set (Qualified UserId) +existingRemoteMembers lconv = + Set.fromList . map (tUntagged . rmId) . convRemoteMembers . tUnqualified $ + lconv + +existingMembers :: Local Data.Conversation -> Set (Qualified UserId) +existingMembers lconv = existingLocalMembers lconv <> existingRemoteMembers lconv diff --git a/services/galley/src/Galley/API/MLS/Conversation.hs b/services/galley/src/Galley/API/MLS/Conversation.hs index fb2396d9c83..5d91d1e4ba6 100644 --- a/services/galley/src/Galley/API/MLS/Conversation.hs +++ b/services/galley/src/Galley/API/MLS/Conversation.hs @@ -34,7 +34,7 @@ mkMLSConversation :: Sem r (Maybe MLSConversation) mkMLSConversation conv = for (Data.mlsMetadata conv) $ \mlsData -> do - cm <- lookupMLSClients (cnvmlsGroupId mlsData) + (cm, im) <- lookupMLSClientLeafIndices (cnvmlsGroupId mlsData) pure MLSConversation { mcId = Data.convId conv, @@ -42,7 +42,8 @@ mkMLSConversation conv = mcLocalMembers = Data.convLocalMembers conv, mcRemoteMembers = Data.convRemoteMembers conv, mcMLSData = mlsData, - mcMembers = cm + mcMembers = cm, + mcIndexMap = im } mcConv :: MLSConversation -> Data.Conversation diff --git a/services/galley/src/Galley/API/MLS/GroupInfo.hs b/services/galley/src/Galley/API/MLS/GroupInfo.hs index 34fed731c0f..dfbe65f3c0a 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfo.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfo.hs @@ -36,7 +36,7 @@ import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error -import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation type MLSGroupInfoStaticErrors = @@ -55,7 +55,7 @@ getGroupInfo :: Members MLSGroupInfoStaticErrors r => Local UserId -> Qualified ConvId -> - Sem r OpaquePublicGroupState + Sem r GroupInfoData getGroupInfo lusr qcnvId = do assertMLSEnabled foldQualified @@ -71,10 +71,10 @@ getGroupInfoFromLocalConv :: Members MLSGroupInfoStaticErrors r => Qualified UserId -> Local ConvId -> - Sem r OpaquePublicGroupState + Sem r GroupInfoData getGroupInfoFromLocalConv qusr lcnvId = do void $ getLocalConvForUser qusr lcnvId - E.getPublicGroupState (tUnqualified lcnvId) + E.getGroupInfo (tUnqualified lcnvId) >>= noteS @'MLSMissingGroupInfo getGroupInfoFromRemoteConv :: @@ -84,7 +84,7 @@ getGroupInfoFromRemoteConv :: Members MLSGroupInfoStaticErrors r => Local UserId -> Remote ConvOrSubConvId -> - Sem r OpaquePublicGroupState + Sem r GroupInfoData getGroupInfoFromRemoteConv lusr rcnv = do let getRequest = GetGroupInfoRequest @@ -96,6 +96,6 @@ getGroupInfoFromRemoteConv lusr rcnv = do GetGroupInfoResponseError e -> rethrowErrors @MLSGroupInfoStaticErrors e GetGroupInfoResponseState s -> pure - . OpaquePublicGroupState + . GroupInfoData . fromBase64ByteString $ s diff --git a/services/galley/src/Galley/API/MLS/IncomingMessage.hs b/services/galley/src/Galley/API/MLS/IncomingMessage.hs new file mode 100644 index 00000000000..96b63cc6975 --- /dev/null +++ b/services/galley/src/Galley/API/MLS/IncomingMessage.hs @@ -0,0 +1,131 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.API.MLS.IncomingMessage + ( IncomingMessage (..), + IncomingMessageContent (..), + IncomingPublicMessageContent (..), + IncomingBundle (..), + mkIncomingMessage, + incomingMessageAuthenticatedContent, + mkIncomingBundle, + ) +where + +import GHC.Records +import Imports +import Wire.API.MLS.AuthenticatedContent +import Wire.API.MLS.Commit +import Wire.API.MLS.CommitBundle +import Wire.API.MLS.Epoch +import Wire.API.MLS.Group +import Wire.API.MLS.GroupInfo +import Wire.API.MLS.Message +import Wire.API.MLS.Serialisation +import Wire.API.MLS.Welcome + +data IncomingMessage = IncomingMessage + { epoch :: Epoch, + groupId :: GroupId, + content :: IncomingMessageContent, + rawMessage :: RawMLS Message + } + +instance HasField "sender" IncomingMessage (Maybe Sender) where + getField msg = case msg.content of + IncomingMessageContentPublic pub -> Just pub.sender + _ -> Nothing + +data IncomingMessageContent + = IncomingMessageContentPublic IncomingPublicMessageContent + | IncomingMessageContentPrivate + +data IncomingPublicMessageContent = IncomingPublicMessageContent + { sender :: Sender, + content :: FramedContentData, + -- for verification + framedContent :: RawMLS FramedContent, + authData :: RawMLS FramedContentAuthData + } + +data IncomingBundle = IncomingBundle + { epoch :: Epoch, + groupId :: GroupId, + sender :: Sender, + commit :: RawMLS Commit, + rawMessage :: RawMLS Message, + welcome :: Maybe (RawMLS Welcome), + groupInfo :: GroupInfoData, + serialized :: ByteString + } + +mkIncomingMessage :: RawMLS Message -> Maybe IncomingMessage +mkIncomingMessage msg = case msg.value.content of + MessagePublic pmsg -> + Just + IncomingMessage + { epoch = pmsg.content.value.epoch, + groupId = pmsg.content.value.groupId, + content = + IncomingMessageContentPublic + IncomingPublicMessageContent + { sender = pmsg.content.value.sender, + content = pmsg.content.value.content, + framedContent = pmsg.content, + authData = pmsg.authData + }, + rawMessage = msg + } + MessagePrivate pmsg + | pmsg.value.tag == FramedContentApplicationDataTag -> + Just + IncomingMessage + { epoch = pmsg.value.epoch, + groupId = pmsg.value.groupId, + content = IncomingMessageContentPrivate, + rawMessage = msg + } + _ -> Nothing + +incomingMessageAuthenticatedContent :: IncomingPublicMessageContent -> AuthenticatedContent +incomingMessageAuthenticatedContent pmsg = + AuthenticatedContent + { wireFormat = WireFormatPublicTag, + content = pmsg.framedContent, + authData = pmsg.authData + } + +mkIncomingBundle :: RawMLS CommitBundle -> Maybe IncomingBundle +mkIncomingBundle bundle = do + imsg <- mkIncomingMessage bundle.value.commitMsg + content <- case imsg.content of + IncomingMessageContentPublic c -> pure c + _ -> Nothing + commit <- case content.content of + FramedContentCommit c -> pure c + _ -> Nothing + pure + IncomingBundle + { epoch = imsg.epoch, + groupId = imsg.groupId, + sender = content.sender, + commit = commit, + rawMessage = bundle.value.commitMsg, + welcome = bundle.value.welcome, + groupInfo = GroupInfoData bundle.value.groupInfo.raw, + serialized = bundle.raw + } diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 967c005baf8..06674681267 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -16,53 +16,40 @@ -- with this program. If not, see . module Galley.API.MLS.Message - ( postMLSCommitBundle, + ( IncomingBundle (..), + mkIncomingBundle, + IncomingMessage (..), + mkIncomingMessage, + postMLSCommitBundle, postMLSCommitBundleFromLocalUser, postMLSMessageFromLocalUser, - postMLSMessageFromLocalUserV1, postMLSMessage, MLSMessageStaticErrors, MLSBundleStaticErrors, ) where -import Control.Arrow ((>>>)) import Control.Comonad -import Control.Error.Util (hush) -import Control.Lens (forOf_, preview) -import Control.Lens.Extras (is) import Data.Id import Data.Json.Util -import Data.List.NonEmpty (NonEmpty, nonEmpty) -import qualified Data.Map as Map import Data.Qualified -import qualified Data.Set as Set -import qualified Data.Text as T -import Data.Time import Data.Tuple.Extra import Galley.API.Action -import Galley.API.Error +import Galley.API.MLS.Commit import Galley.API.MLS.Conversation import Galley.API.MLS.Enabled -import Galley.API.MLS.KeyPackage +import Galley.API.MLS.IncomingMessage import Galley.API.MLS.Propagate -import Galley.API.MLS.Removal +import Galley.API.MLS.Proposal import Galley.API.MLS.Types import Galley.API.MLS.Util -import Galley.API.MLS.Welcome (postMLSWelcome) +import Galley.API.MLS.Welcome (sendWelcomes) import Galley.API.Util -import Galley.Data.Conversation.Types hiding (Conversation) -import qualified Galley.Data.Conversation.Types as Data import Galley.Effects -import Galley.Effects.BrigAccess import Galley.Effects.ConversationStore import Galley.Effects.FederatorAccess import Galley.Effects.MemberStore -import Galley.Effects.ProposalStore import Galley.Effects.SubConversationStore -import Galley.Env -import Galley.Options -import Galley.Types.Conversations.Members import Imports import Polysemy import Polysemy.Error @@ -70,32 +57,25 @@ import Polysemy.Input import Polysemy.Internal import Polysemy.Resource (Resource) import Polysemy.TinyLog -import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol -import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.Event.Conversation import Wire.API.Federation.API -import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Galley import Wire.API.Federation.Error -import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.CommitBundle import Wire.API.MLS.Credential -import Wire.API.MLS.GroupInfoBundle -import Wire.API.MLS.KeyPackage +import Wire.API.MLS.GroupInfo import Wire.API.MLS.Message -import Wire.API.MLS.Proposal -import qualified Wire.API.MLS.Proposal as Proposal -import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation -import Wire.API.MLS.Welcome -import Wire.API.Message -import Wire.API.Routes.Internal.Brig -import Wire.API.User.Client + +-- FUTUREWORK +-- - Check that the capabilities of a leaf node in an add proposal contains all +-- the required_capabilities of the group context. This would require fetching +-- the group info from the DB in order to read the group context. +-- - Verify message signature, this also requires the group context. (see above) type MLSMessageStaticErrors = '[ ErrorS 'ConvAccessDenied, @@ -106,14 +86,13 @@ type MLSMessageStaticErrors = ErrorS 'MLSStaleMessage, ErrorS 'MLSProposalNotFound, ErrorS 'MissingLegalholdConsent, - ErrorS 'MLSKeyPackageRefNotFound, + ErrorS 'MLSInvalidLeafNodeIndex, ErrorS 'MLSClientMismatch, ErrorS 'MLSUnsupportedProposal, ErrorS 'MLSCommitMissingReferences, ErrorS 'MLSSelfRemovalNotAllowed, ErrorS 'MLSClientSenderUserMismatch, ErrorS 'MLSGroupConversationMismatch, - ErrorS 'MLSMissingSenderClient, ErrorS 'MLSSubConvClientNotInParent ] @@ -122,39 +101,6 @@ type MLSBundleStaticErrors = MLSMessageStaticErrors '[ErrorS 'MLSWelcomeMismatch] -postMLSMessageFromLocalUserV1 :: - ( HasProposalEffects r, - Member (Error FederationError) r, - Member (ErrorS 'ConvAccessDenied) r, - Member (ErrorS 'ConvMemberNotFound) r, - Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MissingLegalholdConsent) r, - Member (ErrorS 'MLSClientSenderUserMismatch) r, - Member (ErrorS 'MLSCommitMissingReferences) r, - Member (ErrorS 'MLSGroupConversationMismatch) r, - Member (ErrorS 'MLSMissingSenderClient) r, - Member (ErrorS 'MLSNotEnabled) r, - Member (ErrorS 'MLSProposalNotFound) r, - Member (ErrorS 'MLSSelfRemovalNotAllowed) r, - Member (ErrorS 'MLSStaleMessage) r, - Member (ErrorS 'MLSUnsupportedMessage) r, - Member (ErrorS 'MLSSubConvClientNotInParent) r, - Member Resource r, - Member SubConversationStore r - ) => - Local UserId -> - Maybe ClientId -> - ConnId -> - RawMLS SomeMessage -> - Sem r [Event] -postMLSMessageFromLocalUserV1 lusr mc conn smsg = do - assertMLSEnabled - case rmValue smsg of - SomeMessage _ msg -> do - cnvOrSub <- lookupConvByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound - fst . first (map lcuEvent) - <$> postMLSMessage lusr (tUntagged lusr) mc cnvOrSub (Just conn) smsg - postMLSMessageFromLocalUser :: ( HasProposalEffects r, Member (Error FederationError) r, @@ -165,29 +111,26 @@ postMLSMessageFromLocalUser :: Member (ErrorS 'MLSClientSenderUserMismatch) r, Member (ErrorS 'MLSCommitMissingReferences) r, Member (ErrorS 'MLSGroupConversationMismatch) r, - Member (ErrorS 'MLSMissingSenderClient) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'MLSProposalNotFound) r, Member (ErrorS 'MLSSelfRemovalNotAllowed) r, Member (ErrorS 'MLSStaleMessage) r, Member (ErrorS 'MLSUnsupportedMessage) r, Member (ErrorS 'MLSSubConvClientNotInParent) r, - Member Resource r, Member SubConversationStore r ) => Local UserId -> - Maybe ClientId -> + ClientId -> ConnId -> - RawMLS SomeMessage -> + RawMLS Message -> Sem r MLSMessageSendingStatus -postMLSMessageFromLocalUser lusr mc conn smsg = do +postMLSMessageFromLocalUser lusr c conn smsg = do assertMLSEnabled + imsg <- noteS @'MLSUnsupportedMessage $ mkIncomingMessage smsg + cnvOrSub <- lookupConvByGroupId imsg.groupId >>= noteS @'ConvNotFound (events, unreachables) <- - case rmValue smsg of - SomeMessage _ msg -> do - cnvOrSub <- lookupConvByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound - first (map lcuEvent) - <$> postMLSMessage lusr (tUntagged lusr) mc cnvOrSub (Just conn) smsg + first (map lcuEvent) + <$> postMLSMessage lusr (tUntagged lusr) c cnvOrSub (Just conn) imsg t <- toUTCTimeMillis <$> input pure $ MLSMessageSendingStatus events t unreachables @@ -200,16 +143,16 @@ postMLSCommitBundle :: ) => Local x -> Qualified UserId -> - Maybe ClientId -> + ClientId -> Qualified ConvOrSubConvId -> Maybe ConnId -> - CommitBundle -> + IncomingBundle -> Sem r ([LocalConversationUpdate], UnreachableUsers) -postMLSCommitBundle loc qusr mc qConvOrSub conn rawBundle = +postMLSCommitBundle loc qusr c qConvOrSub conn bundle = foldQualified loc - (postMLSCommitBundleToLocalConv qusr mc conn rawBundle) - (postMLSCommitBundleToRemoteConv loc qusr mc conn rawBundle) + (postMLSCommitBundleToLocalConv qusr c conn bundle) + (postMLSCommitBundleToRemoteConv loc qusr c conn bundle) qConvOrSub postMLSCommitBundleFromLocalUser :: @@ -220,17 +163,17 @@ postMLSCommitBundleFromLocalUser :: Member SubConversationStore r ) => Local UserId -> - Maybe ClientId -> + ClientId -> ConnId -> - CommitBundle -> + RawMLS CommitBundle -> Sem r MLSMessageSendingStatus -postMLSCommitBundleFromLocalUser lusr mc conn bundle = do +postMLSCommitBundleFromLocalUser lusr c conn bundle = do assertMLSEnabled - let msg = rmValue (cbCommitMsg bundle) - qConvOrSub <- lookupConvByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound + ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle + qConvOrSub <- lookupConvByGroupId ibundle.groupId >>= noteS @'ConvNotFound (events, unreachables) <- first (map lcuEvent) - <$> postMLSCommitBundle lusr (tUntagged lusr) mc qConvOrSub (Just conn) bundle + <$> postMLSCommitBundle lusr (tUntagged lusr) c qConvOrSub (Just conn) ibundle t <- toUTCTimeMillis <$> input pure $ MLSMessageSendingStatus events t unreachables @@ -241,49 +184,44 @@ postMLSCommitBundleToLocalConv :: Member SubConversationStore r ) => Qualified UserId -> - Maybe ClientId -> + ClientId -> Maybe ConnId -> - CommitBundle -> + IncomingBundle -> Local ConvOrSubConvId -> Sem r ([LocalConversationUpdate], UnreachableUsers) -postMLSCommitBundleToLocalConv qusr mc conn bundle lConvOrSubId = do +postMLSCommitBundleToLocalConv qusr c conn bundle lConvOrSubId = do lConvOrSub <- fetchConvOrSub qusr lConvOrSubId - let msg = rmValue (cbCommitMsg bundle) - - senderClient <- fmap ciClient <$> getSenderIdentity qusr mc SMLSPlainText msg - - events <- case msgPayload msg of - CommitMessage commit -> - do - action <- getCommitData lConvOrSub (msgEpoch msg) commit - -- check that the welcome message matches the action - for_ (cbWelcome bundle) $ \welcome -> - when - ( Set.fromList (map gsNewMember (welSecrets (rmValue welcome))) - /= Set.fromList (map (snd . snd) (cmAssocs (paAdd action))) - ) - $ throwS @'MLSWelcomeMismatch - updates <- - processCommitWithAction - qusr - senderClient - conn - lConvOrSub - (msgEpoch msg) - action - (msgSender msg) - commit - storeGroupInfoBundle (idForConvOrSub . tUnqualified $ lConvOrSub) (cbGroupInfoBundle bundle) - pure updates - ApplicationMessage _ -> throwS @'MLSUnsupportedMessage - ProposalMessage _ -> throwS @'MLSUnsupportedMessage + senderIdentity <- getSenderIdentity qusr c bundle.sender lConvOrSub + + (events, newClients) <- case bundle.sender of + SenderMember _index -> do + action <- getCommitData senderIdentity lConvOrSub bundle.epoch bundle.commit.value + events <- + processInternalCommit + senderIdentity + conn + lConvOrSub + bundle.epoch + action + bundle.commit.value + pure (events, cmIdentities (paAdd action)) + SenderExternal _ -> throw (mlsProtocolError "Unexpected sender") + SenderNewMemberProposal -> throw (mlsProtocolError "Unexpected sender") + SenderNewMemberCommit -> do + action <- getExternalCommitData senderIdentity lConvOrSub bundle.epoch bundle.commit.value + processExternalCommit + senderIdentity + lConvOrSub + bundle.epoch + action + bundle.commit.value.path + pure ([], []) + + storeGroupInfo (idForConvOrSub . tUnqualified $ lConvOrSub) bundle.groupInfo let cm = membersConvOrSub (tUnqualified lConvOrSub) - unreachables <- propagateMessage qusr lConvOrSub conn (rmRaw (cbCommitMsg bundle)) cm - - for_ (cbWelcome bundle) $ - postMLSWelcome lConvOrSub conn - + unreachables <- propagateMessage qusr lConvOrSub conn bundle.rawMessage cm + traverse_ (sendWelcomes lConvOrSub conn newClients) bundle.welcome pure (events, unreachables) postMLSCommitBundleToRemoteConv :: @@ -301,34 +239,26 @@ postMLSCommitBundleToRemoteConv :: ) => Local x -> Qualified UserId -> - Maybe ClientId -> + ClientId -> Maybe ConnId -> - CommitBundle -> + IncomingBundle -> Remote ConvOrSubConvId -> Sem r ([LocalConversationUpdate], UnreachableUsers) -postMLSCommitBundleToRemoteConv loc qusr mc con bundle rConvOrSubId = do +postMLSCommitBundleToRemoteConv loc qusr c con bundle rConvOrSubId = do -- only local users can send messages to remote conversations lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr -- only members may send commit bundles to a remote conversation flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) (convOfConvOrSub <$> rConvOrSubId) - senderIdentity <- - noteS @'MLSMissingSenderClient - =<< getSenderIdentity - qusr - mc - SMLSPlainText - (rmValue (cbCommitMsg bundle)) - resp <- runFederated rConvOrSubId $ fedClient @'Galley @"send-mls-commit-bundle" $ MLSMessageSendRequest { mmsrConvOrSubId = tUnqualified rConvOrSubId, mmsrSender = tUnqualified lusr, - mmsrSenderClient = ciClient senderIdentity, - mmsrRawMessage = Base64ByteString (serializeCommitBundle bundle) + mmsrSenderClient = c, + mmsrRawMessage = Base64ByteString bundle.serialized } case resp of MLSMessageResponseError e -> rethrowErrors @MLSBundleStaticErrors e @@ -351,119 +281,79 @@ postMLSMessage :: Member (ErrorS 'MLSClientSenderUserMismatch) r, Member (ErrorS 'MLSCommitMissingReferences) r, Member (ErrorS 'MLSGroupConversationMismatch) r, - Member (ErrorS 'MLSMissingSenderClient) r, Member (ErrorS 'MLSProposalNotFound) r, Member (ErrorS 'MLSSelfRemovalNotAllowed) r, Member (ErrorS 'MLSStaleMessage) r, Member (ErrorS 'MLSUnsupportedMessage) r, Member (ErrorS 'MLSSubConvClientNotInParent) r, - Member Resource r, Member SubConversationStore r ) => Local x -> Qualified UserId -> - Maybe ClientId -> + ClientId -> Qualified ConvOrSubConvId -> Maybe ConnId -> - RawMLS SomeMessage -> + IncomingMessage -> Sem r ([LocalConversationUpdate], UnreachableUsers) -postMLSMessage loc qusr mc qconvOrSub con smsg = case rmValue smsg of - SomeMessage tag msg -> do - mSender <- fmap ciClient <$> getSenderIdentity qusr mc tag msg - foldQualified - loc - (postMLSMessageToLocalConv qusr mSender con smsg) - (postMLSMessageToRemoteConv loc qusr mSender con smsg) - qconvOrSub - --- Check that the MLS client who created the message belongs to the user who --- is the sender of the REST request, identified by HTTP header. --- --- The check is skipped in case of conversation creation and encrypted messages. -getSenderClient :: - ( Member (ErrorS 'MLSKeyPackageRefNotFound) r, - Member (ErrorS 'MLSClientSenderUserMismatch) r, - Member BrigAccess r - ) => - Qualified UserId -> - SWireFormatTag tag -> - Message tag -> - Sem r (Maybe ClientId) -getSenderClient _ SMLSCipherText _ = pure Nothing -getSenderClient _ _ msg | msgEpoch msg == Epoch 0 = pure Nothing -getSenderClient qusr SMLSPlainText msg = case msgSender msg of - PreconfiguredSender _ -> pure Nothing - NewMemberSender -> pure Nothing - MemberSender ref -> do - cid <- derefKeyPackage ref - when (fmap fst (cidQualifiedClient cid) /= qusr) $ - throwS @'MLSClientSenderUserMismatch - pure (Just (ciClient cid)) +postMLSMessage loc qusr c qconvOrSub con msg = do + foldQualified + loc + (postMLSMessageToLocalConv qusr c con msg) + (postMLSMessageToRemoteConv loc qusr c con msg) + qconvOrSub --- FUTUREWORK: once we can assume that the Z-Client header is present (i.e. --- when v2 is dropped), remove the Maybe in the return type. getSenderIdentity :: - ( Member (ErrorS 'MLSKeyPackageRefNotFound) r, - Member (ErrorS 'MLSClientSenderUserMismatch) r, - Member BrigAccess r + ( Member (ErrorS 'MLSClientSenderUserMismatch) r, + Member (Error MLSProtocolError) r ) => Qualified UserId -> - Maybe ClientId -> - SWireFormatTag tag -> - Message tag -> - Sem r (Maybe ClientIdentity) -getSenderIdentity qusr mc fmt msg = do - mSender <- getSenderClient qusr fmt msg - -- At this point, mc is the client ID of the request, while mSender is the - -- one contained in the message. We throw an error if the two don't match. - when (((==) <$> mc <*> mSender) == Just False) $ - throwS @'MLSClientSenderUserMismatch - pure (mkClientIdentity qusr <$> (mc <|> mSender)) + ClientId -> + Sender -> + Local ConvOrSubConv -> + Sem r ClientIdentity +getSenderIdentity qusr c mSender lConvOrSubConv = do + let cid = mkClientIdentity qusr c + let idxMap = indexMapConvOrSub $ tUnqualified lConvOrSubConv + let epoch = epochNumber . cnvmlsEpoch . mlsMetaConvOrSub . tUnqualified $ lConvOrSubConv + case mSender of + SenderMember idx | epoch > 0 -> do + cid' <- note (mlsProtocolError "unknown sender leaf index") $ imLookup idxMap idx + unless (cid' == cid) $ throwS @'MLSClientSenderUserMismatch + _ -> pure () + pure cid postMLSMessageToLocalConv :: ( HasProposalEffects r, Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MissingLegalholdConsent) r, Member (ErrorS 'MLSClientSenderUserMismatch) r, - Member (ErrorS 'MLSCommitMissingReferences) r, - Member (ErrorS 'MLSMissingSenderClient) r, - Member (ErrorS 'MLSProposalNotFound) r, - Member (ErrorS 'MLSSelfRemovalNotAllowed) r, Member (ErrorS 'MLSStaleMessage) r, Member (ErrorS 'MLSUnsupportedMessage) r, - Member (ErrorS 'MLSSubConvClientNotInParent) r, - Member Resource r, Member SubConversationStore r ) => Qualified UserId -> - Maybe ClientId -> + ClientId -> Maybe ConnId -> - RawMLS SomeMessage -> + IncomingMessage -> Local ConvOrSubConvId -> Sem r ([LocalConversationUpdate], UnreachableUsers) -postMLSMessageToLocalConv qusr senderClient con smsg convOrSubId = - case rmValue smsg of - SomeMessage tag msg -> do - lConvOrSub <- fetchConvOrSub qusr convOrSubId +postMLSMessageToLocalConv qusr c con msg convOrSubId = do + lConvOrSub <- fetchConvOrSub qusr convOrSubId - -- validate message - events <- case tag of - SMLSPlainText -> case msgPayload msg of - CommitMessage c -> - processCommit qusr senderClient con lConvOrSub (msgEpoch msg) (msgSender msg) c - ApplicationMessage _ -> throwS @'MLSUnsupportedMessage - ProposalMessage prop -> - processProposal qusr lConvOrSub msg prop $> mempty - SMLSCipherText -> case toMLSEnum' (msgContentType (msgPayload msg)) of - Right CommitMessageTag -> throwS @'MLSUnsupportedMessage - Right ProposalMessageTag -> throwS @'MLSUnsupportedMessage - Right ApplicationMessageTag -> pure mempty - Left _ -> throwS @'MLSUnsupportedMessage + for_ msg.sender $ \sender -> + void $ getSenderIdentity qusr c sender lConvOrSub - let cm = membersConvOrSub (tUnqualified lConvOrSub) - -- forward message - unreachables <- propagateMessage qusr lConvOrSub con (rmRaw smsg) cm - pure (events, unreachables) + -- validate message + case msg.content of + IncomingMessageContentPublic pub -> case pub.content of + FramedContentCommit _commit -> throwS @'MLSUnsupportedMessage + FramedContentApplicationData _ -> throwS @'MLSUnsupportedMessage + FramedContentProposal prop -> + processProposal qusr lConvOrSub msg.groupId msg.epoch pub prop + IncomingMessageContentPrivate -> pure () + + let cm = membersConvOrSub (tUnqualified lConvOrSub) + unreachables <- propagateMessage qusr lConvOrSub con msg.rawMessage cm + pure ([], unreachables) postMLSMessageToRemoteConv :: ( Members MLSMessageStaticErrors r, @@ -474,18 +364,17 @@ postMLSMessageToRemoteConv :: ) => Local x -> Qualified UserId -> - Maybe ClientId -> + ClientId -> Maybe ConnId -> - RawMLS SomeMessage -> + IncomingMessage -> Remote ConvOrSubConvId -> Sem r ([LocalConversationUpdate], UnreachableUsers) -postMLSMessageToRemoteConv loc qusr mc con smsg rConvOrSubId = do +postMLSMessageToRemoteConv loc qusr senderClient con msg rConvOrSubId = do -- only local users can send messages to remote conversations lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr -- only members may send messages to the remote conversation flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) (convOfConvOrSub <$> rConvOrSubId) - senderClient <- noteS @'MLSMissingSenderClient mc resp <- runFederated rConvOrSubId $ fedClient @'Galley @"send-mls-message" $ @@ -493,7 +382,7 @@ postMLSMessageToRemoteConv loc qusr mc con smsg rConvOrSubId = do { mmsrConvOrSubId = tUnqualified rConvOrSubId, mmsrSender = tUnqualified lusr, mmsrSenderClient = senderClient, - mmsrRawMessage = Base64ByteString (rmRaw smsg) + mmsrRawMessage = Base64ByteString msg.rawMessage.raw } case resp of MLSMessageResponseError e -> rethrowErrors @MLSMessageStaticErrors e @@ -506,885 +395,16 @@ postMLSMessageToRemoteConv loc qusr mc con smsg rConvOrSubId = do pure (LocalConversationUpdate e update) pure (lcus, unreachables) -type HasProposalEffects r = - ( Member BrigAccess r, - Member ConversationStore r, - Member (Error InternalError) r, - Member (Error MLSProposalFailure) r, - Member (Error MLSProtocolError) r, - Member (ErrorS 'MLSClientMismatch) r, - Member (ErrorS 'MLSKeyPackageRefNotFound) r, - Member (ErrorS 'MLSUnsupportedProposal) r, - Member ExternalAccess r, - Member FederatorAccess r, - Member GundeckAccess r, - Member (Input Env) r, - Member (Input (Local ())) r, - Member (Input Opts) r, - Member (Input UTCTime) r, - Member LegalHoldStore r, - Member MemberStore r, - Member ProposalStore r, - Member TeamStore r, - Member TeamStore r, - Member TinyLog r - ) - -data ProposalAction = ProposalAction - { paAdd :: ClientMap, - paRemove :: ClientMap, - -- The backend does not process external init proposals, but still it needs - -- to know if a commit has one when processing external commits - paExternalInit :: Any - } - deriving (Show) - -instance Semigroup ProposalAction where - ProposalAction add1 rem1 init1 <> ProposalAction add2 rem2 init2 = - ProposalAction - (Map.unionWith mappend add1 add2) - (Map.unionWith mappend rem1 rem2) - (init1 <> init2) - -instance Monoid ProposalAction where - mempty = ProposalAction mempty mempty mempty - -paAddClient :: Qualified (UserId, (ClientId, KeyPackageRef)) -> ProposalAction -paAddClient quc = mempty {paAdd = Map.singleton (fmap fst quc) (uncurry Map.singleton (snd (qUnqualified quc)))} - -paRemoveClient :: Qualified (UserId, (ClientId, KeyPackageRef)) -> ProposalAction -paRemoveClient quc = mempty {paRemove = Map.singleton (fmap fst quc) (uncurry Map.singleton (snd (qUnqualified quc)))} - -paExternalInitPresent :: ProposalAction -paExternalInitPresent = mempty {paExternalInit = Any True} - -getCommitData :: - ( HasProposalEffects r, - Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MLSProposalNotFound) r, - Member (ErrorS 'MLSStaleMessage) r - ) => - Local ConvOrSubConv -> - Epoch -> - Commit -> - Sem r ProposalAction -getCommitData lConvOrSub epoch commit = do - let convOrSub = tUnqualified lConvOrSub - mlsMeta = mlsMetaConvOrSub convOrSub - curEpoch = cnvmlsEpoch mlsMeta - groupId = cnvmlsGroupId mlsMeta - suite = cnvmlsCipherSuite mlsMeta - - -- check epoch number - when (epoch /= curEpoch) $ throwS @'MLSStaleMessage - foldMap (applyProposalRef (idForConvOrSub convOrSub) mlsMeta groupId epoch suite) (cProposals commit) - -processCommit :: - ( HasProposalEffects r, - Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MissingLegalholdConsent) r, - Member (ErrorS 'MLSClientSenderUserMismatch) r, - Member (ErrorS 'MLSCommitMissingReferences) r, - Member (ErrorS 'MLSMissingSenderClient) r, - Member (ErrorS 'MLSProposalNotFound) r, - Member (ErrorS 'MLSSelfRemovalNotAllowed) r, - Member (ErrorS 'MLSStaleMessage) r, - Member (ErrorS 'MLSSubConvClientNotInParent) r, - Member Resource r, - Member SubConversationStore r - ) => - Qualified UserId -> - Maybe ClientId -> - Maybe ConnId -> - Local ConvOrSubConv -> - Epoch -> - Sender 'MLSPlainText -> - Commit -> - Sem r [LocalConversationUpdate] -processCommit qusr senderClient con lConvOrSub epoch sender commit = do - action <- getCommitData lConvOrSub epoch commit - processCommitWithAction qusr senderClient con lConvOrSub epoch action sender commit - -processExternalCommit :: - forall r. - ( Member BrigAccess r, - Member ConversationStore r, - Member (Error MLSProtocolError) r, - Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MLSClientSenderUserMismatch) r, - Member (ErrorS 'MLSKeyPackageRefNotFound) r, - Member (ErrorS 'MLSStaleMessage) r, - Member (ErrorS 'MLSMissingSenderClient) r, - Member (ErrorS 'MLSSubConvClientNotInParent) r, - Member ExternalAccess r, - Member FederatorAccess r, - Member GundeckAccess r, - Member (Input Env) r, - Member (Input UTCTime) r, - Member MemberStore r, - Member ProposalStore r, - Member Resource r, - Member SubConversationStore r, - Member TinyLog r - ) => - Qualified UserId -> - Maybe ClientId -> - Local ConvOrSubConv -> - Epoch -> - ProposalAction -> - Maybe UpdatePath -> - Sem r () -processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = - withCommitLock (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub) epoch $ do - let convOrSub = tUnqualified lConvOrSub - newKeyPackage <- - upLeaf - <$> note - (mlsProtocolError "External commits need an update path") - updatePath - when (paExternalInit action == mempty) $ - throw . mlsProtocolError $ - "The external commit is missing an external init proposal" - unless (paAdd action == mempty) $ - throw . mlsProtocolError $ - "The external commit must not have add proposals" - - newRef <- - kpRef' newKeyPackage - & note (mlsProtocolError "An invalid key package in the update path") - - -- validate and update mapping in brig - eithCid <- - nkpresClientIdentity - <$$> validateAndAddKeyPackageRef - NewKeyPackage - { nkpConversation = tUntagged (convOfConvOrSub . idForConvOrSub <$> lConvOrSub), - nkpKeyPackage = KeyPackageData (rmRaw newKeyPackage) - } - cid <- either (\errMsg -> throw (mlsProtocolError ("Tried to add invalid KeyPackage: " <> errMsg))) pure eithCid - - unless (cidQualifiedUser cid == qusr) $ - throw . mlsProtocolError $ - "The external commit attempts to add another user" - - senderClient <- noteS @'MLSMissingSenderClient mSenderClient - - unless (ciClient cid == senderClient) $ - throw . mlsProtocolError $ - "The external commit attempts to add another client of the user, it must only add itself" - - -- only members can join a subconversation - forOf_ _SubConv convOrSub $ \(mlsConv, _) -> - unless (isClientMember cid (mcMembers mlsConv)) $ - throwS @'MLSSubConvClientNotInParent - - -- check if there is a key package ref in the remove proposal - remRef <- - if Map.null (paRemove action) - then pure Nothing - else do - (remCid, r) <- derefUser (paRemove action) qusr - unless (cidQualifiedUser cid == cidQualifiedUser remCid) - . throw - . mlsProtocolError - $ "The external commit attempts to remove a client from a user other than themselves" - pure (Just r) - - updateKeyPackageMapping lConvOrSub qusr (ciClient cid) remRef newRef - - -- increment epoch number - lConvOrSub' <- for lConvOrSub incrementEpoch - - -- fetch backend remove proposals of the previous epoch - kpRefs <- - -- skip remove proposals of already removed by the external commit - filter (maybe (const True) (/=) remRef) - <$> getPendingBackendRemoveProposals (cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub') epoch - -- requeue backend remove proposals for the current epoch - let cm = membersConvOrSub (tUnqualified lConvOrSub') - createAndSendRemoveProposals lConvOrSub' kpRefs qusr cm - where - derefUser :: ClientMap -> Qualified UserId -> Sem r (ClientIdentity, KeyPackageRef) - derefUser cm user = case Map.assocs cm of - [(u, clients)] -> do - unless (user == u) $ - throwS @'MLSClientSenderUserMismatch - ref <- ensureSingleton clients - ci <- derefKeyPackage ref - unless (cidQualifiedUser ci == user) $ - throwS @'MLSClientSenderUserMismatch - pure (ci, ref) - _ -> throwRemProposal - ensureSingleton :: Map k a -> Sem r a - ensureSingleton m = case Map.elems m of - [e] -> pure e - _ -> throwRemProposal - throwRemProposal = - throw . mlsProtocolError $ - "The external commit must have at most one remove proposal" - -processCommitWithAction :: - forall r. - ( HasProposalEffects r, - Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MissingLegalholdConsent) r, - Member (ErrorS 'MLSClientSenderUserMismatch) r, - Member (ErrorS 'MLSCommitMissingReferences) r, - Member (ErrorS 'MLSMissingSenderClient) r, - Member (ErrorS 'MLSSelfRemovalNotAllowed) r, - Member (ErrorS 'MLSStaleMessage) r, - Member (ErrorS 'MLSSubConvClientNotInParent) r, - Member Resource r, - Member SubConversationStore r - ) => - Qualified UserId -> - Maybe ClientId -> - Maybe ConnId -> - Local ConvOrSubConv -> - Epoch -> - ProposalAction -> - Sender 'MLSPlainText -> - Commit -> - Sem r [LocalConversationUpdate] -processCommitWithAction qusr senderClient con lConvOrSub epoch action sender commit = - case sender of - MemberSender ref -> processInternalCommit qusr senderClient con lConvOrSub epoch action ref commit - NewMemberSender -> processExternalCommit qusr senderClient lConvOrSub epoch action (cPath commit) $> [] - _ -> throw (mlsProtocolError "Unexpected sender") - -processInternalCommit :: - forall r. - ( HasProposalEffects r, - Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MLSCommitMissingReferences) r, - Member (ErrorS 'MLSMissingSenderClient) r, - Member (ErrorS 'MLSSelfRemovalNotAllowed) r, - Member (ErrorS 'MLSStaleMessage) r, - Member (ErrorS 'MissingLegalholdConsent) r, - Member (ErrorS 'MLSSubConvClientNotInParent) r, - Member SubConversationStore r, - Member Resource r - ) => - Qualified UserId -> - Maybe ClientId -> - Maybe ConnId -> - Local ConvOrSubConv -> - Epoch -> - ProposalAction -> - KeyPackageRef -> - Commit -> - Sem r [LocalConversationUpdate] -processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef commit = do - let convOrSub = tUnqualified lConvOrSub - mlsMeta = mlsMetaConvOrSub convOrSub - localSelf = isLocal lConvOrSub qusr - - updatePathRef <- - for - (cPath commit) - (upLeaf >>> kpRef' >>> note (mlsProtocolError "Could not compute key package ref")) - - withCommitLock (cnvmlsGroupId . mlsMetaConvOrSub $ convOrSub) epoch $ do - postponedKeyPackageRefUpdate <- - if epoch == Epoch 0 - then do - let cType = cnvmType . mcMetadata . convOfConvOrSub $ convOrSub - case (localSelf, cType, cmAssocs . membersConvOrSub $ convOrSub, convOrSub) of - (True, SelfConv, [], Conv _) -> do - creatorClient <- noteS @'MLSMissingSenderClient senderClient - let creatorRef = fromMaybe senderRef updatePathRef - updateKeyPackageMapping lConvOrSub qusr creatorClient Nothing creatorRef - (True, SelfConv, _, _) -> - -- this is a newly created (sub)conversation, and it should - -- contain exactly one client (the creator) - throw (InternalErrorWithDescription "Unexpected creator client set") - (True, _, [(qu, (creatorClient, _))], Conv _) - | qu == qusr -> do - -- use update path as sender reference and if not existing fall back to sender - let creatorRef = fromMaybe senderRef updatePathRef - -- register the creator client - updateKeyPackageMapping - lConvOrSub - qusr - creatorClient - Nothing - creatorRef - -- remote clients cannot send the first commit - (False, _, _, _) -> throwS @'MLSStaleMessage - (True, _, [], SubConv parentConv _) -> do - creatorClient <- noteS @'MLSMissingSenderClient senderClient - unless (isClientMember (mkClientIdentity qusr creatorClient) (mcMembers parentConv)) $ - throwS @'MLSSubConvClientNotInParent - let creatorRef = fromMaybe senderRef updatePathRef - updateKeyPackageMapping lConvOrSub qusr creatorClient Nothing creatorRef - (_, _, _, _) -> - throw (InternalErrorWithDescription "Unexpected creator client set") - pure $ pure () -- no key package ref update necessary - else case updatePathRef of - Just updatedRef -> do - -- postpone key package ref update until other checks/processing passed - case senderClient of - Just cli -> - pure - ( updateKeyPackageMapping - lConvOrSub - qusr - cli - (Just senderRef) - updatedRef - ) - Nothing -> pure (pure ()) - Nothing -> pure (pure ()) -- ignore commits without update path - - -- check all pending proposals are referenced in the commit - allPendingProposals <- getAllPendingProposalRefs (cnvmlsGroupId mlsMeta) epoch - let referencedProposals = Set.fromList $ mapMaybe (\x -> preview Proposal._Ref x) (cProposals commit) - unless (all (`Set.member` referencedProposals) allPendingProposals) $ - throwS @'MLSCommitMissingReferences - - -- process and execute proposals - updates <- executeProposalAction qusr con lConvOrSub action - - -- update key package ref if necessary - postponedKeyPackageRefUpdate - -- increment epoch number - for_ lConvOrSub incrementEpoch - - pure updates - --- | Note: Use this only for KeyPackage that are already validated -updateKeyPackageMapping :: - ( Member BrigAccess r, - Member MemberStore r - ) => - Local ConvOrSubConv -> - Qualified UserId -> - ClientId -> - Maybe KeyPackageRef -> - KeyPackageRef -> - Sem r () -updateKeyPackageMapping lConvOrSub qusr cid mOld new = do - let qconv = tUntagged (convOfConvOrSub . idForConvOrSub <$> lConvOrSub) - -- update actual mapping in brig - case mOld of - Nothing -> - addKeyPackageRef new qusr cid qconv - Just old -> - updateKeyPackageRef - KeyPackageUpdate - { kpupPrevious = old, - kpupNext = new - } - let groupId = cnvmlsGroupId . mlsMetaConvOrSub . tUnqualified $ lConvOrSub - - -- remove old (client, key package) pair - removeMLSClients groupId qusr (Set.singleton cid) - -- add new (client, key package) pair - addMLSClients groupId qusr (Set.singleton (cid, new)) - -applyProposalRef :: - ( HasProposalEffects r, - ( Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MLSProposalNotFound) r, - Member (ErrorS 'MLSStaleMessage) r, - Member ProposalStore r - ) - ) => - ConvOrSubConvId -> - ConversationMLSData -> - GroupId -> - Epoch -> - CipherSuiteTag -> - ProposalOrRef -> - Sem r ProposalAction -applyProposalRef convOrSubConvId mlsMeta groupId epoch _suite (Ref ref) = do - p <- getProposal groupId epoch ref >>= noteS @'MLSProposalNotFound - checkEpoch epoch mlsMeta - checkGroup groupId mlsMeta - applyProposal convOrSubConvId groupId (rmValue p) -applyProposalRef convOrSubConvId _mlsMeta groupId _epoch suite (Inline p) = do - checkProposalCipherSuite suite p - applyProposal convOrSubConvId groupId p - -applyProposal :: - forall r. - HasProposalEffects r => - ConvOrSubConvId -> - GroupId -> - Proposal -> - Sem r ProposalAction -applyProposal convOrSubConvId groupId (AddProposal kp) = do - ref <- kpRef' kp & note (mlsProtocolError "Could not compute ref of a key package in an Add proposal") - mbClientIdentity <- getClientByKeyPackageRef ref - clientIdentity <- case mbClientIdentity of - Nothing -> do - -- external add proposal for a new key package unknown to the backend - lConvOrSubConvId <- qualifyLocal convOrSubConvId - addKeyPackageMapping lConvOrSubConvId ref (KeyPackageData (rmRaw kp)) - Just ci -> - -- ad-hoc add proposal in commit, the key package has been claimed before - pure ci - pure (paAddClient . (<$$>) (,ref) . cidQualifiedClient $ clientIdentity) - where - addKeyPackageMapping :: Local ConvOrSubConvId -> KeyPackageRef -> KeyPackageData -> Sem r ClientIdentity - addKeyPackageMapping lConvOrSubConvId ref kpdata = do - -- validate and update mapping in brig - eithCid <- - nkpresClientIdentity - <$$> validateAndAddKeyPackageRef - NewKeyPackage - { nkpConversation = tUntagged (convOfConvOrSub <$> lConvOrSubConvId), - nkpKeyPackage = kpdata - } - cid <- either (\errMsg -> throw (mlsProtocolError ("Tried to add invalid KeyPackage: " <> errMsg))) pure eithCid - let qcid = cidQualifiedClient cid - let qusr = fst <$> qcid - -- update mapping in galley - addMLSClients groupId qusr (Set.singleton (ciClient cid, ref)) - pure cid -applyProposal _convOrSubConvId _groupId (RemoveProposal ref) = do - qclient <- cidQualifiedClient <$> derefKeyPackage ref - pure (paRemoveClient ((,ref) <$$> qclient)) -applyProposal _convOrSubConvId _groupId (ExternalInitProposal _) = - -- only record the fact there was an external init proposal, but do not - -- process it in any way. - pure paExternalInitPresent -applyProposal _convOrSubConvId _groupId _ = pure mempty - -checkProposalCipherSuite :: - Member (Error MLSProtocolError) r => - CipherSuiteTag -> - Proposal -> - Sem r () -checkProposalCipherSuite suite (AddProposal kpRaw) = do - let kp = rmValue kpRaw - unless (kpCipherSuite kp == tagCipherSuite suite) - . throw - . mlsProtocolError - . T.pack - $ "The group's cipher suite " - <> show (cipherSuiteNumber (tagCipherSuite suite)) - <> " and the cipher suite of the proposal's key package " - <> show (cipherSuiteNumber (kpCipherSuite kp)) - <> " do not match." -checkProposalCipherSuite _suite _prop = pure () - -processProposal :: - HasProposalEffects r => - ( Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MLSStaleMessage) r - ) => - Qualified UserId -> - Local ConvOrSubConv -> - Message 'MLSPlainText -> - RawMLS Proposal -> - Sem r () -processProposal qusr lConvOrSub msg prop = do - let mlsMeta = mlsMetaConvOrSub (tUnqualified lConvOrSub) - checkEpoch (msgEpoch msg) mlsMeta - checkGroup (msgGroupId msg) mlsMeta - let suiteTag = cnvmlsCipherSuite mlsMeta - let cid = mcId . convOfConvOrSub . tUnqualified $ lConvOrSub - - -- validate the proposal - -- - -- is the user a member of the conversation? - loc <- qualifyLocal () - isMember' <- - foldQualified - loc - ( fmap isJust - . getLocalMember cid - . tUnqualified - ) - ( fmap isJust - . getRemoteMember cid - ) - qusr - unless isMember' $ throwS @'ConvNotFound - - -- FUTUREWORK: validate the member's conversation role - let propValue = rmValue prop - checkProposalCipherSuite suiteTag propValue - when (isExternalProposal msg) $ do - checkExternalProposalSignature suiteTag msg prop - checkExternalProposalUser qusr propValue - let propRef = proposalRef suiteTag prop - storeProposal (msgGroupId msg) (msgEpoch msg) propRef ProposalOriginClient prop - -checkExternalProposalSignature :: - Member (ErrorS 'MLSUnsupportedProposal) r => - CipherSuiteTag -> - Message 'MLSPlainText -> - RawMLS Proposal -> - Sem r () -checkExternalProposalSignature csTag msg prop = case rmValue prop of - AddProposal kp -> do - let pubKey = bcSignatureKey . kpCredential $ rmValue kp - unless (verifyMessageSignature csTag msg pubKey) $ throwS @'MLSUnsupportedProposal - _ -> pure () -- FUTUREWORK: check signature of other proposals as well - -isExternalProposal :: Message 'MLSPlainText -> Bool -isExternalProposal msg = case msgSender msg of - NewMemberSender -> True - PreconfiguredSender _ -> True - _ -> False - --- check owner/subject of the key package exists and belongs to the user -checkExternalProposalUser :: - ( Member BrigAccess r, - Member (ErrorS 'MLSUnsupportedProposal) r, - Member (Input (Local ())) r - ) => - Qualified UserId -> - Proposal -> - Sem r () -checkExternalProposalUser qusr prop = do - loc <- qualifyLocal () - foldQualified - loc - ( \lusr -> case prop of - AddProposal keyPackage -> do - ClientIdentity {ciUser, ciClient} <- - either - (const $ throwS @'MLSUnsupportedProposal) - pure - . kpIdentity - . rmValue - $ keyPackage - -- requesting user must match key package owner - when (tUnqualified lusr /= ciUser) $ throwS @'MLSUnsupportedProposal - -- client referenced in key package must be one of the user's clients - UserClients {userClients} <- lookupClients [ciUser] - maybe - (throwS @'MLSUnsupportedProposal) - (flip when (throwS @'MLSUnsupportedProposal) . Set.null . Set.filter (== ciClient)) - $ userClients Map.!? ciUser - _ -> throwS @'MLSUnsupportedProposal - ) - (const $ pure ()) -- FUTUREWORK: check external proposals from remote backends - qusr - -type HasProposalActionEffects r = - ( Member BrigAccess r, - Member ConversationStore r, - Member (Error InternalError) r, - Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MLSClientMismatch) r, - Member (Error MLSProposalFailure) r, - Member (ErrorS 'MissingLegalholdConsent) r, - Member (ErrorS 'MLSUnsupportedProposal) r, - Member (Error MLSProtocolError) r, - Member (ErrorS 'MLSSelfRemovalNotAllowed) r, - Member ExternalAccess r, - Member FederatorAccess r, - Member GundeckAccess r, - Member (Input Env) r, - Member (Input Opts) r, - Member (Input UTCTime) r, - Member LegalHoldStore r, - Member MemberStore r, - Member ProposalStore r, - Member SubConversationStore r, - Member TeamStore r, - Member TinyLog r - ) - -executeProposalAction :: - forall r. - HasProposalActionEffects r => - Qualified UserId -> - Maybe ConnId -> - Local ConvOrSubConv -> - ProposalAction -> - Sem r [LocalConversationUpdate] -executeProposalAction qusr con lconvOrSub action = do - let convOrSub = tUnqualified lconvOrSub - mlsMeta = mlsMetaConvOrSub convOrSub - cm = membersConvOrSub convOrSub - ss = csSignatureScheme (cnvmlsCipherSuite mlsMeta) - newUserClients = Map.assocs (paAdd action) - - -- no client can be directly added to a subconversation - when (is _SubConv convOrSub && not (null newUserClients)) $ - throw (mlsProtocolError "Add proposals in subconversations are not supported") - - -- Note [client removal] - -- We support two types of removals: - -- 1. when a user is removed from a group, all their clients have to be removed - -- 2. when a client is deleted, that particular client (but not necessarily - -- other clients of the same user) has to be removed. - -- - -- Type 2 requires no special processing on the backend, so here we filter - -- out all removals of that type, so that further checks and processing can - -- be applied only to type 1 removals. - -- - -- Furthermore, subconversation clients can be removed arbitrarily, so this - -- processing is only necessary for main conversations. In the - -- subconversation case, an empty list is returned. - removedUsers <- case convOrSub of - SubConv _ _ -> pure [] - Conv _ -> mapMaybe hush <$$> for (Map.assocs (paRemove action)) $ - \(qtarget, Map.keysSet -> clients) -> runError @() $ do - -- fetch clients from brig - clientInfo <- Set.map ciId <$> getClientInfo lconvOrSub qtarget ss - -- if the clients being removed don't exist, consider this as a removal of - -- type 2, and skip it - when (Set.null (clientInfo `Set.intersection` clients)) $ - throw () - pure (qtarget, clients) - - -- FUTUREWORK: remove this check after remote admins are implemented in federation https://wearezeta.atlassian.net/browse/FS-216 - foldQualified lconvOrSub (\_ -> pure ()) (\_ -> throwS @'MLSUnsupportedProposal) qusr - - -- for each user, we compare their clients with the ones being added to the conversation - for_ newUserClients $ \(qtarget, newclients) -> case Map.lookup qtarget cm of - -- user is already present, skip check in this case - Just _ -> pure () - -- new user - Nothing -> do - -- final set of clients in the conversation - let clients = Map.keysSet (newclients <> Map.findWithDefault mempty qtarget cm) - -- get list of mls clients from brig - clientInfo <- getClientInfo lconvOrSub qtarget ss - let allClients = Set.map ciId clientInfo - let allMLSClients = Set.map ciId (Set.filter ciMLS clientInfo) - -- We check the following condition: - -- allMLSClients ⊆ clients ⊆ allClients - -- i.e. - -- - if a client has at least 1 key package, it has to be added - -- - if a client is being added, it has to still exist - -- - -- The reason why we can't simply check that clients == allMLSClients is - -- that a client with no remaining key packages might be added by a user - -- who just fetched its last key package. - unless - ( Set.isSubsetOf allMLSClients clients - && Set.isSubsetOf clients allClients - ) - $ do - -- unless (Set.isSubsetOf allClients clients) $ do - -- FUTUREWORK: turn this error into a proper response - throwS @'MLSClientMismatch - - membersToRemove <- catMaybes <$> for removedUsers (uncurry (checkRemoval (is _SubConv convOrSub) cm)) - - -- add users to the conversation and send events - addEvents <- - foldMap (addMembers qusr con lconvOrSub) - . nonEmpty - . map fst - $ newUserClients - - -- add clients in the conversation state - for_ newUserClients $ \(qtarget, newClients) -> do - addMLSClients (cnvmlsGroupId mlsMeta) qtarget (Set.fromList (Map.assocs newClients)) - - -- remove users from the conversation and send events - removeEvents <- - foldMap - (removeMembers qusr con lconvOrSub) - (nonEmpty membersToRemove) - - -- Remove clients from the conversation state. This includes client removals - -- of all types (see Note [client removal]). - for_ (Map.assocs (paRemove action)) $ \(qtarget, clients) -> do - removeMLSClients (cnvmlsGroupId mlsMeta) qtarget (Map.keysSet clients) - - -- if this is a new subconversation, call `on-new-remote-conversation` on all - -- the remote backends involved in the main conversation - forOf_ _SubConv convOrSub $ \(mlsConv, subConv) -> do - when (cnvmlsEpoch (scMLSData subConv) == Epoch 0) $ do - let remoteDomains = - Set.fromList - ( map - (void . rmId) - (mcRemoteMembers mlsConv) - ) - let nrc = - NewRemoteSubConversation - { nrscConvId = mcId mlsConv, - nrscSubConvId = scSubConvId subConv, - nrscMlsData = scMLSData subConv - } - runFederatedConcurrently_ (toList remoteDomains) $ \_ -> do - void $ fedClient @'Galley @"on-new-remote-subconversation" nrc - - pure (addEvents <> removeEvents) - where - checkRemoval :: - Bool -> - ClientMap -> - Qualified UserId -> - Set ClientId -> - Sem r (Maybe (Qualified UserId)) - checkRemoval isSubConv cm qtarget clients = do - let clientsInConv = Map.keysSet (Map.findWithDefault mempty qtarget cm) - -- FUTUREWORK: add tests against this situation for conv v subconv - when (not isSubConv && clients /= clientsInConv) $ do - -- FUTUREWORK: turn this error into a proper response - throwS @'MLSClientMismatch - when (qusr == qtarget) $ - throwS @'MLSSelfRemovalNotAllowed - pure (Just qtarget) - -existingLocalMembers :: Local Data.Conversation -> Set (Qualified UserId) -existingLocalMembers lconv = - (Set.fromList . map (fmap lmId . tUntagged)) (traverse convLocalMembers lconv) - -existingRemoteMembers :: Local Data.Conversation -> Set (Qualified UserId) -existingRemoteMembers lconv = - Set.fromList . map (tUntagged . rmId) . convRemoteMembers . tUnqualified $ - lconv - -existingMembers :: Local Data.Conversation -> Set (Qualified UserId) -existingMembers lconv = existingLocalMembers lconv <> existingRemoteMembers lconv - -addMembers :: - HasProposalActionEffects r => - Qualified UserId -> - Maybe ConnId -> - Local ConvOrSubConv -> - NonEmpty (Qualified UserId) -> - Sem r [LocalConversationUpdate] -addMembers qusr con lconvOrSub users = case tUnqualified lconvOrSub of - Conv mlsConv -> do - let lconv = qualifyAs lconvOrSub (mcConv mlsConv) - -- FUTUREWORK: update key package ref mapping to reflect conversation membership - foldMap - ( handleNoChanges - . handleMLSProposalFailures @ProposalErrors - . fmap pure - . updateLocalConversationUnchecked @'ConversationJoinTag lconv qusr con - . flip ConversationJoin roleNameWireMember - ) - . nonEmpty - . filter (flip Set.notMember (existingMembers lconv)) - . toList - $ users - SubConv _ _ -> pure [] - -removeMembers :: - HasProposalActionEffects r => - Qualified UserId -> - Maybe ConnId -> - Local ConvOrSubConv -> - NonEmpty (Qualified UserId) -> - Sem r [LocalConversationUpdate] -removeMembers qusr con lconvOrSub users = case tUnqualified lconvOrSub of - Conv mlsConv -> do - let lconv = qualifyAs lconvOrSub (mcConv mlsConv) - foldMap - ( handleNoChanges - . handleMLSProposalFailures @ProposalErrors - . fmap pure - . updateLocalConversationUnchecked @'ConversationRemoveMembersTag lconv qusr con - ) - . nonEmpty - . filter (flip Set.member (existingMembers lconv)) - . toList - $ users - SubConv _ _ -> pure [] - -handleNoChanges :: Monoid a => Sem (Error NoChanges ': r) a -> Sem r a -handleNoChanges = fmap fold . runError - -getClientInfo :: - ( Member BrigAccess r, - Member FederatorAccess r - ) => - Local x -> - Qualified UserId -> - SignatureSchemeTag -> - Sem r (Set ClientInfo) -getClientInfo loc = foldQualified loc getLocalMLSClients getRemoteMLSClients - -getRemoteMLSClients :: - ( Member FederatorAccess r - ) => - Remote UserId -> - SignatureSchemeTag -> - Sem r (Set ClientInfo) -getRemoteMLSClients rusr ss = do - runFederated rusr $ - fedClient @'Brig @"get-mls-clients" $ - MLSClientsRequest - { mcrUserId = tUnqualified rusr, - mcrSignatureScheme = ss - } - --- | Check if the epoch number matches that of a conversation -checkEpoch :: - Member (ErrorS 'MLSStaleMessage) r => - Epoch -> - ConversationMLSData -> - Sem r () -checkEpoch epoch mlsMeta = do - unless (epoch == cnvmlsEpoch mlsMeta) $ throwS @'MLSStaleMessage - --- | Check if the group ID matches that of a conversation -checkGroup :: - Member (ErrorS 'ConvNotFound) r => - GroupId -> - ConversationMLSData -> - Sem r () -checkGroup gId mlsMeta = do - unless (gId == cnvmlsGroupId mlsMeta) $ throwS @'ConvNotFound - --------------------------------------------------------------------------------- --- Error handling of proposal execution - --- The following errors are caught by 'executeProposalAction' and wrapped in a --- 'MLSProposalFailure'. This way errors caused by the execution of proposals are --- separated from those caused by the commit processing itself. -type ProposalErrors = - '[ Error FederationError, - Error InvalidInput, - ErrorS ('ActionDenied 'AddConversationMember), - ErrorS ('ActionDenied 'LeaveConversation), - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'ConvAccessDenied, - ErrorS 'InvalidOperation, - ErrorS 'NotATeamMember, - ErrorS 'NotConnected, - ErrorS 'TooManyMembers - ] - -class HandleMLSProposalFailures effs r where - handleMLSProposalFailures :: Sem (Append effs r) a -> Sem r a - -class HandleMLSProposalFailure eff r where - handleMLSProposalFailure :: Sem (eff ': r) a -> Sem r a - -instance HandleMLSProposalFailures '[] r where - handleMLSProposalFailures = id - -instance - ( HandleMLSProposalFailures effs r, - HandleMLSProposalFailure eff (Append effs r) - ) => - HandleMLSProposalFailures (eff ': effs) r - where - handleMLSProposalFailures = handleMLSProposalFailures @effs . handleMLSProposalFailure @eff - -instance - (APIError e, Member (Error MLSProposalFailure) r) => - HandleMLSProposalFailure (Error e) r - where - handleMLSProposalFailure = mapError (MLSProposalFailure . toWai) - -storeGroupInfoBundle :: +storeGroupInfo :: ( Member ConversationStore r, Member SubConversationStore r ) => ConvOrSubConvId -> - GroupInfoBundle -> + GroupInfoData -> Sem r () -storeGroupInfoBundle convOrSub bundle = do - let gs = toOpaquePublicGroupState (gipGroupState bundle) - case convOrSub of - Conv cid -> setPublicGroupState cid gs - SubConv cid subconvid -> setSubConversationPublicGroupState cid subconvid (Just gs) +storeGroupInfo convOrSub ginfo = case convOrSub of + Conv cid -> setGroupInfo cid ginfo + SubConv cid subconvid -> setSubConversationGroupInfo cid subconvid (Just ginfo) fetchConvOrSub :: forall r. @@ -1409,23 +429,3 @@ fetchConvOrSub qusr convOrSubId = for convOrSubId $ \case getLocalConvForUser u >=> mkMLSConversation >=> noteS @'ConvNotFound - -incrementEpoch :: - ( Member ConversationStore r, - Member (ErrorS 'ConvNotFound) r, - Member MemberStore r, - Member SubConversationStore r - ) => - ConvOrSubConv -> - Sem r ConvOrSubConv -incrementEpoch (Conv c) = do - let epoch' = succ (cnvmlsEpoch (mcMLSData c)) - setConversationEpoch (mcId c) epoch' - conv <- getConversation (mcId c) >>= noteS @'ConvNotFound - fmap Conv (mkMLSConversation conv >>= noteS @'ConvNotFound) -incrementEpoch (SubConv c s) = do - let epoch' = succ (cnvmlsEpoch (scMLSData s)) - setSubConversationEpoch (scParentConvId s) (scSubConvId s) epoch' - subconv <- - getSubConversation (mcId c) (scSubConvId s) >>= noteS @'ConvNotFound - pure (SubConv c subconv) diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 31a60d97eb6..10d0dcedebe 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -44,6 +44,7 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.MLS.Message +import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Message @@ -58,10 +59,10 @@ propagateMessage :: Qualified UserId -> Local ConvOrSubConv -> Maybe ConnId -> - ByteString -> + RawMLS Message -> ClientMap -> Sem r UnreachableUsers -propagateMessage qusr lConvOrSub con raw cm = do +propagateMessage qusr lConvOrSub con msg cm = do now <- input @UTCTime let mlsConv = convOfConvOrSub <$> lConvOrSub lmems = mcLocalMembers . tUnqualified $ mlsConv @@ -77,7 +78,7 @@ propagateMessage qusr lConvOrSub con raw cm = do SubConv c s -> (mcId c, Just (scSubConvId s)) qcnv = fst <$> qt sconv = snd (qUnqualified qt) - e = Event qcnv sconv qusr now $ EdMLSMessage raw + e = Event qcnv sconv qusr now $ EdMLSMessage msg.raw mkPush :: UserId -> ClientId -> MessagePush 'NormalMessage mkPush u c = newMessagePush mlsConv botMap con mm (u, c) e runMessagePush mlsConv (Just qcnv) $ @@ -95,7 +96,7 @@ propagateMessage qusr lConvOrSub con raw cm = do rmmMetadata = mm, rmmConversation = qUnqualified qcnv, rmmRecipients = rs >>= remoteMemberMLSClients, - rmmMessage = Base64ByteString raw + rmmMessage = Base64ByteString msg.raw } where localMemberMLSClients :: Local x -> LocalMember -> [(UserId, ClientId)] diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs new file mode 100644 index 00000000000..437e1cba429 --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Proposal.hs @@ -0,0 +1,295 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.API.MLS.Proposal + ( -- * Proposal processing + derefOrCheckProposal, + checkProposal, + processProposal, + proposalProcessingStage, + addProposedClient, + applyProposals, + + -- * Proposal actions + paAddClient, + paRemoveClient, + + -- * Types + ProposalAction (..), + HasProposalEffects, + ) +where + +import Data.Id +import qualified Data.Map as Map +import Data.Qualified +import qualified Data.Set as Set +import Data.Time +import Galley.API.Error +import Galley.API.MLS.IncomingMessage +import Galley.API.MLS.Types +import Galley.API.Util +import Galley.Effects +import Galley.Effects.BrigAccess +import Galley.Effects.ProposalStore +import Galley.Env +import Galley.Options +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.Input +import Polysemy.State +import Polysemy.TinyLog +import Wire.API.Conversation hiding (Member) +import Wire.API.Conversation.Protocol +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.MLS.AuthenticatedContent +import Wire.API.MLS.Credential +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode +import Wire.API.MLS.Message +import Wire.API.MLS.Proposal +import Wire.API.MLS.Serialisation +import Wire.API.MLS.Validation +import Wire.API.Message + +data ProposalAction = ProposalAction + { paAdd :: ClientMap, + paRemove :: ClientMap + } + deriving (Show) + +instance Semigroup ProposalAction where + ProposalAction add1 rem1 <> ProposalAction add2 rem2 = + ProposalAction + (Map.unionWith mappend add1 add2) + (Map.unionWith mappend rem1 rem2) + +instance Monoid ProposalAction where + mempty = ProposalAction mempty mempty + +paAddClient :: ClientIdentity -> LeafIndex -> ProposalAction +paAddClient cid idx = mempty {paAdd = cmSingleton cid idx} + +paRemoveClient :: ClientIdentity -> LeafIndex -> ProposalAction +paRemoveClient cid idx = mempty {paRemove = cmSingleton cid idx} + +-- | This is used to sort proposals into the correct processing order, as defined by the spec +data ProposalProcessingStage + = ProposalProcessingStageExtensions + | ProposalProcessingStageUpdate + | ProposalProcessingStageRemove + | ProposalProcessingStageAdd + | ProposalProcessingStagePreSharedKey + | ProposalProcessingStageExternalInit + | ProposalProcessingStageReInit + deriving (Eq, Ord) + +proposalProcessingStage :: Proposal -> ProposalProcessingStage +proposalProcessingStage (AddProposal _) = ProposalProcessingStageAdd +proposalProcessingStage (RemoveProposal _) = ProposalProcessingStageRemove +proposalProcessingStage (UpdateProposal _) = ProposalProcessingStageUpdate +proposalProcessingStage (PreSharedKeyProposal _) = ProposalProcessingStagePreSharedKey +proposalProcessingStage (ReInitProposal _) = ProposalProcessingStageReInit +proposalProcessingStage (ExternalInitProposal _) = ProposalProcessingStageExternalInit +proposalProcessingStage (GroupContextExtensionsProposal _) = ProposalProcessingStageExtensions + +type HasProposalEffects r = + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error InternalError) r, + Member (Error MLSProposalFailure) r, + Member (Error MLSProtocolError) r, + Member (ErrorS 'MLSClientMismatch) r, + Member (ErrorS 'MLSInvalidLeafNodeIndex) r, + Member (ErrorS 'MLSUnsupportedProposal) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input (Local ())) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member MemberStore r, + Member ProposalStore r, + Member TeamStore r, + Member TeamStore r, + Member TinyLog r + ) + +derefOrCheckProposal :: + ( Member (Error MLSProtocolError) r, + Member (ErrorS 'MLSInvalidLeafNodeIndex) r, + Member ProposalStore r, + Member (State IndexMap) r, + Member (ErrorS 'MLSProposalNotFound) r + ) => + ConversationMLSData -> + GroupId -> + Epoch -> + ProposalOrRef -> + Sem r Proposal +derefOrCheckProposal _mlsMeta groupId epoch (Ref ref) = do + p <- getProposal groupId epoch ref >>= noteS @'MLSProposalNotFound + pure p.value +derefOrCheckProposal mlsMeta _ _ (Inline p) = do + im <- get + checkProposal mlsMeta im p + pure p + +checkProposal :: + ( Member (Error MLSProtocolError) r, + Member (ErrorS 'MLSInvalidLeafNodeIndex) r + ) => + ConversationMLSData -> + IndexMap -> + Proposal -> + Sem r () +checkProposal mlsMeta im p = + case p of + AddProposal kp -> do + (cs, _lifetime) <- + either + (\msg -> throw (mlsProtocolError ("Invalid key package in Add proposal: " <> msg))) + pure + $ validateKeyPackage Nothing kp.value + -- we are not checking lifetime constraints here + unless (mlsMeta.cnvmlsCipherSuite == cs) $ + throw (mlsProtocolError "Key package ciphersuite does not match conversation") + RemoveProposal idx -> do + void $ noteS @'MLSInvalidLeafNodeIndex $ imLookup im idx + _ -> pure () + +addProposedClient :: Member (State IndexMap) r => ClientIdentity -> Sem r ProposalAction +addProposedClient cid = do + im <- get + let (idx, im') = imAddClient im cid + put im' + pure (paAddClient cid idx) + +applyProposals :: + ( Member (State IndexMap) r, + Member (Error MLSProtocolError) r, + Member (ErrorS 'MLSUnsupportedProposal) r, + Member (ErrorS 'MLSInvalidLeafNodeIndex) r + ) => + ConversationMLSData -> + GroupId -> + [Proposal] -> + Sem r ProposalAction +applyProposals mlsMeta groupId = + -- proposals are sorted before processing + foldMap (applyProposal mlsMeta groupId) + . sortOn proposalProcessingStage + +applyProposal :: + ( Member (State IndexMap) r, + Member (Error MLSProtocolError) r, + Member (ErrorS 'MLSUnsupportedProposal) r, + Member (ErrorS 'MLSInvalidLeafNodeIndex) r + ) => + ConversationMLSData -> + GroupId -> + Proposal -> + Sem r ProposalAction +applyProposal mlsMeta _groupId (AddProposal kp) = do + (cs, _lifetime) <- + either + (\msg -> throw (mlsProtocolError ("Invalid key package in Add proposal: " <> msg))) + pure + $ validateKeyPackage Nothing kp.value + unless (mlsMeta.cnvmlsCipherSuite == cs) $ + throw (mlsProtocolError "Key package ciphersuite does not match conversation") + -- we are not checking lifetime constraints here + cid <- getKeyPackageIdentity kp.value + addProposedClient cid +applyProposal _mlsMeta _groupId (RemoveProposal idx) = do + im <- get + (cid, im') <- noteS @'MLSInvalidLeafNodeIndex $ imRemoveClient im idx + put im' + pure (paRemoveClient cid idx) +applyProposal _mlsMeta _groupId _ = pure mempty + +processProposal :: + HasProposalEffects r => + ( Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'MLSStaleMessage) r + ) => + Qualified UserId -> + Local ConvOrSubConv -> + GroupId -> + Epoch -> + IncomingPublicMessageContent -> + RawMLS Proposal -> + Sem r () +processProposal qusr lConvOrSub groupId epoch pub prop = do + let mlsMeta = mlsMetaConvOrSub (tUnqualified lConvOrSub) + -- Check if the epoch number matches that of a conversation + unless (epoch == cnvmlsEpoch mlsMeta) $ throwS @'MLSStaleMessage + -- Check if the group ID matches that of a conversation + unless (groupId == cnvmlsGroupId mlsMeta) $ throwS @'ConvNotFound + let suiteTag = cnvmlsCipherSuite mlsMeta + + -- FUTUREWORK: validate the member's conversation role + let im = indexMapConvOrSub $ tUnqualified lConvOrSub + checkProposal mlsMeta im prop.value + when (isExternal pub.sender) $ checkExternalProposalUser qusr prop.value + let propRef = authContentRef suiteTag (incomingMessageAuthenticatedContent pub) + storeProposal groupId epoch propRef ProposalOriginClient prop + +getKeyPackageIdentity :: + Member (ErrorS 'MLSUnsupportedProposal) r => + KeyPackage -> + Sem r ClientIdentity +getKeyPackageIdentity = + either (\_ -> throwS @'MLSUnsupportedProposal) pure + . keyPackageIdentity + +isExternal :: Sender -> Bool +isExternal (SenderMember _) = False +isExternal _ = True + +-- check owner/subject of the key package exists and belongs to the user +checkExternalProposalUser :: + ( Member BrigAccess r, + Member (ErrorS 'MLSUnsupportedProposal) r, + Member (Input (Local ())) r + ) => + Qualified UserId -> + Proposal -> + Sem r () +checkExternalProposalUser qusr prop = do + loc <- qualifyLocal () + foldQualified + loc + ( \lusr -> case prop of + AddProposal kp -> do + ClientIdentity {ciUser, ciClient} <- getKeyPackageIdentity kp.value + -- requesting user must match key package owner + when (tUnqualified lusr /= ciUser) $ throwS @'MLSUnsupportedProposal + -- client referenced in key package must be one of the user's clients + UserClients {userClients} <- lookupClients [ciUser] + maybe + (throwS @'MLSUnsupportedProposal) + (flip when (throwS @'MLSUnsupportedProposal) . Set.null . Set.filter (== ciClient)) + $ userClients Map.!? ciUser + _ -> throwS @'MLSUnsupportedProposal + ) + (const $ pure ()) -- FUTUREWORK: check external proposals from remote backends + qusr diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 27d314ef685..f801bf06b5b 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -22,6 +22,7 @@ module Galley.API.MLS.Removal ) where +import Data.Bifunctor import Data.Id import qualified Data.Map as Map import Data.Qualified @@ -32,9 +33,9 @@ import Galley.API.MLS.Propagate import Galley.API.MLS.Types import qualified Galley.Data.Conversation.Types as Data import Galley.Effects +import Galley.Effects.MemberStore import Galley.Effects.ProposalStore import Galley.Effects.SubConversationStore -import qualified Galley.Effects.SubConversationStore as E import Galley.Env import Imports import Polysemy @@ -42,8 +43,9 @@ import Polysemy.Input import Polysemy.TinyLog import qualified System.Logger as Log import Wire.API.Conversation.Protocol +import Wire.API.MLS.AuthenticatedContent import Wire.API.MLS.Credential -import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode import Wire.API.MLS.Message import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation @@ -61,7 +63,7 @@ createAndSendRemoveProposals :: Foldable t ) => Local ConvOrSubConv -> - t KeyPackageRef -> + t LeafIndex -> Qualified UserId -> -- | The client map that has all the recipients of the message. This is an -- argument, and not constructed within the function, because of a special @@ -71,24 +73,31 @@ createAndSendRemoveProposals :: -- conversation/subconversation client maps. ClientMap -> Sem r () -createAndSendRemoveProposals lConvOrSubConv cs qusr cm = do +createAndSendRemoveProposals lConvOrSubConv indices qusr cm = do let meta = mlsMetaConvOrSub (tUnqualified lConvOrSubConv) mKeyPair <- getMLSRemovalKey case mKeyPair of Nothing -> do warn $ Log.msg ("No backend removal key is configured (See 'mlsPrivateKeyPaths' in galley's config). Not able to remove client from MLS conversation." :: Text) Just (secKey, pubKey) -> do - for_ cs $ \kpref -> do - let proposal = mkRemoveProposal kpref - msg = mkSignedMessage secKey pubKey (cnvmlsGroupId meta) (cnvmlsEpoch meta) (ProposalMessage proposal) - msgEncoded = encodeMLS' msg + for_ indices $ \idx -> do + let proposal = mkRawMLS (RemoveProposal idx) + pmsg = + mkSignedPublicMessage + secKey + pubKey + (cnvmlsGroupId meta) + (cnvmlsEpoch meta) + (TaggedSenderExternal 0) + (FramedContentProposal proposal) + msg = mkRawMLS (mkMessage (MessagePublic pmsg)) storeProposal (cnvmlsGroupId meta) (cnvmlsEpoch meta) - (proposalRef (cnvmlsCipherSuite meta) proposal) + (publicMessageRef (cnvmlsCipherSuite meta) pmsg) ProposalOriginBackend proposal - propagateMessage qusr lConvOrSubConv Nothing msgEncoded cm + propagateMessage qusr lConvOrSubConv Nothing msg cm removeClientsWithClientMapRecursively :: ( Members @@ -97,30 +106,41 @@ removeClientsWithClientMapRecursively :: ExternalAccess, FederatorAccess, GundeckAccess, + MemberStore, ProposalStore, SubConversationStore, Input Env ] r, + Functor f, Foldable f ) => Local MLSConversation -> - (ConvOrSubConv -> f KeyPackageRef) -> + (ConvOrSubConv -> f (ClientIdentity, LeafIndex)) -> + -- | Originating user. The resulting proposals will appear to be sent by this user. Qualified UserId -> Sem r () -removeClientsWithClientMapRecursively lMlsConv getKPs qusr = do +removeClientsWithClientMapRecursively lMlsConv getClients qusr = do let mainConv = fmap Conv lMlsConv cm = mcMembers (tUnqualified lMlsConv) - createAndSendRemoveProposals mainConv (getKPs (tUnqualified mainConv)) qusr cm + do + let gid = cnvmlsGroupId . mcMLSData . tUnqualified $ lMlsConv + clients = getClients (tUnqualified mainConv) + + planClientRemoval gid (fmap fst clients) + createAndSendRemoveProposals mainConv (fmap snd clients) qusr cm -- remove this client from all subconversations subs <- listSubConversations' (mcId (tUnqualified lMlsConv)) for_ subs $ \sub -> do let subConv = fmap (flip SubConv sub) lMlsConv + sgid = cnvmlsGroupId . scMLSData $ sub + clients = getClients (tUnqualified subConv) + planClientRemoval sgid (fmap fst clients) createAndSendRemoveProposals subConv - (getKPs (tUnqualified subConv)) + (fmap snd clients) qusr cm @@ -140,11 +160,12 @@ removeClient :: Qualified UserId -> ClientId -> Sem r () -removeClient lc qusr cid = do +removeClient lc qusr c = do mMlsConv <- mkMLSConversation (tUnqualified lc) for_ mMlsConv $ \mlsConv -> do - let getKPs = cmLookupRef (mkClientIdentity qusr cid) . membersConvOrSub - removeClientsWithClientMapRecursively (qualifyAs lc mlsConv) getKPs qusr + let cid = mkClientIdentity qusr c + let getClients = fmap (cid,) . cmLookupIndex cid . membersConvOrSub + removeClientsWithClientMapRecursively (qualifyAs lc mlsConv) getClients qusr -- | Send remove proposals for all clients of the user to the local conversation. removeUser :: @@ -164,8 +185,13 @@ removeUser :: removeUser lc qusr = do mMlsConv <- mkMLSConversation (tUnqualified lc) for_ mMlsConv $ \mlsConv -> do - let getKPs = Map.findWithDefault mempty qusr . membersConvOrSub - removeClientsWithClientMapRecursively (qualifyAs lc mlsConv) getKPs qusr + let getClients :: ConvOrSubConv -> [(ClientIdentity, LeafIndex)] + getClients = + map (first (mkClientIdentity qusr)) + . Map.assocs + . Map.findWithDefault mempty qusr + . membersConvOrSub + removeClientsWithClientMapRecursively (qualifyAs lc mlsConv) getClients qusr -- | Convert cassandra subconv maps into SubConversations listSubConversations' :: @@ -173,7 +199,7 @@ listSubConversations' :: ConvId -> Sem r [SubConversation] listSubConversations' cid = do - subs <- E.listSubConversations cid + subs <- listSubConversations cid msubs <- for (Map.assocs subs) $ \(subId, _) -> do getSubConversation cid subId pure (catMaybes msubs) diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index d22bad99d5e..461836174df 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -35,7 +35,6 @@ import Control.Arrow import Data.Id import qualified Data.Map as Map import Data.Qualified -import qualified Data.Set as Set import Data.Time.Clock import Galley.API.MLS import Galley.API.MLS.Conversation @@ -69,7 +68,7 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.MLS.Credential -import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation type MLSGetSubConvStaticErrors = @@ -142,7 +141,8 @@ getLocalSubConversation qusr lconv sconv = do cnvmlsEpochTimestamp = Nothing, cnvmlsCipherSuite = suite }, - scMembers = mkClientMap [] + scMembers = mkClientMap [], + scIndexMap = mempty } pure sub Just sub -> pure sub @@ -192,7 +192,7 @@ getSubConversationGroupInfo :: Local UserId -> Qualified ConvId -> SubConvId -> - Sem r OpaquePublicGroupState + Sem r GroupInfoData getSubConversationGroupInfo lusr qcnvId subconv = do assertMLSEnabled foldQualified @@ -212,10 +212,10 @@ getSubConversationGroupInfoFromLocalConv :: Qualified UserId -> SubConvId -> Local ConvId -> - Sem r OpaquePublicGroupState + Sem r GroupInfoData getSubConversationGroupInfoFromLocalConv qusr subConvId lcnvId = do void $ getLocalConvForUser qusr lcnvId - Eff.getSubConversationPublicGroupState (tUnqualified lcnvId) subConvId + Eff.getSubConversationGroupInfo (tUnqualified lcnvId) subConvId >>= noteS @'MLSMissingGroupInfo type MLSDeleteSubConvStaticErrors = @@ -279,9 +279,10 @@ deleteLocalSubConversation :: deleteLocalSubConversation qusr lcnvId scnvId dsc = do assertMLSEnabled let cnvId = tUnqualified lcnvId + lConvOrSubId = qualifyAs lcnvId (SubConv cnvId scnvId) cnv <- getConversationAndCheckMembership qusr lcnvId cs <- cnvmlsCipherSuite <$> noteS @'ConvNotFound (mlsMetadata cnv) - (mlsData, oldGid) <- withCommitLock (dscGroupId dsc) (dscEpoch dsc) $ do + (mlsData, oldGid) <- withCommitLock lConvOrSubId (dscGroupId dsc) (dscEpoch dsc) $ do sconv <- Eff.getSubConversation cnvId scnvId >>= noteS @'ConvNotFound @@ -423,12 +424,12 @@ leaveLocalSubConversation cid lcnv sub = do subConv <- noteS @'ConvNotFound =<< Eff.getSubConversation (tUnqualified lcnv) sub - kp <- + idx <- note (mlsProtocolError "Client is not a member of the subconversation") $ - cmLookupRef cid (scMembers subConv) - -- remove the leaver from the member list + cmLookupIndex cid (scMembers subConv) let (gid, epoch) = (cnvmlsGroupId &&& cnvmlsEpoch) (scMLSData subConv) - Eff.removeMLSClients gid (cidQualifiedUser cid) . Set.singleton . ciClient $ cid + -- plan to remove the leaver from the member list + Eff.planClientRemoval gid (Identity cid) let cm = cmRemoveClient cid (scMembers subConv) if Map.null cm then do @@ -440,7 +441,7 @@ leaveLocalSubConversation cid lcnv sub = do else createAndSendRemoveProposals (qualifyAs lcnv (SubConv mlsConv subConv)) - (Identity kp) + (Identity idx) (cidQualifiedUser cid) cm diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index 69f0f795a00..59cdbe327bf 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -20,6 +20,8 @@ module Galley.API.MLS.Types where import Data.Domain import Data.Id +import Data.IntMap (IntMap) +import qualified Data.IntMap as IntMap import qualified Data.Map as Map import Data.Qualified import Galley.Types.Conversations.Members @@ -27,20 +29,64 @@ import Imports import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.MLS.Credential -import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode import Wire.API.MLS.SubConversation -type ClientMap = Map (Qualified UserId) (Map ClientId KeyPackageRef) +-- | A map of leaf index to members. +-- +-- This is used to reconstruct client +-- identities from leaf indices in remove proposals, as well as to allocate new +-- indices for added clients. +-- +-- Note that clients that are in the process of being removed from a group +-- (i.e. there is a pending remove proposals for them) are included in this +-- mapping. +newtype IndexMap = IndexMap {unIndexMap :: IntMap ClientIdentity} + deriving (Eq, Show) + deriving newtype (Semigroup, Monoid) + +mkIndexMap :: [(Domain, UserId, ClientId, Int32, Bool)] -> IndexMap +mkIndexMap = IndexMap . foldr addEntry mempty + where + addEntry (dom, usr, c, leafidx, _pending_removal) = + IntMap.insert (fromIntegral leafidx) (ClientIdentity dom usr c) + +imLookup :: IndexMap -> LeafIndex -> Maybe ClientIdentity +imLookup m i = IntMap.lookup (fromIntegral i) (unIndexMap m) + +imNextIndex :: IndexMap -> LeafIndex +imNextIndex im = + fromIntegral . fromJust $ + find (\n -> not $ IntMap.member n (unIndexMap im)) [0 ..] + +imAddClient :: IndexMap -> ClientIdentity -> (LeafIndex, IndexMap) +imAddClient im cid = let idx = imNextIndex im in (idx, IndexMap $ IntMap.insert (fromIntegral idx) cid $ unIndexMap im) -mkClientMap :: [(Domain, UserId, ClientId, KeyPackageRef)] -> ClientMap +imRemoveClient :: IndexMap -> LeafIndex -> Maybe (ClientIdentity, IndexMap) +imRemoveClient im idx = do + cid <- imLookup im idx + pure (cid, IndexMap . IntMap.delete (fromIntegral idx) $ unIndexMap im) + +-- | A two-level map of users to clients to leaf indices. +-- +-- This is used to keep track of the state of an MLS group for e.g. propagating +-- a message to all the clients that are supposed to receive it. +-- +-- Note that clients that are in the process of being removed from a group +-- (i.e. there is a pending remove proposals for them) are __not__ included in +-- this mapping. +type ClientMap = Map (Qualified UserId) (Map ClientId LeafIndex) + +mkClientMap :: [(Domain, UserId, ClientId, Int32, Bool)] -> ClientMap mkClientMap = foldr addEntry mempty where - addEntry :: (Domain, UserId, ClientId, KeyPackageRef) -> ClientMap -> ClientMap - addEntry (dom, usr, c, kpr) = - Map.insertWith (<>) (Qualified usr dom) (Map.singleton c kpr) + addEntry :: (Domain, UserId, ClientId, Int32, Bool) -> ClientMap -> ClientMap + addEntry (dom, usr, c, leafidx, pending_removal) + | pending_removal = id -- treat as removed, don't add to ClientMap + | otherwise = Map.insertWith (<>) (Qualified usr dom) (Map.singleton c (fromIntegral leafidx)) -cmLookupRef :: ClientIdentity -> ClientMap -> Maybe KeyPackageRef -cmLookupRef cid cm = do +cmLookupIndex :: ClientIdentity -> ClientMap -> Maybe LeafIndex +cmLookupIndex cid cm = do clients <- Map.lookup (cidQualifiedUser cid) cm Map.lookup (ciClient cid) clients @@ -54,13 +100,22 @@ cmRemoveClient cid cm = case Map.lookup (cidQualifiedUser cid) cm of else Map.insert (cidQualifiedUser cid) clients' cm isClientMember :: ClientIdentity -> ClientMap -> Bool -isClientMember ci = isJust . cmLookupRef ci +isClientMember ci = isJust . cmLookupIndex ci -cmAssocs :: ClientMap -> [(Qualified UserId, (ClientId, KeyPackageRef))] +cmAssocs :: ClientMap -> [(ClientIdentity, LeafIndex)] cmAssocs cm = do (quid, clients) <- Map.assocs cm - (clientId, ref) <- Map.assocs clients - pure (quid, (clientId, ref)) + (clientId, idx) <- Map.assocs clients + pure (mkClientIdentity quid clientId, idx) + +cmIdentities :: ClientMap -> [ClientIdentity] +cmIdentities = map fst . cmAssocs + +cmSingleton :: ClientIdentity -> LeafIndex -> ClientMap +cmSingleton cid idx = + Map.singleton + (cidQualifiedUser cid) + (Map.singleton (ciClient cid) idx) -- | Inform a handler for 'POST /conversations/list-ids' if the MLS global team -- conversation and the MLS self-conversation should be included in the @@ -74,7 +129,8 @@ data MLSConversation = MLSConversation mcMLSData :: ConversationMLSData, mcLocalMembers :: [LocalMember], mcRemoteMembers :: [RemoteMember], - mcMembers :: ClientMap + mcMembers :: ClientMap, + mcIndexMap :: IndexMap } deriving (Show) @@ -82,13 +138,14 @@ data SubConversation = SubConversation { scParentConvId :: ConvId, scSubConvId :: SubConvId, scMLSData :: ConversationMLSData, - scMembers :: ClientMap + scMembers :: ClientMap, + scIndexMap :: IndexMap } deriving (Eq, Show) toPublicSubConv :: Qualified SubConversation -> PublicSubConversation toPublicSubConv (Qualified (SubConversation {..}) domain) = - let members = fmap (\(quid, (cid, _kp)) -> mkClientIdentity quid cid) (cmAssocs scMembers) + let members = map fst (cmAssocs scMembers) in PublicSubConversation { pscParentConvId = Qualified scParentConvId domain, pscSubConvId = scSubConvId, @@ -109,6 +166,10 @@ membersConvOrSub :: ConvOrSubConv -> ClientMap membersConvOrSub (Conv c) = mcMembers c membersConvOrSub (SubConv _ s) = scMembers s +indexMapConvOrSub :: ConvOrSubConv -> IndexMap +indexMapConvOrSub (Conv c) = mcIndexMap c +indexMapConvOrSub (SubConv _ s) = scIndexMap s + convOfConvOrSub :: ConvOrSubChoice c s -> c convOfConvOrSub (Conv c) = c convOfConvOrSub (SubConv c _) = c diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs index 61d2445bf5d..7091a4989c7 100644 --- a/services/galley/src/Galley/API/MLS/Util.hs +++ b/services/galley/src/Galley/API/MLS/Util.hs @@ -27,6 +27,7 @@ import Galley.Effects import Galley.Effects.ConversationStore import Galley.Effects.MemberStore import Galley.Effects.ProposalStore +import Galley.Effects.SubConversationStore import Imports import Polysemy import Polysemy.Resource (Resource, bracket) @@ -37,9 +38,10 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.MLS.Epoch import Wire.API.MLS.Group -import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation +import Wire.API.MLS.SubConversation getLocalConvForUser :: ( Member (ErrorS 'ConvNotFound) r, @@ -72,15 +74,15 @@ getPendingBackendRemoveProposals :: ) => GroupId -> Epoch -> - Sem r [KeyPackageRef] + Sem r [LeafIndex] getPendingBackendRemoveProposals gid epoch = do proposals <- getAllPendingProposals gid epoch catMaybes <$> for proposals ( \case - (Just ProposalOriginBackend, proposal) -> case rmValue proposal of - RemoveProposal kp -> pure . Just $ kp + (Just ProposalOriginBackend, proposal) -> case value proposal of + RemoveProposal i -> pure (Just i) _ -> pure Nothing (Just ProposalOriginClient, _) -> pure Nothing (Nothing, _) -> do @@ -93,15 +95,17 @@ withCommitLock :: ( Members '[ Resource, ConversationStore, - ErrorS 'MLSStaleMessage + ErrorS 'MLSStaleMessage, + SubConversationStore ] r ) => + Local ConvOrSubConvId -> GroupId -> Epoch -> Sem r a -> Sem r a -withCommitLock gid epoch action = +withCommitLock lConvOrSubId gid epoch action = bracket ( acquireCommitLock gid epoch ttl >>= \lockAcquired -> when (lockAcquired == NotAcquired) $ @@ -109,7 +113,11 @@ withCommitLock gid epoch action = ) (const $ releaseCommitLock gid epoch) $ \_ -> do - -- FUTUREWORK: fetch epoch again and check that is matches + actualEpoch <- + fromMaybe (Epoch 0) <$> case tUnqualified lConvOrSubId of + Conv cnv -> getConversationEpoch cnv + SubConv cnv sub -> getSubConversationEpoch cnv sub + unless (actualEpoch == epoch) $ throwS @'MLSStaleMessage action where ttl = fromIntegral (600 :: Int) -- 10 minutes diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index 936855d15aa..213ad9a8659 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -16,26 +16,20 @@ -- with this program. If not, see . module Galley.API.MLS.Welcome - ( postMLSWelcome, - postMLSWelcomeFromLocalUser, + ( sendWelcomes, sendLocalWelcomes, ) where -import Control.Comonad import Data.Domain import Data.Id import Data.Json.Util import Data.Qualified import Data.Time -import Galley.API.MLS.Enabled -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 Galley.Env import Imports import qualified Network.Wai.Utilities.Error as Wai import Network.Wai.Utilities.Server @@ -50,69 +44,37 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.MLS.Credential +import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.Welcome import Wire.API.Message -postMLSWelcome :: - ( Member BrigAccess r, - Member FederatorAccess r, +sendWelcomes :: + ( Member FederatorAccess r, Member GundeckAccess r, - Member (ErrorS 'MLSKeyPackageRefNotFound) r, - Member (Input UTCTime) r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input UTCTime) r ) => Local x -> Maybe ConnId -> + [ClientIdentity] -> RawMLS Welcome -> Sem r () -postMLSWelcome loc con wel = do +sendWelcomes loc con cids welcome = do now <- input - rcpts <- welcomeRecipients (rmValue wel) - let (locals, remotes) = partitionQualified loc rcpts - sendLocalWelcomes con now (rmRaw wel) (qualifyAs loc locals) - sendRemoteWelcomes (rmRaw wel) remotes - -postMLSWelcomeFromLocalUser :: - ( Member BrigAccess r, - Member FederatorAccess r, - Member GundeckAccess r, - Member (ErrorS 'MLSKeyPackageRefNotFound) r, - Member (ErrorS 'MLSNotEnabled) r, - Member (Input UTCTime) r, - Member (Input Env) r, - Member P.TinyLog r - ) => - Local x -> - ConnId -> - RawMLS Welcome -> - Sem r () -postMLSWelcomeFromLocalUser loc con wel = do - assertMLSEnabled - postMLSWelcome loc (Just con) wel - -welcomeRecipients :: - ( Member BrigAccess r, - Member (ErrorS 'MLSKeyPackageRefNotFound) r - ) => - Welcome -> - Sem r [Qualified (UserId, ClientId)] -welcomeRecipients = - traverse - ( fmap cidQualifiedClient - . derefKeyPackage - . gsNewMember - ) - . welSecrets + let (locals, remotes) = partitionQualified loc (map cidQualifiedClient cids) + let msg = mkRawMLS $ mkMessage (MessageWelcome welcome) + sendLocalWelcomes con now msg (qualifyAs loc locals) + sendRemoteWelcomes msg remotes sendLocalWelcomes :: Member GundeckAccess r => Maybe ConnId -> UTCTime -> - ByteString -> + RawMLS Message -> Local [(UserId, ClientId)] -> Sem r () -sendLocalWelcomes con now rawWelcome lclients = do +sendLocalWelcomes con now welcome lclients = do runMessagePush lclients Nothing $ foldMap (uncurry mkPush) (tUnqualified lclients) where @@ -121,21 +83,24 @@ sendLocalWelcomes con now rawWelcome lclients = do -- FUTUREWORK: use the conversation ID stored in the key package mapping table let lcnv = qualifyAs lclients (selfConv u) lusr = qualifyAs lclients u - e = Event (tUntagged lcnv) Nothing (tUntagged lusr) now $ EdMLSWelcome rawWelcome + e = Event (tUntagged lcnv) Nothing (tUntagged lusr) now $ EdMLSWelcome welcome.raw in newMessagePush lclients mempty con defMessageMetadata (u, c) e sendRemoteWelcomes :: ( Member FederatorAccess r, Member P.TinyLog r ) => - ByteString -> + RawMLS Message -> [Remote (UserId, ClientId)] -> Sem r () -sendRemoteWelcomes rawWelcome clients = do - let req = MLSWelcomeRequest . Base64ByteString $ rawWelcome - rpc = fedClient @'Galley @"mls-welcome" req - traverse_ handleError <=< runFederatedConcurrentlyEither clients $ - const rpc +sendRemoteWelcomes welcome clients = do + let msg = Base64ByteString welcome.raw + traverse_ handleError <=< runFederatedConcurrentlyEither clients $ \rcpts -> + fedClient @'Galley @"mls-welcome" + MLSWelcomeRequest + { welcomeMessage = msg, + recipients = tUnqualified rcpts + } where handleError :: Member P.TinyLog r => diff --git a/services/galley/src/Galley/API/Public/MLS.hs b/services/galley/src/Galley/API/Public/MLS.hs index 73187b06da9..7de0a232acf 100644 --- a/services/galley/src/Galley/API/Public/MLS.hs +++ b/services/galley/src/Galley/API/Public/MLS.hs @@ -25,8 +25,6 @@ import Wire.API.Routes.Public.Galley.MLS mlsAPI :: API MLSAPI GalleyEffects mlsAPI = - mkNamedAPI @"mls-welcome-message" (callsFed (exposeAnnotations postMLSWelcomeFromLocalUser)) - <@> mkNamedAPI @"mls-message-v1" (callsFed (exposeAnnotations postMLSMessageFromLocalUserV1)) - <@> mkNamedAPI @"mls-message" (callsFed (exposeAnnotations postMLSMessageFromLocalUser)) + mkNamedAPI @"mls-message" (callsFed (exposeAnnotations postMLSMessageFromLocalUser)) <@> mkNamedAPI @"mls-commit-bundle" (callsFed (exposeAnnotations postMLSCommitBundleFromLocalUser)) <@> mkNamedAPI @"mls-public-keys" getMLSPublicKeys diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 45c36abf16d..39297cf6b20 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -86,7 +86,6 @@ import Data.Time import Galley.API.Action import Galley.API.Error import Galley.API.Federation (onConversationUpdated) -import Galley.API.MLS.KeyPackage (nullKeyPackageRef) import Galley.API.Mapping import Galley.API.Message import qualified Galley.API.Query as Query @@ -135,7 +134,6 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite -import Wire.API.MLS.Group import Wire.API.Message import Wire.API.Password (mkSafePassword) import Wire.API.Provider.Service (ServiceRef) @@ -690,19 +688,17 @@ updateConversationProtocolWithLocalUser :: Member (ErrorS 'ConvInvalidProtocolTransition) r, Member (ErrorS 'ConvMemberNotFound) r, Member (Error FederationError) r, - Member MemberStore r, Member ConversationStore r ) => Local UserId -> - ClientId -> ConnId -> Qualified ConvId -> ProtocolUpdate -> Sem r () -updateConversationProtocolWithLocalUser lusr client conn qcnv update = +updateConversationProtocolWithLocalUser lusr _conn qcnv update = foldQualified lusr - (\lcnv -> updateLocalConversationProtocol (tUntagged lusr) client (Just conn) lcnv update) + (\lcnv -> updateLocalConversationProtocol (tUntagged lusr) lcnv update) (\_rcnv -> throw FederationNotImplemented) qcnv @@ -711,22 +707,18 @@ updateLocalConversationProtocol :: ( Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvInvalidProtocolTransition) r, Member (ErrorS 'ConvMemberNotFound) r, - Member MemberStore r, Member ConversationStore r ) => Qualified UserId -> - ClientId -> - Maybe ConnId -> Local ConvId -> ProtocolUpdate -> Sem r () -updateLocalConversationProtocol qusr client _mconn lcnv (ProtocolUpdate newProtocol) = do +updateLocalConversationProtocol qusr lcnv (ProtocolUpdate newProtocol) = do conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound void $ ensureOtherMember lcnv qusr conv case (protocolTag (convProtocol conv), newProtocol) of - (ProtocolProteusTag, ProtocolMixedTag) -> do + (ProtocolProteusTag, ProtocolMixedTag) -> E.updateToMixedProtocol lcnv MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - E.addMLSClients (convToGroupId lcnv) qusr (Set.singleton (client, nullKeyPackageRef)) (ProtocolProteusTag, ProtocolProteusTag) -> pure () (ProtocolMixedTag, ProtocolMixedTag) -> diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 520cd3ea9c2..98000e95232 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -58,7 +58,7 @@ import Wire.API.Conversation hiding (Conversation, Member) import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group -import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation createMLSSelfConversation :: @@ -199,16 +199,15 @@ conversationMeta conv = accessRoles = maybeRole t $ parseAccessRoles r mbAccessRolesV2 pure $ ConversationMetadata t c (defAccess t a) accessRoles n i mt rm -getPublicGroupState :: ConvId -> Client (Maybe OpaquePublicGroupState) -getPublicGroupState cid = do - fmap join $ - runIdentity - <$$> retry - x1 - ( query1 - Cql.selectPublicGroupState - (params LocalQuorum (Identity cid)) - ) +getGroupInfo :: ConvId -> Client (Maybe GroupInfoData) +getGroupInfo cid = do + runIdentity + <$$> retry + x1 + ( query1 + Cql.selectGroupInfo + (params LocalQuorum (Identity cid)) + ) isConvAlive :: ConvId -> Client Bool isConvAlive cid = do @@ -238,12 +237,19 @@ updateConvReceiptMode cid receiptMode = retry x5 $ write Cql.updateConvReceiptMo updateConvMessageTimer :: ConvId -> Maybe Milliseconds -> Client () updateConvMessageTimer cid mtimer = retry x5 $ write Cql.updateConvMessageTimer (params LocalQuorum (mtimer, cid)) +getConvEpoch :: ConvId -> Client (Maybe Epoch) +getConvEpoch cid = + (runIdentity =<<) + <$> retry + x1 + (query1 Cql.getConvEpoch (params LocalQuorum (Identity cid))) + updateConvEpoch :: ConvId -> Epoch -> Client () updateConvEpoch cid epoch = retry x5 $ write Cql.updateConvEpoch (params LocalQuorum (epoch, cid)) -setPublicGroupState :: ConvId -> OpaquePublicGroupState -> Client () -setPublicGroupState conv gib = - write Cql.updatePublicGroupState (params LocalQuorum (gib, conv)) +setGroupInfo :: ConvId -> GroupInfoData -> Client () +setGroupInfo conv gid = + write Cql.updateGroupInfo (params LocalQuorum (gid, conv)) getConversation :: ConvId -> Client (Maybe Conversation) getConversation conv = do @@ -460,10 +466,11 @@ interpretConversationStoreToCassandra = interpret $ \case CreateConversation loc nc -> embedClient $ createConversation loc nc CreateMLSSelfConversation lusr -> embedClient $ createMLSSelfConversation lusr GetConversation cid -> embedClient $ getConversation cid + GetConversationEpoch cid -> embedClient $ getConvEpoch cid LookupConvByGroupId gId -> embedClient $ lookupConvByGroupId gId GetConversations cids -> localConversations cids GetConversationMetadata cid -> embedClient $ conversationMeta cid - GetPublicGroupState cid -> embedClient $ getPublicGroupState cid + GetGroupInfo cid -> embedClient $ getGroupInfo cid IsConversationAlive cid -> embedClient $ isConvAlive cid SelectConversations uid cids -> embedClient $ localConversationIdsOf uid cids GetRemoteConversationStatus uid cids -> embedClient $ remoteConversationStatus uid cids @@ -476,7 +483,7 @@ interpretConversationStoreToCassandra = interpret $ \case DeleteConversation cid -> embedClient $ deleteConversation cid SetGroupIdForConversation gId cid -> embedClient $ setGroupIdForConversation gId cid DeleteGroupIdForConversation gId -> embedClient $ deleteGroupIdForConversation gId - SetPublicGroupState cid gib -> embedClient $ setPublicGroupState cid gib + SetGroupInfo cid gib -> embedClient $ setGroupInfo cid gib AcquireCommitLock gId epoch ttl -> embedClient $ acquireCommitLock gId epoch ttl ReleaseCommitLock gId epoch -> embedClient $ releaseCommitLock gId epoch DeleteGroupIds gIds -> deleteGroupIds gIds diff --git a/services/galley/src/Galley/Cassandra/Conversation/MLS.hs b/services/galley/src/Galley/Cassandra/Conversation/MLS.hs index 7ca5f89d358..06e2e65d917 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/MLS.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/MLS.hs @@ -19,11 +19,13 @@ module Galley.Cassandra.Conversation.MLS ( acquireCommitLock, releaseCommitLock, lookupMLSClients, + lookupMLSClientLeafIndices, ) where import Cassandra import Cassandra.Settings (fromRow) +import Control.Arrow import Data.Time import Galley.API.MLS.Types import qualified Galley.Cassandra.Queries as Cql @@ -61,9 +63,10 @@ checkTransSuccess :: [Row] -> Bool checkTransSuccess [] = False checkTransSuccess (row : _) = either (const False) (fromMaybe False) $ fromRow 0 row +lookupMLSClientLeafIndices :: GroupId -> Client (ClientMap, IndexMap) +lookupMLSClientLeafIndices groupId = do + entries <- retry x5 (query Cql.lookupMLSClients (params LocalQuorum (Identity groupId))) + pure $ (mkClientMap &&& mkIndexMap) entries + lookupMLSClients :: GroupId -> Client ClientMap -lookupMLSClients groupId = - mkClientMap - <$> retry - x5 - (query Cql.lookupMLSClients (params LocalQuorum (Identity groupId))) +lookupMLSClients = fmap fst . lookupMLSClientLeafIndices diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index 7665edb26e2..da67f5e52f0 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -32,7 +32,7 @@ import qualified Data.List.Extra as List import Data.Monoid import Data.Qualified import qualified Data.Set as Set -import Galley.Cassandra.Conversation.MLS (lookupMLSClients) +import Galley.Cassandra.Conversation.MLS import Galley.Cassandra.Instances () import qualified Galley.Cassandra.Queries as Cql import Galley.Cassandra.Services @@ -47,8 +47,9 @@ import Polysemy.Input import qualified UnliftIO import Wire.API.Conversation.Member hiding (Member) import Wire.API.Conversation.Role +import Wire.API.MLS.Credential import Wire.API.MLS.Group -import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode (LeafIndex) import Wire.API.Provider.Service -- | Add members to a local conversation. @@ -342,12 +343,22 @@ removeLocalMembersFromRemoteConv (tUntagged -> Qualified conv convDomain) victim setConsistency LocalQuorum for_ victims $ \u -> addPrepQuery Cql.deleteUserRemoteConv (u, convDomain, conv) -addMLSClients :: GroupId -> Qualified UserId -> Set.Set (ClientId, KeyPackageRef) -> Client () +addMLSClients :: GroupId -> Qualified UserId -> Set.Set (ClientId, LeafIndex) -> Client () addMLSClients groupId (Qualified usr domain) cs = retry x5 . batch $ do setType BatchLogged setConsistency LocalQuorum - for_ cs $ \(c, kpr) -> - addPrepQuery Cql.addMLSClient (groupId, domain, usr, c, kpr) + for_ cs $ \(c, idx) -> + addPrepQuery Cql.addMLSClient (groupId, domain, usr, c, fromIntegral idx) + +planMLSClientRemoval :: Foldable f => GroupId -> f ClientIdentity -> Client () +planMLSClientRemoval groupId cids = + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + for_ cids $ \cid -> do + addPrepQuery + Cql.planMLSClientRemoval + (groupId, ciDomain cid, ciUser cid, ciClient cid) removeMLSClients :: GroupId -> Qualified UserId -> Set.Set ClientId -> Client () removeMLSClients groupId (Qualified usr domain) cs = retry x5 . batch $ do @@ -385,6 +396,8 @@ interpretMemberStoreToCassandra = interpret $ \case embedClient $ removeLocalMembersFromRemoteConv rcnv uids AddMLSClients lcnv quid cs -> embedClient $ addMLSClients lcnv quid cs + PlanClientRemoval lcnv cids -> embedClient $ planMLSClientRemoval lcnv cids RemoveMLSClients lcnv quid cs -> embedClient $ removeMLSClients lcnv quid cs RemoveAllMLSClients gid -> embedClient $ removeAllMLSClients gid LookupMLSClients lcnv -> embedClient $ lookupMLSClients lcnv + LookupMLSClientLeafIndices lcnv -> embedClient $ lookupMLSClientLeafIndices lcnv diff --git a/services/galley/src/Galley/Cassandra/Instances.hs b/services/galley/src/Galley/Cassandra/Instances.hs index 4610857013d..eaeaa875050 100644 --- a/services/galley/src/Galley/Cassandra/Instances.hs +++ b/services/galley/src/Galley/Cassandra/Instances.hs @@ -37,8 +37,8 @@ import Wire.API.Asset (AssetKey, assetKeyToText) import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite +import Wire.API.MLS.GroupInfo import Wire.API.MLS.Proposal -import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Routes.Internal.Galley.TeamsIntra @@ -201,12 +201,12 @@ instance Cql GroupId where fromCql (CqlBlob b) = Right . GroupId . LBS.toStrict $ b fromCql _ = Left "group_id: blob expected" -instance Cql OpaquePublicGroupState where +instance Cql GroupInfoData where ctype = Tagged BlobColumn - toCql = CqlBlob . LBS.fromStrict . unOpaquePublicGroupState - fromCql (CqlBlob b) = Right $ OpaquePublicGroupState (LBS.toStrict b) - fromCql _ = Left "OpaquePublicGroupState: blob expected" + toCql = CqlBlob . LBS.fromStrict . unGroupInfoData + fromCql (CqlBlob b) = Right $ GroupInfoData (LBS.toStrict b) + fromCql _ = Left "GroupInfoData: blob expected" instance Cql Icon where ctype = Tagged TextColumn @@ -244,7 +244,7 @@ instance Cql ProposalRef where instance Cql (RawMLS Proposal) where ctype = Tagged BlobColumn - toCql = CqlBlob . LBS.fromStrict . rmRaw + toCql = CqlBlob . LBS.fromStrict . raw fromCql (CqlBlob b) = mapLeft T.unpack $ decodeMLS b fromCql _ = Left "Proposal: blob expected" diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index c838ddd00f2..7e491df3662 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -34,8 +34,7 @@ import Wire.API.Conversation.Code import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.MLS.CipherSuite -import Wire.API.MLS.KeyPackage -import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation import Wire.API.Password (Password) import Wire.API.Provider @@ -276,6 +275,9 @@ updateConvName = "update conversation set name = ? where conv = ?" updateConvType :: PrepQuery W (ConvType, ConvId) () updateConvType = "update conversation set type = ? where conv = ?" +getConvEpoch :: PrepQuery R (Identity ConvId) (Identity (Maybe Epoch)) +getConvEpoch = "select epoch from conversation where conv = ?" + updateConvEpoch :: PrepQuery W (Epoch, ConvId) () updateConvEpoch = "update conversation set epoch = ? where conv = ?" @@ -285,11 +287,11 @@ deleteConv = "delete from conversation using timestamp 32503680000000000 where c markConvDeleted :: PrepQuery W (Identity ConvId) () markConvDeleted = "update conversation set deleted = true where conv = ?" -selectPublicGroupState :: PrepQuery R (Identity ConvId) (Identity (Maybe OpaquePublicGroupState)) -selectPublicGroupState = "select public_group_state from conversation where conv = ?" +selectGroupInfo :: PrepQuery R (Identity ConvId) (Identity GroupInfoData) +selectGroupInfo = "select public_group_state from conversation where conv = ?" -updatePublicGroupState :: PrepQuery W (OpaquePublicGroupState, ConvId) () -updatePublicGroupState = "update conversation set public_group_state = ? where conv = ?" +updateGroupInfo :: PrepQuery W (GroupInfoData, ConvId) () +updateGroupInfo = "update conversation set public_group_state = ? where conv = ?" -- Conversations accessible by code ----------------------------------------- @@ -332,14 +334,17 @@ lookupGroupId = "SELECT conv_id, domain, subconv_id from group_id_conv_id where selectSubConversation :: PrepQuery R (ConvId, SubConvId) (CipherSuiteTag, Epoch, Writetime Epoch, GroupId) selectSubConversation = "SELECT cipher_suite, epoch, WRITETIME(epoch), group_id FROM subconversation WHERE conv_id = ? and subconv_id = ?" -insertSubConversation :: PrepQuery W (ConvId, SubConvId, CipherSuiteTag, Epoch, GroupId, Maybe OpaquePublicGroupState) () +insertSubConversation :: PrepQuery W (ConvId, SubConvId, CipherSuiteTag, Epoch, GroupId, Maybe GroupInfoData) () insertSubConversation = "INSERT INTO subconversation (conv_id, subconv_id, cipher_suite, epoch, group_id, public_group_state) VALUES (?, ?, ?, ?, ?, ?)" -updateSubConvPublicGroupState :: PrepQuery W (ConvId, SubConvId, Maybe OpaquePublicGroupState) () -updateSubConvPublicGroupState = "INSERT INTO subconversation (conv_id, subconv_id, public_group_state) VALUES (?, ?, ?)" +updateSubConvGroupInfo :: PrepQuery W (ConvId, SubConvId, Maybe GroupInfoData) () +updateSubConvGroupInfo = "INSERT INTO subconversation (conv_id, subconv_id, public_group_state) VALUES (?, ?, ?)" + +selectSubConvGroupInfo :: PrepQuery R (ConvId, SubConvId) (Identity (Maybe GroupInfoData)) +selectSubConvGroupInfo = "SELECT public_group_state FROM subconversation WHERE conv_id = ? AND subconv_id = ?" -selectSubConvPublicGroupState :: PrepQuery R (ConvId, SubConvId) (Identity (Maybe OpaquePublicGroupState)) -selectSubConvPublicGroupState = "SELECT public_group_state FROM subconversation WHERE conv_id = ? AND subconv_id = ?" +selectSubConvEpoch :: PrepQuery R (ConvId, SubConvId) (Identity (Maybe Epoch)) +selectSubConvEpoch = "SELECT epoch FROM subconversation WHERE conv_id = ? AND subconv_id = ?" deleteGroupId :: PrepQuery W (Identity GroupId) () deleteGroupId = "DELETE FROM group_id_conv_id WHERE group_id = ?" @@ -462,8 +467,11 @@ rmMemberClient c = -- MLS Clients -------------------------------------------------------------- -addMLSClient :: PrepQuery W (GroupId, Domain, UserId, ClientId, KeyPackageRef) () -addMLSClient = "insert into mls_group_member_client (group_id, user_domain, user, client, key_package_ref) values (?, ?, ?, ?, ?)" +addMLSClient :: PrepQuery W (GroupId, Domain, UserId, ClientId, Int32) () +addMLSClient = "insert into mls_group_member_client (group_id, user_domain, user, client, leaf_node_index, removal_pending) values (?, ?, ?, ?, ?, false)" + +planMLSClientRemoval :: PrepQuery W (GroupId, Domain, UserId, ClientId) () +planMLSClientRemoval = "update mls_group_member_client set removal_pending = true where group_id = ? and user_domain = ? and user = ? and client = ?" removeMLSClient :: PrepQuery W (GroupId, Domain, UserId, ClientId) () removeMLSClient = "delete from mls_group_member_client where group_id = ? and user_domain = ? and user = ? and client = ?" @@ -471,8 +479,8 @@ removeMLSClient = "delete from mls_group_member_client where group_id = ? and us removeAllMLSClients :: PrepQuery W (Identity GroupId) () removeAllMLSClients = "DELETE FROM mls_group_member_client WHERE group_id = ?" -lookupMLSClients :: PrepQuery R (Identity GroupId) (Domain, UserId, ClientId, KeyPackageRef) -lookupMLSClients = "select user_domain, user, client, key_package_ref from mls_group_member_client where group_id = ?" +lookupMLSClients :: PrepQuery R (Identity GroupId) (Domain, UserId, ClientId, Int32, Bool) +lookupMLSClients = "select user_domain, user, client, leaf_node_index, removal_pending from mls_group_member_client where group_id = ?" acquireCommitLock :: PrepQuery W (GroupId, Epoch, Int32) Row acquireCommitLock = "insert into mls_commit_locks (group_id, epoch) values (?, ?) if not exists using ttl ?" diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index ad143121146..9dd9dd02d0d 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -26,8 +26,8 @@ import Data.Id import qualified Data.Map as Map import Data.Qualified import Data.Time.Clock -import Galley.API.MLS.Types (SubConversation (..)) -import Galley.Cassandra.Conversation.MLS (lookupMLSClients) +import Galley.API.MLS.Types +import Galley.Cassandra.Conversation.MLS import qualified Galley.Cassandra.Queries as Cql import Galley.Cassandra.Store (embedClient) import Galley.Effects.SubConversationStore (SubConversationStore (..)) @@ -37,14 +37,14 @@ import Polysemy.Input import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group -import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation selectSubConversation :: ConvId -> SubConvId -> Client (Maybe SubConversation) selectSubConversation convId subConvId = do m <- retry x5 (query1 Cql.selectSubConversation (params LocalQuorum (convId, subConvId))) for m $ \(suite, epoch, epochWritetime, groupId) -> do - cm <- lookupMLSClients groupId + (cm, im) <- lookupMLSClientLeafIndices groupId pure $ SubConversation { scParentConvId = convId, @@ -56,20 +56,32 @@ selectSubConversation convId subConvId = do cnvmlsEpochTimestamp = epochTimestamp epoch epochWritetime, cnvmlsCipherSuite = suite }, - scMembers = cm + scMembers = cm, + scIndexMap = im } -insertSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> Epoch -> GroupId -> Maybe OpaquePublicGroupState -> Client () -insertSubConversation convId subConvId suite epoch groupId mPgs = - retry x5 (write Cql.insertSubConversation (params LocalQuorum (convId, subConvId, suite, epoch, groupId, mPgs))) +insertSubConversation :: + ConvId -> + SubConvId -> + CipherSuiteTag -> + Epoch -> + GroupId -> + Maybe GroupInfoData -> + Client () +insertSubConversation convId subConvId suite epoch groupId mGroupInfo = + retry x5 (write Cql.insertSubConversation (params LocalQuorum (convId, subConvId, suite, epoch, groupId, mGroupInfo))) -updateSubConvPublicGroupState :: ConvId -> SubConvId -> Maybe OpaquePublicGroupState -> Client () -updateSubConvPublicGroupState convId subConvId mPgs = - retry x5 (write Cql.updateSubConvPublicGroupState (params LocalQuorum (convId, subConvId, mPgs))) +updateSubConvGroupInfo :: ConvId -> SubConvId -> Maybe GroupInfoData -> Client () +updateSubConvGroupInfo convId subConvId mGroupInfo = + retry x5 (write Cql.updateSubConvGroupInfo (params LocalQuorum (convId, subConvId, mGroupInfo))) -selectSubConvPublicGroupState :: ConvId -> SubConvId -> Client (Maybe OpaquePublicGroupState) -selectSubConvPublicGroupState convId subConvId = - (runIdentity =<<) <$> retry x5 (query1 Cql.selectSubConvPublicGroupState (params LocalQuorum (convId, subConvId))) +selectSubConvGroupInfo :: ConvId -> SubConvId -> Client (Maybe GroupInfoData) +selectSubConvGroupInfo convId subConvId = + (runIdentity =<<) <$> retry x5 (query1 Cql.selectSubConvGroupInfo (params LocalQuorum (convId, subConvId))) + +selectSubConvEpoch :: ConvId -> SubConvId -> Client (Maybe Epoch) +selectSubConvEpoch convId subConvId = + (runIdentity =<<) <$> retry x5 (query1 Cql.selectSubConvEpoch (params LocalQuorum (convId, subConvId))) setGroupIdForSubConversation :: GroupId -> Qualified ConvId -> SubConvId -> Client () setGroupIdForSubConversation groupId qconv sconv = @@ -107,10 +119,12 @@ interpretSubConversationStoreToCassandra :: Sem (SubConversationStore ': r) a -> Sem r a interpretSubConversationStoreToCassandra = interpret $ \case - CreateSubConversation convId subConvId suite epoch groupId mPgs -> embedClient (insertSubConversation convId subConvId suite epoch groupId mPgs) + CreateSubConversation convId subConvId suite epoch groupId mGroupInfo -> + embedClient (insertSubConversation convId subConvId suite epoch groupId mGroupInfo) GetSubConversation convId subConvId -> embedClient (selectSubConversation convId subConvId) - GetSubConversationPublicGroupState convId subConvId -> embedClient (selectSubConvPublicGroupState convId subConvId) - SetSubConversationPublicGroupState convId subConvId mPgs -> embedClient (updateSubConvPublicGroupState convId subConvId mPgs) + GetSubConversationGroupInfo convId subConvId -> embedClient (selectSubConvGroupInfo convId subConvId) + GetSubConversationEpoch convId subConvId -> embedClient (selectSubConvEpoch convId subConvId) + SetSubConversationGroupInfo convId subConvId mPgs -> embedClient (updateSubConvGroupInfo convId subConvId mPgs) SetGroupIdForSubConversation gId cid sconv -> embedClient $ setGroupIdForSubConversation gId cid sconv SetSubConversationEpoch cid sconv epoch -> embedClient $ setEpochForSubConversation cid sconv epoch DeleteGroupIdForSubConversation groupId -> embedClient $ deleteGroupId groupId diff --git a/services/galley/src/Galley/Effects/BrigAccess.hs b/services/galley/src/Galley/Effects/BrigAccess.hs index be713c6fbcf..8631ef1f7d8 100644 --- a/services/galley/src/Galley/Effects/BrigAccess.hs +++ b/services/galley/src/Galley/Effects/BrigAccess.hs @@ -48,12 +48,7 @@ module Galley.Effects.BrigAccess removeLegalHoldClientFromUser, -- * MLS - getClientByKeyPackageRef, getLocalMLSClients, - addKeyPackageRef, - validateAndAddKeyPackageRef, - updateKeyPackageRef, - deleteKeyPackageRefs, -- * Features getAccountConferenceCallingConfigClient, @@ -73,9 +68,7 @@ import Polysemy import Polysemy.Error import Wire.API.Connection import Wire.API.Error.Galley -import Wire.API.MLS.Credential -import Wire.API.MLS.KeyPackage -import Wire.API.Routes.Internal.Brig +import Wire.API.MLS.CipherSuite import Wire.API.Routes.Internal.Brig.Connection import qualified Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti as Multi import Wire.API.Team.Feature @@ -130,12 +123,7 @@ data BrigAccess m a where BrigAccess m (Either AuthenticationError ClientId) RemoveLegalHoldClientFromUser :: UserId -> BrigAccess m () GetAccountConferenceCallingConfigClient :: UserId -> BrigAccess m (WithStatusNoLock ConferenceCallingConfig) - GetClientByKeyPackageRef :: KeyPackageRef -> BrigAccess m (Maybe ClientIdentity) GetLocalMLSClients :: Local UserId -> SignatureSchemeTag -> BrigAccess m (Set ClientInfo) - AddKeyPackageRef :: KeyPackageRef -> Qualified UserId -> ClientId -> Qualified ConvId -> BrigAccess m () - ValidateAndAddKeyPackageRef :: NewKeyPackage -> BrigAccess m (Either Text NewKeyPackageResult) - UpdateKeyPackageRef :: KeyPackageUpdate -> BrigAccess m () - DeleteKeyPackageRefs :: [KeyPackageRef] -> BrigAccess m () UpdateSearchVisibilityInbound :: Multi.TeamStatus SearchVisibilityInboundConfig -> BrigAccess m () diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index 5d9fa1d51cf..fe47bb376cc 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -28,10 +28,11 @@ module Galley.Effects.ConversationStore -- * Read conversation getConversation, + getConversationEpoch, lookupConvByGroupId, getConversations, getConversationMetadata, - getPublicGroupState, + getGroupInfo, isConversationAlive, getRemoteConversationStatus, selectConversations, @@ -46,7 +47,7 @@ module Galley.Effects.ConversationStore acceptConnectConversation, setGroupIdForConversation, deleteGroupIdForConversation, - setPublicGroupState, + setGroupInfo, deleteGroupIds, updateToMixedProtocol, @@ -72,7 +73,7 @@ import Polysemy import Wire.API.Conversation hiding (Conversation, Member) import Wire.API.MLS.CipherSuite (CipherSuiteTag) import Wire.API.MLS.Epoch -import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation data ConversationStore m a where @@ -83,12 +84,11 @@ data ConversationStore m a where ConversationStore m Conversation DeleteConversation :: ConvId -> ConversationStore m () GetConversation :: ConvId -> ConversationStore m (Maybe Conversation) + GetConversationEpoch :: ConvId -> ConversationStore m (Maybe Epoch) LookupConvByGroupId :: GroupId -> ConversationStore m (Maybe (Qualified ConvOrSubConvId)) GetConversations :: [ConvId] -> ConversationStore m [Conversation] GetConversationMetadata :: ConvId -> ConversationStore m (Maybe ConversationMetadata) - GetPublicGroupState :: - ConvId -> - ConversationStore m (Maybe OpaquePublicGroupState) + GetGroupInfo :: ConvId -> ConversationStore m (Maybe GroupInfoData) IsConversationAlive :: ConvId -> ConversationStore m Bool GetRemoteConversationStatus :: UserId -> @@ -103,10 +103,7 @@ data ConversationStore m a where SetConversationEpoch :: ConvId -> Epoch -> ConversationStore m () SetGroupIdForConversation :: GroupId -> Qualified ConvId -> ConversationStore m () DeleteGroupIdForConversation :: GroupId -> ConversationStore m () - SetPublicGroupState :: - ConvId -> - OpaquePublicGroupState -> - ConversationStore m () + SetGroupInfo :: ConvId -> GroupInfoData -> ConversationStore m () AcquireCommitLock :: GroupId -> Epoch -> NominalDiffTime -> ConversationStore m LockAcquired ReleaseCommitLock :: GroupId -> Epoch -> ConversationStore m () DeleteGroupIds :: [GroupId] -> ConversationStore m () diff --git a/services/galley/src/Galley/Effects/MemberStore.hs b/services/galley/src/Galley/Effects/MemberStore.hs index bdc61c90160..bb8d1c6c33f 100644 --- a/services/galley/src/Galley/Effects/MemberStore.hs +++ b/services/galley/src/Galley/Effects/MemberStore.hs @@ -39,9 +39,11 @@ module Galley.Effects.MemberStore setSelfMember, setOtherMember, addMLSClients, + planClientRemoval, removeMLSClients, removeAllMLSClients, lookupMLSClients, + lookupMLSClientLeafIndices, -- * Delete members deleteMembers, @@ -59,8 +61,9 @@ import Galley.Types.UserList import Imports import Polysemy import Wire.API.Conversation.Member hiding (Member) +import Wire.API.MLS.Credential import Wire.API.MLS.Group -import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode import Wire.API.Provider.Service data MemberStore m a where @@ -77,10 +80,12 @@ data MemberStore m a where SetOtherMember :: Local ConvId -> Qualified UserId -> OtherMemberUpdate -> MemberStore m () DeleteMembers :: ConvId -> UserList UserId -> MemberStore m () DeleteMembersInRemoteConversation :: Remote ConvId -> [UserId] -> MemberStore m () - AddMLSClients :: GroupId -> Qualified UserId -> Set (ClientId, KeyPackageRef) -> MemberStore m () + AddMLSClients :: GroupId -> Qualified UserId -> Set (ClientId, LeafIndex) -> MemberStore m () + PlanClientRemoval :: Foldable f => GroupId -> f ClientIdentity -> MemberStore m () RemoveMLSClients :: GroupId -> Qualified UserId -> Set ClientId -> MemberStore m () RemoveAllMLSClients :: GroupId -> MemberStore m () LookupMLSClients :: GroupId -> MemberStore m ClientMap + LookupMLSClientLeafIndices :: GroupId -> MemberStore m (ClientMap, IndexMap) makeSem ''MemberStore diff --git a/services/galley/src/Galley/Effects/SubConversationStore.hs b/services/galley/src/Galley/Effects/SubConversationStore.hs index 056eec34d81..4dff138c6ad 100644 --- a/services/galley/src/Galley/Effects/SubConversationStore.hs +++ b/services/galley/src/Galley/Effects/SubConversationStore.hs @@ -27,14 +27,15 @@ import Polysemy import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group -import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation data SubConversationStore m a where - CreateSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> Epoch -> GroupId -> Maybe OpaquePublicGroupState -> SubConversationStore m () + CreateSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> Epoch -> GroupId -> Maybe GroupInfoData -> SubConversationStore m () GetSubConversation :: ConvId -> SubConvId -> SubConversationStore m (Maybe SubConversation) - GetSubConversationPublicGroupState :: ConvId -> SubConvId -> SubConversationStore m (Maybe OpaquePublicGroupState) - SetSubConversationPublicGroupState :: ConvId -> SubConvId -> Maybe OpaquePublicGroupState -> SubConversationStore m () + GetSubConversationGroupInfo :: ConvId -> SubConvId -> SubConversationStore m (Maybe GroupInfoData) + GetSubConversationEpoch :: ConvId -> SubConvId -> SubConversationStore m (Maybe Epoch) + SetSubConversationGroupInfo :: ConvId -> SubConvId -> Maybe GroupInfoData -> SubConversationStore m () SetGroupIdForSubConversation :: GroupId -> Qualified ConvId -> SubConvId -> SubConversationStore m () SetSubConversationEpoch :: ConvId -> SubConvId -> Epoch -> SubConversationStore m () DeleteGroupIdForSubConversation :: GroupId -> SubConversationStore m () diff --git a/services/galley/src/Galley/Intra/Client.hs b/services/galley/src/Galley/Intra/Client.hs index 697c588465d..96cce82ece2 100644 --- a/services/galley/src/Galley/Intra/Client.hs +++ b/services/galley/src/Galley/Intra/Client.hs @@ -22,12 +22,7 @@ module Galley.Intra.Client addLegalHoldClientToUser, removeLegalHoldClientFromUser, getLegalHoldAuthToken, - getClientByKeyPackageRef, getLocalMLSClients, - addKeyPackageRef, - updateKeyPackageRef, - validateAndAddKeyPackageRef, - deleteKeyPackageRefs, ) where @@ -35,14 +30,12 @@ import Bilge hiding (getHeader, options, statusCode) import Bilge.RPC import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) -import Control.Monad.Catch import Data.ByteString.Conversion (toByteString') import Data.Id import Data.Misc import Data.Qualified import qualified Data.Set as Set import Data.Text.Encoding -import Data.Text.Lazy (toStrict) import Galley.API.Error import Galley.Effects import Galley.Env @@ -50,22 +43,16 @@ import Galley.External.LegalHoldService.Types import Galley.Intra.Util import Galley.Monad import Imports -import qualified Network.HTTP.Client as Rq -import qualified Network.HTTP.Types as HTTP import Network.HTTP.Types.Method import Network.HTTP.Types.Status import Network.Wai.Utilities.Error hiding (Error) -import qualified Network.Wai.Utilities.Error as Error import Polysemy import Polysemy.Error import Polysemy.Input import qualified Polysemy.TinyLog as P -import Servant import qualified System.Logger.Class as Logger import Wire.API.Error.Galley -import Wire.API.MLS.Credential -import Wire.API.MLS.KeyPackage -import Wire.API.Routes.Internal.Brig +import Wire.API.MLS.CipherSuite import Wire.API.User.Auth.LegalHold import Wire.API.User.Client import Wire.API.User.Client.Prekey @@ -183,18 +170,6 @@ brigAddClient uid connId client = do then Right <$> parseResponse (mkError status502 "server-error") r else pure (Left ReAuthFailed) --- | Calls 'Brig.API.Internal.getClientByKeyPackageRef'. -getClientByKeyPackageRef :: KeyPackageRef -> App (Maybe ClientIdentity) -getClientByKeyPackageRef ref = do - r <- - call Brig $ - method GET - . paths ["i", "mls", "key-packages", toHeader ref] - . expectStatus (flip elem [200, 404]) - if statusCode (responseStatus r) == 200 - then Just <$> parseResponse (mkError status502 "server-error") r - else pure Nothing - -- | Calls 'Brig.API.Internal.getMLSClients'. getLocalMLSClients :: Local UserId -> SignatureSchemeTag -> App (Set ClientInfo) getLocalMLSClients lusr ss = @@ -211,53 +186,3 @@ getLocalMLSClients lusr ss = . expect2xx ) >>= parseResponse (mkError status502 "server-error") - -deleteKeyPackageRefs :: [KeyPackageRef] -> App () -deleteKeyPackageRefs refs = - void $ - call - Brig - ( method DELETE - . paths ["i", "mls", "key-packages"] - . json (DeleteKeyPackageRefsRequest refs) - . expect2xx - ) - -addKeyPackageRef :: KeyPackageRef -> Qualified UserId -> ClientId -> Qualified ConvId -> App () -addKeyPackageRef ref qusr cl qcnv = - void $ - call - Brig - ( method PUT - . paths ["i", "mls", "key-packages", toHeader ref] - . json (NewKeyPackageRef qusr cl qcnv) - . expect2xx - ) - -updateKeyPackageRef :: KeyPackageUpdate -> App () -updateKeyPackageRef keyPackageRef = - void $ - call - Brig - ( method POST - . paths ["i", "mls", "key-packages", toHeader $ kpupPrevious keyPackageRef] - . json (kpupNext keyPackageRef) - . expect2xx - ) - -validateAndAddKeyPackageRef :: NewKeyPackage -> App (Either Text NewKeyPackageResult) -validateAndAddKeyPackageRef nkp = do - res <- - call - Brig - ( method PUT - . paths ["i", "mls", "key-package-add"] - . json nkp - ) - let statusCode = HTTP.statusCode (Rq.responseStatus res) - if - | statusCode `div` 100 == 2 -> Right <$> parseResponse (mkError status502 "server-error") res - | statusCode `div` 100 == 4 -> do - err <- parseResponse (mkError status502 "server-error") res - pure (Left ("Error validating keypackage: " <> toStrict (Error.label err) <> ": " <> toStrict (Error.message err))) - | otherwise -> throwM (mkError status502 "server-error" "Unexpected http status returned from /i/mls/key-packages/add") diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs index 3d38b4b5c62..782228140cf 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/services/galley/src/Galley/Intra/Effects.hs @@ -80,21 +80,7 @@ interpretBrigAccess = interpret $ \case embedApp $ removeLegalHoldClientFromUser uid GetAccountConferenceCallingConfigClient uid -> embedApp $ getAccountConferenceCallingConfigClient uid - GetClientByKeyPackageRef ref -> - embedApp $ getClientByKeyPackageRef ref GetLocalMLSClients qusr ss -> embedApp $ getLocalMLSClients qusr ss - AddKeyPackageRef ref qusr cl qcnv -> - embedApp $ - addKeyPackageRef ref qusr cl qcnv - ValidateAndAddKeyPackageRef nkp -> - embedApp $ - validateAndAddKeyPackageRef nkp - UpdateKeyPackageRef update -> - embedApp $ - updateKeyPackageRef update - DeleteKeyPackageRefs refs -> - embedApp $ - deleteKeyPackageRefs refs UpdateSearchVisibilityInbound status -> embedApp $ updateSearchVisibilityInbound status diff --git a/services/galley/src/Galley/Keys.hs b/services/galley/src/Galley/Keys.hs index 129b42396a3..287191f53e0 100644 --- a/services/galley/src/Galley/Keys.hs +++ b/services/galley/src/Galley/Keys.hs @@ -33,6 +33,7 @@ import qualified Data.Map as Map import Data.PEM import Data.X509 import Imports +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.Keys diff --git a/services/galley/test/integration.hs b/services/galley/test/integration.hs new file mode 100644 index 00000000000..a26473d24ee --- /dev/null +++ b/services/galley/test/integration.hs @@ -0,0 +1 @@ +import Run diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index b6c6bd1023a..d25796947d8 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -32,8 +32,6 @@ import qualified Control.Monad.State as State import Crypto.Error import qualified Crypto.PubKey.Ed25519 as Ed25519 import qualified Data.Aeson as Aeson -import Data.Binary.Put -import qualified Data.ByteString.Lazy as LBS import Data.Domain import Data.Id import Data.Json.Util hiding ((#)) @@ -64,13 +62,14 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley +import Wire.API.MLS.AuthenticatedContent import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.Keys import Wire.API.MLS.Message +import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation -import Wire.API.MLS.Welcome import Wire.API.Message import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.Version @@ -88,10 +87,7 @@ tests s = testGroup "Welcome" [ test s "local welcome" testLocalWelcome, - test s "local welcome (client with no public key)" testWelcomeNoKey, - test s "remote welcome" testRemoteWelcome, - test s "post a remote MLS welcome message" sendRemoteMLSWelcome, - test s "post a remote MLS welcome message (key package ref not found)" sendRemoteMLSWelcomeKPNotFound + test s "post a remote MLS welcome message" sendRemoteMLSWelcome ], testGroup "Creation" @@ -100,12 +96,11 @@ tests s = ], testGroup "Deletion" - [ test s "delete a MLS conversation" testDeleteMLSConv + [ test s "delete an MLS conversation" testDeleteMLSConv ], testGroup "Commit" [ test s "add user to a conversation" testAddUser, - test s "add user with an incomplete welcome" testAddUserWithBundleIncompleteWelcome, test s "add user (not connected)" testAddUserNotConnected, test s "add user (partial client list)" testAddUserPartial, test s "add client of existing user" testAddClientPartial, @@ -257,7 +252,7 @@ tests s = test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv, test s "delete subconversation as a remote member" (testRemoteMemberDeleteSubConv True), test s "delete subconversation as a remote non-member" (testRemoteMemberDeleteSubConv False), - test s "delete parent conversation of a remote subconveration" testDeleteRemoteParentOfSubConv + test s "delete parent conversation of a remote subconversation" testDeleteRemoteParentOfSubConv ] ], testGroup @@ -342,7 +337,7 @@ testLocalWelcome = do Nothing -> assertFailure "Expected welcome message" Just w -> pure w events <- mlsBracket [bob1] $ \wss -> do - es <- sendAndConsumeCommit commit + es <- sendAndConsumeCommitBundle commit WS.assertMatchN_ (5 # Second) wss $ wsAssertMLSWelcome (cidQualifiedUser bob1) welcome @@ -352,50 +347,6 @@ testLocalWelcome = do event <- assertOne events liftIO $ assertJoinEvent qcnv alice [bob] roleNameWireMember event -testWelcomeNoKey :: TestM () -testWelcomeNoKey = do - users <- createAndConnectUsers [Nothing, Nothing] - runMLSTest $ do - [alice1, bob1] <- traverse createMLSClient users - void $ setupMLSGroup alice1 - - -- add bob using an "out-of-band" key package - (_, ref) <- generateKeyPackage bob1 - kp <- keyPackageFile bob1 ref - commit <- createAddCommitWithKeyPackages alice1 [(bob1, kp)] - welcome <- liftIO $ case mpWelcome commit of - Nothing -> assertFailure "Expected welcome message" - Just w -> pure w - - err <- - responseJsonError - =<< postWelcome (ciUser alice1) welcome - assertFailure "Expected welcome message" - Just w -> pure w - (_, reqs) <- - withTempMockFederator' welcomeMock $ - postWelcome (ciUser (mpSender commit)) welcome - !!! const 201 === statusCode - consumeWelcome welcome - fedWelcome <- assertOne (filter ((== "mls-welcome") . frRPC) reqs) - let req :: Maybe MLSWelcomeRequest = Aeson.decode (frBody fedWelcome) - liftIO $ req @?= (Just . MLSWelcomeRequest . Base64ByteString) welcome - testAddUserWithBundle :: TestM () testAddUserWithBundle = do [alice, bob] <- createAndConnectUsers [Nothing, Nothing] @@ -426,35 +377,8 @@ testAddUserWithBundle = do (qcnv `elem` map cnvQualifiedId convs) returnedGS <- getGroupInfo alice (fmap Conv qcnv) - liftIO $ assertBool "Commit does not contain a public group State" (isJust (mpPublicGroupState commit)) - liftIO $ mpPublicGroupState commit @?= Just returnedGS - -testAddUserWithBundleIncompleteWelcome :: TestM () -testAddUserWithBundleIncompleteWelcome = do - [alice, bob] <- createAndConnectUsers [Nothing, Nothing] - - runMLSTest $ do - (alice1 : bobClients) <- traverse createMLSClient [alice, bob, bob] - traverse_ uploadNewKeyPackage bobClients - void $ setupMLSGroup alice1 - - -- create commit, but remove first recipient from welcome message - commit <- do - commit <- createAddCommit alice1 [bob] - liftIO $ do - welcome <- assertJust (mpWelcome commit) - w <- either (assertFailure . T.unpack) pure $ decodeMLS' welcome - let w' = w {welSecrets = take 1 (welSecrets w)} - welcome' = encodeMLS' w' - commit' = commit {mpWelcome = Just welcome'} - pure commit' - - bundle <- createBundle commit - err <- - responseJsonError - =<< localPostCommitBundle (mpSender commit) bundle - >= sendAndConsumeCommit + events <- createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle event <- assertOne events liftIO $ assertJoinEvent qcnv alice [bob] roleNameWireMember event pure qcnv @@ -486,10 +412,11 @@ testAddUserNotConnected = do void $ setupMLSGroup alice1 -- add unconnected user with a commit commit <- createAddCommit alice1 [bob] + bundle <- createBundle commit err <- mlsBracket [alice1, bob1] $ \wss -> do err <- responseJsonError - =<< postMessage (mpSender commit) (mpMessage commit) + =<< localPostCommitBundle (mpSender commit) bundle >= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle testAddUserPartial :: TestM () testAddUserPartial = do @@ -534,9 +461,10 @@ testAddUserPartial = do void $ uploadNewKeyPackage bob3 -- alice sends a commit now, and should get a conflict error + bundle <- createBundle commit err <- responseJsonError - =<< postMessage (mpSender commit) (mpMessage commit) + =<< localPostCommitBundle (mpSender commit) bundle >= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle -- now bob2 and bob3 upload key packages, and alice adds bob2 only - kp <- uploadNewKeyPackage bob2 >>= keyPackageFile bob2 + kp <- uploadNewKeyPackage bob2 void $ uploadNewKeyPackage bob3 void $ - createAddCommitWithKeyPackages alice1 [(bob2, kp)] - >>= sendAndConsumeCommit + createAddCommitWithKeyPackages alice1 [(bob2, kp.raw)] + >>= sendAndConsumeCommitBundle testSendAnotherUsersCommit :: TestM () testSendAnotherUsersCommit = do @@ -573,7 +501,7 @@ testSendAnotherUsersCommit = do -- create group with alice1 and bob1 void $ setupMLSGroup alice1 - createAddCommit alice1 [bob] >>= void . sendAndConsumeCommit + createAddCommit alice1 [bob] >>= void . sendAndConsumeCommitBundle -- Alice creates a commit that adds bob2 bob2 <- createMLSClient bob @@ -583,7 +511,7 @@ testSendAnotherUsersCommit = do -- and the corresponding commit is sent from Bob instead of Alice err <- responseJsonError - =<< postMessage bob1 (mpMessage mp) + =<< (localPostCommitBundle bob1 =<< createBundle mp) setupMLSGroup alice1 - createAddCommit alice1 [bob] >>= void . sendAndConsumeCommit + createAddCommit alice1 [bob] >>= void . sendAndConsumeCommitBundle e <- responseJsonError =<< postMembers @@ -626,7 +555,7 @@ testRemoveUsersDirectly = do [alice1, bob1] <- traverse createMLSClient [alice, bob] void $ uploadNewKeyPackage bob1 qcnv <- snd <$> setupMLSGroup alice1 - createAddCommit alice1 [bob] >>= void . sendAndConsumeCommit + createAddCommit alice1 [bob] >>= void . sendAndConsumeCommitBundle e <- responseJsonError =<< deleteMemberQualified @@ -643,7 +572,7 @@ testProteusMessage = do [alice1, bob1] <- traverse createMLSClient [alice, bob] void $ uploadNewKeyPackage bob1 qcnv <- snd <$> setupMLSGroup alice1 - createAddCommit alice1 [bob] >>= void . sendAndConsumeCommit + createAddCommit alice1 [bob] >>= void . sendAndConsumeCommitBundle e <- responseJsonError =<< postProteusMessageQualified @@ -669,15 +598,16 @@ testStaleCommit = do gsBackup <- getClientGroupState alice1 -- add the first batch of users to the conversation - void $ createAddCommit alice1 users1 >>= sendAndConsumeCommit + void $ createAddCommit alice1 users1 >>= sendAndConsumeCommitBundle -- now roll back alice1 and try to add the second batch of users setClientGroupState alice1 gsBackup commit <- createAddCommit alice1 users2 + bundle <- createBundle commit err <- responseJsonError - =<< postMessage (mpSender commit) (mpMessage commit) + =<< localPostCommitBundle (mpSender commit) bundle welcomeMock) $ - sendAndConsumeCommit commit + sendAndConsumeCommitBundle commit pure (events, reqs, qcnv) liftIO $ do @@ -725,10 +655,10 @@ testCommitLock = do traverse_ uploadNewKeyPackage [bob1, charlie1, dee1] -- alice adds add bob - void $ createAddCommit alice1 [cidQualifiedUser bob1] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [cidQualifiedUser bob1] >>= sendAndConsumeCommitBundle -- alice adds charlie - void $ createAddCommit alice1 [cidQualifiedUser charlie1] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [cidQualifiedUser charlie1] >>= sendAndConsumeCommitBundle -- simulate concurrent commit by blocking epoch casClient <- view tsCass @@ -737,9 +667,10 @@ testCommitLock = do -- commit should fail due to competing lock do commit <- createAddCommit alice1 [cidQualifiedUser dee1] + bundle <- createBundle commit err <- responseJsonError - =<< postMessage alice1 (mpMessage commit) + =<< localPostCommitBundle alice1 bundle >= traverse_ sendAndConsumeMessage commit <- createPendingProposalCommit alice1 void $ assertJust (mpWelcome commit) - void $ sendAndConsumeCommit commit + void $ sendAndConsumeCommitBundle commit -- check that bob can now see the conversation liftTest $ do @@ -790,9 +721,10 @@ testUnknownProposalRefCommit = do commit <- createPendingProposalCommit alice1 -- send commit before proposal + bundle <- createBundle commit err <- responseJsonError - =<< postMessage alice1 (mpMessage commit) + =<< localPostCommitBundle alice1 bundle >= sendAndConsumeCommit - events <- createRemoveCommit alice1 [bob1, bob2] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + events <- createRemoveCommit alice1 [bob1, bob2] >>= sendAndConsumeCommitBundle pure (qcnv, events) liftIO $ assertOne events >>= assertLeaveEvent qcnv alice [bob] @@ -855,12 +788,13 @@ testRemoveClientsIncomplete = do [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] void $ setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle commit <- createRemoveCommit alice1 [bob1] + bundle <- createBundle commit err <- responseJsonError - =<< postMessage alice1 (mpMessage commit) + =<< localPostCommitBundle alice1 bundle messageSentMock <|> welcomeMock ((message, events), reqs) <- withTempMockFederator' mock $ do - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle message <- createApplicationMessage alice1 "hello" (events, _) <- sendAndConsumeMessage message pure (message, events) @@ -998,7 +932,8 @@ testLocalToRemoteNonMember = do . paths ["mls", "messages"] . zUser (qUnqualified bob) . zConn "conn" - . content "message/mls" + . zClient (ciClient bob1) + . Bilge.content "message/mls" . bytes (mpMessage message) ) !!! do @@ -1078,7 +1013,7 @@ testExternalCommitNewClientResendBackendProposal = do forM_ [bob1, bob2] uploadNewKeyPackage (_, qcnv) <- setupMLSGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - Just (_, kpBob2) <- find (\(ci, _) -> ci == bob2) <$> getClientsFromGroupState alice1 bob + Just (_, bobIdx2) <- find (\(ci, _) -> ci == bob2) <$> getClientsFromGroupState alice1 bob mlsBracket [alice1, bob1] $ \[wsA, wsB] -> do liftTest $ @@ -1093,7 +1028,7 @@ testExternalCommitNewClientResendBackendProposal = do } WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ - wsAssertBackendRemoveProposalWithEpoch bob qcnv kpBob2 (Epoch 1) + wsAssertBackendRemoveProposalWithEpoch bob qcnv bobIdx2 (Epoch 1) [bob3, bob4] <- for [bob, bob] $ \qusr' -> do ci <- createMLSClient qusr' @@ -1104,6 +1039,7 @@ testExternalCommitNewClientResendBackendProposal = do void $ createExternalAddProposal bob3 >>= sendAndConsumeMessage + WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ void . wsAssertAddProposal bob qcnv @@ -1111,6 +1047,7 @@ testExternalCommitNewClientResendBackendProposal = do ecEvents <- sendAndConsumeCommitBundle mp liftIO $ assertBool "No events after external commit expected" (null ecEvents) + WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ wsAssertMLSMessage (fmap Conv qcnv) bob (mpMessage mp) @@ -1118,7 +1055,7 @@ testExternalCommitNewClientResendBackendProposal = do -- proposal for bob3 has to replayed by the client and is thus not found -- here. WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ - wsAssertBackendRemoveProposalWithEpoch bob qcnv kpBob2 (Epoch 2) + wsAssertBackendRemoveProposalWithEpoch bob qcnv bobIdx2 (Epoch 2) WS.assertNoEvent (2 # WS.Second) [wsA, wsB] testAppMessage :: TestM () @@ -1129,7 +1066,7 @@ testAppMessage = do clients@(alice1 : _) <- traverse createMLSClient users traverse_ uploadNewKeyPackage (tail clients) (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 (tail users) >>= sendAndConsumeCommit + void $ createAddCommit alice1 (tail users) >>= sendAndConsumeCommitBundle message <- createApplicationMessage alice1 "some text" mlsBracket clients $ \wss -> do @@ -1154,7 +1091,7 @@ testAppMessage2 = do -- create group with alice1 and other clients conversation <- snd <$> setupMLSGroup alice1 mp <- createAddCommit alice1 [bob, charlie] - void $ sendAndConsumeCommit mp + void $ sendAndConsumeCommitBundle mp traverse_ consumeWelcome (mpWelcome mp) @@ -1191,7 +1128,7 @@ testAppMessageSomeReachable = do <|> welcomeMock ([event], _) <- withTempMockFederator' mocks $ do - sendAndConsumeCommit commit + sendAndConsumeCommitBundle commit let unreachables = Set.singleton (Domain "charlie.example.com") withTempMockFederator' (mockUnreachableFor unreachables) $ do @@ -1222,7 +1159,7 @@ testAppMessageUnreachable = do commit <- createAddCommit alice1 [bob] ([event], _) <- withTempMockFederator' (receiveCommitMock [bob1] <|> welcomeMock) $ - sendAndConsumeCommit commit + sendAndConsumeCommitBundle commit message <- createApplicationMessage alice1 "hi, bob!" (_, us) <- sendAndConsumeMessage message @@ -1310,7 +1247,7 @@ testRemoteToLocal = do let mock = receiveCommitMock [bob1] <|> welcomeMock <|> claimKeyPackagesMock kpb void . withTempMockFederator' mock $ - sendAndConsumeCommit mp + sendAndConsumeCommitBundle mp traverse_ consumeWelcome (mpWelcome mp) message <- createApplicationMessage bob1 "hello from another backend" @@ -1355,7 +1292,7 @@ testRemoteToLocalWrongConversation = do mp <- createAddCommit alice1 [bob] let mock = receiveCommitMock [bob1] <|> welcomeMock - void . withTempMockFederator' mock $ sendAndConsumeCommit mp + void . withTempMockFederator' mock $ sendAndConsumeCommitBundle mp traverse_ consumeWelcome (mpWelcome mp) message <- createApplicationMessage bob1 "hello from another backend" @@ -1445,7 +1382,7 @@ propInvalidEpoch = do -- Add bob -> epoch 1 void $ uploadNewKeyPackage bob1 gsBackup <- getClientGroupState alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle gsBackup2 <- getClientGroupState alice1 -- try to send a proposal from an old epoch (0) @@ -1463,14 +1400,14 @@ propInvalidEpoch = do do void $ uploadNewKeyPackage dee1 void $ uploadNewKeyPackage charlie1 - setClientGroupState alice1 gsBackup - void $ createAddCommit alice1 [charlie] + setClientGroupState alice1 gsBackup2 + void $ createAddCommit alice1 [charlie] -- --> epoch 2 [prop] <- createAddProposals alice1 [dee] err <- responseJsonError =<< postMessage alice1 (mpMessage prop) - mls {mlsNewMembers = mempty} @@ -1478,7 +1415,7 @@ propInvalidEpoch = do void $ uploadNewKeyPackage dee1 setClientGroupState alice1 gsBackup2 createAddProposals alice1 [dee] >>= traverse_ sendAndConsumeMessage - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommit + void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle -- scenario: -- alice1 creates a group and adds bob1 @@ -1505,7 +1442,7 @@ testExternalAddProposal = do (_, qcnv) <- setupMLSGroup alice1 void $ createAddCommit alice1 [bob] - >>= sendAndConsumeCommit + >>= sendAndConsumeCommitBundle -- bob joins with an external proposal bob2 <- createMLSClient bob @@ -1519,7 +1456,7 @@ testExternalAddProposal = do void $ createPendingProposalCommit alice1 - >>= sendAndConsumeCommit + >>= sendAndConsumeCommitBundle -- alice sends a message do @@ -1538,7 +1475,7 @@ testExternalAddProposal = do qcnv !!! const 200 === statusCode createAddCommit bob2 [charlie] - >>= sendAndConsumeCommit + >>= sendAndConsumeCommitBundle testExternalAddProposalNonAdminCommit :: TestM () testExternalAddProposalNonAdminCommit = do @@ -1560,7 +1497,7 @@ testExternalAddProposalNonAdminCommit = do (_, qcnv) <- setupMLSGroup alice1 void $ createAddCommit alice1 [bob] - >>= sendAndConsumeCommit + >>= sendAndConsumeCommitBundle -- bob joins with an external proposal mlsBracket [alice1, bob1] $ \wss -> do @@ -1574,7 +1511,7 @@ testExternalAddProposalNonAdminCommit = do -- bob1 commits void $ createPendingProposalCommit bob1 - >>= sendAndConsumeCommit + >>= sendAndConsumeCommitBundle -- scenario: -- alice adds bob and charlie @@ -1596,7 +1533,7 @@ testExternalAddProposalWrongClient = do void $ setupMLSGroup alice1 void $ createAddCommit alice1 [bob, charlie] - >>= sendAndConsumeCommit + >>= sendAndConsumeCommitBundle prop <- createExternalAddProposal bob2 postMessage charlie1 (mpMessage prop) @@ -1619,7 +1556,7 @@ testExternalAddProposalWrongUser = do void $ setupMLSGroup alice1 void $ createAddCommit alice1 [bob] - >>= sendAndConsumeCommit + >>= sendAndConsumeCommitBundle prop <- createExternalAddProposal charlie1 postMessage charlie1 (mpMessage prop) @@ -1650,10 +1587,9 @@ testPublicKeys = do ) @?= [Ed25519] --- | The test manually reads from mls-test-cli's store and extracts a private --- key. The key is needed for signing an AppAck proposal, which as of August 24, --- 2022 only gets forwarded by the backend, i.e., there's no action taken by the --- backend. +--- | The test manually reads from mls-test-cli's store and extracts a private +--- key. The key is needed for signing an unsupported proposal, which is then +-- forwarded by the backend without being inspected. propUnsupported :: TestM () propUnsupported = do users@[_alice, bob] <- createAndConnectUsers (replicate 2 Nothing) @@ -1661,26 +1597,26 @@ propUnsupported = do [alice1, bob1] <- traverse createMLSClient users void $ uploadNewKeyPackage bob1 (gid, _) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit - - mems <- readGroupState <$> getClientGroupState alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - (_, ref) <- assertJust $ find ((== alice1) . fst) mems (priv, pub) <- clientKeyPair alice1 - msg <- - assertJust $ - maybeCryptoError $ - mkAppAckProposalMessage - gid - (Epoch 1) - ref - [] - <$> Ed25519.secretKey priv - <*> Ed25519.publicKey pub - let msgData = LBS.toStrict (runPut (serialiseMLS msg)) - - -- we cannot use sendAndConsumeMessage here, because openmls does not yet - -- support AppAck proposals + pmsg <- + liftIO . throwCryptoErrorIO $ + mkSignedPublicMessage + <$> Ed25519.secretKey priv + <*> Ed25519.publicKey pub + <*> pure gid + <*> pure (Epoch 1) + <*> pure (TaggedSenderMember 0 "foo") + <*> pure + ( FramedContentProposal + (mkRawMLS (GroupContextExtensionsProposal [])) + ) + + let msg = mkMessage (MessagePublic pmsg) + let msgData = encodeMLS' msg + + -- we cannot consume this message, because the membership tag is fake postMessage alice1 msgData !!! const 201 === statusCode testBackendRemoveProposalRecreateClient :: TestM () @@ -1694,7 +1630,7 @@ testBackendRemoveProposalRecreateClient = do void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - (_, ref) <- assertOne =<< getClientsFromGroupState alice1 alice + (_, idx) <- assertOne =<< getClientsFromGroupState alice1 alice liftTest $ deleteClient (qUnqualified alice) (ciClient alice1) (Just defPassword) @@ -1706,11 +1642,13 @@ testBackendRemoveProposalRecreateClient = do alice2 <- createMLSClient alice proposal <- mlsBracket [alice2] $ \[wsA] -> do + -- alice2 joins the conversation, causing the external remove proposal to + -- be re-established void $ createExternalCommit alice2 Nothing cnv >>= sendAndConsumeCommitBundle WS.assertMatch (5 # WS.Second) wsA $ - wsAssertBackendRemoveProposal alice (Conv <$> qcnv) ref + wsAssertBackendRemoveProposal alice (Conv <$> qcnv) idx consumeMessage1 alice2 proposal void $ createPendingProposalCommit alice2 >>= sendAndConsumeCommitBundle @@ -1724,7 +1662,7 @@ testBackendRemoveProposalLocalConvLocalUser = do [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle bobClients <- getClientsFromGroupState alice1 bob mlsBracket [alice1] $ \wss -> void $ do @@ -1735,13 +1673,13 @@ testBackendRemoveProposalLocalConvLocalUser = do { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [bob1, bob2]) } - for bobClients $ \(_, ref) -> do + for bobClients $ \(_, idx) -> do [msg] <- WS.assertMatchN (5 # Second) wss $ \n -> - wsAssertBackendRemoveProposal bob (Conv <$> qcnv) ref n + wsAssertBackendRemoveProposal bob (Conv <$> qcnv) idx n consumeMessage1 alice1 msg -- alice commits the external proposals - events <- createPendingProposalCommit alice1 >>= sendAndConsumeCommit + events <- createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle liftIO $ events @?= [] testBackendRemoveProposalLocalConvRemoteUser :: TestM () @@ -1755,7 +1693,7 @@ testBackendRemoveProposalLocalConvRemoteUser = do let mock = receiveCommitMock [bob1, bob2] <|> welcomeMock <|> messageSentMock void . withTempMockFederator' mock $ do mlsBracket [alice1] $ \[wsA] -> do - void $ sendAndConsumeCommit commit + void $ sendAndConsumeCommitBundle commit bobClients <- getClientsFromGroupState alice1 bob fedGalleyClient <- view tsFedGalleyClient @@ -1770,19 +1708,20 @@ testBackendRemoveProposalLocalConvRemoteUser = do } ) - for_ bobClients $ \(_, ref) -> + for_ bobClients $ \(_, idx) -> WS.assertMatch (5 # WS.Second) wsA $ - wsAssertBackendRemoveProposal bob (Conv <$> qcnv) ref + wsAssertBackendRemoveProposal bob (Conv <$> qcnv) idx sendRemoteMLSWelcome :: TestM () sendRemoteMLSWelcome = do -- Alice is from the originating domain and Bob is local, i.e., on the receiving domain [alice, bob] <- createAndConnectUsers [Just "alice.example.com", Nothing] - commit <- runMLSTest $ do + (commit, bob1) <- runMLSTest $ do [alice1, bob1] <- traverse createMLSClient [alice, bob] void $ setupFakeMLSGroup alice1 void $ uploadNewKeyPackage bob1 - createAddCommit alice1 [bob] + commit <- createAddCommit alice1 [bob] + pure (commit, bob1) welcome <- assertJust (mpWelcome commit) @@ -1795,35 +1734,13 @@ sendRemoteMLSWelcome = do runFedClient @"mls-welcome" fedGalleyClient (qDomain alice) $ MLSWelcomeRequest (Base64ByteString welcome) + [qUnqualified (cidQualifiedClient bob1)] -- check that the corresponding event is received liftIO $ do WS.assertMatch_ (5 # WS.Second) wsB $ wsAssertMLSWelcome bob welcome -sendRemoteMLSWelcomeKPNotFound :: TestM () -sendRemoteMLSWelcomeKPNotFound = do - [alice, bob] <- createAndConnectUsers [Just "alice.example.com", Nothing] - commit <- runMLSTest $ do - [alice1, bob1] <- traverse createMLSClient [alice, bob] - void $ setupFakeMLSGroup alice1 - kp <- generateKeyPackage bob1 >>= keyPackageFile bob1 . snd - createAddCommitWithKeyPackages alice1 [(bob1, kp)] - welcome <- assertJust (mpWelcome commit) - - fedGalleyClient <- view tsFedGalleyClient - cannon <- view tsCannon - WS.bracketR cannon (qUnqualified bob) $ \wsB -> do - -- send welcome message - void $ - runFedClient @"mls-welcome" fedGalleyClient (qDomain alice) $ - MLSWelcomeRequest - (Base64ByteString welcome) - - liftIO $ do - -- check that no event is received - WS.assertNoEvent (1 # Second) [wsB] - testBackendRemoveProposalLocalConvLocalLeaverCreator :: TestM () testBackendRemoveProposalLocalConvLocalLeaverCreator = do [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) @@ -1832,7 +1749,7 @@ testBackendRemoveProposalLocalConvLocalLeaverCreator = do [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle aliceClients <- getClientsFromGroupState alice1 alice mlsBracket [alice1, bob1, bob2] $ \wss -> void $ do @@ -1845,10 +1762,10 @@ testBackendRemoveProposalLocalConvLocalLeaverCreator = do { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [alice1]) } - for_ aliceClients $ \(_, ref) -> do + for_ aliceClients $ \(_, idx) -> do -- only bob's clients should receive the external proposals msgs <- WS.assertMatchN (5 # Second) (drop 1 wss) $ \n -> - wsAssertBackendRemoveProposal alice (Conv <$> qcnv) ref n + wsAssertBackendRemoveProposal alice (Conv <$> qcnv) idx n traverse_ (uncurry consumeMessage1) (zip [bob1, bob2] msgs) -- but everyone should receive leave events @@ -1860,7 +1777,7 @@ testBackendRemoveProposalLocalConvLocalLeaverCreator = do WS.assertNoEvent (1 # WS.Second) wss -- bob commits the external proposals - events <- createPendingProposalCommit bob1 >>= sendAndConsumeCommit + events <- createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle liftIO $ events @?= [] testBackendRemoveProposalLocalConvLocalLeaverCommitter :: TestM () @@ -1871,13 +1788,13 @@ testBackendRemoveProposalLocalConvLocalLeaverCommitter = do [alice1, bob1, bob2, charlie1] <- traverse createMLSClient [alice, bob, bob, charlie] traverse_ uploadNewKeyPackage [bob1, bob2, charlie1] (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle -- promote bob putOtherMemberQualified (ciUser alice1) bob (OtherMemberUpdate (Just roleNameWireAdmin)) qcnv !!! const 200 === statusCode - void $ createAddCommit bob1 [charlie] >>= sendAndConsumeCommit + void $ createAddCommit bob1 [charlie] >>= sendAndConsumeCommitBundle bobClients <- getClientsFromGroupState alice1 bob mlsBracket [alice1, charlie1, bob1, bob2] $ \wss -> void $ do @@ -1890,10 +1807,10 @@ testBackendRemoveProposalLocalConvLocalLeaverCommitter = do { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [bob1, bob2]) } - for_ bobClients $ \(_, ref) -> do + for_ bobClients $ \(_, idx) -> do -- only alice and charlie should receive the external proposals msgs <- WS.assertMatchN (5 # Second) (take 2 wss) $ \n -> - wsAssertBackendRemoveProposal bob (Conv <$> qcnv) ref n + wsAssertBackendRemoveProposal bob (Conv <$> qcnv) idx n traverse_ (uncurry consumeMessage1) (zip [alice1, charlie1] msgs) -- but everyone should receive leave events @@ -1905,7 +1822,7 @@ testBackendRemoveProposalLocalConvLocalLeaverCommitter = do WS.assertNoEvent (1 # WS.Second) wss -- alice commits the external proposals - events <- createPendingProposalCommit alice1 >>= sendAndConsumeCommit + events <- createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle liftIO $ events @?= [] testBackendRemoveProposalLocalConvRemoteLeaver :: TestM () @@ -1921,7 +1838,7 @@ testBackendRemoveProposalLocalConvRemoteLeaver = do bobClients <- getClientsFromGroupState alice1 bob void . withTempMockFederator' mock $ do mlsBracket [alice1] $ \[wsA] -> void $ do - void $ sendAndConsumeCommit commit + void $ sendAndConsumeCommitBundle commit fedGalleyClient <- view tsFedGalleyClient void $ runFedClient @@ -1934,9 +1851,9 @@ testBackendRemoveProposalLocalConvRemoteLeaver = do curAction = SomeConversationAction SConversationLeaveTag () } - for_ bobClients $ \(_, ref) -> + for_ bobClients $ \(_, idx) -> WS.assertMatch_ (5 # WS.Second) wsA $ - wsAssertBackendRemoveProposal bob (Conv <$> qcnv) ref + wsAssertBackendRemoveProposal bob (Conv <$> qcnv) idx testBackendRemoveProposalLocalConvLocalClient :: TestM () testBackendRemoveProposalLocalConvLocalClient = do @@ -1946,10 +1863,10 @@ testBackendRemoveProposalLocalConvLocalClient = do [alice1, bob1, bob2, charlie1] <- traverse createMLSClient [alice, bob, bob, charlie] traverse_ uploadNewKeyPackage [bob1, bob2, charlie1] (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommit - Just (_, kpBob1) <- find (\(ci, _) -> ci == bob1) <$> getClientsFromGroupState alice1 bob + void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle + Just (_, idxBob1) <- find (\(ci, _) -> ci == bob1) <$> getClientsFromGroupState alice1 bob - mlsBracket [alice1, bob1] $ \[wsA, wsB] -> do + mlsBracket [alice1, bob1, charlie1] $ \[wsA, wsB, wsC] -> do liftTest $ deleteClient (ciUser bob1) (ciClient bob1) (Just defPassword) !!! statusCode === const 200 @@ -1963,15 +1880,15 @@ testBackendRemoveProposalLocalConvLocalClient = do wsAssertClientRemoved (ciClient bob1) msg <- WS.assertMatch (5 # WS.Second) wsA $ \notification -> do - wsAssertBackendRemoveProposal bob (Conv <$> qcnv) kpBob1 notification + wsAssertBackendRemoveProposal bob (Conv <$> qcnv) idxBob1 notification for_ [alice1, bob2, charlie1] $ flip consumeMessage1 msg mp <- createPendingProposalCommit charlie1 - events <- sendAndConsumeCommit mp + events <- sendAndConsumeCommitBundle mp liftIO $ events @?= [] - WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ \n -> do + WS.assertMatchN_ (5 # WS.Second) [wsA, wsC] $ \n -> do wsAssertMLSMessage (Conv <$> qcnv) charlie (mpMessage mp) n testBackendRemoveProposalLocalConvRemoteClient :: TestM () @@ -1983,11 +1900,11 @@ testBackendRemoveProposalLocalConvRemoteClient = do (_, qcnv) <- setupMLSGroup alice1 commit <- createAddCommit alice1 [bob] - [(_, bob1KP)] <- getClientsFromGroupState alice1 bob + [(_, idxBob1)] <- getClientsFromGroupState alice1 bob let mock = receiveCommitMock [bob1] <|> welcomeMock <|> messageSentMock void . withTempMockFederator' mock $ do mlsBracket [alice1] $ \[wsA] -> void $ do - void $ sendAndConsumeCommit commit + void $ sendAndConsumeCommitBundle commit fedGalleyClient <- view tsFedGalleyClient void $ @@ -1999,7 +1916,7 @@ testBackendRemoveProposalLocalConvRemoteClient = do WS.assertMatch_ (5 # WS.Second) wsA $ \notification -> - void $ wsAssertBackendRemoveProposal bob (Conv <$> qcnv) bob1KP notification + void $ wsAssertBackendRemoveProposal bob (Conv <$> qcnv) idxBob1 notification testGetGroupInfoOfLocalConv :: TestM () testGetGroupInfoOfLocalConv = do @@ -2014,7 +1931,7 @@ testGetGroupInfoOfLocalConv = do void $ sendAndConsumeCommitBundle commit -- check the group info matches - gs <- assertJust (mpPublicGroupState commit) + gs <- assertJust (mpGroupInfo commit) returnedGS <- liftTest $ getGroupInfo alice (fmap Conv qcnv) liftIO $ gs @=? returnedGS @@ -2057,7 +1974,7 @@ testFederatedGetGroupInfo = do [alice1, bob1] <- traverse createMLSClient [alice, bob] (_, qcnv) <- setupMLSGroup alice1 commit <- createAddCommit alice1 [bob] - groupState <- assertJust (mpPublicGroupState commit) + groupState <- assertJust (mpGroupInfo commit) let mock = receiveCommitMock [bob1] <|> welcomeMock void . withTempMockFederator' mock $ do @@ -2165,7 +2082,7 @@ testRemoteUserPostsCommitBundle = do void $ do let mock = receiveCommitMock [bob1] <|> welcomeMock withTempMockFederator' mock $ do - void $ sendAndConsumeCommit commit + void $ sendAndConsumeCommitBundle commit putOtherMemberQualified (qUnqualified alice) bob (OtherMemberUpdate (Just roleNameWireAdmin)) qcnv !!! const 200 === statusCode @@ -2253,8 +2170,9 @@ testSelfConversationOtherUser = do void $ uploadNewKeyPackage bob1 void $ setupMLSSelfGroup alice1 commit <- createAddCommit alice1 [bob] + bundle <- createBundle commit mlsBracket [alice1, bob1] $ \wss -> do - postMessage (mpSender commit) (mpMessage commit) + localPostCommitBundle (mpSender commit) bundle !!! do const 403 === statusCode const (Just "invalid-op") === fmap Wai.label . responseJsonError @@ -2267,7 +2185,7 @@ testSelfConversationLeave = do clients@(creator : others) <- traverse createMLSClient (replicate 3 alice) traverse_ uploadNewKeyPackage others (_, qcnv) <- setupMLSSelfGroup creator - void $ createAddCommit creator [alice] >>= sendAndConsumeCommit + void $ createAddCommit creator [alice] >>= sendAndConsumeCommitBundle mlsBracket clients $ \wss -> do liftTest $ deleteMemberQualified (qUnqualified alice) alice qcnv @@ -2299,8 +2217,9 @@ postMLSMessageDisabled = do void $ uploadNewKeyPackage bob1 void $ setupMLSGroup alice1 mp <- createAddCommit alice1 [bob] + bundle <- createBundle mp withMLSDisabled $ - postMessage (mpSender mp) (mpMessage mp) + localPostCommitBundle (mpSender mp) bundle !!! assertMLSNotEnabled postMLSBundleDisabled :: TestM () @@ -2323,7 +2242,7 @@ getGroupInfoDisabled = do [alice1, bob1] <- traverse createMLSClient [alice, bob] void $ uploadNewKeyPackage bob1 (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle withMLSDisabled $ localGetGroupInfo (qUnqualified alice) (fmap Conv qcnv) @@ -2384,7 +2303,7 @@ testJoinSubConv = do [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle let subId = SubConvId "conference" sub <- @@ -2395,7 +2314,6 @@ testJoinSubConv = do resetGroup bob1 (fmap (flip SubConv subId) qcnv) (pscGroupId sub) - bobRefsBefore <- getClientsFromGroupState bob1 bob -- bob adds his first client to the subconversation void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle @@ -2404,11 +2322,7 @@ testJoinSubConv = do responseJsonError =<< getSubConv (qUnqualified bob) qcnv subId >= sendAndConsumeCommitBundle - Just (_, kpBob1) <- find (\(ci, _) -> ci == bob1) <$> getClientsFromGroupState alice1 bob + Just (_, idxBob1) <- find (\(ci, _) -> ci == bob1) <$> getClientsFromGroupState alice1 bob -- bob1 leaves and immediately rejoins mlsBracket [alice1, bob1] $ \[wsA, wsB] -> do void $ leaveCurrentConv bob1 qsub WS.assertMatchN_ (5 # WS.Second) [wsA] $ - wsAssertBackendRemoveProposal bob qsub kpBob1 + wsAssertBackendRemoveProposal bob qsub idxBob1 void $ createExternalCommit bob1 Nothing qsub >>= sendAndConsumeCommitBundle @@ -2457,7 +2371,7 @@ testJoinSubNonMemberClient = do traverse createMLSClient [alice, alice, bob] traverse_ uploadNewKeyPackage [bob1, alice2] (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [alice] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [alice] >>= sendAndConsumeCommitBundle qcs <- createSubConv qcnv alice1 (SubConvId "conference") @@ -2474,7 +2388,7 @@ testAddClientSubConvFailure = do [alice1, bob1] <- traverse createMLSClient [alice, bob] void $ uploadNewKeyPackage bob1 (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle let subId = SubConvId "conference" void $ createSubConv qcnv alice1 subId @@ -2534,7 +2448,7 @@ testJoinRemoteSubConv = do receiveNewRemoteConv qcs subGroupId -- bob joins subconversation - let pgs = mpPublicGroupState initialCommit + let pgs = mpGroupInfo initialCommit let mock = ("send-mls-commit-bundle" ~> MLSMessageResponseUpdates [] (UnreachableUsers [])) <|> queryGroupStateMock (fold pgs) bob @@ -2597,7 +2511,7 @@ testRemoteUserJoinSubConv = do void $ do commit <- createAddCommit alice1 [bob] withTempMockFederator' (receiveCommitMock [bob1] <|> welcomeMock) $ - sendAndConsumeCommit commit + sendAndConsumeCommitBundle commit let mock = asum @@ -2650,7 +2564,7 @@ testSendMessageSubConv = do [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle qcs <- createSubConv qcnv bob1 (SubConvId "conference") @@ -2716,7 +2630,7 @@ testRemoteMemberGetSubConv isAMember = do let mock = receiveCommitMock [bob1] <|> welcomeMock <|> claimKeyPackagesMock kpb void . withTempMockFederator' mock $ - sendAndConsumeCommit mp + sendAndConsumeCommitBundle mp let subconv = SubConvId "conference" @@ -2765,7 +2679,7 @@ testRemoteMemberDeleteSubConv isAMember = do mp <- createAddCommit alice1 [bob] let mock = receiveCommitMock [bob1] <|> welcomeMock - void . withTempMockFederator' mock . sendAndConsumeCommit $ mp + void . withTempMockFederator' mock . sendAndConsumeCommitBundle $ mp sub <- liftTest $ @@ -2953,7 +2867,7 @@ testDeleteParentOfSubConv = do (parentGroupId, qcnv) <- setupMLSGroup alice1 (qcs, _) <- withTempMockFederator' (receiveCommitMock [bob1]) $ do - void $ createAddCommit alice1 [arthur, bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [arthur, bob] >>= sendAndConsumeCommitBundle createSubConv qcnv alice1 sconv subGid <- getCurrentGroupId @@ -3014,7 +2928,7 @@ testDeleteRemoteParentOfSubConv = do -- inform backend about the subconversation receiveNewRemoteConv qcs subGroupId - let pgs = mpPublicGroupState initialCommit + let pgs = mpGroupInfo initialCommit let mock = ("send-mls-commit-bundle" ~> MLSMessageResponseUpdates [] (UnreachableUsers [])) <|> queryGroupStateMock (fold pgs) bob @@ -3134,7 +3048,7 @@ testLeaveSubConv isSubConvCreator = do <|> ("on-mls-message-sent" ~> RemoteMLSMessageOk) ) $ do - void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle qsub <- createSubConv qcnv bob1 subId void $ createExternalCommit alice1 Nothing qsub >>= sendAndConsumeCommitBundle @@ -3144,7 +3058,7 @@ testLeaveSubConv isSubConvCreator = do let firstLeaver = if isSubConvCreator then bob1 else alice1 -- a member leaves the subconversation - [firstLeaverKP] <- + [idxFirstLeaver] <- map snd . filter (\(cid, _) -> cid == firstLeaver) <$> getClientsFromGroupState alice1 @@ -3169,7 +3083,7 @@ testLeaveSubConv isSubConvCreator = do wsAssertBackendRemoveProposal (cidQualifiedUser firstLeaver) (Conv <$> qcnv) - firstLeaverKP + idxFirstLeaver traverse_ (uncurry consumeMessage1) (zip others msgs) -- assert the leaver gets no proposal or event void . liftIO $ WS.assertNoEvent (5 # WS.Second) [wsLeaver] @@ -3178,7 +3092,7 @@ testLeaveSubConv isSubConvCreator = do do leaveCommit <- createPendingProposalCommit (head others) mlsBracket (firstLeaver : others) $ \(wsLeaver : wss) -> do - events <- sendAndConsumeCommit leaveCommit + events <- sendAndConsumeCommitBundle leaveCommit liftIO $ events @?= [] WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do wsAssertMLSMessage qsub (cidQualifiedUser . head $ others) (mpMessage leaveCommit) n @@ -3205,7 +3119,7 @@ testLeaveSubConv isSubConvCreator = do liftIO $ length (pscMembers psc) @?= 3 -- charlie1 leaves - [charlie1KP] <- + [idxCharlie1] <- map snd . filter (\(cid, _) -> cid == charlie1) <$> getClientsFromGroupState (head others) charlie mlsBracket others $ \wss -> do @@ -3213,7 +3127,7 @@ testLeaveSubConv isSubConvCreator = do msgs <- WS.assertMatchN (5 # WS.Second) wss $ - wsAssertBackendRemoveProposal charlie (Conv <$> qcnv) charlie1KP + wsAssertBackendRemoveProposal charlie (Conv <$> qcnv) idxCharlie1 traverse_ (uncurry consumeMessage1) (zip others msgs) -- a member commits the pending proposal @@ -3239,7 +3153,7 @@ testLeaveSubConvNonMember = do [alice1, bob1] <- traverse createMLSClient [alice, bob] void $ uploadNewKeyPackage bob1 (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle let subId = SubConvId "conference" _qsub <- createSubConv qcnv bob1 subId @@ -3284,7 +3198,7 @@ testLeaveRemoteSubConv = do -- inform backend about the subconversation receiveNewRemoteConv qcs subGroupId - let pgs = mpPublicGroupState initialCommit + let pgs = mpGroupInfo initialCommit let mock = ("send-mls-commit-bundle" ~> MLSMessageResponseUpdates [] (UnreachableUsers [])) <|> queryGroupStateMock (fold pgs) bob @@ -3305,18 +3219,18 @@ testLeaveRemoteSubConv = do testRemoveUserParent :: TestM () testRemoveUserParent = do [alice, bob, charlie] <- createAndConnectUsers [Nothing, Nothing, Nothing] + let subname = SubConvId "conference" - runMLSTest $ + (qcnv, [alice1, bob1, bob2, _charlie1, _charlie2]) <- runMLSTest $ do - [alice1, bob1, bob2, charlie1, charlie2] <- + clients@[alice1, bob1, bob2, charlie1, charlie2] <- traverse createMLSClient [alice, bob, bob, charlie, charlie] traverse_ uploadNewKeyPackage [bob1, bob2, charlie1, charlie2] (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommit + void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle - let subname = SubConvId "conference" void $ createSubConv qcnv bob1 subname let qcs = fmap (flip SubConv subname) qcnv @@ -3324,61 +3238,40 @@ testRemoveUserParent = do for_ [alice1, bob2, charlie1, charlie2] $ \c -> void $ createExternalCommit c Nothing qcs >>= sendAndConsumeCommitBundle - [(_, kpref1), (_, kpref2)] <- getClientsFromGroupState alice1 charlie - - -- charlie leaves the main conversation - mlsBracket [alice1, bob1, bob2] $ \wss -> do - liftTest $ do - deleteMemberQualified (qUnqualified charlie) charlie qcnv - !!! const 200 === statusCode - - -- Remove charlie from our state as well - State.modify $ \mls -> - mls - { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [charlie1, charlie2]) - } - - msg1 <- WS.assertMatchN (5 # Second) wss $ \n -> - wsAssertBackendRemoveProposal charlie (Conv <$> qcnv) kpref1 n - - traverse_ (uncurry consumeMessage1) (zip [alice1, bob1, bob2] msg1) - - msg2 <- WS.assertMatchN (5 # Second) wss $ \n -> - wsAssertBackendRemoveProposal charlie (Conv <$> qcnv) kpref2 n + pure (qcnv, clients) - traverse_ (uncurry consumeMessage1) (zip [alice1, bob1, bob2] msg2) + -- charlie leaves the main conversation + deleteMemberQualified (qUnqualified charlie) charlie qcnv + !!! const 200 === statusCode - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + getSubConv (qUnqualified charlie) qcnv subname + !!! const 403 === statusCode - liftTest $ do - getSubConv (qUnqualified charlie) qcnv (SubConvId "conference") - !!! const 403 === statusCode - - sub :: PublicSubConversation <- - responseJsonError - =<< getSubConv (qUnqualified bob) qcnv (SubConvId "conference") - >= sendAndConsumeCommit + void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle - let subname = SubConvId "conference" void $ createSubConv qcnv alice1 subname let qcs = fmap (flip SubConv subname) qcnv @@ -3386,54 +3279,36 @@ testRemoveCreatorParent = do for_ [bob1, bob2, charlie1, charlie2] $ \c -> void $ createExternalCommit c Nothing qcs >>= sendAndConsumeCommitBundle - [(_, kpref1)] <- getClientsFromGroupState alice1 alice - - -- creator leaves the main conversation - mlsBracket [bob1, bob2, charlie1, charlie2] $ \wss -> do - liftTest $ do - deleteMemberQualified (qUnqualified alice) alice qcnv - !!! const 200 === statusCode - - -- Remove alice1 from our state as well - State.modify $ \mls -> - mls - { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [alice1]) - } + pure (qcnv, clients) - msg <- WS.assertMatchN (5 # Second) wss $ \n -> - -- Checks proposal for subconv, parent doesn't get one - -- since alice is not notified of her own removal - wsAssertBackendRemoveProposal alice (Conv <$> qcnv) kpref1 n + -- creator leaves the main conversation + deleteMemberQualified (qUnqualified alice) alice qcnv + !!! const 200 === statusCode - traverse_ (uncurry consumeMessage1) (zip [bob1, bob2, charlie1, charlie2] msg) + getSubConv (qUnqualified alice) qcnv subname + !!! const 403 === statusCode - void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle - - liftTest $ do - getSubConv (qUnqualified alice) qcnv subname - !!! const 403 === statusCode - - -- charlie sees updated memberlist - sub :: PublicSubConversation <- - responseJsonError - =<< getSubConv (qUnqualified charlie) qcnv subname - >= sendAndConsumeCommit + void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle stateParent <- State.get @@ -3474,13 +3349,13 @@ testCreatorRemovesUserFromParent = do State.put stateSub -- Get client state for alice and fetch bob client identities - [(_, kprefBob1), (_, kprefBob2)] <- getClientsFromGroupState alice1 bob + [(_, idxBob1), (_, idxBob2)] <- getClientsFromGroupState alice1 bob -- handle bob1 removal msgs <- WS.assertMatchN (5 # Second) wss $ \n -> do -- it was an alice proposal for the parent, -- but it's a backend proposal for the sub - wsAssertBackendRemoveProposal bob qcs kprefBob1 n + wsAssertBackendRemoveProposal bob qcs idxBob1 n traverse_ (uncurry consumeMessage1) (zip [alice1, charlie1, charlie2] msgs) @@ -3488,7 +3363,7 @@ testCreatorRemovesUserFromParent = do msgs2 <- WS.assertMatchN (5 # Second) wss $ \n -> do -- it was an alice proposal for the parent, -- but it's a backend proposal for the sub - wsAssertBackendRemoveProposal bob qcs kprefBob2 n + wsAssertBackendRemoveProposal bob qcs idxBob2 n traverse_ (uncurry consumeMessage1) (zip [alice1, charlie1, charlie2] msgs2) diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index bb59cb8cdb2..542f3e6cd61 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -26,21 +26,22 @@ import Bilge import Bilge.Assert import Control.Arrow ((&&&)) import Control.Error.Util -import Control.Lens (preview, to, view, (.~), (^..)) +import Control.Lens (preview, to, view, (.~), (^..), (^?)) import Control.Monad.Catch +import Control.Monad.Cont import Control.Monad.State (StateT, evalStateT) import qualified Control.Monad.State as State import Control.Monad.Trans.Maybe -import Crypto.PubKey.Ed25519 import Data.Aeson.Lens +import Data.Bifunctor import Data.Binary.Builder (toLazyByteString) +import Data.Binary.Get import qualified Data.ByteArray as BA import qualified Data.ByteString as BS import qualified Data.ByteString.Base64.URL as B64U import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as LBS import Data.Domain -import Data.Hex import Data.Id import Data.Json.Util hiding ((#)) import qualified Data.Map as Map @@ -75,11 +76,10 @@ import Wire.API.Federation.API.Galley import Wire.API.MLS.CipherSuite import Wire.API.MLS.CommitBundle import Wire.API.MLS.Credential -import Wire.API.MLS.GroupInfoBundle import Wire.API.MLS.KeyPackage import Wire.API.MLS.Keys +import Wire.API.MLS.LeafNode import Wire.API.MLS.Message -import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.User.Client @@ -89,24 +89,10 @@ cid2Str :: ClientIdentity -> String cid2Str cid = show (ciUser cid) <> ":" - <> T.unpack (client . ciClient $ cid) + <> T.unpack cid.ciClient.client <> "@" <> T.unpack (domainText (ciDomain cid)) -mapRemoteKeyPackageRef :: - (MonadIO m, MonadHttp m, MonadCatch m) => - (Request -> Request) -> - KeyPackageBundle -> - m () -mapRemoteKeyPackageRef brig bundle = - void $ - put - ( brig - . paths ["i", "mls", "key-package-refs"] - . json bundle - ) - !!! const 204 === statusCode - postMessage :: ( HasCallStack, MonadIO m, @@ -124,7 +110,7 @@ postMessage sender msg = do . zUser (ciUser sender) . zClient (ciClient sender) . zConn "conn" - . content "message/mls" + . Bilge.content "message/mls" . bytes msg ) @@ -145,7 +131,7 @@ localPostCommitBundle sender bundle = do . zUser (ciUser sender) . zClient (ciClient sender) . zConn "conn" - . content "application/x-protobuf" + . Bilge.content "message/mls" . bytes bundle ) @@ -201,49 +187,6 @@ postCommitBundle sender qcs bundle = do (\rsender -> remotePostCommitBundle rsender qcs bundle) (cidQualifiedUser sender $> sender) --- FUTUREWORK: remove this and start using commit bundles everywhere in tests -postWelcome :: - ( MonadIO m, - MonadHttp m, - MonadReader TestSetup m, - HasCallStack - ) => - UserId -> - ByteString -> - m ResponseLBS -postWelcome uid welcome = do - galley <- view tsUnversionedGalley - post - ( galley - . paths ["v2", "mls", "welcome"] - . zUser uid - . zConn "conn" - . content "message/mls" - . bytes welcome - ) - -mkAppAckProposalMessage :: - GroupId -> - Epoch -> - KeyPackageRef -> - [MessageRange] -> - SecretKey -> - PublicKey -> - Message 'MLSPlainText -mkAppAckProposalMessage gid epoch ref mrs priv pub = do - let tbs = - mkRawMLS $ - MessageTBS - { tbsMsgFormat = KnownFormatTag, - tbsMsgGroupId = gid, - tbsMsgEpoch = epoch, - tbsMsgAuthData = mempty, - tbsMsgSender = MemberSender ref, - tbsMsgPayload = ProposalMessage (mkAppAckProposal mrs) - } - sig = BA.convert $ sign priv pub (rmRaw tbs) - in Message tbs (MessageExtraFields sig Nothing Nothing) - saveRemovalKey :: FilePath -> TestM () saveRemovalKey fp = do keys <- fromJust <$> view (tsGConf . optSettings . setMlsPrivateKeyPaths) @@ -316,7 +259,7 @@ data MessagePackage = MessagePackage { mpSender :: ClientIdentity, mpMessage :: ByteString, mpWelcome :: Maybe ByteString, - mpPublicGroupState :: Maybe ByteString + mpGroupInfo :: Maybe ByteString } deriving (Show) @@ -424,7 +367,7 @@ createFakeMLSClient qusr = do pure cid -- | create and upload to backend -uploadNewKeyPackage :: HasCallStack => ClientIdentity -> MLSTest KeyPackageRef +uploadNewKeyPackage :: HasCallStack => ClientIdentity -> MLSTest (RawMLS KeyPackage) uploadNewKeyPackage qcid = do (kp, _) <- generateKeyPackage qcid @@ -437,14 +380,13 @@ uploadNewKeyPackage qcid = do . json (KeyPackageUpload [kp]) ) !!! const 201 === statusCode - pure $ fromJust (kpRef' kp) + pure kp generateKeyPackage :: HasCallStack => ClientIdentity -> MLSTest (RawMLS KeyPackage, KeyPackageRef) generateKeyPackage qcid = do - kp <- liftIO . decodeMLSError =<< mlscli qcid ["key-package", "create"] Nothing + kpData <- mlscli qcid ["key-package", "create"] Nothing + kp <- liftIO $ decodeMLSError kpData let ref = fromJust (kpRef' kp) - fp <- keyPackageFile qcid ref - liftIO $ BS.writeFile fp (rmRaw kp) pure (kp, ref) setClientGroupState :: HasCallStack => ClientIdentity -> ByteString -> MLSTest () @@ -530,7 +472,17 @@ resetGroup cid qcs gid = do resetClientGroup :: ClientIdentity -> GroupId -> MLSTest () resetClientGroup cid gid = do - groupJSON <- mlscli cid ["group", "create", T.unpack (toBase64Text (unGroupId gid))] Nothing + bd <- State.gets mlsBaseDir + groupJSON <- + mlscli + cid + [ "group", + "create", + "--removal-key", + bd "removal.key", + T.unpack (toBase64Text (unGroupId gid)) + ] + Nothing setClientGroupState cid groupJSON getConvId :: MLSTest (Qualified ConvOrSubConvId) @@ -572,13 +524,6 @@ fakeGroupId = liftIO $ fmap (GroupId . BS.pack) (replicateM 32 (generate arbitrary)) -keyPackageFile :: HasCallStack => ClientIdentity -> KeyPackageRef -> MLSTest FilePath -keyPackageFile qcid ref = - State.gets $ \mls -> - mlsBaseDir mls - cid2Str qcid - T.unpack (T.decodeUtf8 (hex (unKeyPackageRef ref))) - claimLocalKeyPackages :: HasCallStack => ClientIdentity -> Local UserId -> MLSTest KeyPackageBundle claimLocalKeyPackages qcid lusr = do brig <- viewBrig @@ -604,20 +549,17 @@ getUserClients qusr = do -- | Generate one key package for each client of a remote user claimRemoteKeyPackages :: HasCallStack => Remote UserId -> MLSTest KeyPackageBundle claimRemoteKeyPackages (tUntagged -> qusr) = do - brig <- viewBrig clients <- getUserClients qusr - bundle <- fmap (KeyPackageBundle . Set.fromList) $ + fmap (KeyPackageBundle . Set.fromList) $ for clients $ \cid -> do (kp, ref) <- generateKeyPackage cid pure $ KeyPackageBundleEntry - { kpbeUser = qusr, - kpbeClient = ciClient cid, - kpbeRef = ref, - kpbeKeyPackage = KeyPackageData (rmRaw kp) + { user = qusr, + client = ciClient cid, + ref = ref, + keyPackage = KeyPackageData (raw kp) } - mapRemoteKeyPackageRef brig bundle - pure bundle -- | Claim key package for a local user, or generate and map key packages for remote ones. claimKeyPackages :: @@ -629,16 +571,13 @@ claimKeyPackages cid qusr = do loc <- liftTest $ qualifyLocal () foldQualified loc (claimLocalKeyPackages cid) claimRemoteKeyPackages qusr -bundleKeyPackages :: KeyPackageBundle -> MLSTest [(ClientIdentity, FilePath)] -bundleKeyPackages bundle = do - let bundleEntries = kpbEntries bundle - entryIdentity be = mkClientIdentity (kpbeUser be) (kpbeClient be) - for (toList bundleEntries) $ \be -> do - let d = kpData . kpbeKeyPackage $ be - qcid = entryIdentity be - fn <- keyPackageFile qcid (kpbeRef be) - liftIO $ BS.writeFile fn d - pure (qcid, fn) +bundleKeyPackages :: KeyPackageBundle -> [(ClientIdentity, ByteString)] +bundleKeyPackages bundle = + let getEntry be = + ( mkClientIdentity be.user be.client, + kpData be.keyPackage + ) + in map getEntry (toList bundle.entries) -- | Claim keypackages and create a commit/welcome pair on a given client. -- Note that this alters the state of the group immediately. If we want to test @@ -646,7 +585,7 @@ bundleKeyPackages bundle = do -- group to the previous state by using an older version of the group file. createAddCommit :: HasCallStack => ClientIdentity -> [Qualified UserId] -> MLSTest MessagePackage createAddCommit cid users = do - kps <- concat <$> traverse (bundleKeyPackages <=< claimKeyPackages cid) users + kps <- fmap (concatMap bundleKeyPackages) . traverse (claimKeyPackages cid) $ users liftIO $ assertBool "no key packages could be claimed" (not (null kps)) createAddCommitWithKeyPackages cid kps @@ -666,9 +605,9 @@ createExternalCommit qcid mpgs qcs = do mlscli qcid [ "external-commit", - "--group-state-in", + "--group-info-in", "-", - "--group-state-out", + "--group-info-out", pgsFile, "--group-out", "" @@ -677,8 +616,8 @@ createExternalCommit qcid mpgs qcs = do State.modify $ \mls -> mls - { mlsNewMembers = Set.singleton qcid -- This might be a different client - -- than those that have been in the + { mlsNewMembers = Set.singleton qcid + -- This might be a different client than those that have been in the -- group from before. } @@ -688,12 +627,12 @@ createExternalCommit qcid mpgs qcs = do { mpSender = qcid, mpMessage = commit, mpWelcome = Nothing, - mpPublicGroupState = Just newPgs + mpGroupInfo = Just newPgs } createAddProposals :: HasCallStack => ClientIdentity -> [Qualified UserId] -> MLSTest [MessagePackage] createAddProposals cid users = do - kps <- concat <$> traverse (bundleKeyPackages <=< claimKeyPackages cid) users + kps <- fmap (concatMap bundleKeyPackages) . traverse (claimKeyPackages cid) $ users traverse (createAddProposalWithKeyPackage cid) kps -- | Create an application message. @@ -714,18 +653,19 @@ createApplicationMessage cid messageContent = do { mpSender = cid, mpMessage = message, mpWelcome = Nothing, - mpPublicGroupState = Nothing + mpGroupInfo = Nothing } createAddCommitWithKeyPackages :: ClientIdentity -> - [(ClientIdentity, FilePath)] -> + [(ClientIdentity, ByteString)] -> MLSTest MessagePackage createAddCommitWithKeyPackages qcid clientsAndKeyPackages = do bd <- State.gets mlsBaseDir welcomeFile <- liftIO $ emptyTempFile bd "welcome" - pgsFile <- liftIO $ emptyTempFile bd "pgs" - commit <- + giFile <- liftIO $ emptyTempFile bd "gi" + + commit <- runContT (traverse (withTempKeyPackageFile . snd) clientsAndKeyPackages) $ \kpFiles -> mlscli qcid ( [ "member", @@ -734,12 +674,12 @@ createAddCommitWithKeyPackages qcid clientsAndKeyPackages = do "", "--welcome-out", welcomeFile, - "--group-state-out", - pgsFile, + "--group-info-out", + giFile, "--group-out", "" ] - <> map snd clientsAndKeyPackages + <> kpFiles ) Nothing @@ -749,31 +689,31 @@ createAddCommitWithKeyPackages qcid clientsAndKeyPackages = do } welcome <- liftIO $ BS.readFile welcomeFile - pgs <- liftIO $ BS.readFile pgsFile + gi <- liftIO $ BS.readFile giFile pure $ MessagePackage { mpSender = qcid, mpMessage = commit, mpWelcome = Just welcome, - mpPublicGroupState = Just pgs + mpGroupInfo = Just gi } createAddProposalWithKeyPackage :: ClientIdentity -> - (ClientIdentity, FilePath) -> + (ClientIdentity, ByteString) -> MLSTest MessagePackage createAddProposalWithKeyPackage cid (_, kp) = do - prop <- + prop <- runContT (withTempKeyPackageFile kp) $ \kpFile -> mlscli cid - ["proposal", "--group-in", "", "--group-out", "", "add", kp] + ["proposal", "--group-in", "", "--group-out", "", "add", kpFile] Nothing pure MessagePackage { mpSender = cid, mpMessage = prop, mpWelcome = Nothing, - mpPublicGroupState = Nothing + mpGroupInfo = Nothing } createPendingProposalCommit :: HasCallStack => ClientIdentity -> MLSTest MessagePackage @@ -791,7 +731,7 @@ createPendingProposalCommit qcid = do "", "--welcome-out", welcomeFile, - "--group-state-out", + "--group-info-out", pgsFile ] Nothing @@ -803,7 +743,7 @@ createPendingProposalCommit qcid = do { mpSender = qcid, mpMessage = commit, mpWelcome = welcome, - mpPublicGroupState = Just pgs + mpGroupInfo = Just pgs } readWelcome :: FilePath -> IO (Maybe ByteString) @@ -821,10 +761,8 @@ createRemoveCommit cid targets = do g <- getClientGroupState cid - let kprefByClient = Map.fromList (readGroupState g) - let fetchKeyPackage c = keyPackageFile c (kprefByClient Map.! c) - kps <- traverse fetchKeyPackage targets - + let groupStateMap = Map.fromList (readGroupState g) + let indices = map (fromMaybe (error "could not find target") . flip Map.lookup groupStateMap) targets commit <- mlscli cid @@ -836,10 +774,10 @@ createRemoveCommit cid targets = do "", "--welcome-out", welcomeFile, - "--group-state-out", + "--group-info-out", pgsFile ] - <> kps + <> map show indices ) Nothing welcome <- liftIO $ readWelcome welcomeFile @@ -849,7 +787,7 @@ createRemoveCommit cid targets = do { mpSender = cid, mpMessage = commit, mpWelcome = welcome, - mpPublicGroupState = Just pgs + mpGroupInfo = Just pgs } createExternalAddProposal :: HasCallStack => ClientIdentity -> MLSTest MessagePackage @@ -862,7 +800,7 @@ createExternalAddProposal joiner = do proposal <- mlscli joiner - [ "proposal-external", + [ "external-proposal", "--group-id", T.unpack (toBase64Text (unGroupId groupId)), "--epoch", @@ -880,7 +818,7 @@ createExternalAddProposal joiner = do { mpSender = joiner, mpMessage = proposal, mpWelcome = Nothing, - mpPublicGroupState = Nothing + mpGroupInfo = Nothing } consumeWelcome :: HasCallStack => ByteString -> MLSTest () @@ -888,7 +826,7 @@ consumeWelcome welcome = do qcids <- State.gets mlsNewMembers for_ qcids $ \qcid -> do hasState <- hasClientGroupState qcid - liftIO $ assertBool "Existing clients in a conversation should not consume commits" (not hasState) + liftIO $ assertBool "Existing clients in a conversation should not consume welcomes" (not hasState) void $ mlscli qcid @@ -908,8 +846,7 @@ consumeMessage msg = do consumeMessage1 cid (mpMessage msg) consumeMessage1 :: HasCallStack => ClientIdentity -> ByteString -> MLSTest () -consumeMessage1 cid msg = do - bd <- State.gets mlsBaseDir +consumeMessage1 cid msg = void $ mlscli cid @@ -918,8 +855,6 @@ consumeMessage1 cid msg = do "", "--group-out", "", - "--signer-key", - bd "removal.key", "-" ] (Just msg) @@ -928,55 +863,33 @@ consumeMessage1 cid msg = do -- commit, the 'sendAndConsumeCommit' function should be used instead. sendAndConsumeMessage :: HasCallStack => MessagePackage -> MLSTest ([Event], UnreachableUsers) sendAndConsumeMessage mp = do + for_ mp.mpWelcome $ \_ -> liftIO $ assertFailure "use sendAndConsumeCommitBundle" res <- fmap (mmssEvents Tuple.&&& mmssUnreachableUsers) $ responseJsonError =<< postMessage (mpSender mp) (mpMessage mp) do - postWelcome (ciUser (mpSender mp)) welcome - !!! const 201 === statusCode - consumeWelcome welcome - pure res --- | Send an MLS commit message, simulate clients receiving it, and update the --- test state accordingly. -sendAndConsumeCommit :: - HasCallStack => - MessagePackage -> - MLSTest [Event] -sendAndConsumeCommit mp = do - (events, _) <- sendAndConsumeMessage mp - - -- increment epoch and add new clients - State.modify $ \mls -> - mls - { mlsEpoch = mlsEpoch mls + 1, - mlsMembers = mlsMembers mls <> mlsNewMembers mls, - mlsNewMembers = mempty - } - - pure events - mkBundle :: MessagePackage -> Either Text CommitBundle mkBundle mp = do - commitB <- decodeMLS' (mpMessage mp) - welcomeB <- traverse decodeMLS' (mpWelcome mp) - pgs <- note "public group state unavailable" (mpPublicGroupState mp) - pgsB <- decodeMLS' pgs - pure $ - CommitBundle commitB welcomeB $ - GroupInfoBundle UnencryptedGroupInfo TreeFull pgsB - -createBundle :: MonadIO m => MessagePackage -> m ByteString + commitB <- first ("Commit: " <>) $ decodeMLS' (mpMessage mp) + welcomeB <- first ("Welcome: " <>) $ for (mpWelcome mp) $ \m -> do + w <- decodeMLS' @Message m + case w.content of + MessageWelcome welcomeB -> pure welcomeB + _ -> Left "expected welcome" + ginfo <- note "group info unavailable" (mpGroupInfo mp) + ginfoB <- first ("GroupInfo: " <>) $ decodeMLS' ginfo + pure $ CommitBundle commitB welcomeB ginfoB + +createBundle :: (HasCallStack, MonadIO m) => MessagePackage -> m ByteString createBundle mp = do bundle <- either (liftIO . assertFailure . T.unpack) pure $ mkBundle mp - pure (serializeCommitBundle bundle) + pure (encodeMLS' bundle) sendAndConsumeCommitBundle :: HasCallStack => @@ -1008,20 +921,23 @@ mlsBracket clients k = do c <- view tsCannon WS.bracketAsClientRN c (map (ciUser &&& ciClient) clients) k -readGroupState :: ByteString -> [(ClientIdentity, KeyPackageRef)] +readGroupState :: ByteString -> [(ClientIdentity, LeafIndex)] readGroupState j = do - node <- j ^.. key "group" . key "tree" . key "tree" . key "nodes" . _Array . traverse - leafNode <- node ^.. key "node" . key "LeafNode" - identity <- - either (const []) pure . decodeMLS' . BS.pack . map fromIntegral $ - leafNode ^.. key "key_package" . key "payload" . key "credential" . key "credential" . key "Basic" . key "identity" . key "vec" . _Array . traverse . _Integer - kpr <- (unhexM . T.encodeUtf8 =<<) $ leafNode ^.. key "key_package_ref" . _String - pure (identity, KeyPackageRef kpr) + (node, n) <- zip (j ^.. key "group" . key "public_group" . key "treesync" . key "tree" . key "leaf_nodes" . _Array . traverse) [0 ..] + case node ^? key "node" of + Just leafNode -> do + identityBytes <- leafNode ^.. key "payload" . key "credential" . key "credential" . key "Basic" . key "identity" . key "vec" + let identity = BS.pack (identityBytes ^.. _Array . traverse . _Integer . to fromIntegral) + cid <- case decodeMLS' identity of + Left _ -> [] + Right x -> pure x + pure (cid, n) + Nothing -> [] getClientsFromGroupState :: ClientIdentity -> Qualified UserId -> - MLSTest [(ClientIdentity, KeyPackageRef)] + MLSTest [(ClientIdentity, LeafIndex)] getClientsFromGroupState cid u = do groupState <- readGroupState <$> getClientGroupState cid pure $ filter (\(cid', _) -> cidQualifiedUser cid' == u) groupState @@ -1032,11 +948,11 @@ clientKeyPair cid = do credential <- liftIO . BS.readFile $ bd cid2Str cid "store" T.unpack (T.decodeUtf8 (B64U.encode "self")) - let s = - credential ^.. key "signature_private_key" . key "value" . _Array . traverse . _Integer - & fmap fromIntegral - & BS.pack - pure $ BS.splitAt 32 s + case runGetOrFail + ((,) <$> parseMLSBytes @VarInt <*> parseMLSBytes @VarInt) + (LBS.fromStrict credential) of + Left (_, _, msg) -> liftIO $ assertFailure msg + Right (_, _, keys) -> pure keys receiveNewRemoteConv :: (MonadReader TestSetup m, MonadIO m) => @@ -1313,3 +1229,14 @@ getCurrentGroupId = do State.gets mlsGroupId >>= \case Nothing -> liftIO $ assertFailure "Creating add proposal for non-existing group" Just g -> pure g + +withTempKeyPackageFile :: ByteString -> ContT a MLSTest FilePath +withTempKeyPackageFile bs = do + bd <- State.gets mlsBaseDir + ContT $ \k -> + bracket + (liftIO (openBinaryTempFile bd "kp")) + (\(fp, _) -> liftIO (removeFile fp)) + $ \(fp, h) -> do + liftIO $ BS.hPut h bs `finally` hClose h + k fp diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 0d35e8fafe2..d00521efdd4 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -121,7 +121,7 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Domain (originDomainHeaderName) import Wire.API.Internal.Notification hiding (target) -import Wire.API.MLS.KeyPackage +import Wire.API.MLS.LeafNode import Wire.API.MLS.Message import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation @@ -2899,32 +2899,33 @@ wsAssertConvReceiptModeUpdate conv usr new n = do evtFrom e @?= usr evtData e @?= EdConvReceiptModeUpdate (ConversationReceiptModeUpdate new) -wsAssertBackendRemoveProposalWithEpoch :: HasCallStack => Qualified UserId -> Qualified ConvId -> KeyPackageRef -> Epoch -> Notification -> IO ByteString -wsAssertBackendRemoveProposalWithEpoch fromUser convId kpref epoch n = do - bs <- wsAssertBackendRemoveProposal fromUser (Conv <$> convId) kpref n - let msg = fromRight (error "Failed to parse Message 'MLSPlaintext") $ decodeMLS' @(Message 'MLSPlainText) bs - let tbs = rmValue . msgTBS $ msg - tbsMsgEpoch tbs @?= epoch +wsAssertBackendRemoveProposalWithEpoch :: HasCallStack => Qualified UserId -> Qualified ConvId -> LeafIndex -> Epoch -> Notification -> IO ByteString +wsAssertBackendRemoveProposalWithEpoch fromUser convId idx epoch n = do + bs <- wsAssertBackendRemoveProposal fromUser (Conv <$> convId) idx n + let msg = fromRight (error "Failed to parse Message") $ decodeMLS' @Message bs + case msg.content of + MessagePublic pmsg -> liftIO $ pmsg.content.value.epoch @?= epoch + _ -> assertFailure "unexpected message content" pure bs -wsAssertBackendRemoveProposal :: HasCallStack => Qualified UserId -> Qualified ConvOrSubConvId -> KeyPackageRef -> Notification -> IO ByteString -wsAssertBackendRemoveProposal fromUser cnvOrSubCnv kpref n = do +wsAssertBackendRemoveProposal :: HasCallStack => Qualified UserId -> Qualified ConvOrSubConvId -> LeafIndex -> Notification -> IO ByteString +wsAssertBackendRemoveProposal fromUser cnvOrSubCnv idx n = do let e = List1.head (WS.unpackPayload n) ntfTransient n @?= False evtConv e @?= convOfConvOrSub <$> cnvOrSubCnv evtType e @?= MLSMessageAdd evtFrom e @?= fromUser let bs = getMLSMessageData (evtData e) - let msg = fromRight (error "Failed to parse Message 'MLSPlaintext") $ decodeMLS' bs - let tbs = rmValue . msgTBS $ msg - tbsMsgSender tbs @?= PreconfiguredSender 0 - case tbsMsgPayload tbs of - ProposalMessage rp -> - case rmValue rp of - RemoveProposal kpRefRemove -> - kpRefRemove @?= kpref - otherProp -> assertFailure $ "Expected RemoveProposal but got " <> show otherProp - otherPayload -> assertFailure $ "Expected ProposalMessage but got " <> show otherPayload + let msg = fromRight (error "Failed to parse Message") $ decodeMLS' @Message bs + liftIO $ case msg.content of + MessagePublic pmsg -> do + pmsg.content.value.sender @?= SenderExternal 0 + case pmsg.content.value.content of + FramedContentProposal prop -> case prop.value of + RemoveProposal removedIdx -> removedIdx @?= idx + otherProp -> assertFailure $ "Expected RemoveProposal but got " <> show otherProp + otherPayload -> assertFailure $ "Expected ProposalMessage but got " <> show otherPayload + _ -> assertFailure $ "Expected PublicMessage" pure bs where getMLSMessageData :: Conv.EventData -> ByteString @@ -2944,19 +2945,16 @@ wsAssertAddProposal fromUser convId n = do evtType e @?= MLSMessageAdd evtFrom e @?= fromUser let bs = getMLSMessageData (evtData e) - let msg = fromRight (error "Failed to parse Message 'MLSPlaintext") $ decodeMLS' bs - let tbs = rmValue . msgTBS $ msg - tbsMsgSender tbs @?= NewMemberSender - case tbsMsgPayload tbs of - ProposalMessage rp -> - case rmValue rp of - AddProposal _ -> pure () - otherProp -> - assertFailure $ - "Expected AddProposal but got " <> show otherProp - otherPayload -> - assertFailure $ - "Expected ProposalMessage but got " <> show otherPayload + let msg = fromRight (error "Failed to parse Message 'MLSPlaintext") $ decodeMLS' @Message bs + liftIO $ case msg.content of + MessagePublic pmsg -> do + pmsg.content.value.sender @?= SenderNewMemberProposal + case pmsg.content.value.content of + FramedContentProposal prop -> case prop.value of + AddProposal _ -> pure () + otherProp -> assertFailure $ "Expected AddProposal but got " <> show otherProp + otherPayload -> assertFailure $ "Expected ProposalMessage but got " <> show otherPayload + _ -> assertFailure $ "Expected PublicMessage" pure bs where getMLSMessageData :: Conv.EventData -> ByteString diff --git a/services/galley/test/integration/Main.hs b/services/galley/test/integration/Run.hs similarity index 99% rename from services/galley/test/integration/Main.hs rename to services/galley/test/integration/Run.hs index c67d355dfad..c35edc8d5eb 100644 --- a/services/galley/test/integration/Main.hs +++ b/services/galley/test/integration/Run.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main +module Run ( main, ) where diff --git a/services/galley/test/unit.hs b/services/galley/test/unit.hs new file mode 100644 index 00000000000..a26473d24ee --- /dev/null +++ b/services/galley/test/unit.hs @@ -0,0 +1 @@ +import Run diff --git a/services/galley/test/unit/Main.hs b/services/galley/test/unit/Run.hs similarity index 99% rename from services/galley/test/unit/Main.hs rename to services/galley/test/unit/Run.hs index fbf969775e9..57963cefef5 100644 --- a/services/galley/test/unit/Main.hs +++ b/services/galley/test/unit/Run.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main +module Run ( main, ) where diff --git a/services/nginz/integration-test/conf/nginz/README.md b/services/nginz/integration-test/conf/nginz/README.md new file mode 100644 index 00000000000..c8e81957c62 --- /dev/null +++ b/services/nginz/integration-test/conf/nginz/README.md @@ -0,0 +1,7 @@ +# How to regenerate certificates in this directory + +Run from this directory: + +```bash +../../../../../hack/bin/selfsigned.sh +``` diff --git a/services/nginz/integration-test/conf/nginz/integration-ca-key.pem b/services/nginz/integration-test/conf/nginz/integration-ca-key.pem index 961e87aa67d..774b9d30c99 100644 --- a/services/nginz/integration-test/conf/nginz/integration-ca-key.pem +++ b/services/nginz/integration-test/conf/nginz/integration-ca-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEApwf/2d2YraQDpCipPVtYR+7BNu47AgkD7kFvGhoxJhDP7CsU -VdpqU5gsVVo8kvhkh4k1tsJyuWWeKn6piNSXxUCFIc80KkUPgsYf5v+RBXr73Fdg -ezHQNhNi0dRZCh+YG/hN7pOX46+B0PyKwUEMTeUqizkmFU5tILPMMyDAGx1Bp2LB -oJi4u+48fzTDMaWSXnCVF04G9+A4LDzw0fPdDMgKLEiXJ8GPoPs0cNs6MJoFDgpe -gzy1mv7X7otmRVTaafZGd4TTo6lGC2VVSS5tpj4Qfz/PxyCLK7tf5033HNWEJzAw -6izRXp849VferHuYEbP+2lexNk9tl45BsFhkrwIDAQABAoIBAQCFkzYeSsJginuG -+iVttfEBhYPqo9V4qTEFhjqNS0jmwiclHMZkagkB1P4PO9yZRB9Q7H+SKiqI7STx -ot19WVYOHqzY/tUewJ/I2xyEJPkawuFLsmyr2IhD1nj+iKy0FdQU+huIoWukX6SX -Nn7YUWa/nHbLY+Z6v38x2deBQ72dcBtDcOh1vtUR3fVfsiX5uzCcfvNZAw4cCyB2 -j8ySDIiP10Ic81da3FIeCm8g2yp3DrnvTa77xsr0IfSykB3UcSrGqDwZxs9pS82Q -1fog//4xAfBYC9LEcnQrCvz2kqLSLICtjkgK+dlzgvY3rZMq9c/OY1nR7Wp2BIyp -kKB5AEnRAoGBANTM3fq4YGzUodf+Xla4MDvQFJsYjQuig/CJboQ7JSFZi2uLnSHX -+7JDiHtQd3uifYMhzSxXXKV82CK7SsJOQlIVoCZ5eTsyYGyAu1fUqfBvfHYN4Gbr -3QyZJE0Hut2rvn5DaT/dpgh7Uy9QWKhpAsmxzhKa/iADUTiNAO8pxxRFAoGBAMjw -iZV43XWLvzP90P5jANHuk9tR/B5cM9zK40aWglNsMlK9cUgW3ovohMzTFce/LQWy -zGZ1WZZcUUcR/pHot3fyjWKeJadZhSZ/7hN/0d/UDuFY5nQ8eGQoy2qrrtY+6MMU -Eiz09EFnKKA7hUoDnbhOH1hCKsfrOVse55RDkTZjAoGABrzRzm1mCCwXT7prDD3a -sRoefOajGJo1qTkAuckRnOOz6VzLRdYLzxIaUSU0E0MKzEsWru+5LDgus7LQZCSM -LwMmRfGUqA4pRWYyCE7gbo9pFmfMEhYnso1qu9Gh1gDpECBcRbxj1GLrOFVH6VUh -1Hb/ulET+LmCKdM1E110Qy0CgYEAimbDHSUGxHPg2pq0XMMsSWyegq3RjcfMIQPN -z0zTr0oSz1KUuCaoWo1pCvtJQS+4fvhMOTYS4rHreZw3T6CO3hs+rvJm1QGf6Iit -HtknYZfaN/TXprAP7Ez87xgZcJAcGmG0syp1Iqc/ID5e7D/ZXpzQkiXg+ZpXAyAi -OcjgOCkCgYEAmsCsqtPn5vgB+/vr0n28UsFS4Of9whlgEPYndNss3nAmVEohQJRg -QlBlJd2iDa7R0TrJZCuAwuqK7TxB/RoHL8UkryUt2nag39GYAyE+lfPM558/AWyt -9yyLQNfiJnqTC2Ne2j7EyicBLha4J9NoBeNE5UqLlzrH4LRJ3fRX9Ps= +MIIEpAIBAAKCAQEAoYyNk0aNoe2AYoWa4ey6P4LR4BxKGk0A9LeFiCP4tWqbU/aZ +DzDATytklxaQiDMDbZQboFngf5/X0S+pjSiZ+LSgIR30/g0yoDEubfUXvF+q+rEh +Om91OHnkwwNoSN1EK687N1nATFXd7YL6Lv2SOrMcyOCtqwnGFwRrH8MR3z87nL+H +vuot2ciXvyeJ3q4RG2G9t8UTjqo1jK/NJHyNZYSY4vGTGZTwGi1BCuNlizi6xzmI +Mh3HS/px/kihR7wLkQ7NpovqjfQVef3JwiJutrRYG6lJT9xXpNu2gKg8KKiZJUgb +gqnPWl+4IdRdZ/q/12Jsg9qAf8tbS+tQ2CnlLQIDAQABAoIBAQCJKkrm+me1Tm/M +tz4bh6FX3Z6Pl9V/YVRndA9n2YsJljvOXbn1wOH4FpLxChKr4gyOFMwkKUvJcRGQ +ptRia0/YcJzpoYLr1o7enwOaDxkZM218L7tT32D7E9wdjJ4WB/Ei2kUAKS9yYRHu +4V/FWD25o2zUTpiGeeT8lB7UuA9Lqg529dGlJcanlZjMe0Wj92ec1jjelERGuGdr +lujikHl8whZRwxCGC09WM48myWnsCVdJ1oqGhYM8nzqImsiMc10K6/8CmVrl3aXV +KrExPLtxCRK3pe5olyCLIkPn3OwSc/ZPSkxVQF4j/PwatqqHE98TQBi5bzKIF2JE +17+DBVxNAoGBAM4lR1WRAtXvAe6/jl5zYHr/v2D69o7v85PuXrnmSLK29h3ACSDM +svTsIkoPIZ/lotM8O/OpOHKWmbXH7MOIu9mRKQAKFlTKtw4xl36SPynegq1H5JBv +bd8N8pQtf8pLuh8qxZvZplBsg9HJHBPlbZo/dMQa8oYDI4BakMyYJEMbAoGBAMie +PyHPgI7RpE5GPXcl/rOxeMF++7qOsOX5XGUhoGoH2feYzj19V2/ptx4KdmD+M3NF +dT1ucmQKqocrE6U3sEMok3BmgajGoGOLQMPXsK18bs0VowI+mmt+uL2BwOr9hHPK +IuZrzwm9vtLYldBU3sdxGA1sLXPB2oUZalwCf7VXAoGAZo77X2GmtIKVRo98qBbk +sCzerMQOuGw+laFo9TnRf0AxT/nDUNMmUV3NbWT7yI45pLf5566Py8qLLHoLm/hB +5OsoJ8Hc/FBiJCieAzWFQTJXdxgmaYlWczuALSI5yo5ESc9AwtnUuXxTVKKmWmux +TKU3VX1GnU+gcPIdyfwDRnMCgYAWg8P8DGiWHqr86d8eDxKNoh42QQUJQ9hQhvK6 +mtKA886fffOvbPCyK52UboIokn69sg7dTRbjaVsH/mqfASfz8YrSc36brWb0pP0o +vX0jizJ4K7R2nQYBiGA9TGGVPcxunkHacED1C+ltikcN8WhrI6MaZoiXVCstAtQv +7Uvd0wKBgQDCC9xoSTr7kFiwp76f7dIBdxLKBiL1tZM/qJIP3lnX9TnLhBiHNxoR +4DbIF5yEdRRNBVfS4rJLa1zAAY3d5u4LENaZEvf7fmsjHTLEIf3gJVviHZSBMP6C +kSPQbfcNTNZaEt/40GAZzgjNiO0rTpsLLI4fGDiHeaMMBHEzAiXJmw== -----END RSA PRIVATE KEY----- diff --git a/services/nginz/integration-test/conf/nginz/integration-ca.pem b/services/nginz/integration-test/conf/nginz/integration-ca.pem index f9479d65a06..2aff84d758b 100644 --- a/services/nginz/integration-test/conf/nginz/integration-ca.pem +++ b/services/nginz/integration-test/conf/nginz/integration-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDAjCCAeqgAwIBAgIUBTz/WN3KPdXZnUyhrinjprCSy2QwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjIwNDIwMTMyMDAwWhcN -MjcwNDE5MTMyMDAwWjAZMRcwFQYDVQQDEw5jYS5leGFtcGxlLmNvbTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBALfeLu+by+cSkuhiB2eJJpFb0OUTNBzT -YiNK5yfLhHhfQRUkWJQMn9+Zis+thgJDUOuiZ7A6OaDNGNRnlJL62Wz7OgpUFWdt -kdHvmRK+rfAbYeCTOjTWTRBbMrqmRX1WO6tqn6EBttIcS4ND+Bl0tjpf2i8JR+AV -37yHuj/zoBtWHtEFhkCs2vYS09KWuYYBaaj90QKt16f1+Mp3s6OUreB/YzxsCb7d -C4aPPKrloBcI/HZu71AYiQb6WPO1LjyMFMvpYz/ty6V+l69tupYIBJUyoZ+mrY2F -XemRd/Xv3HcJRCBrwx70gER5XNg/IO5vAuRQ9DZqsbEsZApArSbM9RMCAwEAAaNC -MEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCiX -sxgN51cGwv39O8DGk6bVuL4aMA0GCSqGSIb3DQEBCwUAA4IBAQBvQU5dywshZbUp -8/MJI36hrI3IGsf9Asc9Yb3g9Zdc1npWY7F4Mtb6wsaQt1QUWgGcZ2Om6aYQu2iH -TN5a7D1Lxm99BgSLBWeGky/Wgl3XaGKV/2ch9n2eYyz1ukiOF1yvghsNovBvQF11 -nnHLTKZQLtEvawicYB/wdRJOiGp30Ze8DjOeoiPEHHolQa/a1DFlO58tPU1TAr+b -BLmxIEPP6BiIbZHZVQY8aosITMqvY1MCZKTtlXxzRZpxNfQNPYAVjA9D/UWfxpPS -b45eCIIQmctfL5smaY32QFuYsmqOH6OiVm7wm/hkGZCTqfumPR7MpJmJ4LYhpSC4 -IZ1eInXn +MIIDAjCCAeqgAwIBAgIUaq5Rk0z4WRqKc9dEtkxgVdL0LBIwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjMwNDI0MDkzMTAwWhcN +MjgwNDIyMDkzMTAwWjAZMRcwFQYDVQQDEw5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGMjZNGjaHtgGKFmuHsuj+C0eAcShpN +APS3hYgj+LVqm1P2mQ8wwE8rZJcWkIgzA22UG6BZ4H+f19EvqY0omfi0oCEd9P4N +MqAxLm31F7xfqvqxITpvdTh55MMDaEjdRCuvOzdZwExV3e2C+i79kjqzHMjgrasJ +xhcEax/DEd8/O5y/h77qLdnIl78nid6uERthvbfFE46qNYyvzSR8jWWEmOLxkxmU +8BotQQrjZYs4usc5iDIdx0v6cf5IoUe8C5EOzaaL6o30FXn9ycIibra0WBupSU/c +V6TbtoCoPCiomSVIG4Kpz1pfuCHUXWf6v9dibIPagH/LW0vrUNgp5S0CAwEAAaNC +MEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBuq +33I+JaC5KOsrFeHkzhBuFqtzMA0GCSqGSIb3DQEBCwUAA4IBAQBaOq1YyLjpMz+O +mxw0yRpROgPaPt0QMsSbUCeNXPrlMFi+7QarmKfz0EGoGJEfU8Eu22+mqnAC2tTO +iSLy89tlR21i0+x+0V+qedzZCQfMlm00SS29wzbXomeUunQxlHNuGuRzkzh7g80G ++wIJuIZRvs+qgGofd4yp2BGGQNOlNRhPmc0LP5DSB+snmIscx+sDnVUn7MWunH80 +Doj+CL6wSbP79hfJXeK5LxSBmAtQU8dpZlgNaRCO5TAU10xgzFNCKWbKJ7nf4wC5 +cMGhRWFYP3babARd42KWViRYLZ7bxTtNBnKOvo7AtQJ3YIOUwk1ofq3/PhLHDxiG +XWlMKqrV -----END CERTIFICATE----- diff --git a/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem b/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem index 1e25ca85b5d..b1718af2d07 100644 --- a/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem +++ b/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA1iDPn3gxH1jNo5zzYqOJX664NeaPcbPZXPx29CAj+fLN8u8g -rknfVTve+JvqplZiJkf1hw82HnDUdOjCVRl9C0J6uRnncq2G+y13fzL05T751WJA -6S/HCc4oRzWSLMvW36ULiehESkriM91MMXlOltZjgbOLxXJbmN8JTer/yhDnnDXe -TmhoaCqB6WaLnLIMMHYbvWqgdeaANUJSQ2aVIK78eFSM/5yIHyTR3zXIojfSvSBA -H2fItdGxlvr+TmjnpUNGWPlbWddnNnOPc/2ezXh9Hz0z2Qp66Go7pDAhZULV4mUO -QwE/EIpDTCq1pn6BW+Pdc2neasHjPhQ+wt5PfwIDAQABAoIBAQCQaFRdcct/Hn6g -xuqFLVEqDEUDZNh8kBQZV9JJVZutp4gpPAfIQt2xN81p0IzxXPSYaJM3YJTY9rLx -nT/h8GyaOV1WlBe5CTotDz61tAHg0RXgSIEKQkRzYmtbis9oEph4/2/Gs7PKfrIK -1EXcX7kWlMNK53Ft2W/YqyI1QDT9aikgyR3NlNczIgWNB2fKIDKrJACj9sb3KZBz -ePXxDvIy2JcRo6o2B3023AKNdIA+PUMUAGgLnGXQv3bf8n3N+E984PdB5oWLHOg3 -RF9wjedIj7alfHSH9/4x1xhP6lmUF4U27FGN7jC6AcF0iV8QdPlSpa877tjeSrN6 -BnOuhCKxAoGBAPNQv7AMAtxYn7Rt6Qj4W147f3T30Muu3Kg2U5znU9rcQPSDFPYX -QFmBC4VrA31bgqzJ06mjutmn5hCtw0LzaWtA/JQIj42GMV5+hcAOMA6n2oMAT8FC -bpo93rdLpLhy8b3FJUaD9Qc7wtICu1XTyJv5gSe9nC9YJzl6MYRVEdFFAoGBAOFK -iJKzxJFsozrX0pp2O8DO6r5/IDPxPJurtAC/74n8RFzzQGymb5oGSrtjOyVqJYrd -a2bQWozQPRFiwGdyu8GK7hxxoGdaz5FCxjAe41qdDm4GlSn/wYaBzq511lb3GzVW -tauVZG2gkO4j2dvhMMthQY5xG8TAc4V3slHFdS/zAoGAF3Aa1vGBQQqEb9P6k7Og -0YX3tCO/CC/S7500FrQt3rJCy4ro9P+uYjDNFFAHqQasospaSkgMUrUas1aZrZRW -/k7nRbdBZMedb9XOOn7jYDYJFX9tL1ef4dm933g46M+hu78G5TEG5Gh8TtCWjSD2 -fRfeuh5IskLSnHXJ2U58heUCgYBENY6369l9tgiNjj5jKZzZuUv1NQQI9ebFsuyi -tXnOqyP/iF5fBt0PIwyJQ3fq0gJf0r3ruPVRYNK8asuaBnC2HlwNHJHV+PaTIkZi -11c6Xga6ZR/QQXDUSoTK6T5lwhboxUHnmyl2z4BRuWUCX2Gokd+JQtGHdkUDicPh -Ygki5QKBgQCNAkPMUEP5e44IXmecnIh1XCv9see7+jYyyjHfDmDCVNt7qwMLn1vT -sqoZtDWsTG2Dvp5ctYTfI5bOsNa2sEU/VSeccf6lHjiw2N+NhCQXffYZTLGMyAVH -78s8Xq7glmd4k08YkPpsOYpXUqB3DDQEV6v2XpDN2LI9RnYWewOF7g== +MIIEowIBAAKCAQEAr8i0VsoPb1ITTQO1O+uZ4b3+19F42kwXSpaBmgGwK9PQMjiw ++mNGKQf0AM8HISPAEWN3+7ildrl7o9gaFW6e6L00LGyRrKr9hJ46yWNhLb7auJi6 +sq5WK6Wjt+BDMWHmokfKDGOTyh4d+Q5R3uoY/Smi+QQLxUb8VkAESy6lLvff1HXy +jmcvoHigCMedOX0ipgoDg0OOMUiwDaJslsKnJ+Irn7VpfUjmIPPz4J8VRRlqxK6u +tSktq8uzZEUP03elZvlDYGuKEar5qLwgVENJKjgWWG6+gSJniQRNFKIOEvMsybip +wGdA/+da/s27NLBZvnMCLfSKVe15PnBfcEi3FwIDAQABAoIBABM4gO+UfIeRk+ax +5xk8M8FJQxpaHzrPYySWvGkYkijYqkUzibZ3MG7AHeAQwxjOjevY0n/FuuH2ehx6 +Pq/lPp74QUIyRON6duoPWyI2KaQU4Fma6Z8sDOQM4o/yh6ZYrB1GeENOiBRrop9e +/3i+ZCkaamWMGbVig6jyqwWFfi5aYZmL9BB3g7mMYz+DAnSD9eAI0Fl+dCjY3PLq +I5+BjnjHDdA9ixjyNhobBPUN67qAQLox7b5+joM+dW9TD2+2wLF8ubBP/ZjZxJpR +WRGG9tikdyR0ojC9cx4hg9+tN1OV9lAfOgWZO4ZwgCMsDFrKCf76DpG8nNbGMkUi +D8mGmhECgYEA6M6mlQuax9jvd7PhN/E5pqgDDr9gT0+6i9JRSNdX2zGxcH8QPMuE +WQN9gIT+HGfgZQR9r7DvEtl58IzMadF3Jj+zq2C1UMQujWktTp2wA+Lj+JTmSkSx +OdhFwOnouWqeHacdrP+LDahrxTAoQLWkFY7gbzYJARhT8U+MD17yFOMCgYEAwUvG +KY2H4SHqA2V3gjxjaGpj01D4Q4zaK4cDdLYofkkEIECbDXQ0MBPrhEng0bH/P4ld +8H9Sbsfaave/kdTpQunrGRG6cUnLG2/b3NPwf2FcROJ6bVP2JjQLSHZroV1WNLbO +WokoLn61AllkjHisyHjgeBx1oCBE08OVCyJ43z0CgYEAvbUHkZSvQALKwGRYNlnf +fKqUM0RHmtmBTcbIbe7srLVFvkIMXT4KTu7FKiE1YLhU5nxOXwhzCI0nDJnvSJtj +2Es4gYKAvZvfw2Pdg56De+c7lajgL8ziDhzqWlVBSzZSOh+f0wU5rpt7lmezpWde +miKfSIBjvfyxCoajvzLDWbkCgYBtFY8yeg3ZzqLa4dNM6zmKfqfxZHuG26Fv+RTJ +M9esVRaAARW/xPmCvGsoT+0RSitrNuGNzLy/igfIYCJ7cTVmrs4farLWJjf6NulU +OUM7D73bnhhLRJvgOXS4oyPgf+UbgKL50vebLaSHO92TrLKNvDGpdx4mjK9q9rBR +BVZDXQKBgBxHESayFWS0tAyV67GlOaiy3mbjVvxpRT7IGwXZAX+3NMvRmCzN8sIB +zkYMuRC3P/9RAZkBQ2qp8Fu0W8G7b32ImWyP7/HJb0hnBIfwBnePSUA1nS8jEkMp +IkrYAiU2viJTMiHNcqoVuJUY/FmxiZPPewqnJwQYAE4nrUD/oU8F -----END RSA PRIVATE KEY----- diff --git a/services/nginz/integration-test/conf/nginz/integration-leaf.pem b/services/nginz/integration-test/conf/nginz/integration-leaf.pem index 123b522f08a..120d96cda50 100644 --- a/services/nginz/integration-test/conf/nginz/integration-leaf.pem +++ b/services/nginz/integration-test/conf/nginz/integration-leaf.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDXDCCAkSgAwIBAgIUey3LIX14eyWd2sth8HsSSDbhnYcwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjIwNDIwMTMyMDAwWhcN -MjMwNDIwMTMyMDAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -1iDPn3gxH1jNo5zzYqOJX664NeaPcbPZXPx29CAj+fLN8u8grknfVTve+JvqplZi -Jkf1hw82HnDUdOjCVRl9C0J6uRnncq2G+y13fzL05T751WJA6S/HCc4oRzWSLMvW -36ULiehESkriM91MMXlOltZjgbOLxXJbmN8JTer/yhDnnDXeTmhoaCqB6WaLnLIM -MHYbvWqgdeaANUJSQ2aVIK78eFSM/5yIHyTR3zXIojfSvSBAH2fItdGxlvr+Tmjn -pUNGWPlbWddnNnOPc/2ezXh9Hz0z2Qp66Go7pDAhZULV4mUOQwE/EIpDTCq1pn6B -W+Pdc2neasHjPhQ+wt5PfwIDAQABo4G0MIGxMA4GA1UdDwEB/wQEAwIFoDAdBgNV +MIIDXDCCAkSgAwIBAgIUV3PHvpBx77MqGBo+PM2RIuIcBfAwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjMwNDI0MDkzMTAwWhcN +MjQwNDIzMDkzMTAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +r8i0VsoPb1ITTQO1O+uZ4b3+19F42kwXSpaBmgGwK9PQMjiw+mNGKQf0AM8HISPA +EWN3+7ildrl7o9gaFW6e6L00LGyRrKr9hJ46yWNhLb7auJi6sq5WK6Wjt+BDMWHm +okfKDGOTyh4d+Q5R3uoY/Smi+QQLxUb8VkAESy6lLvff1HXyjmcvoHigCMedOX0i +pgoDg0OOMUiwDaJslsKnJ+Irn7VpfUjmIPPz4J8VRRlqxK6utSktq8uzZEUP03el +ZvlDYGuKEar5qLwgVENJKjgWWG6+gSJniQRNFKIOEvMsybipwGdA/+da/s27NLBZ +vnMCLfSKVe15PnBfcEi3FwIDAQABo4G0MIGxMA4GA1UdDwEB/wQEAwIFoDAdBgNV HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E -FgQU9WPYeYNlDXrG0S2iYHZ81js6IYswHwYDVR0jBBgwFoAUKJezGA3nVwbC/f07 -wMaTptW4vhowMgYDVR0RAQH/BCgwJoIZKi5pbnRlZ3JhdGlvbi5leGFtcGxlLmNv -bYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQBoRgeD+blaKlqqKRXGQoEV -7u7H+YvFQOrrF/sx7XOH9qs14SBNt16HwW4U5w6VM5PhIQkz+PaYXYjLltYQMNbT -d5A+g0Tc0zpZkYa1JjW4hKEJ5RnimbrDNzIfe40tQPyz/beg1fVwj8vEGM9Nr+1W -IhVjCFvlgzUXgVZnO++IbZU4MJpI63HHxQKJtmK/N+Ees33SUY8uTt+NPB9w0KiY -9RwDfQO5ux4Xb2ZI3hp8jI3NO08ILHcl2fwifBfexc6OkGVTP8jAZWUhzfCaZ4FQ -BZ6rKYxLbFPHy27dmq/EGcpqzuqHy/GUidXdwidxNC38oxe0uEBEJhYOPJcBctcv +FgQUa7feIJTIqMh5UjDi0UR7Ub5MrvcwHwYDVR0jBBgwFoAUG6rfcj4loLko6ysV +4eTOEG4Wq3MwMgYDVR0RAQH/BCgwJoIZKi5pbnRlZ3JhdGlvbi5leGFtcGxlLmNv +bYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQAIfB/q/+jHWbN5goGMaPh8 +CL8kynzf0dmkwOs6f6sqDIRo+9BQneWCWVOTLbO3LK6ITsZhVTFmKT3bkEmj04sy +ZUnXfqi9CqDHjQKZU9OxIWoCgbe6r4siInI46K3rSYGsmP37x9jWop1fbJBLl1HC +ray3LR8zanzsR9ksbyfA9VbNmWY1nWxTkZZ5RM+IAlU0/8qRgo5Ypsl35Gd9RJiN +DtbU3+rU9bYQ1YgYDk0h1s2woEberjp1xnvGBJLhDjewv9jXXaQXr1GlwfnJBenO +TV+GWqTeXwPclK0mSKDGs/Ixh+dH3J+8GGCGd8CJTnQfCzGZIBf4I7re8QkeNsVb -----END CERTIFICATE----- From b51b75bd50fcdcb242e88dca1a6c48917dd21c88 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Mon, 8 May 2023 10:29:25 +0200 Subject: [PATCH 036/225] Add subconversation ID to RemoteMLSMessage (#3270) --- changelog.d/6-federation/FS-1868 | 1 + .../src/Wire/API/Federation/API/Galley.hs | 1 + services/galley/src/Galley/API/Federation.hs | 2 +- .../galley/src/Galley/API/MLS/Propagate.hs | 1 + services/galley/test/integration/API/MLS.hs | 61 +++++++++++++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6-federation/FS-1868 diff --git a/changelog.d/6-federation/FS-1868 b/changelog.d/6-federation/FS-1868 new file mode 100644 index 00000000000..208bebcf0c3 --- /dev/null +++ b/changelog.d/6-federation/FS-1868 @@ -0,0 +1 @@ +Add subconversation ID to onMLSMessageSent request payload. 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 06d3217cbdc..c649449d5a5 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 @@ -353,6 +353,7 @@ data RemoteMLSMessage = RemoteMLSMessage rmmMetadata :: MessageMetadata, rmmSender :: Qualified UserId, rmmConversation :: ConvId, + rmmSubConversation :: Maybe SubConvId, rmmRecipients :: [(UserId, ClientId)], rmmMessage :: Base64ByteString } diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 389b9a4c225..cdbcdd67d2b 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -783,7 +783,7 @@ onMLSMessageSent domain rmm = let recipients = filter (\(u, _) -> Set.member u members) (F.rmmRecipients rmm) -- FUTUREWORK: support local bots let e = - Event (tUntagged rcnv) Nothing (F.rmmSender rmm) (F.rmmTime rmm) $ + Event (tUntagged rcnv) (F.rmmSubConversation rmm) (F.rmmSender rmm) (F.rmmTime rmm) $ EdMLSMessage (fromBase64ByteString (F.rmmMessage rmm)) let mkPush :: (UserId, ClientId) -> MessagePush 'NormalMessage mkPush uc = newMessagePush loc mempty Nothing (F.rmmMetadata rmm) uc e diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 10d0dcedebe..dc58e9b350c 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -95,6 +95,7 @@ propagateMessage qusr lConvOrSub con msg cm = do rmmSender = qusr, rmmMetadata = mm, rmmConversation = qUnqualified qcnv, + rmmSubConversation = sconv, rmmRecipients = rs >>= remoteMemberMLSClients, rmmMessage = Base64ByteString msg.raw } diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index d25796947d8..14d8722f333 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -253,6 +253,10 @@ tests s = test s "delete subconversation as a remote member" (testRemoteMemberDeleteSubConv True), test s "delete subconversation as a remote non-member" (testRemoteMemberDeleteSubConv False), test s "delete parent conversation of a remote subconversation" testDeleteRemoteParentOfSubConv + ], + testGroup + "Remote Sender/Remote SubConversation" + [ test s "on-mls-message-sent in subconversation" testRemoteToRemoteInSub ] ], testGroup @@ -1206,6 +1210,7 @@ testRemoteToRemote = do rmmMetadata = defMessageMetadata, rmmSender = qbob, rmmConversation = conv, + rmmSubConversation = Nothing, rmmRecipients = rcpts, rmmMessage = Base64ByteString txt } @@ -1221,6 +1226,62 @@ testRemoteToRemote = do -- eve should not receive the message WS.assertNoEvent (1 # Second) [wsE] +testRemoteToRemoteInSub :: TestM () +testRemoteToRemoteInSub = do + localDomain <- viewFederationDomain + c <- view tsCannon + alice <- randomUser + eve <- randomUser + bob <- randomId + conv <- randomId + let subConvId = SubConvId "conference" + aliceC1 = newClientId 0 + aliceC2 = newClientId 1 + eveC = newClientId 0 + bdom = Domain "bob.example.com" + qconv = Qualified conv bdom + qbob = Qualified bob bdom + qalice = Qualified alice localDomain + now <- liftIO getCurrentTime + fedGalleyClient <- view tsFedGalleyClient + + -- only add alice to the remote conversation + connectWithRemoteUser alice qbob + let cu = + ConversationUpdate + { cuTime = now, + cuOrigUserId = qbob, + cuConvId = conv, + cuAlreadyPresentUsers = [], + cuAction = + SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qalice) roleNameWireMember) + } + runFedClient @"on-conversation-updated" fedGalleyClient bdom cu + + let txt = "Hello from another backend" + rcpts = [(alice, aliceC1), (alice, aliceC2), (eve, eveC)] + rm = + RemoteMLSMessage + { rmmTime = now, + rmmMetadata = defMessageMetadata, + rmmSender = qbob, + rmmConversation = conv, + rmmSubConversation = Just subConvId, + rmmRecipients = rcpts, + rmmMessage = Base64ByteString txt + } + + -- send message to alice and check reception + WS.bracketAsClientRN c [(alice, aliceC1), (alice, aliceC2), (eve, eveC)] $ \[wsA1, wsA2, wsE] -> do + void $ runFedClient @"on-mls-message-sent" fedGalleyClient bdom rm + liftIO $ do + -- alice should receive the message on her first client + WS.assertMatch_ (5 # Second) wsA1 $ \n -> wsAssertMLSMessage (fmap (flip SubConv subConvId) qconv) qbob txt n + WS.assertMatch_ (5 # Second) wsA2 $ \n -> wsAssertMLSMessage (fmap (flip SubConv subConvId) qconv) qbob txt n + + -- eve should not receive the message + WS.assertNoEvent (1 # Second) [wsE] + testRemoteToLocal :: TestM () testRemoteToLocal = do -- alice is local, bob is remote From b5aa0814e1d74e4b648221c5fb1f9766780e8f47 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 9 May 2023 13:16:01 +0200 Subject: [PATCH 037/225] Add conversation.protocol-update event (#3277) --- integration/integration.cabal | 1 + integration/test/Test/MLS.hs | 30 +++++++++++++++ .../src/Wire/API/Conversation/Protocol.hs | 2 + .../src/Wire/API/Event/Conversation.hs | 9 ++++- .../API/Routes/Public/Galley/Conversation.hs | 6 +-- .../golden/Test/Wire/API/Golden/Manual.hs | 3 +- .../API/Golden/Manual/ConversationEvent.hs | 11 ++++++ ...estObject_Event_conversation_manual_2.json | 17 +++++++++ services/galley/src/Galley/API/Update.hs | 37 +++++++++++++------ services/galley/test/integration/API/MLS.hs | 32 +--------------- 10 files changed, 98 insertions(+), 50 deletions(-) create mode 100644 integration/test/Test/MLS.hs create mode 100644 libs/wire-api/test/golden/testObject_Event_conversation_manual_2.json diff --git a/integration/integration.cabal b/integration/integration.cabal index f6bb83cd21c..b363f1e341f 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -89,6 +89,7 @@ library Test.B2B Test.Brig Test.Demo + Test.MLS Testlib.App Testlib.Assertions Testlib.Cannon diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs new file mode 100644 index 00000000000..f770281681f --- /dev/null +++ b/integration/test/Test/MLS.hs @@ -0,0 +1,30 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +module Test.MLS where + +import qualified API.Galley as Public +import SetupHelpers +import Testlib.Prelude + +testMixedProtocolUpgrade :: HasCallStack => App () +testMixedProtocolUpgrade = do + [alice, bob] <- createAndConnectUsers [ownDomain, ownDomain] + + qcnv <- bindResponseR (Public.postConversation alice noValue Public.defProteus {Public.qualifiedUsers = [bob]}) $ \resp -> do + resp.status `shouldMatchInt` 201 + + withWebSocket alice $ \wsAlice -> do + bindResponse (Public.putConversationProtocol bob qcnv noValue "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 + resp %. "conversation" `shouldMatch` (qcnv %. "id") + resp %. "data.protocol" `shouldMatch` "mixed" + + n <- awaitMatch 3 (\value -> nPayload value %. "type" `isEqual` "conversation.protocol-update") wsAlice + nPayload n %. "data.protocol" `shouldMatch` "mixed" + + bindResponse (Public.getConversation alice qcnv) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp %. "protocol" `shouldMatch` "mixed" + + bindResponse (Public.putConversationProtocol alice qcnv noValue "mixed") $ \resp -> do + resp.status `shouldMatchInt` 204 diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 4ee31fc2cdb..15f635b08cc 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -158,6 +158,8 @@ protocolDataSchema ProtocolMLSTag = tag _ProtocolMLS mlsDataSchema protocolDataSchema ProtocolMixedTag = tag _ProtocolMixed mlsDataSchema newtype ProtocolUpdate = ProtocolUpdate {unProtocolUpdate :: ProtocolTag} + deriving (Show, Eq, Generic) + deriving (Arbitrary) via GenericUniform ProtocolUpdate instance ToSchema ProtocolUpdate where schema = object "ProtocolUpdate" (ProtocolUpdate <$> unProtocolUpdate .= protocolTagSchema) diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index e831835eee2..2b0340d0549 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -81,6 +81,7 @@ import qualified Test.QuickCheck as QC import URI.ByteString () import Wire.API.Conversation import Wire.API.Conversation.Code (ConversationCode (..), ConversationCodeInfo) +import qualified Wire.API.Conversation.Protocol as P import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.MLS.SubConversation @@ -133,6 +134,7 @@ data EventType | MLSMessageAdd | MLSWelcome | Typing + | ProtocolUpdate deriving stock (Eq, Show, Generic, Enum, Bounded, Ord) deriving (Arbitrary) via (GenericUniform EventType) deriving (FromJSON, ToJSON, S.ToSchema) via Schema EventType @@ -156,7 +158,8 @@ instance ToSchema EventType where element "conversation.typing" Typing, element "conversation.otr-message-add" OtrMessageAdd, element "conversation.mls-message-add" MLSMessageAdd, - element "conversation.mls-welcome" MLSWelcome + element "conversation.mls-welcome" MLSWelcome, + element "conversation.protocol-update" ProtocolUpdate ] data EventData @@ -176,6 +179,7 @@ data EventData | EdOtrMessage OtrMessage | EdMLSMessage ByteString | EdMLSWelcome ByteString + | EdProtocolUpdate P.ProtocolUpdate deriving stock (Eq, Show, Generic) genEventData :: EventType -> QC.Gen EventData @@ -196,6 +200,7 @@ genEventData = \case MLSMessageAdd -> EdMLSMessage <$> arbitrary MLSWelcome -> EdMLSWelcome <$> arbitrary ConvDelete -> pure EdConvDelete + ProtocolUpdate -> EdProtocolUpdate <$> arbitrary eventDataType :: EventData -> EventType eventDataType (EdMembersJoin _) = MemberJoin @@ -214,6 +219,7 @@ eventDataType (EdOtrMessage _) = OtrMessageAdd eventDataType (EdMLSMessage _) = MLSMessageAdd eventDataType (EdMLSWelcome _) = MLSWelcome eventDataType EdConvDelete = ConvDelete +eventDataType (EdProtocolUpdate _) = ProtocolUpdate -------------------------------------------------------------------------------- -- Event data helpers @@ -394,6 +400,7 @@ taggedEventDataSchema = Typing -> tag _EdTyping (unnamed schema) ConvCodeDelete -> tag _EdConvCodeDelete null_ ConvDelete -> tag _EdConvDelete null_ + ProtocolUpdate -> tag _EdProtocolUpdate (unnamed schema) instance ToSchema Event where schema = object "Event" eventObjectSchema diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index b0da0f7509e..173956dc097 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -1260,9 +1260,5 @@ type ConversationAPI = :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId :> "protocol" :> ReqBody '[JSON] ProtocolUpdate - :> MultiVerb - 'PUT - '[JSON] - '[RespondEmpty 200 "Update successful"] - () + :> MultiVerb 'PUT '[Servant.JSON] ConvUpdateResponses (UpdateResult Event) ) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs index f1c4c996f6e..e1374288ffe 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs @@ -73,7 +73,8 @@ tests = ], testGroup "ConversationEvent" $ testObjects - [ (testObject_Event_conversation_manual_1, "testObject_Event_conversation_manual_1.json") + [ (testObject_Event_conversation_manual_1, "testObject_Event_conversation_manual_1.json"), + (testObject_Event_conversation_manual_2, "testObject_Event_conversation_manual_2.json") ], testGroup "GetPaginatedConversationIds" $ testObjects diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs index 688b476b1fd..7f711fd1f4c 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs @@ -23,6 +23,7 @@ import Data.Qualified (Qualified (..)) import Data.Time import qualified Data.UUID as UUID import Imports +import qualified Wire.API.Conversation.Protocol as P import Wire.API.Event.Conversation import Wire.API.MLS.SubConversation @@ -35,3 +36,13 @@ testObject_Event_conversation_manual_1 = evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, evtData = EdConvCodeDelete } + +testObject_Event_conversation_manual_2 :: Event +testObject_Event_conversation_manual_2 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "example.com"}}, + evtSubConv = Nothing, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "a471447c-aa30-4592-81b0-dec6c1c02bca")), qDomain = Domain {_domainText = "example.com"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = EdProtocolUpdate (P.ProtocolUpdate P.ProtocolMixedTag) + } diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_manual_2.json b/libs/wire-api/test/golden/testObject_Event_conversation_manual_2.json new file mode 100644 index 00000000000..2241f2df415 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_manual_2.json @@ -0,0 +1,17 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": { + "protocol": "mixed" + }, + "from": "a471447c-aa30-4592-81b0-dec6c1c02bca", + "qualified_conversation": { + "domain": "example.com", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "example.com", + "id": "a471447c-aa30-4592-81b0-dec6c1c02bca" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.protocol-update" +} diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 39297cf6b20..6a47f1d9aac 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -124,7 +124,8 @@ import System.Logger (Msg) import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Action import Wire.API.Conversation.Code -import Wire.API.Conversation.Protocol (ProtocolTag (..), ProtocolUpdate (ProtocolUpdate), protocolTag) +import Wire.API.Conversation.Protocol (ProtocolTag (..), protocolTag) +import qualified Wire.API.Conversation.Protocol as P import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Error @@ -688,17 +689,20 @@ updateConversationProtocolWithLocalUser :: Member (ErrorS 'ConvInvalidProtocolTransition) r, Member (ErrorS 'ConvMemberNotFound) r, Member (Error FederationError) r, + Member (Input UTCTime) r, + Member GundeckAccess r, + Member ExternalAccess r, Member ConversationStore r ) => Local UserId -> ConnId -> Qualified ConvId -> - ProtocolUpdate -> - Sem r () -updateConversationProtocolWithLocalUser lusr _conn qcnv update = + P.ProtocolUpdate -> + Sem r (UpdateResult Event) +updateConversationProtocolWithLocalUser lusr conn qcnv update = foldQualified lusr - (\lcnv -> updateLocalConversationProtocol (tUntagged lusr) lcnv update) + (\lcnv -> updateLocalConversationProtocol (tUntagged lusr) (Just conn) lcnv update) (\_rcnv -> throw FederationNotImplemented) qcnv @@ -707,24 +711,33 @@ updateLocalConversationProtocol :: ( Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvInvalidProtocolTransition) r, Member (ErrorS 'ConvMemberNotFound) r, + Member (Input UTCTime) r, + Member GundeckAccess r, + Member ExternalAccess r, Member ConversationStore r ) => Qualified UserId -> + Maybe ConnId -> Local ConvId -> - ProtocolUpdate -> - Sem r () -updateLocalConversationProtocol qusr lcnv (ProtocolUpdate newProtocol) = do + P.ProtocolUpdate -> + Sem r (UpdateResult Event) +updateLocalConversationProtocol qusr mconn lcnv protocolUpdate@(P.ProtocolUpdate newProtocol) = do conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound void $ ensureOtherMember lcnv qusr conv case (protocolTag (convProtocol conv), newProtocol) of - (ProtocolProteusTag, ProtocolMixedTag) -> + (ProtocolProteusTag, ProtocolMixedTag) -> do E.updateToMixedProtocol lcnv MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + let (bots, users) = localBotsAndUsers $ Data.convLocalMembers conv + now <- input + let e = Event (tUntagged lcnv) Nothing qusr now (EdProtocolUpdate protocolUpdate) + pushConversationEvent mconn e (qualifyAs lcnv (map lmId users)) bots + pure (Updated e) (ProtocolProteusTag, ProtocolProteusTag) -> - pure () + pure Unchanged (ProtocolMixedTag, ProtocolMixedTag) -> - pure () + pure Unchanged (ProtocolMLSTag, ProtocolMLSTag) -> - pure () + pure Unchanged (_, _) -> throwS @'ConvInvalidProtocolTransition joinConversationByReusableCode :: diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 14d8722f333..627c4ebc2da 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -261,8 +261,7 @@ tests s = ], testGroup "MixedProtocol" - [ test s "Upgrade a conv from proteus to mixed" testMixedUpgrade, - test s "Add clients to a mixed conversation and send proteus message" testMixedAddClients + [ test s "Add clients to a mixed conversation and send proteus message" testMixedAddClients ] ] @@ -3468,35 +3467,6 @@ testCreatorRemovesUserFromParent = do (sort [alice1, charlie1, charlie2]) (sort $ pscMembers sub2) -testMixedUpgrade :: TestM () -testMixedUpgrade = do - [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) - - runMLSTest $ do - [alice1] <- traverse createMLSClient [alice] - - qcnv <- - cnvQualifiedId - <$> liftTest - ( postConvQualified (qUnqualified alice) Nothing defNewProteusConv {newConvQualifiedUsers = [bob]} - >>= responseJsonError - ) - - putConversationProtocol (qUnqualified alice) (ciClient alice1) qcnv ProtocolMixedTag - !!! const 200 === statusCode - - conv <- - responseJsonError - =<< getConvQualified (qUnqualified alice) qcnv - Date: Thu, 11 May 2023 15:18:08 +0200 Subject: [PATCH 038/225] Port MLS test framework to new integration suite (#3286) * Move some MLS tests to new integration suite * Add CHANGELOG entry --- changelog.d/5-internal/mls-tests | 1 + integration/test/Test/MLS.hs | 110 ++++++++++++++++- services/galley/test/integration/API/MLS.hs | 123 +------------------- 3 files changed, 109 insertions(+), 125 deletions(-) create mode 100644 changelog.d/5-internal/mls-tests diff --git a/changelog.d/5-internal/mls-tests b/changelog.d/5-internal/mls-tests new file mode 100644 index 00000000000..2320d5ebf67 --- /dev/null +++ b/changelog.d/5-internal/mls-tests @@ -0,0 +1 @@ +Move some MLS tests to new integration suite diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index f770281681f..e9f2c19b704 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -2,7 +2,10 @@ module Test.MLS where -import qualified API.Galley as Public +import API.Galley +import qualified Data.ByteString.Base64 as Base64 +import qualified Data.ByteString.Char8 as B8 +import MLS.Util import SetupHelpers import Testlib.Prelude @@ -10,11 +13,11 @@ testMixedProtocolUpgrade :: HasCallStack => App () testMixedProtocolUpgrade = do [alice, bob] <- createAndConnectUsers [ownDomain, ownDomain] - qcnv <- bindResponseR (Public.postConversation alice noValue Public.defProteus {Public.qualifiedUsers = [bob]}) $ \resp -> do + qcnv <- bindResponseR (postConversation alice noValue defProteus {qualifiedUsers = [bob]}) $ \resp -> do resp.status `shouldMatchInt` 201 withWebSocket alice $ \wsAlice -> do - bindResponse (Public.putConversationProtocol bob qcnv noValue "mixed") $ \resp -> do + bindResponse (putConversationProtocol bob qcnv noValue "mixed") $ \resp -> do resp.status `shouldMatchInt` 200 resp %. "conversation" `shouldMatch` (qcnv %. "id") resp %. "data.protocol" `shouldMatch` "mixed" @@ -22,9 +25,106 @@ testMixedProtocolUpgrade = do n <- awaitMatch 3 (\value -> nPayload value %. "type" `isEqual` "conversation.protocol-update") wsAlice nPayload n %. "data.protocol" `shouldMatch` "mixed" - bindResponse (Public.getConversation alice qcnv) $ \resp -> do + bindResponse (getConversation alice qcnv) $ \resp -> do resp.status `shouldMatchInt` 200 resp %. "protocol" `shouldMatch` "mixed" - bindResponse (Public.putConversationProtocol alice qcnv noValue "mixed") $ \resp -> do + bindResponse (putConversationProtocol alice qcnv noValue "mixed") $ \resp -> do resp.status `shouldMatchInt` 204 + +testAddUser :: HasCallStack => App () +testAddUser = do + [alice, bob] <- createAndConnectUsers [ownDomain, ownDomain] + + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + + traverse_ uploadNewKeyPackage [bob1, bob2] + + (_, qcnv) <- setupMLSGroup alice1 + + resp <- createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + events <- resp %. "events" & asList + do + event <- assertOne events + shouldMatch (event %. "qualified_conversation") qcnv + shouldMatch (event %. "type") "conversation.member-join" + shouldMatch (event %. "from") (objId alice) + members <- event %. "data" %. "users" & asList + memberQids <- for members $ \mem -> mem %. "qualified_id" + bobQid <- bob %. "qualified_id" + shouldMatch memberQids [bobQid] + + -- check that bob can now see the conversation + convs <- getAllConvs bob + convIds <- traverse (%. "qualified_id") convs + void $ + assertBool + "Users added to an MLS group should find it when listing conversations" + (qcnv `elem` convIds) + +testCreateSubConv :: HasCallStack => App () +testCreateSubConv = do + alice <- randomUser ownDomain def + alice1 <- createMLSClient alice + (_, conv) <- setupMLSGroup alice1 + bindResponse (getSubConversation alice conv "conference") $ \resp -> do + resp.status `shouldMatchInt` 200 + let tm = resp.json %. "epoch_timestamp" + tm `shouldMatch` Null + +testCreateSubConvProteus :: App () +testCreateSubConvProteus = do + alice <- randomUser ownDomain def + conv <- bindResponse (postConversation alice noValue defProteus) $ \resp -> do + resp.status `shouldMatchInt` 201 + resp.json + bindResponse (getSubConversation alice conv "conference") $ \resp -> + resp.status `shouldMatchInt` 404 + +-- FUTUREWORK: New clients should be adding themselves via external commits, and +-- they shouldn't be added by another client. Change the test so external +-- commits are used. +testSelfConversation :: App () +testSelfConversation = do + alice <- randomUser ownDomain def + creator : others <- traverse createMLSClient (replicate 3 alice) + traverse_ uploadNewKeyPackage others + void $ setupMLSSelfGroup creator + commit <- createAddCommit creator [alice] + welcome <- assertOne (toList commit.welcome) + + withWebSockets others $ \wss -> do + void $ sendAndConsumeCommitBundle commit + let isWelcome n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" + for_ wss $ \ws -> do + n <- awaitMatch 3 isWelcome ws + shouldMatch (nPayload n %. "conversation") (objId alice) + shouldMatch (nPayload n %. "from") (objId alice) + shouldMatch (nPayload n %. "data") (B8.unpack (Base64.encode welcome)) + +testJoinSubConv :: App () +testJoinSubConv = do + [alice, bob] <- createAndConnectUsers [ownDomain, ownDomain] + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + sub <- bindResponse (getSubConversation bob qcnv "conference") $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json + resetGroup bob1 sub + + -- bob adds his first client to the subconversation + void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle + sub' <- bindResponse (getSubConversation bob qcnv "conference") $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json + do + tm <- sub' %. "epoch_timestamp" + assertBool "Epoch timestamp should not be null" (tm /= Null) + + -- now alice joins with her own client + void $ + createExternalCommit alice1 Nothing + >>= sendAndConsumeCommitBundle diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 627c4ebc2da..c236cb8a4bb 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -100,8 +100,7 @@ tests s = ], testGroup "Commit" - [ test s "add user to a conversation" testAddUser, - test s "add user (not connected)" testAddUserNotConnected, + [ test s "add user (not connected)" testAddUserNotConnected, test s "add user (partial client list)" testAddUserPartial, test s "add client of existing user" testAddClientPartial, test s "add user with some non-MLS clients" testAddUserWithProteusClients, @@ -194,8 +193,7 @@ tests s = ], testGroup "Self conversation" - [ test s "create a self conversation" testSelfConversation, - test s "do not list a self conversation below v3" $ testSelfConversationList True, + [ test s "do not list a self conversation below v3" $ testSelfConversationList True, test s "list a self conversation automatically from v3" $ testSelfConversationList False, test s "listing conversations without MLS configured" testSelfConversationMLSNotConfigured, test s "attempt to add another user to a conversation fails" testSelfConversationOtherUser, @@ -213,10 +211,7 @@ tests s = "SubConversation" [ testGroup "Local Sender/Local Subconversation" - [ test s "get subconversation of MLS conv - 200" (testCreateSubConv True), - test s "get subconversation of Proteus conv - 404" (testCreateSubConv False), - test s "join subconversation with an external commit bundle" testJoinSubConv, - test s "rejoin a subconversation with the same client" testExternalCommitSameClientSubConv, + [ test s "rejoin a subconversation with the same client" testExternalCommitSameClientSubConv, test s "join subconversation with a client that is not in the parent conv" testJoinSubNonMemberClient, test s "fail to add another client to a subconversation via internal commit" testAddClientSubConvFailure, test s "remove another client from a subconversation" testRemoveClientSubConv, @@ -383,28 +378,6 @@ testAddUserWithBundle = do liftIO $ assertBool "Commit does not contain a public group State" (isJust (mpGroupInfo commit)) liftIO $ mpGroupInfo commit @?= Just returnedGS -testAddUser :: TestM () -testAddUser = do - [alice, bob] <- createAndConnectUsers [Nothing, Nothing] - - qcnv <- runMLSTest $ do - [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] - - traverse_ uploadNewKeyPackage [bob1, bob2] - - (_, qcnv) <- setupMLSGroup alice1 - events <- createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - event <- assertOne events - liftIO $ assertJoinEvent qcnv alice [bob] roleNameWireMember event - pure qcnv - - -- check that bob can now see the conversation - convs <- getAllConvs (qUnqualified bob) - liftIO $ - assertBool - "Users added to an MLS group should find it when listing conversations" - (qcnv `elem` map cnvQualifiedId convs) - testAddUserNotConnected :: TestM () testAddUserNotConnected = do users@[alice, bob] <- replicateM 2 randomQualifiedUser @@ -2165,24 +2138,6 @@ testRemoteUserPostsCommitBundle = do pure () --- FUTUREWORK: New clients should be adding themselves via external commits, and --- they shouldn't be added by another client. Change the test so external --- commits are used. -testSelfConversation :: TestM () -testSelfConversation = do - alice <- randomQualifiedUser - runMLSTest $ do - creator : others <- traverse createMLSClient (replicate 3 alice) - traverse_ uploadNewKeyPackage others - void $ setupMLSSelfGroup creator - commit <- createAddCommit creator [alice] - welcome <- assertJust (mpWelcome commit) - mlsBracket others $ \wss -> do - void $ sendAndConsumeCommitBundle commit - WS.assertMatchN_ (5 # Second) wss $ - wsAssertMLSWelcome alice welcome - WS.assertNoEvent (1 # WS.Second) wss - -- | The MLS self-conversation should be available even without explicitly -- creating it by calling `GET /conversations/mls-self` starting from version 3 -- of the client API and should not be listed in versions less than 3. @@ -2320,78 +2275,6 @@ deleteSubConversationDisabled = do withMLSDisabled $ deleteSubConv alice cnvId scnvId dsc !!! assertMLSNotEnabled -testCreateSubConv :: Bool -> TestM () -testCreateSubConv parentIsMLSConv = do - alice <- randomQualifiedUser - runMLSTest $ do - qcnv <- - if parentIsMLSConv - then do - creator <- createMLSClient alice - (_, qcnv) <- setupMLSGroup creator - pure qcnv - else - cnvQualifiedId - <$> liftTest - ( postConvQualified (qUnqualified alice) Nothing defNewProteusConv - >>= responseJsonError - ) - let sconv = SubConvId "conference" - if parentIsMLSConv - then do - sub <- - liftTest $ - responseJsonError - =<< getSubConv (qUnqualified alice) qcnv sconv - >= sendAndConsumeCommitBundle - - let subId = SubConvId "conference" - sub <- - liftTest $ - responseJsonError - =<< getSubConv (qUnqualified bob) qcnv subId - >= sendAndConsumeCommitBundle - subAfter <- - liftTest $ - responseJsonError - =<< getSubConv (qUnqualified bob) qcnv subId - >= sendAndConsumeCommitBundle - testExternalCommitSameClientSubConv :: TestM () testExternalCommitSameClientSubConv = do [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) From e39664804e7c45f59fc236367d1ea00bb61bca9d Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 15 May 2023 13:30:41 +0200 Subject: [PATCH 039/225] Refactor protocol update as conversation actions (#3292) --- changelog.d/5-internal/mixed-protocol | 1 + integration/test/Test/MLS.hs | 20 ++++-- .../src/Wire/API/Conversation/Action.hs | 5 ++ .../src/Wire/API/Conversation/Action/Tag.hs | 4 +- .../src/Wire/API/Event/Conversation.hs | 5 +- .../API/Routes/Public/Galley/Conversation.hs | 3 +- .../API/Golden/Manual/ConversationEvent.hs | 2 +- services/galley/src/Galley/API/Action.hs | 24 +++++++ services/galley/src/Galley/API/Federation.hs | 5 ++ services/galley/src/Galley/API/Update.hs | 68 +++++++------------ .../galley/test/integration/API/Federation.hs | 2 + 11 files changed, 86 insertions(+), 53 deletions(-) create mode 100644 changelog.d/5-internal/mixed-protocol diff --git a/changelog.d/5-internal/mixed-protocol b/changelog.d/5-internal/mixed-protocol new file mode 100644 index 00000000000..235a1f2ac41 --- /dev/null +++ b/changelog.d/5-internal/mixed-protocol @@ -0,0 +1 @@ +Add intermediate "mixed" protocol for migrating from Proteus to MLS diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 7a83931f0e5..7bec6c7d9b2 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -10,19 +10,29 @@ import SetupHelpers import Testlib.Prelude testMixedProtocolUpgrade :: HasCallStack => App () -testMixedProtocolUpgrade = do - [alice, bob] <- createAndConnectUsers [ownDomain, ownDomain] +testMixedProtocolUpgrade = ptestMixedProtocolUpgrade ownDomain + +testMixedProtocolUpgradeFed :: HasCallStack => App () +testMixedProtocolUpgradeFed = ptestMixedProtocolUpgrade otherDomain + +ptestMixedProtocolUpgrade :: (HasCallStack, MakesValue domain) => domain -> App () +ptestMixedProtocolUpgrade secondDomain = do + [alice, bob, charlie] <- do + d <- ownDomain + d2 <- secondDomain & asString + createAndConnectUsers [d, d2, d2] qcnv <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 - withWebSocket alice $ \wsAlice -> do + withWebSockets [alice, charlie] $ \websockets -> do bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "conversation" `shouldMatch` (qcnv %. "id") resp.json %. "data.protocol" `shouldMatch` "mixed" - n <- awaitMatch 3 (\value -> nPayload value %. "type" `isEqual` "conversation.protocol-update") wsAlice - nPayload n %. "data.protocol" `shouldMatch` "mixed" + for_ websockets $ \ws -> do + n <- awaitMatch 3 (\value -> nPayload value %. "type" `isEqual` "conversation.protocol-update") ws + nPayload n %. "data.protocol" `shouldMatch` "mixed" bindResponse (getConversation alice qcnv) $ \resp -> do resp.status `shouldMatchInt` 200 diff --git a/libs/wire-api/src/Wire/API/Conversation/Action.hs b/libs/wire-api/src/Wire/API/Conversation/Action.hs index 5d98193fd37..c7a1c0e0ad8 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action.hs @@ -46,6 +46,7 @@ import Data.Time.Clock import Imports import Wire.API.Conversation import Wire.API.Conversation.Action.Tag +import Wire.API.Conversation.Protocol (ProtocolTag) import Wire.API.Conversation.Role import Wire.API.Event.Conversation import Wire.API.MLS.SubConversation @@ -63,6 +64,7 @@ type family ConversationAction (tag :: ConversationActionTag) :: Type where ConversationAction 'ConversationReceiptModeUpdateTag = ConversationReceiptModeUpdate ConversationAction 'ConversationAccessDataTag = ConversationAccessData ConversationAction 'ConversationRemoveMembersTag = NonEmptyList.NonEmpty (Qualified UserId) + ConversationAction 'ConversationUpdateProtocolTag = ProtocolTag data SomeConversationAction where SomeConversationAction :: Sing tag -> ConversationAction tag -> SomeConversationAction @@ -105,6 +107,7 @@ conversationActionSchema SConversationRenameTag = schema conversationActionSchema SConversationMessageTimerUpdateTag = schema conversationActionSchema SConversationReceiptModeUpdateTag = schema conversationActionSchema SConversationAccessDataTag = schema +conversationActionSchema SConversationUpdateProtocolTag = schema instance FromJSON SomeConversationAction where parseJSON = A.withObject "SomeConversationAction" $ \ob -> do @@ -136,6 +139,7 @@ $( singletons conversationActionPermission ConversationMessageTimerUpdateTag = ModifyConversationMessageTimer conversationActionPermission ConversationReceiptModeUpdateTag = ModifyConversationReceiptMode conversationActionPermission ConversationAccessDataTag = ModifyConversationAccess + conversationActionPermission ConversationUpdateProtocolTag = LeaveConversation |] ) @@ -166,4 +170,5 @@ conversationActionToEvent tag now quid qcnv subconv action = SConversationMessageTimerUpdateTag -> EdConvMessageTimerUpdate action SConversationReceiptModeUpdateTag -> EdConvReceiptModeUpdate action SConversationAccessDataTag -> EdConvAccessUpdate action + SConversationUpdateProtocolTag -> EdProtocolUpdate action in Event qcnv subconv quid now edata diff --git a/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs b/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs index 3445e3794ff..00e46cdfdf5 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs @@ -38,6 +38,7 @@ data ConversationActionTag | ConversationMessageTimerUpdateTag | ConversationReceiptModeUpdateTag | ConversationAccessDataTag + | ConversationUpdateProtocolTag deriving (Show, Eq, Generic, Bounded, Enum) instance Arbitrary ConversationActionTag where @@ -55,7 +56,8 @@ instance ToSchema ConversationActionTag where element "ConversationRenameTag" ConversationRenameTag, element "ConversationMessageTimerUpdateTag" ConversationMessageTimerUpdateTag, element "ConversationReceiptModeUpdateTag" ConversationReceiptModeUpdateTag, - element "ConversationAccessDataTag" ConversationAccessDataTag + element "ConversationAccessDataTag" ConversationAccessDataTag, + element "ConversationUpdateProtocolTag" ConversationUpdateProtocolTag ] instance ToJSON ConversationActionTag where diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index 2b0340d0549..843de0020c4 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -81,6 +81,7 @@ import qualified Test.QuickCheck as QC import URI.ByteString () import Wire.API.Conversation import Wire.API.Conversation.Code (ConversationCode (..), ConversationCodeInfo) +import Wire.API.Conversation.Protocol (ProtocolUpdate (unProtocolUpdate)) import qualified Wire.API.Conversation.Protocol as P import Wire.API.Conversation.Role import Wire.API.Conversation.Typing @@ -179,7 +180,7 @@ data EventData | EdOtrMessage OtrMessage | EdMLSMessage ByteString | EdMLSWelcome ByteString - | EdProtocolUpdate P.ProtocolUpdate + | EdProtocolUpdate P.ProtocolTag deriving stock (Eq, Show, Generic) genEventData :: EventType -> QC.Gen EventData @@ -400,7 +401,7 @@ taggedEventDataSchema = Typing -> tag _EdTyping (unnamed schema) ConvCodeDelete -> tag _EdConvCodeDelete null_ ConvDelete -> tag _EdConvDelete null_ - ProtocolUpdate -> tag _EdProtocolUpdate (unnamed schema) + ProtocolUpdate -> tag _EdProtocolUpdate (unnamed (unProtocolUpdate <$> P.ProtocolUpdate .= schema)) instance ToSchema Event where schema = object "Event" eventObjectSchema diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 173956dc097..cdd91ba959b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -1253,7 +1253,8 @@ type ConversationAPI = :> Description "**Note**: Only proteus->mixed upgrade is supported." :> CanThrow 'ConvNotFound :> CanThrow 'ConvInvalidProtocolTransition - :> CanThrow 'ConvMemberNotFound + :> CanThrow ('ActionDenied 'LeaveConversation) + :> CanThrow 'InvalidOperation :> ZLocalUser :> ZConn :> "conversations" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs index 7f711fd1f4c..6132fec18a6 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs @@ -44,5 +44,5 @@ testObject_Event_conversation_manual_2 = evtSubConv = Nothing, evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "a471447c-aa30-4592-81b0-dec6c1c02bca")), qDomain = Domain {_domainText = "example.com"}}, evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, - evtData = EdProtocolUpdate (P.ProtocolUpdate P.ProtocolMixedTag) + evtData = EdProtocolUpdate P.ProtocolMixedTag } diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 48e0e3c6a2f..e2cb8b7d7de 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -98,6 +98,7 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API (Component (Galley), fedClient) import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.MLS.CipherSuite import Wire.API.Team.LegalHold import Wire.API.Team.Member import qualified Wire.API.User as User @@ -209,6 +210,11 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con ( Member ConversationStore r, Member (Error NoChanges) r ) + HasConversationActionEffects 'ConversationUpdateProtocolTag r = + ( Member ConversationStore r, + Member (ErrorS 'ConvInvalidProtocolTransition) r, + Member (Error NoChanges) r + ) type family HasConversationActionGalleyErrors (tag :: ConversationActionTag) :: EffectRow where HasConversationActionGalleyErrors 'ConversationJoinTag = @@ -266,6 +272,12 @@ type family HasConversationActionGalleyErrors (tag :: ConversationActionTag) :: ErrorS 'InvalidTargetAccess, ErrorS 'ConvNotFound ] + HasConversationActionGalleyErrors 'ConversationUpdateProtocolTag = + '[ ErrorS ('ActionDenied 'LeaveConversation), + ErrorS 'InvalidOperation, + ErrorS 'ConvNotFound, + ErrorS 'ConvInvalidProtocolTransition + ] noChanges :: Member (Error NoChanges) r => Sem r a noChanges = throw NoChanges @@ -384,6 +396,18 @@ performAction tag origUser lconv action = do SConversationAccessDataTag -> do (bm, act) <- performConversationAccessData origUser lconv action pure (bm, act) + SConversationUpdateProtocolTag -> do + case (protocolTag (convProtocol (tUnqualified lconv)), action) of + (ProtocolProteusTag, ProtocolMixedTag) -> do + E.updateToMixedProtocol lcnv MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + pure (mempty, action) + (ProtocolProteusTag, ProtocolProteusTag) -> + noChanges + (ProtocolMixedTag, ProtocolMixedTag) -> + noChanges + (ProtocolMLSTag, ProtocolMLSTag) -> + noChanges + (_, _) -> throwS @'ConvInvalidProtocolTransition performConversationJoin :: ( HasConversationActionEffects 'ConversationJoinTag r diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 83fca15d352..c916c0376f4 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -522,6 +522,11 @@ updateConversation origDomain updateRequest = do @(HasConversationActionGalleyErrors 'ConversationAccessDataTag) . fmap lcuUpdate $ updateLocalConversation @'ConversationAccessDataTag lcnv (tUntagged rusr) Nothing action + SConversationUpdateProtocolTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationUpdateProtocolTag) + . fmap lcuUpdate + $ updateLocalConversation @'ConversationUpdateProtocolTag lcnv (tUntagged rusr) Nothing action where mkResponse = fmap toResponse . runError @GalleyError . runError @NoChanges diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index b1002a06386..d8eb5970e0d 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -133,7 +133,6 @@ import Wire.API.Connection (Relation (Accepted)) import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Action import Wire.API.Conversation.Code -import Wire.API.Conversation.Protocol (ProtocolTag (..), protocolTag) import qualified Wire.API.Conversation.Protocol as P import Wire.API.Conversation.Role import Wire.API.Conversation.Typing @@ -144,7 +143,6 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import qualified Wire.API.Federation.API.Galley as F import Wire.API.Federation.Error -import Wire.API.MLS.CipherSuite import Wire.API.Message import Wire.API.Password (mkSafePassword) import Wire.API.Provider.Service (ServiceRef) @@ -355,7 +353,9 @@ updateConversationReceiptMode lusr zcon qcnv update = (Just zcon) update ) - (\rcnv -> updateRemoteConversation @'ConversationReceiptModeUpdateTag rcnv lusr zcon update) + ( \rcnv -> + updateRemoteConversation @'ConversationReceiptModeUpdateTag rcnv lusr zcon update + ) qcnv updateRemoteConversation :: @@ -367,7 +367,7 @@ updateRemoteConversation :: Member (Input (Local ())) r, Member MemberStore r, Member TinyLog r, - RethrowErrors (HasConversationActionGalleyErrors tag) (Error NoChanges : r), + RethrowErrors (HasConversationActionGalleyErrors tag) r, SingI tag ) => Remote ConvId -> @@ -385,7 +385,7 @@ updateRemoteConversation rcnv lusr conn action = getUpdateResult $ do response <- E.runFederated rcnv (fedClient @'Galley @"update-conversation" updateRequest) convUpdate <- case response of ConversationUpdateResponseNoChanges -> throw NoChanges - ConversationUpdateResponseError err' -> rethrowErrors @(HasConversationActionGalleyErrors tag) err' + ConversationUpdateResponseError err' -> raise $ rethrowErrors @(HasConversationActionGalleyErrors tag) err' ConversationUpdateResponseUpdate convUpdate -> pure convUpdate updateLocalStateOfRemoteConv (tDomain rcnv) convUpdate @@ -697,11 +697,18 @@ updateConversationProtocolWithLocalUser :: forall r. ( Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvInvalidProtocolTransition) r, - Member (ErrorS 'ConvMemberNotFound) r, + Member (ErrorS ('ActionDenied 'LeaveConversation)) r, + Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member (Input UTCTime) r, + Member (Input (Local ())) r, + Member BrigAccess r, + Member MemberStore r, + Member TinyLog r, Member GundeckAccess r, Member ExternalAccess r, + Member FederatorAccess r, + Member SubConversationStore r, Member ConversationStore r ) => Local UserId -> @@ -709,47 +716,21 @@ updateConversationProtocolWithLocalUser :: Qualified ConvId -> P.ProtocolUpdate -> Sem r (UpdateResult Event) -updateConversationProtocolWithLocalUser lusr conn qcnv update = +updateConversationProtocolWithLocalUser lusr conn qcnv (P.ProtocolUpdate newProtocol) = foldQualified lusr - (\lcnv -> updateLocalConversationProtocol (tUntagged lusr) (Just conn) lcnv update) - (\_rcnv -> throw FederationNotImplemented) + ( \lcnv -> do + fmap (maybe Unchanged (Updated . lcuEvent) . hush) + . runError @NoChanges + . updateLocalConversation @'ConversationUpdateProtocolTag lcnv (tUntagged lusr) (Just conn) + $ newProtocol + ) + ( \rcnv -> + updateRemoteConversation @'ConversationUpdateProtocolTag rcnv lusr conn $ + newProtocol + ) qcnv -updateLocalConversationProtocol :: - forall r. - ( Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'ConvInvalidProtocolTransition) r, - Member (ErrorS 'ConvMemberNotFound) r, - Member (Input UTCTime) r, - Member GundeckAccess r, - Member ExternalAccess r, - Member ConversationStore r - ) => - Qualified UserId -> - Maybe ConnId -> - Local ConvId -> - P.ProtocolUpdate -> - Sem r (UpdateResult Event) -updateLocalConversationProtocol qusr mconn lcnv protocolUpdate@(P.ProtocolUpdate newProtocol) = do - conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound - void $ ensureOtherMember lcnv qusr conv - case (protocolTag (convProtocol conv), newProtocol) of - (ProtocolProteusTag, ProtocolMixedTag) -> do - E.updateToMixedProtocol lcnv MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - let (bots, users) = localBotsAndUsers $ Data.convLocalMembers conv - now <- input - let e = Event (tUntagged lcnv) Nothing qusr now (EdProtocolUpdate protocolUpdate) - pushConversationEvent mconn e (qualifyAs lcnv (map lmId users)) bots - pure (Updated e) - (ProtocolProteusTag, ProtocolProteusTag) -> - pure Unchanged - (ProtocolMixedTag, ProtocolMixedTag) -> - pure Unchanged - (ProtocolMLSTag, ProtocolMLSTag) -> - pure Unchanged - (_, _) -> throwS @'ConvInvalidProtocolTransition - joinConversationByReusableCode :: forall db r. ( Member BrigAccess r, @@ -1750,6 +1731,7 @@ updateLocalStateOfRemoteConv requestingDomain cu = do SConversationMessageTimerUpdateTag -> pure (Just sca, []) SConversationReceiptModeUpdateTag -> pure (Just sca, []) SConversationAccessDataTag -> pure (Just sca, []) + SConversationUpdateProtocolTag -> pure (Just sca, []) unless allUsersArePresent $ P.warn $ diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index d0e6fe37a9c..6229b2c36a3 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -1247,3 +1247,5 @@ getConvAction tquery (SomeConversationAction tag action) = (SConversationAccessDataTag, _) -> Nothing (SConversationRemoveMembersTag, SConversationRemoveMembersTag) -> Just action (SConversationRemoveMembersTag, _) -> Nothing + (SConversationUpdateProtocolTag, SConversationUpdateProtocolTag) -> Just action + (SConversationUpdateProtocolTag, _) -> Nothing From 8a55e2854a2c459e3af9e58112e74c0464f3c816 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 15 May 2023 15:22:24 +0200 Subject: [PATCH 040/225] Endpoint for deleting key packages (#3295) * Add endpoint to delete key packages * Add integration test for key package deletion --- changelog.d/1-api-changes/delete-keypackages | 1 + integration/integration.cabal | 1 + integration/test/API/Brig.hs | 10 +++++++ integration/test/MLS/Util.hs | 10 ++++--- integration/test/Test/MLS/KeyPackage.hs | 21 +++++++++++++++ libs/wire-api/src/Wire/API/MLS/KeyPackage.hs | 16 ++++++++++++ .../src/Wire/API/Routes/Public/Brig.hs | 9 +++++++ services/brig/src/Brig/API/MLS/KeyPackages.hs | 10 +++++++ services/brig/src/Brig/API/Public.hs | 1 + services/brig/src/Brig/Data/MLS/KeyPackage.hs | 26 +++++++++++-------- 10 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 changelog.d/1-api-changes/delete-keypackages create mode 100644 integration/test/Test/MLS/KeyPackage.hs diff --git a/changelog.d/1-api-changes/delete-keypackages b/changelog.d/1-api-changes/delete-keypackages new file mode 100644 index 00000000000..c6ce843eb50 --- /dev/null +++ b/changelog.d/1-api-changes/delete-keypackages @@ -0,0 +1 @@ +Add new endpoint `DELETE /mls/key-packages/self/:client` diff --git a/integration/integration.cabal b/integration/integration.cabal index e94b8cb81aa..09be9908146 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -92,6 +92,7 @@ library Test.Brig Test.Demo Test.MLS + Test.MLS.KeyPackage Testlib.App Testlib.Assertions Testlib.Cannon diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 9a1adb1f0f6..6feac249941 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -168,3 +168,13 @@ claimKeyPackages u v = do baseRequest u Brig Versioned $ "/mls/key-packages/claim/" <> targetDom <> "/" <> targetUid submit "POST" req + +countKeyPackages :: ClientIdentity -> App Response +countKeyPackages cid = do + baseRequest cid Brig Versioned ("/mls/key-packages/self/" <> cid.client <> "/count") + >>= submit "GET" + +deleteKeyPackages :: ClientIdentity -> [String] -> App Response +deleteKeyPackages cid kps = do + req <- baseRequest cid Brig Versioned ("/mls/key-packages/self/" <> cid.client) + submit "DELETE" $ req & addJSONObject ["key_packages" .= kps] diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 606914e3f43..1a54fb4366e 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -16,7 +16,6 @@ import qualified Data.ByteString.Char8 as B8 import Data.Default import Data.Foldable import Data.Function -import Data.Hex import qualified Data.Map as Map import Data.Maybe import qualified Data.Set as Set @@ -155,7 +154,7 @@ uploadNewKeyPackage cid = do generateKeyPackage :: HasCallStack => ClientIdentity -> App (ByteString, String) generateKeyPackage cid = do kp <- mlscli cid ["key-package", "create"] Nothing - ref <- B8.unpack . hex <$> mlscli cid ["key-package", "ref", "-"] (Just kp) + ref <- B8.unpack . Base64.encode <$> mlscli cid ["key-package", "ref", "-"] (Just kp) fp <- keyPackageFile cid ref liftIO $ BS.writeFile fp kp pure (kp, ref) @@ -218,8 +217,13 @@ resetClientGroup cid gid = do keyPackageFile :: HasCallStack => ClientIdentity -> String -> App FilePath keyPackageFile cid ref = do + let ref' = map urlSafe ref bd <- getBaseDir - pure $ bd cid2Str cid ref + pure $ bd cid2Str cid ref' + where + urlSafe '+' = '-' + urlSafe '/' = '_' + urlSafe c = c unbundleKeyPackages :: Value -> App [(ClientIdentity, ByteString)] unbundleKeyPackages bundle = do diff --git a/integration/test/Test/MLS/KeyPackage.hs b/integration/test/Test/MLS/KeyPackage.hs new file mode 100644 index 00000000000..c6649c8838e --- /dev/null +++ b/integration/test/Test/MLS/KeyPackage.hs @@ -0,0 +1,21 @@ +module Test.MLS.KeyPackage where + +import API.Brig +import MLS.Util +import SetupHelpers +import Testlib.Prelude + +testDeleteKeyPackages :: App () +testDeleteKeyPackages = do + alice <- randomUser ownDomain def + alice1 <- createMLSClient alice + kps <- replicateM 3 (uploadNewKeyPackage alice1) + + -- add an extra non-existing key package to the delete request + let kps' = "4B701F521EBE82CEC4AD5CB67FDD8E1C43FC4868DE32D03933CE4993160B75E8" : kps + + bindResponse (deleteKeyPackages alice1 kps') $ \resp -> do + resp.status `shouldMatchInt` 201 + bindResponse (countKeyPackages alice1) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "count" `shouldMatchInt` 0 diff --git a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs index 19e9490993b..1e7403738b1 100644 --- a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs +++ b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs @@ -21,6 +21,7 @@ module Wire.API.MLS.KeyPackage KeyPackageBundleEntry (..), KeyPackageCount (..), KeyPackageData (..), + DeleteKeyPackages (..), KeyPackage (..), keyPackageIdentity, kpRef, @@ -38,6 +39,7 @@ import qualified Data.ByteString.Lazy as LBS import Data.Id import Data.Json.Util import Data.Qualified +import Data.Range import Data.Schema import qualified Data.Swagger as S import GHC.Records @@ -117,6 +119,20 @@ instance ToSchema KeyPackageCount where object "OwnKeyPackages" $ KeyPackageCount <$> unKeyPackageCount .= field "count" schema +newtype DeleteKeyPackages = DeleteKeyPackages + {unDeleteKeyPackages :: [KeyPackageRef]} + deriving newtype (Eq, Ord, Show) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema DeleteKeyPackages + +instance ToSchema DeleteKeyPackages where + schema = + object "DeleteKeyPackages" $ + DeleteKeyPackages + <$> unDeleteKeyPackages + .= field + "key_packages" + (untypedRangedSchema 1 1000 (array schema)) + newtype KeyPackageRef = KeyPackageRef {unKeyPackageRef :: ByteString} deriving stock (Eq, Ord, Show) deriving (FromHttpApiData, ToHttpApiData, S.ToParamSchema) via Base64ByteString diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 75d7ae043a1..bfc3258ac11 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1186,6 +1186,15 @@ type MLSKeyPackageAPI = :> Summary "Return the number of unused key packages for the given client" :> MultiVerb1 'GET '[JSON] (Respond 200 "Number of key packages" KeyPackageCount) ) + :<|> Named + "mls-key-packages-delete" + ( "self" + :> ZLocalUser + :> CaptureClientId "client" + :> Summary "Return the number of unused key packages for the given client" + :> ReqBody '[JSON] DeleteKeyPackages + :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 201 "OK") + ) ) -- Search API ----------------------------------------------------- diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index 53bd3fe1647..982cc17ea1e 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -20,6 +20,7 @@ module Brig.API.MLS.KeyPackages claimKeyPackages, claimLocalKeyPackages, countKeyPackages, + deleteKeyPackages, ) where @@ -137,3 +138,12 @@ countKeyPackages lusr c = do lift $ KeyPackageCount . fromIntegral <$> wrapClient (Data.countKeyPackages lusr c) + +deleteKeyPackages :: + Local UserId -> + ClientId -> + DeleteKeyPackages -> + Handler r () +deleteKeyPackages lusr c (unDeleteKeyPackages -> refs) = do + assertMLSEnabled + lift $ wrapClient (Data.deleteKeyPackages (tUnqualified lusr) c refs) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 4fcb83eaed1..8f593ea2720 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -364,6 +364,7 @@ servantSitemap = Named @"mls-key-packages-upload" uploadKeyPackages :<|> Named @"mls-key-packages-claim" (callsFed (exposeAnnotations claimKeyPackages)) :<|> Named @"mls-key-packages-count" countKeyPackages + :<|> Named @"mls-key-packages-delete" deleteKeyPackages userHandleAPI :: ServerT UserHandleAPI (Handler r) userHandleAPI = diff --git a/services/brig/src/Brig/Data/MLS/KeyPackage.hs b/services/brig/src/Brig/Data/MLS/KeyPackage.hs index 03a69e69ab2..fc3c4183bce 100644 --- a/services/brig/src/Brig/Data/MLS/KeyPackage.hs +++ b/services/brig/src/Brig/Data/MLS/KeyPackage.hs @@ -19,6 +19,7 @@ module Brig.Data.MLS.KeyPackage ( insertKeyPackages, claimKeyPackage, countKeyPackages, + deleteKeyPackages, ) where @@ -66,12 +67,12 @@ claimKeyPackage u c = do kps <- getNonClaimedKeyPackages u c mk <- liftIO (pick kps) for mk $ \(ref, kpd) -> do - retry x5 $ write deleteByRef (params LocalQuorum (tUnqualified u, c, ref)) + retry x5 $ write delete1Query (params LocalQuorum (tUnqualified u, c, ref)) pure (ref, kpd) pure (ref, kpd) where - deleteByRef :: PrepQuery W (UserId, ClientId, KeyPackageRef) () - deleteByRef = "DELETE FROM mls_key_packages WHERE user = ? AND client = ? AND ref = ?" + delete1Query :: PrepQuery W (UserId, ClientId, KeyPackageRef) () + delete1Query = "DELETE FROM mls_key_packages WHERE user = ? AND client = ? AND ref = ?" -- | Fetch all unclaimed non-expired key packages for a given client and delete -- from the database those that have expired. @@ -92,19 +93,12 @@ getNonClaimedKeyPackages u c = do let (kpsExpired, kpsNonExpired) = partition (hasExpired now mMaxLifetime) decodedKps -- delete expired key packages - let kpsExpired' = fmap (\(_, (ref, _)) -> ref) kpsExpired - in retry x5 $ - write - deleteByRefs - (params LocalQuorum (tUnqualified u, c, kpsExpired')) + deleteKeyPackages (tUnqualified u) c (map (\(_, (ref, _)) -> ref) kpsExpired) pure $ fmap snd kpsNonExpired where lookupQuery :: PrepQuery R (UserId, ClientId) (KeyPackageRef, KeyPackageData) lookupQuery = "SELECT ref, data FROM mls_key_packages WHERE user = ? AND client = ?" - deleteByRefs :: PrepQuery W (UserId, ClientId, [KeyPackageRef]) () - deleteByRefs = "DELETE FROM mls_key_packages WHERE user = ? AND client = ? AND ref in ?" - decodeKp :: (a, KeyPackageData) -> Maybe KeyPackage decodeKp = hush . decodeMLS' . kpData . snd @@ -129,6 +123,16 @@ countKeyPackages :: m Int64 countKeyPackages u c = fromIntegral . length <$> getNonClaimedKeyPackages u c +deleteKeyPackages :: MonadClient m => UserId -> ClientId -> [KeyPackageRef] -> m () +deleteKeyPackages u c refs = + retry x5 $ + write + deleteQuery + (params LocalQuorum (u, c, refs)) + where + deleteQuery :: PrepQuery W (UserId, ClientId, [KeyPackageRef]) () + deleteQuery = "DELETE FROM mls_key_packages WHERE user = ? AND client = ? AND ref in ?" + -------------------------------------------------------------------------------- -- Utilities From 76ca50769351f0fc5d1c4451e16a697b55b1b804 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 15 May 2023 16:05:46 +0200 Subject: [PATCH 041/225] Fix mixed protocol upgrade test --- integration/test/Test/MLS.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 7bec6c7d9b2..588714f9a91 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -22,7 +22,7 @@ ptestMixedProtocolUpgrade secondDomain = do d2 <- secondDomain & asString createAndConnectUsers [d, d2, d2] - qcnv <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 + qcnv <- postConversation alice defProteus {qualifiedUsers = [bob, charlie]} >>= getJSON 201 withWebSockets [alice, charlie] $ \websockets -> do bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do From 541bf14268b60082e2d95fc4290b4d8340c73089 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 16 May 2023 09:37:27 +0200 Subject: [PATCH 042/225] Use parametrised tests in MLS test suite (#3298) --- integration/test/Test/MLS.hs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 588714f9a91..8fb9d4c3128 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -9,14 +9,8 @@ import MLS.Util import SetupHelpers import Testlib.Prelude -testMixedProtocolUpgrade :: HasCallStack => App () -testMixedProtocolUpgrade = ptestMixedProtocolUpgrade ownDomain - -testMixedProtocolUpgradeFed :: HasCallStack => App () -testMixedProtocolUpgradeFed = ptestMixedProtocolUpgrade otherDomain - -ptestMixedProtocolUpgrade :: (HasCallStack, MakesValue domain) => domain -> App () -ptestMixedProtocolUpgrade secondDomain = do +testMixedProtocolUpgrade :: HasCallStack => Domain -> App () +testMixedProtocolUpgrade secondDomain = do [alice, bob, charlie] <- do d <- ownDomain d2 <- secondDomain & asString From 330cafaf735a0c7564f9d31203177256eb0cbe62 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 17 May 2023 15:32:49 +0200 Subject: [PATCH 043/225] MLS migration feature config (#3299) --- cassandra-schema.cql | 6 ++ .../1-api-changes/mls-migration-feature | 1 + charts/galley/templates/configmap.yaml | 4 + charts/galley/values.yaml | 9 ++ docs/src/understand/team-feature-settings.md | 55 ++++++++++++ hack/helm_vars/wire-server/values.yaml.gotmpl | 9 ++ libs/galley-types/src/Galley/Types/Teams.hs | 9 +- .../test/unit/Test/Galley/Types.hs | 1 + .../src/Wire/API/Routes/Internal/Galley.hs | 5 ++ .../Wire/API/Routes/Public/Galley/Feature.hs | 2 + libs/wire-api/src/Wire/API/Team/Feature.hs | 83 +++++++++++++++++-- services/galley/galley.cabal | 1 + services/galley/galley.integration.yaml | 9 ++ services/galley/schema/src/Run.hs | 4 +- .../schema/src/V84_TeamFeatureMlsMigration.hs | 38 +++++++++ services/galley/src/Galley/API/Internal.hs | 4 + .../galley/src/Galley/API/Public/Feature.hs | 2 + .../galley/src/Galley/API/Teams/Features.hs | 12 ++- services/galley/src/Galley/Cassandra.hs | 2 +- .../src/Galley/Cassandra/TeamFeatures.hs | 36 +++++++- .../test/integration/API/Teams/Feature.hs | 19 ++++- 21 files changed, 296 insertions(+), 15 deletions(-) create mode 100644 changelog.d/1-api-changes/mls-migration-feature create mode 100644 services/galley/schema/src/V84_TeamFeatureMlsMigration.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index b0a1ba6f19b..10d47738d1b 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -142,6 +142,12 @@ CREATE TABLE galley_test.team_features ( mls_e2eid_lock_status int, mls_e2eid_status int, mls_e2eid_ver_exp timestamp, + mls_migration_clients_threshold int, + mls_migration_finalise_regardless_after timestamp, + mls_migration_lock_status int, + mls_migration_start_time timestamp, + mls_migration_status int, + mls_migration_users_threshold int, mls_protocol_toggle_users set, mls_status int, outlook_cal_integration_lock_status int, diff --git a/changelog.d/1-api-changes/mls-migration-feature b/changelog.d/1-api-changes/mls-migration-feature new file mode 100644 index 00000000000..67470870c70 --- /dev/null +++ b/changelog.d/1-api-changes/mls-migration-feature @@ -0,0 +1 @@ +Add MLS migration feature config diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 22fd61a8308..f589c2f80d1 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -123,5 +123,9 @@ data: mlsE2EId: {{- toYaml .settings.featureFlags.mlsE2EId | nindent 10 }} {{- end }} + {{- if .settings.featureFlags.mlsMigration }} + mlsMigration: + {{- toYaml .settings.featureFlags.mlsMigration | nindent 10 }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index cd675a747a1..72d33ae8b81 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -95,6 +95,15 @@ config: verificationExpiration: 86400 acmeDiscoveryUrl: null lockStatus: unlocked + mlsMigration: + defaults: + status: disabled + config: + startTime: null # "2029-05-16T10:11:12.123Z" + finaliseRegardlessAfter: null # "2029-10-17T00:00:00.000Z" + usersThreshold: 100 + clientsThreshold: 100 + lockStatus: locked aws: region: "eu-west-1" diff --git a/docs/src/understand/team-feature-settings.md b/docs/src/understand/team-feature-settings.md index 0382e51612d..e6ea26aeca7 100644 --- a/docs/src/understand/team-feature-settings.md +++ b/docs/src/understand/team-feature-settings.md @@ -109,3 +109,58 @@ galley: acmeDiscoveryUrl: null lockStatus: unlocked ``` + +## MLS Migration + +The MLS migration configuration determines client behaviour related to +migration from Proteus to MSL, and defines the criteria enforced by the backend +when a conversation is finally migrated to MLS. + +The settings are the following: + + - `startTime`: migration start timestamp. Once this time arrives, clients will + initialise the migration process (no migration-related action will take + place before that time). If the migration feature is enabled, but + `startTime` value is not set (or is set to `null`), migration is never + started. + + - `finaliseRegardlessAfter`: timestamp of the date by which the migration must + be finalised. + + - `usersThreshold`: percentage of migrated users needed for migration to + finalise (0-100). + + - `clientsThreshold`: percentage of migrated clients needed for migration to + finalise (0-100). + +All of the migration finalisation values are technically optional, but at least +one of them must be specified for the configuration to be valid. If +`finaliseRegardlessAfter` is not set, `usersThreshold` or `clientsThreshold` +should be specified. In case both `usersThreshold` and `clientsThreshold` are +specified, even if one of them is set to 0, both have to be fulfilled for the +migration to be finalised. + +The `finaliseRegardlessAfter` timestamp determines a time after which the +threshold criteria are dropped, and finalisation is allowed in any case. + +An example configuration follows: + +``` +galley: + # ... + config: + # ... + settings: + # ... + featureFlags: + # ... + mlsMigration: + defaults: + status: enabled + config: + startTime: "2024-05-16T00:00:00.000Z" + finaliseRegardlessAfter: "2024-10-17T00:00:00.000Z" + usersThreshold: 100 + clientsThreshold: 50 + lockStatus: locked +``` diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index bb92bc56e07..cf84f4dad12 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -191,6 +191,15 @@ galley: status: enabled config: domains: ["example.com"] + mlsMigration: + defaults: + status: enabled + config: + startTime: "2029-05-16T10:11:12.123Z" + finaliseRegardlessAfter: "2029-10-17T00:00:00.000Z" + usersThreshold: 100 + clientsThreshold: 50 + lockStatus: locked journal: endpoint: http://fake-aws-sqs:4568 queueName: integration-team-events.fifo diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index a906a1cd0d9..eeca1627dee 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -41,6 +41,7 @@ module Galley.Types.Teams flagOutlookCalIntegration, flagMLS, flagMlsE2EId, + flagMlsMigration, Defaults (..), ImplicitLockStatus (..), unImplicitLockStatus, @@ -154,7 +155,8 @@ data FeatureFlags = FeatureFlags _flagTeamFeatureSearchVisibilityInbound :: !(Defaults (ImplicitLockStatus SearchVisibilityInboundConfig)), _flagMLS :: !(Defaults (ImplicitLockStatus MLSConfig)), _flagOutlookCalIntegration :: !(Defaults (WithStatus OutlookCalIntegrationConfig)), - _flagMlsE2EId :: !(Defaults (WithStatus MlsE2EIdConfig)) + _flagMlsE2EId :: !(Defaults (WithStatus MlsE2EIdConfig)), + _flagMlsMigration :: !(Defaults (WithStatus MlsMigrationConfig)) } deriving (Eq, Show, Generic) @@ -206,6 +208,7 @@ instance FromJSON FeatureFlags where <*> withImplicitLockStatusOrDefault obj "mls" <*> (fromMaybe (Defaults (defFeatureStatus @OutlookCalIntegrationConfig)) <$> (obj .:? "outlookCalIntegration")) <*> (fromMaybe (Defaults (defFeatureStatus @MlsE2EIdConfig)) <$> (obj .:? "mlsE2EId")) + <*> (fromMaybe (Defaults (defFeatureStatus @MlsMigrationConfig)) <$> (obj .:? "mlsMigration")) where withImplicitLockStatusOrDefault :: forall cfg. (IsFeatureConfig cfg, Schema.ToSchema cfg) => Object -> Key -> A.Parser (Defaults (ImplicitLockStatus cfg)) withImplicitLockStatusOrDefault obj fieldName = fromMaybe (Defaults (ImplicitLockStatus (defFeatureStatus @cfg))) <$> obj .:? fieldName @@ -228,6 +231,7 @@ instance ToJSON FeatureFlags where mls outlookCalIntegration mlsE2EId + mlsMigration ) = object [ "sso" .= sso, @@ -244,7 +248,8 @@ instance ToJSON FeatureFlags where "searchVisibilityInbound" .= searchVisibilityInbound, "mls" .= mls, "outlookCalIntegration" .= outlookCalIntegration, - "mlsE2EId" .= mlsE2EId + "mlsE2EId" .= mlsE2EId, + "mlsMigration" .= mlsMigration ] instance FromJSON FeatureSSO where diff --git a/libs/galley-types/test/unit/Test/Galley/Types.hs b/libs/galley-types/test/unit/Test/Galley/Types.hs index 756ccebc2ae..b154ea60ddc 100644 --- a/libs/galley-types/test/unit/Test/Galley/Types.hs +++ b/libs/galley-types/test/unit/Test/Galley/Types.hs @@ -98,6 +98,7 @@ instance Arbitrary FeatureFlags where <*> fmap (fmap unlocked) arbitrary <*> arbitrary <*> arbitrary + <*> arbitrary where unlocked :: ImplicitLockStatus a -> ImplicitLockStatus a unlocked = ImplicitLockStatus . Public.setLockStatus Public.LockStatusUnlocked . _unImplicitLockStatus diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index 1f7402c3a7b..331735ab337 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -154,6 +154,11 @@ type IFeatureAPI = :<|> IFeatureStatusPut '[] '() MlsE2EIdConfig :<|> IFeatureStatusPatch '[] '() MlsE2EIdConfig :<|> IFeatureStatusLockStatusPut MlsE2EIdConfig + -- MlsMigrationConfig + :<|> IFeatureStatusGet MlsMigrationConfig + :<|> IFeatureStatusPut '[] '() MlsMigrationConfig + :<|> IFeatureStatusPatch '[] '() MlsMigrationConfig + :<|> IFeatureStatusLockStatusPut MlsMigrationConfig -- all feature configs :<|> Named "feature-configs-internal" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index b703ec9f5b8..e084b87e901 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -91,6 +91,8 @@ type FeatureAPI = :<|> FeatureStatusPut '[] '() OutlookCalIntegrationConfig :<|> FeatureStatusGet MlsE2EIdConfig :<|> FeatureStatusPut '[] '() MlsE2EIdConfig + :<|> FeatureStatusGet MlsMigrationConfig + :<|> FeatureStatusPut '[] '() MlsMigrationConfig :<|> AllFeatureConfigsUserGet :<|> AllFeatureConfigsTeamGet :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" LegalholdConfig diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 10d05e8d3f8..ec8d5ad12d6 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -79,6 +79,7 @@ module Wire.API.Team.Feature MLSConfig (..), OutlookCalIntegrationConfig (..), MlsE2EIdConfig (..), + MlsMigrationConfig (..), AllFeatureConfigs (..), unImplicitLockStatus, ImplicitLockStatus (..), @@ -95,6 +96,7 @@ import qualified Data.ByteString.UTF8 as UTF8 import Data.Domain (Domain) import Data.Either.Extra (maybeToEither) import Data.Id +import Data.Json.Util import Data.Kind import Data.Misc (HttpsUrl) import Data.Proxy @@ -105,13 +107,14 @@ import qualified Data.Swagger as S import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as TL -import Data.Time (NominalDiffTime) +import Data.Time import Deriving.Aeson import GHC.TypeLits import Imports import Servant (FromHttpApiData (..), ToHttpApiData (..)) import Test.QuickCheck.Arbitrary (arbitrary) import Test.QuickCheck.Gen (suchThat) +import Test.QuickCheck.Modifiers import Wire.API.Conversation.Protocol (ProtocolTag (ProtocolProteusTag)) import Wire.API.MLS.CipherSuite (CipherSuiteTag (MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519)) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -135,17 +138,22 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- Galley.Cassandra.TeamFeatures together with a schema migration -- -- 4. Add the feature to the config schema of galley in Galley.Types.Teams. --- and extend the Arbitrary instance of FeatureConfigs in the unit tests Test.Galley.Types +-- and extend the Arbitrary instance of FeatureConfigs in the unit tests +-- Test.Galley.Types -- -- 5. Implement 'GetFeatureConfig' and 'SetFeatureConfig' in -- Galley.API.Teams.Features which defines the main business logic for getting --- and setting (with side-effects). Note that we don't have to check the lockstatus inside 'setConfigForTeam' --- because the lockstatus is checked in 'setFeatureStatus' before which is the public API for setting the feature status. +-- and setting (with side-effects). Note that we don't have to check the +-- lockstatus inside 'setConfigForTeam' because the lockstatus is checked in +-- 'setFeatureStatus' before which is the public API for setting the feature +-- status. Also extend FeaturePersistentAllFeatures. -- --- 6. Add public routes to Wire.API.Routes.Public.Galley.Feature: 'FeatureStatusGet', --- 'FeatureStatusPut' (optional). Then implement them in Galley.API.Public.Feature. +-- 6. Add public routes to Wire.API.Routes.Public.Galley.Feature: +-- 'FeatureStatusGet', 'FeatureStatusPut' (optional). Then implement them in +-- Galley.API.Public.Feature. -- --- 7. Add internal routes in Wire.API.Routes.Internal.Galley +-- 7. Add internal routes in Wire.API.Routes.Internal.Galley and implement them +-- in Galley.API.Internal. -- -- 8. If the feature should be configurable via Stern add routes to Stern.API. -- Manually check that the swagger looks okay and works. @@ -940,6 +948,62 @@ instance IsFeatureConfig MlsE2EIdConfig where defValue = MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) Nothing objectSchema = field "config" schema +---------------------------------------------------------------------- +-- MlsMigration + +data MlsMigrationConfig = MlsMigrationConfig + { startTime :: Maybe UTCTime, + finaliseRegardlessAfter :: Maybe UTCTime, + usersThreshold :: Maybe Int, + clientsThreshold :: Maybe Int + } + deriving stock (Eq, Show, Generic) + +instance Arbitrary MlsMigrationConfig where + arbitrary = do + startTime <- fmap fromUTCTimeMillis <$> arbitrary + finaliseRegardlessAfter <- fmap fromUTCTimeMillis <$> arbitrary + usersThreshold <- fmap getNonNegative <$> arbitrary + clientsThreshold <- fmap (fmap getNonNegative) $ + case (finaliseRegardlessAfter, usersThreshold) of + (Nothing, Nothing) -> Just <$> arbitrary + _ -> arbitrary + pure + MlsMigrationConfig + { startTime = startTime, + finaliseRegardlessAfter = finaliseRegardlessAfter, + usersThreshold = usersThreshold, + clientsThreshold = clientsThreshold + } + +instance ToSchema MlsMigrationConfig where + schema = + object "MlsMigration" $ + withParser + ( MlsMigrationConfig + <$> startTime .= maybe_ (optField "startTime" utcTimeSchema) + <*> finaliseRegardlessAfter .= maybe_ (optField "finaliseRegardlessAfter" utcTimeSchema) + <*> usersThreshold .= maybe_ (optField "usersThreshold" schema) + <*> clientsThreshold .= maybe_ (optField "clientsThreshold" schema) + ) + checkConfig + where + checkConfig c = do + when + ( isNothing c.finaliseRegardlessAfter + && isNothing c.usersThreshold + && isNothing c.clientsThreshold + ) + $ fail "At least one of finaliseRegardlessAfter, usersThreshold or clientsThreshold must be set" + pure c + +instance IsFeatureConfig MlsMigrationConfig where + type FeatureSymbol MlsMigrationConfig = "mlsMigration" + defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked defValue FeatureTTLUnlimited + where + defValue = MlsMigrationConfig Nothing Nothing Nothing Nothing + objectSchema = field "config" schema + ---------------------------------------------------------------------- -- FeatureStatus @@ -1016,7 +1080,8 @@ data AllFeatureConfigs = AllFeatureConfigs afcMLS :: WithStatus MLSConfig, afcExposeInvitationURLsToTeamAdmin :: WithStatus ExposeInvitationURLsToTeamAdminConfig, afcOutlookCalIntegration :: WithStatus OutlookCalIntegrationConfig, - afcMlsE2EId :: WithStatus MlsE2EIdConfig + afcMlsE2EId :: WithStatus MlsE2EIdConfig, + afcMlsMigration :: WithStatus MlsMigrationConfig } deriving stock (Eq, Show) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AllFeatureConfigs) @@ -1042,6 +1107,7 @@ instance ToSchema AllFeatureConfigs where <*> afcExposeInvitationURLsToTeamAdmin .= featureField <*> afcOutlookCalIntegration .= featureField <*> afcMlsE2EId .= featureField + <*> afcMlsMigration .= featureField where featureField :: forall cfg. @@ -1069,5 +1135,6 @@ instance Arbitrary AllFeatureConfigs where <*> arbitrary <*> arbitrary <*> arbitrary + <*> arbitrary makeLenses ''ImplicitLockStatus diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index eafa45768ef..8404c6ee556 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -545,6 +545,7 @@ executable galley-schema V81_TeamFeatureMlsE2EIdUpdate V82_MLSSubconversation V83_MLSDraft17 + V84_TeamFeatureMlsMigration hs-source-dirs: schema/src default-extensions: TemplateHaskell diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 35426d4ed35..27166fbb8ab 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -79,6 +79,15 @@ settings: verificationExpiration: 86400 acmeDiscoveryUrl: null lockStatus: unlocked + mlsMigration: + defaults: + status: enabled + config: + startTime: "2029-05-16T10:11:12.123Z" + finaliseRegardlessAfter: "2029-10-17T00:00:00.000Z" + usersThreshold: 100 + clientsThreshold: 50 + lockStatus: locked logLevel: Warn logNetStrings: false diff --git a/services/galley/schema/src/Run.hs b/services/galley/schema/src/Run.hs index 08ee167c16f..adff6cd4e70 100644 --- a/services/galley/schema/src/Run.hs +++ b/services/galley/schema/src/Run.hs @@ -86,6 +86,7 @@ import qualified V80_AddConversationCodePassword import qualified V81_TeamFeatureMlsE2EIdUpdate import qualified V82_MLSSubconversation import qualified V83_MLSDraft17 +import qualified V84_TeamFeatureMlsMigration main :: IO () main = do @@ -157,7 +158,8 @@ main = do V80_AddConversationCodePassword.migration, V81_TeamFeatureMlsE2EIdUpdate.migration, V82_MLSSubconversation.migration, - V83_MLSDraft17.migration + V83_MLSDraft17.migration, + V84_TeamFeatureMlsMigration.migration -- When adding migrations here, don't forget to update -- 'schemaVersion' in Galley.Cassandra -- (see also docs/developer/cassandra-interaction.md) diff --git a/services/galley/schema/src/V84_TeamFeatureMlsMigration.hs b/services/galley/schema/src/V84_TeamFeatureMlsMigration.hs new file mode 100644 index 00000000000..35f07fb8e2b --- /dev/null +++ b/services/galley/schema/src/V84_TeamFeatureMlsMigration.hs @@ -0,0 +1,38 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 V84_TeamFeatureMlsMigration + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = Migration 84 "Add feature config for team feature MLS Migration" $ do + schema' + [r| ALTER TABLE team_features ADD ( + mls_migration_status int, + mls_migration_lock_status int, + mls_migration_start_time timestamp, + mls_migration_finalise_regardless_after timestamp, + mls_migration_users_threshold int, + mls_migration_clients_threshold int + ) + |] diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 240a8e544ce..83bbbb9a35c 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -215,6 +215,10 @@ featureAPI = <@> mkNamedAPI @'("iput", MlsE2EIdConfig) (setFeatureStatusInternal @Cassandra) <@> mkNamedAPI @'("ipatch", MlsE2EIdConfig) (patchFeatureStatusInternal @Cassandra) <@> mkNamedAPI @'("ilock", MlsE2EIdConfig) (updateLockStatus @Cassandra @MlsE2EIdConfig) + <@> mkNamedAPI @'("iget", MlsMigrationConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", MlsMigrationConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("ipatch", MlsMigrationConfig) (patchFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("ilock", MlsMigrationConfig) (updateLockStatus @Cassandra @MlsMigrationConfig) <@> mkNamedAPI @"feature-configs-internal" (maybe getAllFeatureConfigsForServer (getAllFeatureConfigsForUser @Cassandra)) internalSitemap :: Routes a (Sem GalleyEffects) () diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 028c3cfc1a0..9b7ff2c577f 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -64,6 +64,8 @@ featureAPI = <@> mkNamedAPI @'("put", OutlookCalIntegrationConfig) (setFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("get", MlsE2EIdConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("put", MlsE2EIdConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", MlsMigrationConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", MlsMigrationConfig) (setFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @"get-all-feature-configs-for-user" (getAllFeatureConfigsForUser @Cassandra) <@> mkNamedAPI @"get-all-feature-configs-for-team" (getAllFeatureConfigsForTeam @Cassandra) <@> mkNamedAPI @'("get-config", LegalholdConfig) (getFeatureStatusForUser @Cassandra) diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 8c630764d59..6e3699f66b0 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -205,7 +205,8 @@ type FeaturePersistentAllFeatures db = FeaturePersistentConstraint db SearchVisibilityInboundConfig, FeaturePersistentConstraint db ExposeInvitationURLsToTeamAdminConfig, FeaturePersistentConstraint db OutlookCalIntegrationConfig, - FeaturePersistentConstraint db MlsE2EIdConfig + FeaturePersistentConstraint db MlsE2EIdConfig, + FeaturePersistentConstraint db MlsMigrationConfig ) getFeatureStatus :: @@ -439,6 +440,7 @@ getAllFeatureConfigsForServer = <*> getConfigForServer @ExposeInvitationURLsToTeamAdminConfig <*> getConfigForServer @OutlookCalIntegrationConfig <*> getConfigForServer @MlsE2EIdConfig + <*> getConfigForServer @MlsMigrationConfig getAllFeatureConfigsUser :: forall db r. @@ -473,6 +475,7 @@ getAllFeatureConfigsUser uid = <*> getConfigForUser @db @ExposeInvitationURLsToTeamAdminConfig uid <*> getConfigForUser @db @OutlookCalIntegrationConfig uid <*> getConfigForUser @db @MlsE2EIdConfig uid + <*> getConfigForUser @db @MlsMigrationConfig uid getAllFeatureConfigsTeam :: forall db r. @@ -503,6 +506,7 @@ getAllFeatureConfigsTeam tid = <*> getConfigForTeam @db @ExposeInvitationURLsToTeamAdminConfig tid <*> getConfigForTeam @db @OutlookCalIntegrationConfig tid <*> getConfigForTeam @db @MlsE2EIdConfig tid + <*> getConfigForTeam @db @MlsMigrationConfig tid -- | Note: this is an internal function which doesn't cover all features, e.g. LegalholdConfig genericGetConfigForTeam :: @@ -863,6 +867,12 @@ instance GetFeatureConfig db MlsE2EIdConfig where getConfigForServer = input <&> view (optSettings . setFeatureFlags . flagMlsE2EId . unDefaults) +instance SetFeatureConfig db MlsMigrationConfig + +instance GetFeatureConfig db MlsMigrationConfig where + getConfigForServer = + input <&> view (optSettings . setFeatureFlags . flagMlsMigration . unDefaults) + -- -- | If second factor auth is enabled, make sure that end-points that don't support it, but should, are blocked completely. (This is a workaround until we have 2FA for those end-points as well.) -- -- -- This function exists to resolve a cyclic dependency. diff --git a/services/galley/src/Galley/Cassandra.hs b/services/galley/src/Galley/Cassandra.hs index 32044a86a2d..12a69f0e8d6 100644 --- a/services/galley/src/Galley/Cassandra.hs +++ b/services/galley/src/Galley/Cassandra.hs @@ -20,4 +20,4 @@ module Galley.Cassandra (schemaVersion) where import Imports schemaVersion :: Int32 -schemaVersion = 83 +schemaVersion = 84 diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 5bbd2af7769..1249c38aca3 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -29,7 +29,7 @@ import Control.Monad.Trans.Maybe import Data.Id import Data.Misc (HttpsUrl) import Data.Proxy -import Data.Time (NominalDiffTime) +import Data.Time import Galley.Cassandra.Instances () import Galley.Cassandra.Store import qualified Galley.Effects.TeamFeatureStore as TFS @@ -352,6 +352,40 @@ instance FeatureStatusCassandra MlsE2EIdConfig where getFeatureLockStatus _ = getLockStatusC "mls_e2eid_lock_status" setFeatureLockStatus _ = setLockStatusC "mls_e2eid_lock_status" +instance FeatureStatusCassandra MlsMigrationConfig where + getFeatureConfig _ tid = do + let q = query1 select (params LocalQuorum (Identity tid)) + retry x1 q <&> \case + Nothing -> Nothing + Just (Nothing, _, _, _, _) -> Nothing + Just (Just fs, startTime, finaliseRegardlessAfter, usersThreshold, clientsThreshold) -> + Just $ + WithStatusNoLock + fs + MlsMigrationConfig + { startTime = startTime, + finaliseRegardlessAfter = finaliseRegardlessAfter, + usersThreshold = fmap fromIntegral usersThreshold, + clientsThreshold = fmap fromIntegral clientsThreshold + } + FeatureTTLUnlimited + where + select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe UTCTime, Maybe UTCTime, Maybe Int32, Maybe Int32) + select = "select mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after, mls_migration_users_threshold, mls_migration_clients_threshold from team_features where team_id = ?" + + setFeatureConfig _ tid status = do + let statusValue = wssStatus status + config = wssConfig status + + retry x5 $ write insert (params LocalQuorum (tid, statusValue, config.startTime, config.finaliseRegardlessAfter, fmap fromIntegral config.usersThreshold, fmap fromIntegral config.clientsThreshold)) + where + insert :: PrepQuery W (TeamId, FeatureStatus, Maybe UTCTime, Maybe UTCTime, Maybe Int32, Maybe Int32) () + insert = + "insert into team_features (team_id, mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after, mls_migration_users_threshold, mls_migration_clients_threshold) values (?, ?, ?, ?, ?, ?)" + + getFeatureLockStatus _ = getLockStatusC "mls_migration_lock_status" + setFeatureLockStatus _ = setLockStatusC "mls_migration_lock_status" + instance FeatureStatusCassandra ExposeInvitationURLsToTeamAdminConfig where getFeatureConfig _ = getTrivialConfigC "expose_invitation_urls_to_team_admin" setFeatureConfig _ tid statusNoLock = setFeatureStatusC "expose_invitation_urls_to_team_admin" tid (wssStatus statusNoLock) diff --git a/services/galley/test/integration/API/Teams/Feature.hs b/services/galley/test/integration/API/Teams/Feature.hs index c15d2c5e3b0..1ad2f7c064a 100644 --- a/services/galley/test/integration/API/Teams/Feature.hs +++ b/services/galley/test/integration/API/Teams/Feature.hs @@ -36,6 +36,7 @@ import qualified Data.Aeson.KeyMap as KeyMap import Data.ByteString.Char8 (unpack) import Data.Domain (Domain (..)) import Data.Id +import Data.Json.Util (fromUTCTimeMillis, readUTCTimeMillis) import qualified Data.List1 as List1 import Data.Schema (ToSchema) import qualified Data.Set as Set @@ -107,6 +108,8 @@ tests s = (wsConfig (defFeatureStatus @MlsE2EIdConfig)) FeatureTTLUnlimited ), + test s "MlsMigration feature config" $ + testNonTrivialConfigNoTTL defaultMlsMigrationConfig, testGroup "Patch" [ -- Note: `SSOConfig` and `LegalHoldConfig` may not be able to be reset @@ -1047,7 +1050,8 @@ testAllFeatures = do afcSearchVisibilityInboundConfig = withStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityInboundConfig FeatureTTLUnlimited, afcExposeInvitationURLsToTeamAdmin = withStatus FeatureStatusDisabled LockStatusLocked ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited, afcOutlookCalIntegration = withStatus FeatureStatusDisabled LockStatusLocked OutlookCalIntegrationConfig FeatureTTLUnlimited, - afcMlsE2EId = withStatus FeatureStatusDisabled LockStatusUnlocked (wsConfig defFeatureStatus) FeatureTTLUnlimited + afcMlsE2EId = withStatus FeatureStatusDisabled LockStatusUnlocked (wsConfig defFeatureStatus) FeatureTTLUnlimited, + afcMlsMigration = defaultMlsMigrationConfig } testFeatureConfigConsistency :: TestM () @@ -1458,3 +1462,16 @@ wsAssertFeatureConfigUpdate config lockStatus notification = do FeatureConfig._eventType e @?= FeatureConfig.Update FeatureConfig._eventFeatureName e @?= featureName @cfg FeatureConfig._eventData e @?= Aeson.toJSON (withLockStatus lockStatus config) + +defaultMlsMigrationConfig :: WithStatus MlsMigrationConfig +defaultMlsMigrationConfig = + withStatus + FeatureStatusEnabled + LockStatusLocked + MlsMigrationConfig + { startTime = fmap fromUTCTimeMillis (readUTCTimeMillis "2029-05-16T10:11:12.123Z"), + finaliseRegardlessAfter = fmap fromUTCTimeMillis (readUTCTimeMillis "2029-10-17T00:00:00.000Z"), + usersThreshold = Just 100, + clientsThreshold = Just 50 + } + FeatureTTLUnlimited From 5f274a6199b72a6086ceb59fb4e2276c80aad22a Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 22 May 2023 17:09:03 +0200 Subject: [PATCH 044/225] FS-1901: Missing bits for mixed protocol state (#3303) * migrate test that adds user via mls * mls-test-cli: make show use json * testlib: assertOne, asByteString * Add test: user leaves -> remove proposal * wip test: adding partial client set to mixed * migrate test testAddUserPartial * make test fail * Use new test parametrization * asInt -> asIntegral * migrate testRemoveClientsIncomplete * Add HasCallStack to getJSON * Add test for removing partial clients * Refactor: use fields * integration: rename functions and improve errors * Add test: remote backend doesnt know about about mixed protocol convs * Deny application msgs for mixed (with test) * fix mixed remote test * Add testFirstCommitAllowsPartialAdds * Only allow protocol updates for team conversations * Call on-new-remote-conversation on protocol update * Test remote user adding * remove migrated test --------- Co-authored-by: Paolo Capriotti --- changelog.d/5-internal/mls-mixed | 4 + integration/test/API/Galley.hs | 24 ++ integration/test/API/GalleyInternal.hs | 4 +- integration/test/MLS/Util.hs | 105 ++++++- integration/test/SetupHelpers.hs | 18 +- integration/test/Test/B2B.hs | 2 +- integration/test/Test/Brig.hs | 4 +- integration/test/Test/Demo.hs | 20 +- integration/test/Test/MLS.hs | 287 +++++++++++++++++- integration/test/Test/MLS/KeyPackage.hs | 2 +- integration/test/Testlib/App.hs | 9 +- integration/test/Testlib/Assertions.hs | 7 +- integration/test/Testlib/Cannon.hs | 4 +- integration/test/Testlib/HTTP.hs | 2 +- integration/test/Testlib/JSON.hs | 15 +- integration/test/Testlib/ModService.hs | 3 +- integration/test/Testlib/PTest.hs | 9 - .../src/Wire/API/Conversation/Protocol.hs | 8 +- .../src/Wire/API/MLS/SubConversation.hs | 5 + nix/pkgs/mls-test-cli/default.nix | 4 +- services/galley/src/Galley/API/Action.hs | 26 +- services/galley/src/Galley/API/Federation.hs | 2 +- .../galley/src/Galley/API/MLS/Commit/Core.hs | 9 +- .../Galley/API/MLS/Commit/ExternalCommit.hs | 20 +- .../Galley/API/MLS/Commit/InternalCommit.hs | 202 ++++++------ .../galley/src/Galley/API/MLS/Conversation.hs | 5 +- services/galley/src/Galley/API/MLS/Message.hs | 22 +- .../galley/src/Galley/API/MLS/Propagate.hs | 2 +- .../galley/src/Galley/API/MLS/Proposal.hs | 5 +- services/galley/src/Galley/API/MLS/Removal.hs | 6 +- .../src/Galley/API/MLS/SubConversation.hs | 13 +- services/galley/src/Galley/API/MLS/Types.hs | 35 ++- .../src/Galley/Cassandra/Conversation.hs | 16 +- .../src/Galley/Data/Conversation/Types.hs | 11 +- .../src/Galley/Effects/ConversationStore.hs | 4 +- services/galley/test/integration/API/MLS.hs | 139 +-------- services/galley/test/integration/API/Util.hs | 5 +- 37 files changed, 674 insertions(+), 384 deletions(-) create mode 100644 changelog.d/5-internal/mls-mixed diff --git a/changelog.d/5-internal/mls-mixed b/changelog.d/5-internal/mls-mixed new file mode 100644 index 00000000000..7e35c12b3b1 --- /dev/null +++ b/changelog.d/5-internal/mls-mixed @@ -0,0 +1,4 @@ +- Do not perform client checks for add and remove proposals in mixed conversations +- Restrict protocol updates to team conversations +- Disallow MLS application messages in mixed conversations +- Send remove proposals when users leave mixed conversations diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 8dc54f6d952..b10126ab02a 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -164,3 +164,27 @@ getGroupInfo user conv = do Just sub -> ["conversations", convDomain, convId, "subconversations", sub, "groupinfo"] req <- baseRequest user Galley Versioned path submit "GET" req + +removeConversationMember :: + (HasCallStack, MakesValue user, MakesValue conv) => + user -> + conv -> + App Response +removeConversationMember user conv = do + (convDomain, convId) <- objQid conv + (userDomain, userId) <- objQid user + req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "members", userDomain, userId]) + submit "DELETE" req + +updateConversationMember :: + (HasCallStack, MakesValue user, MakesValue conv, MakesValue target) => + user -> + conv -> + target -> + String -> + App Response +updateConversationMember user conv target role = do + (convDomain, convId) <- objQid conv + (targetDomain, targetId) <- objQid target + req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "members", targetDomain, targetId]) + submit "PUT" (req & addJSONObject ["conversation_role" .= role]) diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index 56e811626af..0fb4eb2e184 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -9,7 +9,7 @@ putTeamMember user team perms = do tid <- asString team req <- baseRequest - ownDomain + user Galley Unversioned ("/i/teams/" <> tid <> "/members") @@ -31,5 +31,5 @@ putTeamMember user team perms = do getTeamFeature :: HasCallStack => String -> String -> App Response getTeamFeature featureName tid = do - req <- baseRequest ownDomain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName] + req <- baseRequest OwnDomain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName] submit "GET" $ req diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 1a54fb4366e..f142b674759 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -10,9 +10,11 @@ import Control.Monad.Catch import Control.Monad.Cont import Control.Monad.Reader import Control.Monad.Trans.Maybe +import qualified Data.Aeson as Aeson import qualified Data.ByteString as BS import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as B8 +import qualified Data.ByteString.Char8 as C8 import Data.Default import Data.Foldable import Data.Function @@ -27,7 +29,7 @@ import GHC.Stack import System.Directory import System.Exit import System.FilePath -import System.IO +import System.IO hiding (print, putStrLn) import System.IO.Temp import System.Posix.Files import System.Process @@ -89,12 +91,17 @@ mlscli cid args mbstdin = do pure (argSubst "" fn) else pure id + let args' = map (substIn . substOut) args + for_ args' $ \arg -> + when (arg `elem` ["", ""]) $ + assertFailure ("Unbound arg: " <> arg) + out <- spawn ( proc "mls-test-cli" ( ["--store", cdir "store"] - <> map (substIn . substOut) args + <> args' ) ) mbstdin @@ -160,8 +167,8 @@ generateKeyPackage cid = do pure (kp, ref) -- | Create conversation and corresponding group. -setupMLSGroup :: HasCallStack => ClientIdentity -> App (String, Value) -setupMLSGroup cid = do +createNewGroup :: HasCallStack => ClientIdentity -> App (String, Value) +createNewGroup cid = do conv <- postConversation cid defMLS >>= getJSON 201 groupId <- conv %. "group_id" & asString convId <- conv %. "qualified_id" @@ -169,8 +176,8 @@ setupMLSGroup cid = do pure (groupId, convId) -- | Retrieve self conversation and create the corresponding group. -setupMLSSelfGroup :: HasCallStack => ClientIdentity -> App (String, Value) -setupMLSSelfGroup cid = do +createSelfGroup :: HasCallStack => ClientIdentity -> App (String, Value) +createSelfGroup cid = do conv <- getSelfConversation cid >>= getJSON 200 conv %. "epoch" `shouldMatchInt` 0 groupId <- conv %. "group_id" & asString @@ -225,7 +232,7 @@ keyPackageFile cid ref = do urlSafe '/' = '_' urlSafe c = c -unbundleKeyPackages :: Value -> App [(ClientIdentity, ByteString)] +unbundleKeyPackages :: HasCallStack => Value -> App [(ClientIdentity, ByteString)] unbundleKeyPackages bundle = do let entryIdentity be = do d <- be %. "domain" & asString @@ -263,6 +270,7 @@ withTempKeyPackageFile bs = do k fp createAddCommitWithKeyPackages :: + HasCallStack => ClientIdentity -> [(ClientIdentity, ByteString)] -> App MessagePackage @@ -304,6 +312,44 @@ createAddCommitWithKeyPackages cid clientsAndKeyPackages = do groupInfo = Just gi } +createRemoveCommit :: HasCallStack => ClientIdentity -> [ClientIdentity] -> App MessagePackage +createRemoveCommit cid targets = do + bd <- getBaseDir + welcomeFile <- liftIO $ emptyTempFile bd "welcome" + giFile <- liftIO $ emptyTempFile bd "gi" + + groupStateMap <- Map.fromList <$> (getClientGroupState cid >>= readGroupState) + let indices = map (fromMaybe (error "could not find target") . flip Map.lookup groupStateMap) targets + + commit <- + mlscli + cid + ( [ "member", + "remove", + "--group", + "", + "--group-out", + "", + "--welcome-out", + welcomeFile, + "--group-info-out", + giFile + ] + <> map show indices + ) + Nothing + + welcome <- liftIO $ BS.readFile welcomeFile + gi <- liftIO $ BS.readFile giFile + + pure + MessagePackage + { sender = cid, + message = commit, + welcome = Just welcome, + groupInfo = Just gi + } + createAddProposals :: HasCallStack => ClientIdentity -> [Value] -> App [MessagePackage] createAddProposals cid users = do bundles <- for users $ (claimKeyPackages cid >=> getJSON 200) @@ -509,3 +555,48 @@ setClientGroupState :: HasCallStack => ClientIdentity -> ByteString -> App () setClientGroupState cid g = modifyMLSState $ \s -> s {clientGroupState = Map.insert cid g (clientGroupState s)} + +showMessage :: HasCallStack => ClientIdentity -> ByteString -> App Value +showMessage cid msg = do + bs <- mlscli cid ["show", "message", "-"] (Just msg) + assertOne (Aeson.decode (BS.fromStrict bs)) + +readGroupState :: HasCallStack => ByteString -> App [(ClientIdentity, Word32)] +readGroupState gs = do + v :: Value <- assertJust "Could not decode group state" (Aeson.decode (BS.fromStrict gs)) + lnodes <- v %. "group" %. "public_group" %. "treesync" %. "tree" %. "leaf_nodes" & asList + catMaybes <$$> for (zip lnodes [0 ..]) $ \(el, leafNodeIndex) -> do + lookupField el "node" >>= \case + Just lnode -> do + case lnode of + Null -> pure Nothing + _ -> do + vecb <- lnode %. "payload" %. "credential" %. "credential" %. "Basic" %. "identity" %. "vec" + vec <- asList vecb + ws <- BS.pack <$> for vec (\x -> asIntegral @Word8 x) + [uc, domain] <- pure (C8.split '@' ws) + [uid, client] <- pure (C8.split ':' uc) + let cid = ClientIdentity (C8.unpack domain) (C8.unpack uid) (C8.unpack client) + pure (Just (cid, leafNodeIndex)) + Nothing -> + pure Nothing + +createApplicationMessage :: + HasCallStack => + ClientIdentity -> + String -> + App MessagePackage +createApplicationMessage cid messageContent = do + message <- + mlscli + cid + ["message", "--group", "", messageContent] + Nothing + + pure + MessagePackage + { sender = cid, + message = message, + welcome = Nothing, + groupInfo = Nothing + } diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index a46e8e8cc52..fe79d18a7ac 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -25,7 +25,7 @@ createTeam domain = do -- refreshIndex pure (user, tid) -connectUsers :: +connectUsers2 :: ( HasCallStack, MakesValue alice, MakesValue bob @@ -33,19 +33,21 @@ connectUsers :: alice -> bob -> App () -connectUsers alice bob = do +connectUsers2 alice bob = do bindResponse (Public.postConnection alice bob) (\resp -> resp.status `shouldMatchInt` 201) bindResponse (Public.putConnection bob alice "accepted") (\resp -> resp.status `shouldMatchInt` 200) +connectUsers :: HasCallStack => [Value] -> App () +connectUsers users = traverse_ (uncurry connectUsers2) $ do + t <- tails users + (a, others) <- maybeToList (uncons t) + b <- others + pure (a, b) + createAndConnectUsers :: (HasCallStack, MakesValue domain) => [domain] -> App [Value] createAndConnectUsers domains = do users <- for domains (flip randomUser def) - let userPairs = do - t <- tails users - (a, others) <- maybeToList (uncons t) - b <- others - pure (a, b) - for_ userPairs (uncurry connectUsers) + connectUsers users pure users getAllConvs :: (HasCallStack, MakesValue u) => u -> App [Value] diff --git a/integration/test/Test/B2B.hs b/integration/test/Test/B2B.hs index 48267add7ec..ba9df150f59 100644 --- a/integration/test/Test/B2B.hs +++ b/integration/test/Test/B2B.hs @@ -6,5 +6,5 @@ import Testlib.Prelude testConnectUsers :: App () testConnectUsers = do - _alice <- randomUser ownDomain def + _alice <- randomUser OwnDomain def pure () diff --git a/integration/test/Test/Brig.hs b/integration/test/Test/Brig.hs index 170017c8f8b..f1c8e9664e2 100644 --- a/integration/test/Test/Brig.hs +++ b/integration/test/Test/Brig.hs @@ -10,8 +10,8 @@ import Testlib.Prelude testSearchContactForExternalUsers :: HasCallStack => App () testSearchContactForExternalUsers = do - owner <- randomUser ownDomain def {Internal.team = True} - partner <- randomUser ownDomain def {Internal.team = True} + owner <- randomUser OwnDomain def {Internal.team = True} + partner <- randomUser OwnDomain def {Internal.team = True} bindResponse (Internal.putTeamMember partner (partner %. "team") (API.teamRole "partner")) $ \resp -> resp.status `shouldMatchInt` 200 diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 63d663c0458..dbdc984f4a1 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -10,7 +10,7 @@ import Testlib.Prelude -- | Legalhold clients cannot be deleted. testCantDeleteLHClient :: HasCallStack => App () testCantDeleteLHClient = do - user <- randomUser ownDomain def + user <- randomUser OwnDomain def client <- Public.addClient user def {Public.ctype = "legalhold", Public.internal = True} >>= getJSON 201 @@ -21,7 +21,7 @@ testCantDeleteLHClient = do -- | Deleting unknown clients should fail with 404. testDeleteUnknownClient :: HasCallStack => App () testDeleteUnknownClient = do - user <- randomUser ownDomain def + user <- randomUser OwnDomain def let fakeClientId = "deadbeefdeadbeef" bindResponse (Public.deleteClient user fakeClientId) $ \resp -> do resp.status `shouldMatchInt` 404 @@ -32,14 +32,14 @@ testModifiedBrig = do withModifiedService Brig (setField "optSettings.setFederationDomain" "overridden.example.com") - $ bindResponse (Public.getAPIVersion ownDomain) + $ bindResponse (Public.getAPIVersion OwnDomain) $ \resp -> do resp.status `shouldMatchInt` 200 (resp.json %. "domain") `shouldMatch` "overridden.example.com" testModifiedGalley :: HasCallStack => App () testModifiedGalley = do - (_user, tid) <- createTeam ownDomain + (_user, tid) <- createTeam OwnDomain let getFeatureStatus = do bindResponse (Internal.getTeamFeature "searchVisibility" tid) $ \res -> do @@ -57,7 +57,7 @@ testModifiedGalley = do testWebSockets :: HasCallStack => App () testWebSockets = do - user <- randomUser ownDomain def + user <- randomUser OwnDomain def withWebSocket user $ \ws -> do client <- Public.addClient user def >>= getJSON 201 n <- awaitMatch 3 (\n -> nPayload n %. "type" `isEqual` "user.client-add") ws @@ -65,11 +65,11 @@ testWebSockets = do testMultipleBackends :: App () testMultipleBackends = do - ownDomainRes <- (Public.getAPIVersion ownDomain >>= getJSON 200) %. "domain" - otherDomainRes <- (Public.getAPIVersion otherDomain >>= getJSON 200) %. "domain" - ownDomainRes `shouldMatch` ownDomain - otherDomainRes `shouldMatch` otherDomain - ownDomain `shouldNotMatch` otherDomain + ownDomainRes <- (Public.getAPIVersion OwnDomain >>= getJSON 200) %. "domain" + otherDomainRes <- (Public.getAPIVersion OtherDomain >>= getJSON 200) %. "domain" + ownDomainRes `shouldMatch` OwnDomain + otherDomainRes `shouldMatch` OtherDomain + OwnDomain `shouldNotMatch` OtherDomain testUnrace :: App () testUnrace = do diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 8fb9d4c3128..5027c628f23 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -2,21 +2,32 @@ module Test.MLS where +import API.Brig (claimKeyPackages) import API.Galley import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as B8 +import qualified Data.Text.Encoding as T import MLS.Util import SetupHelpers import Testlib.Prelude testMixedProtocolUpgrade :: HasCallStack => Domain -> App () testMixedProtocolUpgrade secondDomain = do - [alice, bob, charlie] <- do - d <- ownDomain - d2 <- secondDomain & asString - createAndConnectUsers [d, d2, d2] + (alice, tid) <- createTeam OwnDomain + [bob, charlie] <- replicateM 2 (randomUser secondDomain def) + connectUsers [alice, bob, charlie] - qcnv <- postConversation alice defProteus {qualifiedUsers = [bob, charlie]} >>= getJSON 201 + qcnv <- + postConversation + alice + defProteus + { qualifiedUsers = [bob, charlie], + team = Just tid + } + >>= getJSON 201 + + bindResponse (putConversationProtocol bob qcnv "mls") $ \resp -> do + resp.status `shouldMatchInt` 403 withWebSockets [alice, charlie] $ \websockets -> do bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do @@ -35,15 +46,186 @@ testMixedProtocolUpgrade secondDomain = do bindResponse (putConversationProtocol alice qcnv "mixed") $ \resp -> do resp.status `shouldMatchInt` 204 + bindResponse (putConversationProtocol bob qcnv "proteus") $ \resp -> do + resp.status `shouldMatchInt` 403 + + bindResponse (putConversationProtocol bob qcnv "invalid") $ \resp -> do + resp.status `shouldMatchInt` 400 + +testMixedProtocolNonTeam :: HasCallStack => Domain -> App () +testMixedProtocolNonTeam secondDomain = do + [alice, bob] <- createAndConnectUsers [OwnDomain, secondDomain] + qcnv <- + postConversation alice defProteus {qualifiedUsers = [bob]} + >>= getJSON 201 + + bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do + resp.status `shouldMatchInt` 403 + +testMixedProtocolAddUsers :: HasCallStack => Domain -> App () +testMixedProtocolAddUsers secondDomain = do + (alice, tid) <- createTeam OwnDomain + [bob, charlie] <- replicateM 2 (randomUser secondDomain def) + connectUsers [alice, bob, charlie] + + qcnv <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + + bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 + + [alice1, bob1] <- traverse createMLSClient [alice, bob] + + bindResponse (getConversation alice qcnv) $ \resp -> do + resp.status `shouldMatchInt` 200 + createGroup alice1 resp.json + + traverse_ uploadNewKeyPackage [bob1] + + withWebSockets [alice, bob] $ \wss -> do + mp <- createAddCommit alice1 [bob] + welcome <- assertJust "should have welcome" mp.welcome + void $ sendAndConsumeCommitBundle mp + for_ wss $ \ws -> do + n <- awaitMatch 3 (\n -> nPayload n %. "type" `isEqual` "conversation.mls-welcome") ws + nPayload n %. "data" `shouldMatch` T.decodeUtf8 (Base64.encode welcome) + +testMixedProtocolUserLeaves :: HasCallStack => Domain -> App () +testMixedProtocolUserLeaves secondDomain = do + (alice, tid) <- createTeam OwnDomain + bob <- randomUser secondDomain def + connectUsers [alice, bob] + + qcnv <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + + bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 + + [alice1, bob1] <- traverse createMLSClient [alice, bob] + + bindResponse (getConversation alice qcnv) $ \resp -> do + resp.status `shouldMatchInt` 200 + createGroup alice1 resp.json + + traverse_ uploadNewKeyPackage [bob1] + + mp <- createAddCommit alice1 [bob] + void $ sendAndConsumeCommitBundle mp + + withWebSocket alice $ \ws -> do + bindResponse (removeConversationMember bob qcnv) $ \resp -> + resp.status `shouldMatchInt` 200 + + n <- awaitMatch 3 (\n -> nPayload n %. "type" `isEqual` "conversation.mls-message-add") ws + + msg <- asByteString (nPayload n %. "data") >>= showMessage alice1 + let leafIndexBob = 1 + msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob + msg %. "message.content.sender.External" `shouldMatchInt` 0 + +testMixedProtocolAddPartialClients :: HasCallStack => Domain -> App () +testMixedProtocolAddPartialClients secondDomain = do + (alice, tid) <- createTeam OwnDomain + bob <- randomUser secondDomain def + connectUsers [alice, bob] + + qcnv <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + + bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 + + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + + bindResponse (getConversation alice qcnv) $ \resp -> do + resp.status `shouldMatchInt` 200 + createGroup alice1 resp.json + + traverse_ uploadNewKeyPackage [bob1, bob1, bob2, bob2] + + -- create add commit for only one of bob's two clients + do + bundle <- claimKeyPackages alice1 bob >>= getJSON 200 + kps <- unbundleKeyPackages bundle + kp1 <- assertOne (filter ((== bob1) . fst) kps) + mp <- createAddCommitWithKeyPackages alice1 [kp1] + void $ sendAndConsumeCommitBundle mp + + -- this tests that bob's backend has a mapping of group id to the remote conv + -- this test is only interesting when bob is on OtherDomain + do + bundle <- claimKeyPackages bob1 bob >>= getJSON 200 + kps <- unbundleKeyPackages bundle + kp2 <- assertOne (filter ((== bob2) . fst) kps) + mp <- createAddCommitWithKeyPackages bob1 [kp2] + void $ postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 201 + +testMixedProtocolRemovePartialClients :: HasCallStack => Domain -> App () +testMixedProtocolRemovePartialClients secondDomain = do + (alice, tid) <- createTeam OwnDomain + bob <- randomUser secondDomain def + connectUsers [alice, bob] + + qcnv <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + + bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 + + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + + bindResponse (getConversation alice qcnv) $ \resp -> do + resp.status `shouldMatchInt` 200 + createGroup alice1 resp.json + + traverse_ uploadNewKeyPackage [bob1, bob2] + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + mp <- createRemoveCommit alice1 [bob1] + + void $ postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 201 + +testMixedProtocolAppMessagesAreDenied :: HasCallStack => Domain -> App () +testMixedProtocolAppMessagesAreDenied secondDomain = do + (alice, tid) <- createTeam OwnDomain + bob <- randomUser secondDomain def + connectUsers [alice, bob] + + qcnv <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + + bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 + + [alice1, bob1] <- traverse createMLSClient [alice, bob] + + traverse_ uploadNewKeyPackage [bob1] + + bindResponse (getConversation alice qcnv) $ \resp -> do + resp.status `shouldMatchInt` 200 + createGroup alice1 resp.json + + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + mp <- createApplicationMessage bob1 "hello, world" + bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + resp.status `shouldMatchInt` 422 + resp.json %. "label" `shouldMatch` "mls-unsupported-message" + testAddUser :: HasCallStack => App () testAddUser = do - [alice, bob] <- createAndConnectUsers [ownDomain, ownDomain] + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] - (_, qcnv) <- setupMLSGroup alice1 + (_, qcnv) <- createNewGroup alice1 resp <- createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle events <- resp %. "events" & asList @@ -65,11 +247,28 @@ testAddUser = do "Users added to an MLS group should find it when listing conversations" (qcnv `elem` convIds) +testRemoteAddUser :: HasCallStack => App () +testRemoteAddUser = do + [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OwnDomain] + [alice1, bob1, charlie1] <- traverse createMLSClient [alice, bob, charlie] + traverse_ uploadNewKeyPackage [bob1, charlie1] + (_, conv) <- createNewGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + bindResponse (updateConversationMember alice1 conv bob "wire_admin") $ \resp -> + resp.status `shouldMatchInt` 200 + + mp <- createAddCommit bob1 [charlie] + -- Support for remote admins is not implemeted yet, but this shows that add + -- proposal is being applied action + bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do + resp.status `shouldMatchInt` 500 + resp.json %. "label" `shouldMatch` "federation-not-implemented" + testCreateSubConv :: HasCallStack => App () testCreateSubConv = do - alice <- randomUser ownDomain def + alice <- randomUser OwnDomain def alice1 <- createMLSClient alice - (_, conv) <- setupMLSGroup alice1 + (_, conv) <- createNewGroup alice1 bindResponse (getSubConversation alice conv "conference") $ \resp -> do resp.status `shouldMatchInt` 200 let tm = resp.json %. "epoch_timestamp" @@ -77,7 +276,7 @@ testCreateSubConv = do testCreateSubConvProteus :: App () testCreateSubConvProteus = do - alice <- randomUser ownDomain def + alice <- randomUser OwnDomain def conv <- bindResponse (postConversation alice defProteus) $ \resp -> do resp.status `shouldMatchInt` 201 resp.json @@ -89,10 +288,10 @@ testCreateSubConvProteus = do -- commits are used. testSelfConversation :: App () testSelfConversation = do - alice <- randomUser ownDomain def + alice <- randomUser OwnDomain def creator : others <- traverse createMLSClient (replicate 3 alice) traverse_ uploadNewKeyPackage others - void $ setupMLSSelfGroup creator + void $ createSelfGroup creator commit <- createAddCommit creator [alice] welcome <- assertOne (toList commit.welcome) @@ -107,10 +306,10 @@ testSelfConversation = do testJoinSubConv :: App () testJoinSubConv = do - [alice, bob] <- createAndConnectUsers [ownDomain, ownDomain] + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] - (_, qcnv) <- setupMLSGroup alice1 + (_, qcnv) <- createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle sub <- bindResponse (getSubConversation bob qcnv "conference") $ \resp -> do @@ -131,3 +330,63 @@ testJoinSubConv = do void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle + +-- | FUTUREWORK: Don't allow partial adds, not even in the first commit +testFirstCommitAllowsPartialAdds :: HasCallStack => App () +testFirstCommitAllowsPartialAdds = do + alice <- randomUser OwnDomain def + + [alice1, alice2, alice3] <- traverse createMLSClient [alice, alice, alice] + traverse_ uploadNewKeyPackage [alice1, alice2, alice2, alice3, alice3] + + (_, _qcnv) <- createNewGroup alice1 + + bundle <- claimKeyPackages alice1 alice >>= getJSON 200 + kps <- unbundleKeyPackages bundle + + -- first commit only adds kp for alice2 (not alice2 and alice3) + mp <- createAddCommitWithKeyPackages alice1 (filter ((== alice2) . fst) kps) + bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "mls-client-mismatch" + +testAddUserPartial :: HasCallStack => App () +testAddUserPartial = do + [alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) + + -- Bob has 3 clients, Charlie has 2 + alice1 <- createMLSClient alice + bobClients@[_bob1, _bob2, bob3] <- replicateM 3 (createMLSClient bob) + charlieClients <- replicateM 2 (createMLSClient charlie) + + -- Only the first 2 clients of Bob's have uploaded key packages + traverse_ uploadNewKeyPackage (take 2 bobClients <> charlieClients) + + -- alice adds bob's first 2 clients + void $ createNewGroup alice1 + + -- alice sends a commit now, and should get a conflict error + kps <- fmap concat . for [bob, charlie] $ \user -> do + bundle <- claimKeyPackages alice1 user >>= getJSON 200 + unbundleKeyPackages bundle + mp <- createAddCommitWithKeyPackages alice1 kps + + -- before alice can commit, bob3 uploads a key package + void $ uploadNewKeyPackage bob3 + + err <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 409 + err %. "label" `shouldMatch` "mls-client-mismatch" + +-- | admin removes user from a conversation but doesn't list all clients +testRemoveClientsIncomplete :: HasCallStack => App () +testRemoveClientsIncomplete = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + void $ createNewGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + mp <- createRemoveCommit alice1 [bob1] + + err <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 409 + err %. "label" `shouldMatch` "mls-client-mismatch" diff --git a/integration/test/Test/MLS/KeyPackage.hs b/integration/test/Test/MLS/KeyPackage.hs index c6649c8838e..5ec25410f62 100644 --- a/integration/test/Test/MLS/KeyPackage.hs +++ b/integration/test/Test/MLS/KeyPackage.hs @@ -7,7 +7,7 @@ import Testlib.Prelude testDeleteKeyPackages :: App () testDeleteKeyPackages = do - alice <- randomUser ownDomain def + alice <- randomUser OwnDomain def alice1 <- createMLSClient alice kps <- replicateM 3 (uploadNewKeyPackage alice1) diff --git a/integration/test/Testlib/App.hs b/integration/test/Testlib/App.hs index 01c8fb168cc..e3dcbed544d 100644 --- a/integration/test/Testlib/App.hs +++ b/integration/test/Testlib/App.hs @@ -4,6 +4,7 @@ import Control.Monad.Reader import qualified Control.Retry as Retry import Data.Aeson hiding ((.=)) import Data.IORef +import qualified Data.Text as T import qualified Data.Yaml as Yaml import GHC.Exception import System.FilePath @@ -46,11 +47,11 @@ readServiceConfig srv = do Left err -> failApp ("Error while parsing " <> cfgFile <> ": " <> Yaml.prettyPrintParseException err) Right value -> pure value -ownDomain :: App String -ownDomain = asks (.domain1) +data Domain = OwnDomain | OtherDomain -otherDomain :: App String -otherDomain = asks (.domain2) +instance MakesValue Domain where + make OwnDomain = asks (String . T.pack . (.domain1)) + make OtherDomain = asks (String . T.pack . (.domain2)) -- | Run an action, `recoverAll`ing with exponential backoff (min step 8ms, total timeout -- ~15s). Search this package for examples how to use it. diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index c056fe1c8df..c47f8d69d28 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -20,9 +20,10 @@ assertBool :: HasCallStack => String -> Bool -> App () assertBool _ True = pure () assertBool msg False = assertFailure msg -assertOne :: HasCallStack => [a] -> App a -assertOne [x] = pure x -assertOne xs = assertFailure ("Expected one, but got " <> show (length xs)) +assertOne :: (HasCallStack, Foldable t) => t a -> App a +assertOne xs = case toList xs of + [x] -> pure x + other -> assertFailure ("Expected one, but got " <> show (length other)) expectFailure :: HasCallStack => (AssertionFailure -> App ()) -> App a -> App () expectFailure checkFailure action = do diff --git a/integration/test/Testlib/Cannon.hs b/integration/test/Testlib/Cannon.hs index 85a33b14e1c..414ddd37ba1 100644 --- a/integration/test/Testlib/Cannon.hs +++ b/integration/test/Testlib/Cannon.hs @@ -129,7 +129,7 @@ clientApp wsChan latch conn = do -- for the connection to register with Gundeck, and return the 'Async' thread. run :: HasCallStack => WSConnect -> WS.ClientApp () -> App (Async ()) run wsConnect app = do - domain <- ownDomain + domain <- OwnDomain & asString serviceMap <- getServiceMap domain let HostPort caHost caPort = serviceHostPort serviceMap Cannon @@ -166,7 +166,7 @@ run wsConnect app = do let waitForRegistry :: HasCallStack => App () waitForRegistry = unrace $ do - request <- baseRequest ownDomain Cannon Unversioned ("/i/presences/" <> wsConnect.user <> "/" <> connId) + request <- baseRequest OwnDomain Cannon Unversioned ("/i/presences/" <> wsConnect.user <> "/" <> connId) response <- submit "HEAD" request status response `shouldMatchInt` 200 diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index f1d57f1e228..3a385816efe 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -78,7 +78,7 @@ getBody status resp = withResponse resp $ \r -> do pure r.body -- | Check response status code, then return JSON body. -getJSON :: Int -> Response -> App Aeson.Value +getJSON :: HasCallStack => Int -> Response -> App Aeson.Value getJSON status resp = withResponse resp $ \r -> do r.status `shouldMatch` status r.json diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index fc4360a6e0e..d8dec5d65ed 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -9,6 +9,8 @@ import qualified Data.Aeson.Encode.Pretty as Aeson import qualified Data.Aeson.Key as KM import qualified Data.Aeson.KeyMap as KM import qualified Data.Aeson.Types as Aeson +import Data.ByteString +import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Lazy.Char8 as LC8 import Data.Foldable import Data.Function @@ -17,6 +19,7 @@ import Data.List.Split (splitOn) import qualified Data.Scientific as Sci import Data.String import qualified Data.Text as T +import qualified Data.Text.Encoding as T import GHC.Stack import Testlib.Env import Testlib.Types @@ -77,14 +80,22 @@ asStringM x = (String s) -> pure (Just (T.unpack s)) _ -> pure Nothing +asByteString :: (HasCallStack, MakesValue a) => a -> App ByteString +asByteString x = do + s <- asString x + let bs = T.encodeUtf8 (T.pack s) + case Base64.decode bs of + Left _ -> assertFailure "Could not base64 decode" + Right a -> pure a + asObject :: HasCallStack => MakesValue a => a -> App Object asObject x = make x >>= \case (Object o) -> pure o v -> assertFailureWithJSON x ("Object" `typeWasExpectedButGot` v) -asInt :: HasCallStack => MakesValue a => a -> App Int -asInt x = +asIntegral :: (Integral i, HasCallStack) => MakesValue a => a -> App i +asIntegral x = make x >>= \case (Number n) -> case Sci.floatingOrInteger n of diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 2acf740244c..ed9fab6eae7 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -130,13 +130,12 @@ withModifiedServices services k = do waitUntilServiceUp :: HasCallStack => Service -> App () waitUntilServiceUp srv = do - d <- ownDomain isUp <- retrying (limitRetriesByCumulativeDelay (4 * 1000 * 1000) (fibonacciBackoff (200 * 1000))) (\_ isUp -> pure (not isUp)) ( \_ -> do - req <- baseRequest d srv Unversioned "/i/status" + req <- baseRequest OwnDomain srv Unversioned "/i/status" env <- ask eith <- liftIO $ diff --git a/integration/test/Testlib/PTest.hs b/integration/test/Testlib/PTest.hs index 02b8084b336..d2613fa214e 100644 --- a/integration/test/Testlib/PTest.hs +++ b/integration/test/Testlib/PTest.hs @@ -1,9 +1,6 @@ module Testlib.PTest where -import Data.Aeson (Value (..)) -import qualified Data.Text as T import Testlib.App -import Testlib.JSON import Testlib.Types import Prelude @@ -15,12 +12,6 @@ class HasTests x where instance HasTests (App ()) where mkTests m n s f x = [(m, n, s, f, x)] -data Domain = OwnDomain | OtherDomain - -instance MakesValue Domain where - make OwnDomain = String . T.pack <$> ownDomain - make OtherDomain = String . T.pack <$> otherDomain - instance HasTests x => HasTests (Domain -> x) where mkTests m n s f x = mkTests m (n <> "[domain=own]") s f (x OwnDomain) diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 15f635b08cc..efd1205095b 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -28,6 +28,7 @@ module Wire.API.Conversation.Protocol _ProtocolMLS, _ProtocolMixed, _ProtocolProteus, + conversationMLSData, protocolSchema, ConversationMLSData (..), ProtocolUpdate (..), @@ -35,7 +36,7 @@ module Wire.API.Conversation.Protocol where import Control.Arrow -import Control.Lens (makePrisms, (?~)) +import Control.Lens (Traversal', makePrisms, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Schema import qualified Data.Swagger as S @@ -103,6 +104,11 @@ data Protocol $(makePrisms ''Protocol) +conversationMLSData :: Traversal' Protocol ConversationMLSData +conversationMLSData _ ProtocolProteus = pure ProtocolProteus +conversationMLSData f (ProtocolMLS mls) = ProtocolMLS <$> f mls +conversationMLSData f (ProtocolMixed mls) = ProtocolMixed <$> f mls + protocolTag :: Protocol -> ProtocolTag protocolTag ProtocolProteus = ProtocolProteusTag protocolTag (ProtocolMLS _) = ProtocolMLSTag diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index 16d1462127f..4082dc5fde0 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -36,6 +36,7 @@ import Data.Schema import qualified Data.Swagger as S import qualified Data.Text as T import Data.Time.Clock +import GHC.Records import Imports import Servant (FromHttpApiData (..), ToHttpApiData (toQueryParam)) import Test.QuickCheck @@ -125,6 +126,10 @@ deriving via instance (Generic c, Generic s, Arbitrary c, Arbitrary s) => Arbitrary (ConvOrSubChoice c s) +instance HasField "conv" (ConvOrSubChoice c s) c where + getField (Conv c) = c + getField (SubConv c _) = c + type ConvOrSubConvId = ConvOrSubChoice ConvId SubConvId makePrisms ''ConvOrSubChoice diff --git a/nix/pkgs/mls-test-cli/default.nix b/nix/pkgs/mls-test-cli/default.nix index ddbf9b342a7..e7104041ed8 100644 --- a/nix/pkgs/mls-test-cli/default.nix +++ b/nix/pkgs/mls-test-cli/default.nix @@ -13,8 +13,8 @@ let src = fetchFromGitHub { owner = "wireapp"; repo = "mls-test-cli"; - rev = "29109bd32cedae64bdd9a47ef373710fad477590"; - sha256 = "sha256-1GMiEMkzcKPOd5AsQkQTSMLDkNqy3yjCC03K20vyFVY="; + rev = "87845faa7d5ee69652747ceaf1664baa8198c0d8"; + sha256 = "sha256-DoQ6brp1KvglVVCDp4vC5zaRx76IUywu3Rcu/TzJlvo="; }; cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/Cargo.lock"); in rustPlatform.buildRustPackage rec { diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 45d78a64065..a8f0b775d8d 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -215,7 +215,8 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con HasConversationActionEffects 'ConversationUpdateProtocolTag r = ( Member ConversationStore r, Member (ErrorS 'ConvInvalidProtocolTransition) r, - Member (Error NoChanges) r + Member (Error NoChanges) r, + Member FederatorAccess r ) type family HasConversationActionGalleyErrors (tag :: ConversationActionTag) :: EffectRow where @@ -361,7 +362,7 @@ performAction tag origUser lconv action = do E.deleteAllProposals groupId let cid = convId conv - for_ (conv & mlsMetadata <&> cnvmlsGroupId) $ \gidParent -> do + for_ (conv & mlsMetadata <&> cnvmlsGroupId . fst) $ \gidParent -> do sconvs <- E.listSubConversations cid gidSubs <- for (Map.assocs sconvs) $ \(subid, mlsData) -> do let gidSub = cnvmlsGroupId mlsData @@ -400,17 +401,24 @@ performAction tag origUser lconv action = do (bm, act) <- performConversationAccessData origUser lconv action pure (bm, act) SConversationUpdateProtocolTag -> do - case (protocolTag (convProtocol (tUnqualified lconv)), action) of - (ProtocolProteusTag, ProtocolMixedTag) -> do - E.updateToMixedProtocol lcnv MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + case (protocolTag (convProtocol (tUnqualified lconv)), action, convTeam (tUnqualified lconv)) of + (ProtocolProteusTag, ProtocolMixedTag, Just _) -> do + mls <- E.updateToMixedProtocol lcnv MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + E.runFederatedConcurrently_ (map rmId (convRemoteMembers conv)) $ \_ -> do + void $ + fedClient @'Galley @"on-new-remote-conversation" $ + NewRemoteConversation + { nrcConvId = convId conv, + nrcProtocol = ProtocolMixed mls + } pure (mempty, action) - (ProtocolProteusTag, ProtocolProteusTag) -> + (ProtocolProteusTag, ProtocolProteusTag, _) -> noChanges - (ProtocolMixedTag, ProtocolMixedTag) -> + (ProtocolMixedTag, ProtocolMixedTag, _) -> noChanges - (ProtocolMLSTag, ProtocolMLSTag) -> + (ProtocolMLSTag, ProtocolMLSTag, _) -> noChanges - (_, _) -> throwS @'ConvInvalidProtocolTransition + (_, _, _) -> throwS @'ConvInvalidProtocolTransition performConversationJoin :: ( HasConversationActionEffects 'ConversationJoinTag r diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index f5306229c7c..4acb76390da 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -208,7 +208,7 @@ onNewRemoteConversation :: Sem r EmptyResponse onNewRemoteConversation domain nrc = do -- update group_id -> conv_id mapping - for_ (preview (to F.nrcProtocol . _ProtocolMLS) nrc) $ \mls -> + for_ (preview (to F.nrcProtocol . conversationMLSData) nrc) $ \mls -> E.setGroupIdForConversation (cnvmlsGroupId mls) (Qualified (F.nrcConvId nrc) domain) diff --git a/services/galley/src/Galley/API/MLS/Commit/Core.hs b/services/galley/src/Galley/API/MLS/Commit/Core.hs index 50eca037f1b..9375dea2a77 100644 --- a/services/galley/src/Galley/API/MLS/Commit/Core.hs +++ b/services/galley/src/Galley/API/MLS/Commit/Core.hs @@ -96,16 +96,15 @@ getCommitData :: Sem r ProposalAction getCommitData senderIdentity lConvOrSub epoch commit = do let convOrSub = tUnqualified lConvOrSub - mlsMeta = mlsMetaConvOrSub convOrSub - groupId = cnvmlsGroupId mlsMeta + groupId = cnvmlsGroupId convOrSub.meta - evalState (indexMapConvOrSub convOrSub) $ do + evalState convOrSub.indexMap $ do creatorAction <- if epoch == Epoch 0 then addProposedClient senderIdentity else mempty - proposals <- traverse (derefOrCheckProposal mlsMeta groupId epoch) commit.proposals - action <- applyProposals mlsMeta groupId proposals + proposals <- traverse (derefOrCheckProposal convOrSub.meta groupId epoch) commit.proposals + action <- applyProposals convOrSub.meta groupId proposals pure (creatorAction <> action) incrementEpoch :: diff --git a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs index edb792d9328..03321be6f46 100644 --- a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs @@ -69,9 +69,8 @@ getExternalCommitData :: Sem r ExternalCommitAction getExternalCommitData senderIdentity lConvOrSub epoch commit = do let convOrSub = tUnqualified lConvOrSub - mlsMeta = mlsMetaConvOrSub convOrSub - curEpoch = cnvmlsEpoch mlsMeta - groupId = cnvmlsGroupId mlsMeta + curEpoch = cnvmlsEpoch convOrSub.meta + groupId = cnvmlsGroupId convOrSub.meta when (epoch /= curEpoch) $ throwS @'MLSStaleMessage proposals <- traverse getInlineProposal commit.proposals @@ -90,9 +89,9 @@ getExternalCommitData senderIdentity lConvOrSub epoch commit = do unless (null (Map.keys counts \\ allowedProposals)) $ throw (mlsProtocolError "Invalid proposal type in an external commit") - evalState (indexMapConvOrSub convOrSub) $ do + evalState convOrSub.indexMap $ do -- process optional removal - propAction <- applyProposals mlsMeta groupId proposals + propAction <- applyProposals convOrSub.meta groupId proposals removedIndex <- case cmAssocs (paRemove propAction) of [(cid, idx)] | cid /= senderIdentity -> @@ -144,8 +143,8 @@ processExternalCommit senderIdentity lConvOrSub epoch action updatePath = do <$> note (mlsProtocolError "External commits need an update path") updatePath - let cs = cnvmlsCipherSuite (mlsMetaConvOrSub (tUnqualified lConvOrSub)) - let groupId = cnvmlsGroupId (mlsMetaConvOrSub convOrSub) + let cs = cnvmlsCipherSuite (tUnqualified lConvOrSub).meta + let groupId = cnvmlsGroupId convOrSub.meta let extra = LeafNodeTBSExtraCommit groupId action.add case validateLeafNode cs (Just senderIdentity) extra leafNode.value of Left errMsg -> @@ -153,7 +152,7 @@ processExternalCommit senderIdentity lConvOrSub epoch action updatePath = do mlsProtocolError ("Tried to add invalid LeafNode: " <> errMsg) Right _ -> pure () - withCommitLock (fmap idForConvOrSub lConvOrSub) groupId epoch $ do + withCommitLock (fmap (.id) lConvOrSub) groupId epoch $ do executeExternalCommitAction lConvOrSub senderIdentity action -- increment epoch number @@ -166,12 +165,11 @@ processExternalCommit senderIdentity lConvOrSub epoch action updatePath = do <$> getPendingBackendRemoveProposals groupId epoch -- requeue backend remove proposals for the current epoch - let cm = membersConvOrSub (tUnqualified lConvOrSub') createAndSendRemoveProposals lConvOrSub' indicesInRemoveProposals (cidQualifiedUser senderIdentity) - cm + (tUnqualified lConvOrSub').members executeExternalCommitAction :: forall r. @@ -181,7 +179,7 @@ executeExternalCommitAction :: ExternalCommitAction -> Sem r () executeExternalCommitAction lconvOrSub senderIdentity action = do - let mlsMeta = mlsMetaConvOrSub $ tUnqualified lconvOrSub + let mlsMeta = (tUnqualified lconvOrSub).meta -- Remove deprecated sender client from conversation state. for_ action.remove $ \_ -> diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index 11f6092aa12..cc097c92b56 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -77,135 +77,137 @@ processInternalCommit :: Sem r [LocalConversationUpdate] processInternalCommit senderIdentity con lConvOrSub epoch action commit = do let convOrSub = tUnqualified lConvOrSub - mlsMeta = mlsMetaConvOrSub convOrSub qusr = cidQualifiedUser senderIdentity - cm = membersConvOrSub convOrSub - ss = csSignatureScheme (cnvmlsCipherSuite mlsMeta) + cm = convOrSub.members + ss = csSignatureScheme (cnvmlsCipherSuite convOrSub.meta) newUserClients = Map.assocs (paAdd action) -- check all pending proposals are referenced in the commit - allPendingProposals <- getAllPendingProposalRefs (cnvmlsGroupId mlsMeta) epoch + allPendingProposals <- getAllPendingProposalRefs (cnvmlsGroupId convOrSub.meta) epoch let referencedProposals = Set.fromList $ mapMaybe (\x -> preview Proposal._Ref x) commit.proposals unless (all (`Set.member` referencedProposals) allPendingProposals) $ throwS @'MLSCommitMissingReferences - withCommitLock (fmap idForConvOrSub lConvOrSub) (cnvmlsGroupId (mlsMetaConvOrSub convOrSub)) epoch $ do - -- FUTUREWORK: remove this check after remote admins are implemented in federation https://wearezeta.atlassian.net/browse/FS-216 - foldQualified lConvOrSub (\_ -> pure ()) (\_ -> throwS @'MLSUnsupportedProposal) qusr - + withCommitLock (fmap (.id) lConvOrSub) (cnvmlsGroupId convOrSub.meta) epoch $ do -- no client can be directly added to a subconversation when (is _SubConv convOrSub && any ((senderIdentity /=) . fst) (cmAssocs (paAdd action))) $ throw (mlsProtocolError "Add proposals in subconversations are not supported") - -- Note [client removal] - -- We support two types of removals: - -- 1. when a user is removed from a group, all their clients have to be removed - -- 2. when a client is deleted, that particular client (but not necessarily - -- other clients of the same user) has to be removed. - -- - -- Type 2 requires no special processing on the backend, so here we filter - -- out all removals of that type, so that further checks and processing can - -- be applied only to type 1 removals. - -- - -- Furthermore, subconversation clients can be removed arbitrarily, so this - -- processing is only necessary for main conversations. In the - -- subconversation case, an empty list is returned. - membersToRemove <- case convOrSub of - SubConv _ _ -> pure [] - Conv _ -> mapMaybe hush <$$> for (Map.assocs (paRemove action)) $ - \(qtarget, Map.keysSet -> clients) -> runError @() $ do - let clientsInConv = Map.keysSet (Map.findWithDefault mempty qtarget cm) - let removedClients = Set.intersection clients clientsInConv + events <- + if convOrSub.migrationState == MLSMigrationMLS + then do + -- Note [client removal] + -- We support two types of removals: + -- 1. when a user is removed from a group, all their clients have to be removed + -- 2. when a client is deleted, that particular client (but not necessarily + -- other clients of the same user) has to be removed. + -- + -- Type 2 requires no special processing on the backend, so here we filter + -- out all removals of that type, so that further checks and processing can + -- be applied only to type 1 removals. + -- + -- Furthermore, subconversation clients can be removed arbitrarily, so this + -- processing is only necessary for main conversations. In the + -- subconversation case, an empty list is returned. + membersToRemove <- case convOrSub of + SubConv _ _ -> pure [] + Conv _ -> mapMaybe hush <$$> for (Map.assocs (paRemove action)) $ + \(qtarget, Map.keysSet -> clients) -> runError @() $ do + let clientsInConv = Map.keysSet (Map.findWithDefault mempty qtarget cm) + let removedClients = Set.intersection clients clientsInConv + + -- ignore user if none of their clients are being removed + when (Set.null removedClients) $ throw () + + -- return error if the user is trying to remove themself + when (cidQualifiedUser senderIdentity == qtarget) $ + throwS @'MLSSelfRemovalNotAllowed + + -- FUTUREWORK: add tests against this situation for conv v subconv + when (removedClients /= clientsInConv) $ do + -- FUTUREWORK: turn this error into a proper response + throwS @'MLSClientMismatch - -- ignore user if none of their clients are being removed - when (Set.null removedClients) $ throw () + pure qtarget - -- return error if the user is trying to remove themself - when (cidQualifiedUser senderIdentity == qtarget) $ - throwS @'MLSSelfRemovalNotAllowed + -- for each user, we compare their clients with the ones being added to the conversation + for_ newUserClients $ \(qtarget, newclients) -> case Map.lookup qtarget cm of + -- user is already present, skip check in this case + Just _ -> pure () + -- new user + Nothing -> do + -- final set of clients in the conversation + let clients = Map.keysSet (newclients <> Map.findWithDefault mempty qtarget cm) + -- get list of mls clients from brig + clientInfo <- getClientInfo lConvOrSub qtarget ss + let allClients = Set.map ciId clientInfo + let allMLSClients = Set.map ciId (Set.filter ciMLS clientInfo) + -- We check the following condition: + -- allMLSClients ⊆ clients ⊆ allClients + -- i.e. + -- - if a client has at least 1 key package, it has to be added + -- - if a client is being added, it has to exist + -- + -- The reason why we can't simply check that clients == allMLSClients is + -- that a client with no remaining key packages might be added by a user + -- who just fetched its last key package. + unless + ( Set.isSubsetOf allMLSClients clients + && Set.isSubsetOf clients allClients + ) + $ do + -- unless (Set.isSubsetOf allClients clients) $ do + -- FUTUREWORK: turn this error into a proper response + throwS @'MLSClientMismatch - -- FUTUREWORK: add tests against this situation for conv v subconv - when (removedClients /= clientsInConv) $ do - -- FUTUREWORK: turn this error into a proper response - throwS @'MLSClientMismatch + -- remove users from the conversation and send events + removeEvents <- + foldMap + (removeMembers qusr con lConvOrSub) + (nonEmpty membersToRemove) - pure qtarget + -- if this is a new subconversation, call `on-new-remote-conversation` on all + -- the remote backends involved in the main conversation + forOf_ _SubConv convOrSub $ \(mlsConv, subConv) -> do + when (cnvmlsEpoch (scMLSData subConv) == Epoch 0) $ do + let remoteDomains = + Set.fromList + ( map + (void . rmId) + (mcRemoteMembers mlsConv) + ) + let nrc = + NewRemoteSubConversation + { nrscConvId = mcId mlsConv, + nrscSubConvId = scSubConvId subConv, + nrscMlsData = scMLSData subConv + } + runFederatedConcurrently_ (toList remoteDomains) $ \_ -> do + void $ fedClient @'Galley @"on-new-remote-subconversation" nrc - -- for each user, we compare their clients with the ones being added to the conversation - for_ newUserClients $ \(qtarget, newclients) -> case Map.lookup qtarget cm of - -- user is already present, skip check in this case - Just _ -> pure () - -- new user - Nothing -> do - -- final set of clients in the conversation - let clients = Map.keysSet (newclients <> Map.findWithDefault mempty qtarget cm) - -- get list of mls clients from brig - clientInfo <- getClientInfo lConvOrSub qtarget ss - let allClients = Set.map ciId clientInfo - let allMLSClients = Set.map ciId (Set.filter ciMLS clientInfo) - -- We check the following condition: - -- allMLSClients ⊆ clients ⊆ allClients - -- i.e. - -- - if a client has at least 1 key package, it has to be added - -- - if a client is being added, it has to still exist - -- - -- The reason why we can't simply check that clients == allMLSClients is - -- that a client with no remaining key packages might be added by a user - -- who just fetched its last key package. - unless - ( Set.isSubsetOf allMLSClients clients - && Set.isSubsetOf clients allClients - ) - $ do - -- unless (Set.isSubsetOf allClients clients) $ do - -- FUTUREWORK: turn this error into a proper response - throwS @'MLSClientMismatch + -- add users to the conversation and send events + addEvents <- + foldMap (addMembers qusr con lConvOrSub) + . nonEmpty + . map fst + $ newUserClients - -- remove users from the conversation and send events - removeEvents <- - foldMap - (removeMembers qusr con lConvOrSub) - (nonEmpty membersToRemove) + pure (addEvents <> removeEvents) + else pure [] -- Remove clients from the conversation state. This includes client removals -- of all types (see Note [client removal]). for_ (Map.assocs (paRemove action)) $ \(qtarget, clients) -> do - removeMLSClients (cnvmlsGroupId mlsMeta) qtarget (Map.keysSet clients) - - -- if this is a new subconversation, call `on-new-remote-conversation` on all - -- the remote backends involved in the main conversation - forOf_ _SubConv convOrSub $ \(mlsConv, subConv) -> do - when (cnvmlsEpoch (scMLSData subConv) == Epoch 0) $ do - let remoteDomains = - Set.fromList - ( map - (void . rmId) - (mcRemoteMembers mlsConv) - ) - let nrc = - NewRemoteSubConversation - { nrscConvId = mcId mlsConv, - nrscSubConvId = scSubConvId subConv, - nrscMlsData = scMLSData subConv - } - runFederatedConcurrently_ (toList remoteDomains) $ \_ -> do - void $ fedClient @'Galley @"on-new-remote-subconversation" nrc - - -- add users to the conversation and send events - addEvents <- - foldMap (addMembers qusr con lConvOrSub) - . nonEmpty - . map fst - $ newUserClients + removeMLSClients (cnvmlsGroupId convOrSub.meta) qtarget (Map.keysSet clients) -- add clients in the conversation state for_ newUserClients $ \(qtarget, newClients) -> do - addMLSClients (cnvmlsGroupId mlsMeta) qtarget (Set.fromList (Map.assocs newClients)) + addMLSClients (cnvmlsGroupId convOrSub.meta) qtarget (Set.fromList (Map.assocs newClients)) -- increment epoch number for_ lConvOrSub incrementEpoch - pure (addEvents <> removeEvents) + pure events addMembers :: HasProposalActionEffects r => diff --git a/services/galley/src/Galley/API/MLS/Conversation.hs b/services/galley/src/Galley/API/MLS/Conversation.hs index 5d91d1e4ba6..7d38a776579 100644 --- a/services/galley/src/Galley/API/MLS/Conversation.hs +++ b/services/galley/src/Galley/API/MLS/Conversation.hs @@ -33,7 +33,7 @@ mkMLSConversation :: Data.Conversation -> Sem r (Maybe MLSConversation) mkMLSConversation conv = - for (Data.mlsMetadata conv) $ \mlsData -> do + for (Data.mlsMetadata conv) $ \(mlsData, migrationState) -> do (cm, im) <- lookupMLSClientLeafIndices (cnvmlsGroupId mlsData) pure MLSConversation @@ -43,7 +43,8 @@ mkMLSConversation conv = mcRemoteMembers = Data.convRemoteMembers conv, mcMLSData = mlsData, mcMembers = cm, - mcIndexMap = im + mcIndexMap = im, + mcMigrationState = migrationState } mcConv :: MLSConversation -> Data.Conversation diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index c8facdd7c2e..65066ed6efb 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -45,6 +45,7 @@ import Galley.API.MLS.Types import Galley.API.MLS.Util import Galley.API.MLS.Welcome (sendWelcomes) import Galley.API.Util +import Galley.Data.Conversation.Types import Galley.Effects import Galley.Effects.ConversationStore import Galley.Effects.FederatorAccess @@ -218,10 +219,9 @@ postMLSCommitBundleToLocalConv qusr c conn bundle lConvOrSubId = do bundle.commit.value.path pure ([], []) - storeGroupInfo (idForConvOrSub . tUnqualified $ lConvOrSub) bundle.groupInfo + storeGroupInfo (tUnqualified lConvOrSub).id bundle.groupInfo - let cm = membersConvOrSub (tUnqualified lConvOrSub) - unreachables <- propagateMessage qusr lConvOrSub conn bundle.rawMessage cm + unreachables <- propagateMessage qusr lConvOrSub conn bundle.rawMessage (tUnqualified lConvOrSub).members traverse_ (sendWelcomes lConvOrSub conn newClients) bundle.welcome pure (events, unreachables) @@ -250,7 +250,7 @@ postMLSCommitBundleToRemoteConv loc qusr c con bundle rConvOrSubId = do lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr -- only members may send commit bundles to a remote conversation - flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) (convOfConvOrSub <$> rConvOrSubId) + flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) ((.conv) <$> rConvOrSubId) resp <- runFederated rConvOrSubId $ @@ -314,11 +314,10 @@ getSenderIdentity :: Sem r ClientIdentity getSenderIdentity qusr c mSender lConvOrSubConv = do let cid = mkClientIdentity qusr c - let idxMap = indexMapConvOrSub $ tUnqualified lConvOrSubConv - let epoch = epochNumber . cnvmlsEpoch . mlsMetaConvOrSub . tUnqualified $ lConvOrSubConv + let epoch = epochNumber . cnvmlsEpoch . (.meta) . tUnqualified $ lConvOrSubConv case mSender of SenderMember idx | epoch > 0 -> do - cid' <- note (mlsProtocolError "unknown sender leaf index") $ imLookup idxMap idx + cid' <- note (mlsProtocolError "unknown sender leaf index") $ imLookup (tUnqualified lConvOrSubConv).indexMap idx unless (cid' == cid) $ throwS @'MLSClientSenderUserMismatch _ -> pure () pure cid @@ -350,10 +349,11 @@ postMLSMessageToLocalConv qusr c con msg convOrSubId = do FramedContentApplicationData _ -> throwS @'MLSUnsupportedMessage FramedContentProposal prop -> processProposal qusr lConvOrSub msg.groupId msg.epoch pub prop - IncomingMessageContentPrivate -> pure () + IncomingMessageContentPrivate -> do + when ((tUnqualified lConvOrSub).migrationState == MLSMigrationMixed) $ + throwS @'MLSUnsupportedMessage - let cm = membersConvOrSub (tUnqualified lConvOrSub) - unreachables <- propagateMessage qusr lConvOrSub con msg.rawMessage cm + unreachables <- propagateMessage qusr lConvOrSub con msg.rawMessage (tUnqualified lConvOrSub).members pure ([], unreachables) postMLSMessageToRemoteConv :: @@ -374,7 +374,7 @@ postMLSMessageToRemoteConv loc qusr senderClient con msg rConvOrSubId = do -- only local users can send messages to remote conversations lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr -- only members may send messages to the remote conversation - flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) (convOfConvOrSub <$> rConvOrSubId) + flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) ((.conv) <$> rConvOrSubId) resp <- runFederated rConvOrSubId $ diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 177fb57c4fd..a74a8b6cfad 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -65,7 +65,7 @@ propagateMessage :: Sem r (Maybe UnreachableUsers) propagateMessage qusr lConvOrSub con msg cm = do now <- input @UTCTime - let mlsConv = convOfConvOrSub <$> lConvOrSub + let mlsConv = (.conv) <$> lConvOrSub lmems = mcLocalMembers . tUnqualified $ mlsConv rmems = mcRemoteMembers . tUnqualified $ mlsConv botMap = Map.fromList $ do diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs index 437e1cba429..652df873e3f 100644 --- a/services/galley/src/Galley/API/MLS/Proposal.hs +++ b/services/galley/src/Galley/API/MLS/Proposal.hs @@ -239,7 +239,7 @@ processProposal :: RawMLS Proposal -> Sem r () processProposal qusr lConvOrSub groupId epoch pub prop = do - let mlsMeta = mlsMetaConvOrSub (tUnqualified lConvOrSub) + let mlsMeta = (tUnqualified lConvOrSub).meta -- Check if the epoch number matches that of a conversation unless (epoch == cnvmlsEpoch mlsMeta) $ throwS @'MLSStaleMessage -- Check if the group ID matches that of a conversation @@ -247,8 +247,7 @@ processProposal qusr lConvOrSub groupId epoch pub prop = do let suiteTag = cnvmlsCipherSuite mlsMeta -- FUTUREWORK: validate the member's conversation role - let im = indexMapConvOrSub $ tUnqualified lConvOrSub - checkProposal mlsMeta im prop.value + checkProposal mlsMeta (tUnqualified lConvOrSub).indexMap prop.value when (isExternal pub.sender) $ checkExternalProposalUser qusr prop.value let propRef = authContentRef suiteTag (incomingMessageAuthenticatedContent pub) storeProposal groupId epoch propRef ProposalOriginClient prop diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index f801bf06b5b..a7fdfc414fa 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -74,7 +74,7 @@ createAndSendRemoveProposals :: ClientMap -> Sem r () createAndSendRemoveProposals lConvOrSubConv indices qusr cm = do - let meta = mlsMetaConvOrSub (tUnqualified lConvOrSubConv) + let meta = (tUnqualified lConvOrSubConv).meta mKeyPair <- getMLSRemovalKey case mKeyPair of Nothing -> do @@ -164,7 +164,7 @@ removeClient lc qusr c = do mMlsConv <- mkMLSConversation (tUnqualified lc) for_ mMlsConv $ \mlsConv -> do let cid = mkClientIdentity qusr c - let getClients = fmap (cid,) . cmLookupIndex cid . membersConvOrSub + let getClients = fmap (cid,) . cmLookupIndex cid . (.members) removeClientsWithClientMapRecursively (qualifyAs lc mlsConv) getClients qusr -- | Send remove proposals for all clients of the user to the local conversation. @@ -190,7 +190,7 @@ removeUser lc qusr = do map (first (mkClientIdentity qusr)) . Map.assocs . Map.findWithDefault mempty qusr - . membersConvOrSub + . (.members) removeClientsWithClientMapRecursively (qualifyAs lc mlsConv) getClients qusr -- | Convert cassandra subconv maps into SubConversations diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 461836174df..ea9060b9d64 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -122,7 +122,12 @@ getLocalSubConversation qusr lconv sconv = do msub <- Eff.getSubConversation (tUnqualified lconv) sconv sub <- case msub of Nothing -> do - mlsMeta <- noteS @'ConvNotFound (mlsMetadata c) + (mlsMeta, mlsProtocol) <- noteS @'ConvNotFound (mlsMetadata c) + + case mlsProtocol of + MLSMigrationMixed -> throwS @'MLSSubConvUnsupportedConvType + MLSMigrationMLS -> pure () + -- deriving this detemernistically to prevent race condition between -- multiple threads creating the subconversation let groupId = initialGroupId lconv sconv @@ -281,7 +286,11 @@ deleteLocalSubConversation qusr lcnvId scnvId dsc = do let cnvId = tUnqualified lcnvId lConvOrSubId = qualifyAs lcnvId (SubConv cnvId scnvId) cnv <- getConversationAndCheckMembership qusr lcnvId - cs <- cnvmlsCipherSuite <$> noteS @'ConvNotFound (mlsMetadata cnv) + + (mlsMeta, _mlsProtocol) <- noteS @'ConvNotFound (mlsMetadata cnv) + + let cs = cnvmlsCipherSuite mlsMeta + (mlsData, oldGid) <- withCommitLock lConvOrSubId (dscGroupId dsc) (dscEpoch dsc) $ do sconv <- Eff.getSubConversation cnvId scnvId diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index 59cdbe327bf..eda21c18cfb 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -24,6 +24,8 @@ import Data.IntMap (IntMap) import qualified Data.IntMap as IntMap import qualified Data.Map as Map import Data.Qualified +import GHC.Records (HasField (..)) +import Galley.Data.Conversation.Types import Galley.Types.Conversations.Members import Imports import Wire.API.Conversation @@ -130,7 +132,8 @@ data MLSConversation = MLSConversation mcLocalMembers :: [LocalMember], mcRemoteMembers :: [RemoteMember], mcMembers :: ClientMap, - mcIndexMap :: IndexMap + mcIndexMap :: IndexMap, + mcMigrationState :: MLSMigrationState } deriving (Show) @@ -158,22 +161,22 @@ toPublicSubConv (Qualified (SubConversation {..}) domain) = type ConvOrSubConv = ConvOrSubChoice MLSConversation SubConversation -mlsMetaConvOrSub :: ConvOrSubConv -> ConversationMLSData -mlsMetaConvOrSub (Conv c) = mcMLSData c -mlsMetaConvOrSub (SubConv _ s) = scMLSData s +instance HasField "meta" ConvOrSubConv ConversationMLSData where + getField (Conv c) = mcMLSData c + getField (SubConv _ s) = scMLSData s -membersConvOrSub :: ConvOrSubConv -> ClientMap -membersConvOrSub (Conv c) = mcMembers c -membersConvOrSub (SubConv _ s) = scMembers s +instance HasField "members" ConvOrSubConv ClientMap where + getField (Conv c) = mcMembers c + getField (SubConv _ s) = scMembers s -indexMapConvOrSub :: ConvOrSubConv -> IndexMap -indexMapConvOrSub (Conv c) = mcIndexMap c -indexMapConvOrSub (SubConv _ s) = scIndexMap s +instance HasField "indexMap" ConvOrSubConv IndexMap where + getField (Conv c) = mcIndexMap c + getField (SubConv _ s) = scIndexMap s -convOfConvOrSub :: ConvOrSubChoice c s -> c -convOfConvOrSub (Conv c) = c -convOfConvOrSub (SubConv c _) = c +instance HasField "id" ConvOrSubConv ConvOrSubConvId where + getField (Conv c) = Conv (mcId c) + getField (SubConv c s) = SubConv (mcId c) (scSubConvId s) -idForConvOrSub :: ConvOrSubConv -> ConvOrSubConvId -idForConvOrSub (Conv c) = Conv (mcId c) -idForConvOrSub (SubConv c s) = SubConv (mcId c) (scSubConvId s) +instance HasField "migrationState" ConvOrSubConv MLSMigrationState where + getField (Conv c) = c.mcMigrationState + getField (SubConv _ _) = MLSMigrationMLS diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 98000e95232..701a3f11ee2 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -445,14 +445,22 @@ updateToMixedProtocol :: r => Local ConvId -> CipherSuiteTag -> - Sem r () -updateToMixedProtocol lcnv cs = + Sem r ConversationMLSData +updateToMixedProtocol lcnv cs = do + let gid = convToGroupId lcnv + epoch = Epoch 0 embedClient . retry x5 . batch $ do setType BatchLogged setConsistency LocalQuorum - let gid = convToGroupId lcnv addPrepQuery Cql.insertGroupIdForConversation (gid, tUnqualified lcnv, tDomain lcnv) - addPrepQuery Cql.updateToMixedConv (tUnqualified lcnv, ProtocolMixedTag, gid, Epoch 0, cs) + addPrepQuery Cql.updateToMixedConv (tUnqualified lcnv, ProtocolMixedTag, gid, epoch, cs) + pure + ConversationMLSData + { cnvmlsGroupId = gid, + cnvmlsEpoch = epoch, + cnvmlsEpochTimestamp = Nothing, + cnvmlsCipherSuite = cs + } interpretConversationStoreToCassandra :: ( Member (Embed IO) r, diff --git a/services/galley/src/Galley/Data/Conversation/Types.hs b/services/galley/src/Galley/Data/Conversation/Types.hs index edff99fa5c0..beacb1b30b2 100644 --- a/services/galley/src/Galley/Data/Conversation/Types.hs +++ b/services/galley/src/Galley/Data/Conversation/Types.hs @@ -44,9 +44,14 @@ data NewConversation = NewConversation ncProtocol :: ProtocolCreateTag } -mlsMetadata :: Conversation -> Maybe ConversationMLSData +data MLSMigrationState + = MLSMigrationMixed + | MLSMigrationMLS + deriving (Show, Eq, Ord) + +mlsMetadata :: Conversation -> Maybe (ConversationMLSData, MLSMigrationState) mlsMetadata conv = case convProtocol conv of ProtocolProteus -> Nothing - ProtocolMLS meta -> pure meta - ProtocolMixed meta -> pure meta + ProtocolMLS meta -> pure (meta, MLSMigrationMLS) + ProtocolMixed meta -> pure (meta, MLSMigrationMixed) diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index fe47bb376cc..d5bb268f991 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -71,8 +71,8 @@ import Galley.Types.Conversations.Members import Imports import Polysemy import Wire.API.Conversation hiding (Conversation, Member) +import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite (CipherSuiteTag) -import Wire.API.MLS.Epoch import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation @@ -107,7 +107,7 @@ data ConversationStore m a where AcquireCommitLock :: GroupId -> Epoch -> NominalDiffTime -> ConversationStore m LockAcquired ReleaseCommitLock :: GroupId -> Epoch -> ConversationStore m () DeleteGroupIds :: [GroupId] -> ConversationStore m () - UpdateToMixedProtocol :: Local ConvId -> CipherSuiteTag -> ConversationStore m () + UpdateToMixedProtocol :: Local ConvId -> CipherSuiteTag -> ConversationStore m ConversationMLSData makeSem ''ConversationStore diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 791fb54a6f4..a3d746d79a8 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -100,7 +100,6 @@ tests s = testGroup "Commit" [ test s "add user (not connected)" testAddUserNotConnected, - test s "add user (partial client list)" testAddUserPartial, test s "add client of existing user" testAddClientPartial, test s "add user with some non-MLS clients" testAddUserWithProteusClients, test s "send a stale commit" testStaleCommit, @@ -109,8 +108,7 @@ tests s = test s "add user to a conversation with proposal + commit" testAddUserBareProposalCommit, test s "post commit that references an unknown proposal" testUnknownProposalRefCommit, test s "post commit that is not referencing all proposals" testCommitNotReferencingAllProposals, - test s "admin removes user from a conversation" testAdminRemovesUserFromConv, - test s "admin removes user from a conversation but doesn't list all clients" testRemoveClientsIncomplete + test s "admin removes user from a conversation" testAdminRemovesUserFromConv ], testGroup "External commit" @@ -187,8 +185,7 @@ tests s = testGroup "CommitBundle" [ test s "add user with a commit bundle" testAddUserWithBundle, - test s "add user with a commit bundle to a remote conversation" testAddUserToRemoteConvWithBundle, - test s "remote user posts commit bundle" testRemoteUserPostsCommitBundle + test s "add user with a commit bundle to a remote conversation" testAddUserToRemoteConvWithBundle ], testGroup "Self conversation" @@ -252,10 +249,6 @@ tests s = "Remote Sender/Remote SubConversation" [ test s "on-mls-message-sent in subconversation" testRemoteToRemoteInSub ] - ], - testGroup - "MixedProtocol" - [ test s "Add clients to a mixed conversation and send proteus message" testMixedAddClients ] ] @@ -415,34 +408,6 @@ testAddUserWithProteusClients = do void $ setupMLSGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle -testAddUserPartial :: TestM () -testAddUserPartial = do - [alice, bob, charlie] <- createAndConnectUsers (replicate 3 Nothing) - - runMLSTest $ do - -- Bob has 3 clients, Charlie has 2 - alice1 <- createMLSClient alice - bobClients@[_bob1, _bob2, bob3] <- replicateM 3 (createMLSClient bob) - charlieClients <- replicateM 2 (createMLSClient charlie) - - -- Only the first 2 clients of Bob's have uploaded key packages - traverse_ uploadNewKeyPackage (take 2 bobClients <> charlieClients) - - -- alice adds bob's first 2 clients - void $ setupMLSGroup alice1 - commit <- createAddCommit alice1 [bob, charlie] - - -- before alice can commit, bob3 uploads a key package - void $ uploadNewKeyPackage bob3 - - -- alice sends a commit now, and should get a conflict error - bundle <- createBundle commit - err <- - responseJsonError - =<< localPostCommitBundle (mpSender commit) bundle - >= sendAndConsumeCommitBundle - commit <- createRemoveCommit alice1 [bob1] - - bundle <- createBundle commit - err <- - responseJsonError - =<< localPostCommitBundle alice1 bundle - welcomeMock - withTempMockFederator' mock $ do - void $ sendAndConsumeCommitBundle commit - putOtherMemberQualified (qUnqualified alice) bob (OtherMemberUpdate (Just roleNameWireAdmin)) qcnv - !!! const 200 === statusCode - - [_charlie1] <- traverse createMLSClient [charlie] - commitAddCharlie <- createAddCommit bob1 [charlie] - commitBundle <- createBundle commitAddCharlie - - let msr = - MLSMessageSendRequest - { mmsrConvOrSubId = Conv (qUnqualified qcnv), - mmsrSender = qUnqualified bob, - mmsrSenderClient = ciClient bob1, - mmsrRawMessage = Base64ByteString commitBundle - } - - -- we can't fully test it, because remote admins are not implemeted, but - -- at least this proves that proposal processing has started on the - -- backend - MLSMessageResponseError MLSUnsupportedProposal <- runFedClient @"send-mls-commit-bundle" fedGalleyClient (Domain bobDomain) msr - - pure () - -- | The MLS self-conversation should be available even without explicitly -- creating it by calling `GET /conversations/mls-self` starting from version 3 -- of the client API and should not be listed in versions less than 3. @@ -3345,49 +3256,3 @@ testCreatorRemovesUserFromParent = do ) (sort [alice1, charlie1, charlie2]) (sort $ pscMembers sub2) - -testMixedAddClients :: TestM () -testMixedAddClients = do - [alice, bob, charlie] <- createAndConnectUsers (replicate 3 Nothing) - - runMLSTest $ do - clients@[alice1, bob1, charlie1] <- traverse createMLSClient [alice, bob, charlie] - traverse_ uploadNewKeyPackage clients - - -- alice creates conv - qcnv <- - cnvQualifiedId - <$> liftTest - ( postConvQualified (qUnqualified alice) Nothing defNewProteusConv {newConvQualifiedUsers = [bob, charlie]} - >>= responseJsonError - ) - - -- bob upgrades to mixed - putConversationProtocol (qUnqualified bob) (ciClient bob1) qcnv ProtocolMixedTag - !!! const 200 === statusCode - - conv <- - responseJsonError - =<< getConvQualified (qUnqualified alice) qcnv - do - void $ sendAndConsumeCommitBundle commit - for_ (zip [alice1, charlie1] wss) $ \(c, ws) -> - WS.assertMatch (5 # Second) ws $ - wsAssertMLSWelcome (cidQualifiedUser c) welcome - - -- charlie sends a Proteus message - let msgs = - [ (qUnqualified alice, ciClient alice1, toBase64Text "ciphertext-to-alice"), - (qUnqualified bob, ciClient bob1, toBase64Text "ciphertext-to-bob") - ] - liftTest $ - postOtrMessage id (qUnqualified charlie) (ciClient charlie1) (qUnqualified qcnv) msgs !!! do - const 201 === statusCode - assertMismatch [] [] [] diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index a79b76b1deb..2e072039fca 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -77,7 +77,6 @@ import Federator.MockServer import qualified Federator.MockServer as Mock import GHC.TypeLits (KnownSymbol) import GHC.TypeNats -import Galley.API.MLS.Types import Galley.Intra.User (chunkify) import qualified Galley.Options as Opts import qualified Galley.Run as Run @@ -1729,7 +1728,7 @@ assertMLSMessageEvent :: Conv.Event -> IO () assertMLSMessageEvent qcs u message e = do - evtConv e @?= convOfConvOrSub <$> qcs + evtConv e @?= (.conv) <$> qcs case qUnqualified qcs of Conv _ -> pure () SubConv _ subconvId -> @@ -2917,7 +2916,7 @@ wsAssertBackendRemoveProposal :: HasCallStack => Qualified UserId -> Qualified C wsAssertBackendRemoveProposal fromUser cnvOrSubCnv idx n = do let e = List1.head (WS.unpackPayload n) ntfTransient n @?= False - evtConv e @?= convOfConvOrSub <$> cnvOrSubCnv + evtConv e @?= (.conv) <$> cnvOrSubCnv evtType e @?= MLSMessageAdd evtFrom e @?= fromUser let bs = getMLSMessageData (evtData e) From e90ea40c9b59d751155c0e164e171d4bb10ee86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 26 May 2023 16:22:06 +0200 Subject: [PATCH 045/225] [FS-1148] MLS-specific changes to adding members (#3304) * Fix golden tests for MLSMessageSendigStatus * Port what used to be MLS/Message.hs * Throw instead of relying on failed_to_add In MLS a commit is rejected anyway so there is no point in passing through FailedToProcess. Instead, a federator error is thrown if there are unreachable backends when submitting an add commit. * Test adding to an MLS conversation * Fix: use the mls_migration_lock_status DB column - This is a bug to be fixed in the main MLS branch too --- .../mls-conv-add-across-federation | 1 + libs/wire-api-federation/default.nix | 2 + .../src/Wire/API/Federation/API/Galley.hs | 8 +- .../src/Wire/API/Federation/Error.hs | 38 ++++++ .../Wire/API/Federation/Golden/GoldenSpec.hs | 5 +- .../Golden/MLSMessageSendingStatus.hs | 30 +---- .../testObject_MLSMessageSendingStatus4.json | 10 -- .../testObject_MLSMessageSendingStatus5.json | 14 --- .../testObject_MLSMessageSendingStatus6.json | 18 --- .../wire-api-federation.cabal | 1 + libs/wire-api/src/Wire/API/MLS/Message.hs | 7 +- services/galley/src/Galley/API/Action.hs | 8 +- services/galley/src/Galley/API/Federation.hs | 2 +- .../galley/src/Galley/API/MLS/Commit/Core.hs | 9 +- .../Galley/API/MLS/Commit/InternalCommit.hs | 119 ++++++++++-------- services/galley/src/Galley/API/MLS/Message.hs | 41 ++++-- .../src/Galley/Cassandra/TeamFeatures.hs | 2 +- services/galley/test/integration/API/MLS.hs | 86 ++++++++++--- .../galley/test/integration/API/MLS/Util.hs | 21 ++-- 19 files changed, 248 insertions(+), 174 deletions(-) create mode 100644 changelog.d/1-api-changes/mls-conv-add-across-federation delete mode 100644 libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus4.json delete mode 100644 libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus5.json delete mode 100644 libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus6.json diff --git a/changelog.d/1-api-changes/mls-conv-add-across-federation b/changelog.d/1-api-changes/mls-conv-add-across-federation new file mode 100644 index 00000000000..6c86f1106bf --- /dev/null +++ b/changelog.d/1-api-changes/mls-conv-add-across-federation @@ -0,0 +1 @@ +Report a failure to add remote users to an MLS conversation diff --git a/libs/wire-api-federation/default.nix b/libs/wire-api-federation/default.nix index 9e3b9f2162d..4a174e0311f 100644 --- a/libs/wire-api-federation/default.nix +++ b/libs/wire-api-federation/default.nix @@ -26,6 +26,7 @@ , lib , metrics-wai , mtl +, polysemy , QuickCheck , schema-profunctor , servant @@ -66,6 +67,7 @@ mkDerivation { lens metrics-wai mtl + polysemy QuickCheck schema-profunctor servant 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 1cd0ee3079d..a6bbcd873d6 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 @@ -18,6 +18,7 @@ module Wire.API.Federation.API.Galley where import Data.Aeson (FromJSON, ToJSON) +import Data.Domain import Data.Id import Data.Json.Util import Data.Misc (Milliseconds) @@ -466,7 +467,12 @@ data MLSMessageResponse = MLSMessageResponseError GalleyError | MLSMessageResponseProtocolError Text | MLSMessageResponseProposalFailure Wai.Error - | MLSMessageResponseUpdates [ConversationUpdate] (Maybe UnreachableUsers) + | -- | The conversation-owning backend could not reach some of the backends that + -- have users in the conversation when processing a commit. + MLSMessageResponseUnreachableBackends (Set Domain) + | -- | If the list of unreachable users is non-empty, it corresponds to users + -- that an application message could not be sent to. + MLSMessageResponseUpdates [ConversationUpdate] (Maybe UnreachableUsers) deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded MLSMessageResponse) 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 1f6f297e80c..d51fd252f0d 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs @@ -75,9 +75,17 @@ module Wire.API.Federation.Error federationRemoteResponseError, federationNotImplemented, federationNotConfigured, + + -- * utilities + throwUnreachableUsers, + throwUnreachableDomains, ) where +import Data.Domain +import qualified Data.List.NonEmpty as NE +import Data.Qualified +import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as LT @@ -87,8 +95,11 @@ import qualified Network.HTTP.Types.Status as HTTP import qualified Network.HTTP2.Client as HTTP2 import qualified Network.Wai.Utilities.Error as Wai import OpenSSL.Session (SomeSSLException) +import Polysemy +import qualified Polysemy.Error as P import Servant.Client import Wire.API.Error +import Wire.API.Unreachable -- | Transport-layer errors in federator client. data FederatorClientHTTP2Error @@ -151,6 +162,8 @@ data FederationError FederationUnexpectedBody Text | -- | Federator client got an unexpected error response from remote backend FederationUnexpectedError Text + | -- | One or more remote backends is unreachable + FederationUnreachableDomains (Set Domain) deriving (Show, Typeable) data VersionNegotiationError @@ -178,6 +191,7 @@ federationErrorToWai FederationNotConfigured = federationNotConfigured federationErrorToWai (FederationCallFailure err) = federationClientErrorToWai err federationErrorToWai (FederationUnexpectedBody s) = federationUnexpectedBody s federationErrorToWai (FederationUnexpectedError t) = federationUnexpectedError t +federationErrorToWai (FederationUnreachableDomains ds) = federationUnreachableError ds federationClientErrorToWai :: FederatorClientError -> Wai.Error federationClientErrorToWai (FederatorClientHTTP2Error e) = @@ -304,6 +318,16 @@ federationUnexpectedError msg = "federation-unexpected-wai-error" ("Could parse body, but got an unexpected error response: " <> LT.fromStrict msg) +federationUnreachableError :: Set Domain -> Wai.Error +federationUnreachableError (Set.map domainText -> ds) = + Wai.mkError + status + "federation-unreachable-domains-error" + ("The following domains are unreachable: " <> (LT.pack . show . Set.toList) ds) + where + status :: Status + status = HTTP.Status 503 "Unreachable federated domains" + federationNotConfigured :: Wai.Error federationNotConfigured = Wai.mkError @@ -324,3 +348,17 @@ federationUnknownError = unexpectedFederationResponseStatus "unknown-federation-error" "Unknown federation error" + +-------------------------------------------------------------------------------- +-- Utilities + +throwUnreachableUsers :: Member (P.Error FederationError) r => UnreachableUsers -> Sem r a +throwUnreachableUsers = + throwUnreachableDomains + . Set.fromList + . NE.toList + . fmap qDomain + . unreachableUsers + +throwUnreachableDomains :: Member (P.Error FederationError) r => Set Domain -> Sem r a +throwUnreachableDomains = P.throw . FederationUnreachableDomains diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs index 0acf15af8bf..5bc03b0398b 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs @@ -42,10 +42,7 @@ spec = testObjects [ (MLSMessageSendingStatus.testObject_MLSMessageSendingStatus1, "testObject_MLSMessageSendingStatus1.json"), (MLSMessageSendingStatus.testObject_MLSMessageSendingStatus2, "testObject_MLSMessageSendingStatus2.json"), - (MLSMessageSendingStatus.testObject_MLSMessageSendingStatus3, "testObject_MLSMessageSendingStatus3.json"), - (MLSMessageSendingStatus.testObject_MLSMessageSendingStatus4, "testObject_MLSMessageSendingStatus4.json"), - (MLSMessageSendingStatus.testObject_MLSMessageSendingStatus5, "testObject_MLSMessageSendingStatus5.json"), - (MLSMessageSendingStatus.testObject_MLSMessageSendingStatus6, "testObject_MLSMessageSendingStatus6.json") + (MLSMessageSendingStatus.testObject_MLSMessageSendingStatus3, "testObject_MLSMessageSendingStatus3.json") ] testObjects [(LeaveConversationRequest.testObject_LeaveConversationRequest1, "testObject_LeaveConversationRequest1.json")] testObjects diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs index 16226c28cfa..022a522ac2a 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs @@ -31,7 +31,7 @@ testObject_MLSMessageSendingStatus1 = MLSMessageSendingStatus { mmssEvents = [], mmssTime = toUTCTimeMillis (read "1864-04-12 12:22:43.673 UTC"), - mmssUnreachableUsers = mempty + mmssFailedToSendTo = mempty } testObject_MLSMessageSendingStatus2 :: MLSMessageSendingStatus @@ -39,7 +39,7 @@ testObject_MLSMessageSendingStatus2 = MLSMessageSendingStatus { mmssEvents = [], mmssTime = toUTCTimeMillis (read "2001-04-12 12:22:43.673 UTC"), - mmssUnreachableUsers = unreachableFromList failed1 + mmssFailedToSendTo = unreachableFromList failed1 } testObject_MLSMessageSendingStatus3 :: MLSMessageSendingStatus @@ -47,31 +47,7 @@ testObject_MLSMessageSendingStatus3 = MLSMessageSendingStatus { mmssEvents = [], mmssTime = toUTCTimeMillis (read "1999-04-12 12:22:43.673 UTC"), - mmssUnreachableUsers = unreachableFromList failed2 - } - -testObject_MLSMessageSendingStatus4 :: MLSMessageSendingStatus -testObject_MLSMessageSendingStatus4 = - MLSMessageSendingStatus - { mmssEvents = [], - mmssTime = toUTCTimeMillis (read "2023-04-12 12:22:43.673 UTC"), - mmssUnreachableUsers = unreachableFromList failed1 - } - -testObject_MLSMessageSendingStatus5 :: MLSMessageSendingStatus -testObject_MLSMessageSendingStatus5 = - MLSMessageSendingStatus - { mmssEvents = [], - mmssTime = toUTCTimeMillis (read "1901-04-12 12:22:43.673 UTC"), - mmssUnreachableUsers = unreachableFromList failed2 - } - -testObject_MLSMessageSendingStatus6 :: MLSMessageSendingStatus -testObject_MLSMessageSendingStatus6 = - MLSMessageSendingStatus - { mmssEvents = [], - mmssTime = toUTCTimeMillis (read "1905-04-12 12:22:43.673 UTC"), - mmssUnreachableUsers = unreachableFromList failed1 <> unreachableFromList failed2 + mmssFailedToSendTo = unreachableFromList failed2 } failed1 :: [Qualified UserId] diff --git a/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus4.json b/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus4.json deleted file mode 100644 index ecc3d04f8ac..00000000000 --- a/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus4.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "events": [], - "failed_to_send": [ - { - "domain": "offline.example.com", - "id": "00000000-0000-0000-0000-000200000008" - } - ], - "time": "2023-04-12T12:22:43.673Z" -} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus5.json b/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus5.json deleted file mode 100644 index 44d6fbdb7c2..00000000000 --- a/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus5.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "events": [], - "failed_to_send": [ - { - "domain": "golden.example.com", - "id": "00000000-0000-0000-0000-000200000008" - }, - { - "domain": "golden.example.com", - "id": "00000000-0000-0000-0000-000100000007" - } - ], - "time": "1901-04-12T12:22:43.673Z" -} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus6.json b/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus6.json deleted file mode 100644 index cad9aa0d9a1..00000000000 --- a/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus6.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "events": [], - "failed_to_send": [ - { - "domain": "offline.example.com", - "id": "00000000-0000-0000-0000-000200000008" - }, - { - "domain": "golden.example.com", - "id": "00000000-0000-0000-0000-000200000008" - }, - { - "domain": "golden.example.com", - "id": "00000000-0000-0000-0000-000100000007" - } - ], - "time": "1905-04-12T12:22:43.673Z" -} \ No newline at end of file diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index 3809ad17ff9..eeb7dd5b83b 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -97,6 +97,7 @@ library , lens , metrics-wai , mtl + , polysemy , QuickCheck >=2.13 , schema-profunctor , servant >=0.16 diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs index daf90c5bc08..b913943b5f9 100644 --- a/libs/wire-api/src/Wire/API/MLS/Message.hs +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -377,7 +377,10 @@ verifyMessageSignature ctx msgContent authData pubkey = isJust $ do data MLSMessageSendingStatus = MLSMessageSendingStatus { mmssEvents :: [Event], mmssTime :: UTCTimeMillis, - mmssUnreachableUsers :: Maybe UnreachableUsers + -- | An optional list of unreachable users an application message could not + -- be sent to. In case of commits and unreachable users use the + -- MLSMessageResponseUnreachableBackends data constructor. + mmssFailedToSendTo :: Maybe UnreachableUsers } deriving (Eq, Show) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema MLSMessageSendingStatus @@ -396,7 +399,7 @@ instance ToSchema MLSMessageSendingStatus where "time" (description ?~ "The time of sending the message.") schema - <*> mmssUnreachableUsers + <*> mmssFailedToSendTo .= maybe_ ( optFieldWithDocModifier "failed_to_send" diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index a8f0b775d8d..5b4555e39ce 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -817,15 +817,15 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do -- For now these users will not be able to join the conversation until -- queueing and retrying is implemented. let failedNotifies = lefts notifyEithers + for_ failedNotifies $ + logError + "on-new-remote-conversation" + "An error occurred while communicating with federated server: " for_ failedNotifies $ \case -- rethrow invalid-domain errors and mis-configured federation errors (_, ex@(FederationCallFailure (FederatorClientError (Wai.Error (Wai.Status 422 _) _ _ _)))) -> throw ex (_, ex@(FederationCallFailure (FederatorClientHTTP2Error (FederatorClientConnectionError _)))) -> throw ex _ -> pure () - for_ failedNotifies $ - logError - "on-new-remote-conversation" - "An error occurred while communicating with federated server: " updates <- E.runFederatedConcurrentlyEither (toList (bmRemotes targets)) $ \ruids -> do diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 4acb76390da..1539042748b 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -592,7 +592,7 @@ sendMLSCommitBundle remoteDomain msr = ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle qConvOrSub <- E.lookupConvByGroupId ibundle.groupId >>= noteS @'ConvNotFound when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch - uncurry F.MLSMessageResponseUpdates . first (map lcuUpdate) + uncurry F.MLSMessageResponseUpdates . (,mempty) . map lcuUpdate <$> postMLSCommitBundle loc (tUntagged sender) diff --git a/services/galley/src/Galley/API/MLS/Commit/Core.hs b/services/galley/src/Galley/API/MLS/Commit/Core.hs index 9375dea2a77..f0e60188038 100644 --- a/services/galley/src/Galley/API/MLS/Commit/Core.hs +++ b/services/galley/src/Galley/API/MLS/Commit/Core.hs @@ -134,17 +134,18 @@ getClientInfo :: Local x -> Qualified UserId -> SignatureSchemeTag -> - Sem r (Set ClientInfo) -getClientInfo loc = foldQualified loc getLocalMLSClients getRemoteMLSClients + Sem r (Either FederationError (Set ClientInfo)) +getClientInfo loc = + foldQualified loc (\lusr -> fmap Right . getLocalMLSClients lusr) getRemoteMLSClients getRemoteMLSClients :: ( Member FederatorAccess r ) => Remote UserId -> SignatureSchemeTag -> - Sem r (Set ClientInfo) + Sem r (Either FederationError (Set ClientInfo)) getRemoteMLSClients rusr ss = do - runFederated rusr $ + runFederatedEither rusr $ fedClient @'Brig @"get-mls-clients" $ MLSClientsRequest { mcrUserId = tUnqualified rusr, diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index cc097c92b56..c0af9359ea4 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -50,16 +50,19 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.Credential import qualified Wire.API.MLS.Proposal as Proposal import Wire.API.MLS.SubConversation +import Wire.API.Unreachable import Wire.API.User.Client processInternalCommit :: forall r. ( HasProposalEffects r, + Member (Error FederationError) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'MLSCommitMissingReferences) r, Member (ErrorS 'MLSSelfRemovalNotAllowed) r, @@ -131,34 +134,39 @@ processInternalCommit senderIdentity con lConvOrSub epoch action commit = do pure qtarget -- for each user, we compare their clients with the ones being added to the conversation - for_ newUserClients $ \(qtarget, newclients) -> case Map.lookup qtarget cm of - -- user is already present, skip check in this case - Just _ -> pure () - -- new user - Nothing -> do - -- final set of clients in the conversation - let clients = Map.keysSet (newclients <> Map.findWithDefault mempty qtarget cm) - -- get list of mls clients from brig - clientInfo <- getClientInfo lConvOrSub qtarget ss - let allClients = Set.map ciId clientInfo - let allMLSClients = Set.map ciId (Set.filter ciMLS clientInfo) - -- We check the following condition: - -- allMLSClients ⊆ clients ⊆ allClients - -- i.e. - -- - if a client has at least 1 key package, it has to be added - -- - if a client is being added, it has to exist - -- - -- The reason why we can't simply check that clients == allMLSClients is - -- that a client with no remaining key packages might be added by a user - -- who just fetched its last key package. - unless - ( Set.isSubsetOf allMLSClients clients - && Set.isSubsetOf clients allClients - ) - $ do - -- unless (Set.isSubsetOf allClients clients) $ do - -- FUTUREWORK: turn this error into a proper response - throwS @'MLSClientMismatch + failedAddFetching <- fmap catMaybes . forM newUserClients $ + \(qtarget, newclients) -> case Map.lookup qtarget cm of + -- user is already present, skip check in this case + Just _ -> do + -- new user + pure Nothing + Nothing -> do + -- final set of clients in the conversation + let clients = Map.keysSet (newclients <> Map.findWithDefault mempty qtarget cm) + -- get list of mls clients from Brig (local or remote) + getClientInfo lConvOrSub qtarget ss >>= \case + Left _e -> pure (Just qtarget) + Right clientInfo -> do + let allClients = Set.map ciId clientInfo + let allMLSClients = Set.map ciId (Set.filter ciMLS clientInfo) + -- We check the following condition: + -- allMLSClients ⊆ clients ⊆ allClients + -- i.e. + -- - if a client has at least 1 key package, it has to be added + -- - if a client is being added, it has to still exist + -- + -- The reason why we can't simply check that clients == allMLSClients is + -- that a client with no remaining key packages might be added by a user + -- who just fetched its last key package. + unless + ( Set.isSubsetOf allMLSClients clients + && Set.isSubsetOf clients allClients + ) + $ do + -- FUTUREWORK: turn this error into a proper response + throwS @'MLSClientMismatch + pure Nothing + for_ (unreachableFromList failedAddFetching) throwUnreachableUsers -- remove users from the conversation and send events removeEvents <- @@ -191,7 +199,6 @@ processInternalCommit senderIdentity con lConvOrSub epoch action commit = do . nonEmpty . map fst $ newUserClients - pure (addEvents <> removeEvents) else pure [] @@ -200,7 +207,7 @@ processInternalCommit senderIdentity con lConvOrSub epoch action commit = do for_ (Map.assocs (paRemove action)) $ \(qtarget, clients) -> do removeMLSClients (cnvmlsGroupId convOrSub.meta) qtarget (Map.keysSet clients) - -- add clients in the conversation state + -- add clients to the conversation state for_ newUserClients $ \(qtarget, newClients) -> do addMLSClients (cnvmlsGroupId convOrSub.meta) qtarget (Set.fromList (Map.assocs newClients)) @@ -211,6 +218,7 @@ processInternalCommit senderIdentity con lConvOrSub epoch action commit = do addMembers :: HasProposalActionEffects r => + Member (Error FederationError) r => Qualified UserId -> Maybe ConnId -> Local ConvOrSubConv -> @@ -220,21 +228,26 @@ addMembers qusr con lConvOrSub users = case tUnqualified lConvOrSub of Conv mlsConv -> do let lconv = qualifyAs lConvOrSub (mcConv mlsConv) -- FUTUREWORK: update key package ref mapping to reflect conversation membership - foldMap - ( handleNoChanges - . handleMLSProposalFailures @ProposalErrors - . fmap (pure . fst) - . updateLocalConversationUnchecked @'ConversationJoinTag lconv qusr con - . flip ConversationJoin roleNameWireMember - ) - . nonEmpty - . filter (flip Set.notMember (existingMembers lconv)) - . toList - $ users + (lcus, ftp) <- + foldMap + ( handleNoChanges + . handleMLSProposalFailures @ProposalErrors + . fmap (first pure) + . updateLocalConversationUnchecked @'ConversationJoinTag lconv qusr con + . flip ConversationJoin roleNameWireMember + ) + . nonEmpty + . filter (flip Set.notMember (existingMembers lconv)) + . toList + $ users + for_ (add ftp) throwUnreachableUsers + + pure lcus SubConv _ _ -> pure [] removeMembers :: HasProposalActionEffects r => + Member (Error FederationError) r => Qualified UserId -> Maybe ConnId -> Local ConvOrSubConv -> @@ -243,16 +256,20 @@ removeMembers :: removeMembers qusr con lConvOrSub users = case tUnqualified lConvOrSub of Conv mlsConv -> do let lconv = qualifyAs lConvOrSub (mcConv mlsConv) - foldMap - ( handleNoChanges - . handleMLSProposalFailures @ProposalErrors - . fmap (pure . fst) - . updateLocalConversationUnchecked @'ConversationRemoveMembersTag lconv qusr con - ) - . nonEmpty - . filter (flip Set.member (existingMembers lconv)) - . toList - $ users + (lcus, ftp) <- + foldMap + ( handleNoChanges + . handleMLSProposalFailures @ProposalErrors + . fmap (first pure) + . updateLocalConversationUnchecked @'ConversationRemoveMembersTag lconv qusr con + ) + . nonEmpty + . filter (flip Set.member (existingMembers lconv)) + . toList + $ users + for_ (remove ftp) throwUnreachableUsers + + pure lcus SubConv _ _ -> pure [] handleNoChanges :: Monoid a => Sem (Error NoChanges ': r) a -> Sem r a diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 65066ed6efb..4d626abebe5 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -30,11 +30,16 @@ module Galley.API.MLS.Message where import Control.Comonad +import Data.Domain import Data.Id import Data.Json.Util +import qualified Data.List.NonEmpty as NE import Data.Qualified +import qualified Data.Set as Set +import qualified Data.Text.Lazy as LT import Data.Tuple.Extra import Galley.API.Action +import Galley.API.Error import Galley.API.MLS.Commit import Galley.API.MLS.Conversation import Galley.API.MLS.Enabled @@ -149,7 +154,7 @@ postMLSCommitBundle :: Qualified ConvOrSubConvId -> Maybe ConnId -> IncomingBundle -> - Sem r ([LocalConversationUpdate], Maybe UnreachableUsers) + Sem r [LocalConversationUpdate] postMLSCommitBundle loc qusr c qConvOrSub conn bundle = foldQualified loc @@ -173,14 +178,15 @@ postMLSCommitBundleFromLocalUser lusr c conn bundle = do assertMLSEnabled ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle qConvOrSub <- lookupConvByGroupId ibundle.groupId >>= noteS @'ConvNotFound - (events, unreachables) <- - first (map lcuEvent) + events <- + map lcuEvent <$> postMLSCommitBundle lusr (tUntagged lusr) c qConvOrSub (Just conn) ibundle t <- toUTCTimeMillis <$> input - pure $ MLSMessageSendingStatus events t unreachables + pure $ MLSMessageSendingStatus events t mempty postMLSCommitBundleToLocalConv :: ( HasProposalEffects r, + Member (Error FederationError) r, Members MLSBundleStaticErrors r, Member Resource r, Member SubConversationStore r @@ -190,7 +196,7 @@ postMLSCommitBundleToLocalConv :: Maybe ConnId -> IncomingBundle -> Local ConvOrSubConvId -> - Sem r ([LocalConversationUpdate], Maybe UnreachableUsers) + Sem r [LocalConversationUpdate] postMLSCommitBundleToLocalConv qusr c conn bundle lConvOrSubId = do lConvOrSub <- fetchConvOrSub qusr lConvOrSubId senderIdentity <- getSenderIdentity qusr c bundle.sender lConvOrSub @@ -221,14 +227,17 @@ postMLSCommitBundleToLocalConv qusr c conn bundle lConvOrSubId = do storeGroupInfo (tUnqualified lConvOrSub).id bundle.groupInfo - unreachables <- propagateMessage qusr lConvOrSub conn bundle.rawMessage (tUnqualified lConvOrSub).members + propagateMessage qusr lConvOrSub conn bundle.rawMessage (tUnqualified lConvOrSub).members + >>= mapM_ throwUnreachableUsers + traverse_ (sendWelcomes lConvOrSub conn newClients) bundle.welcome - pure (events, unreachables) + pure events postMLSCommitBundleToRemoteConv :: ( Members MLSBundleStaticErrors r, ( Member BrigAccess r, Member (Error FederationError) r, + Member (Error InternalError) r, Member (Error MLSProtocolError) r, Member (Error MLSProposalFailure) r, Member ExternalAccess r, @@ -244,7 +253,7 @@ postMLSCommitBundleToRemoteConv :: Maybe ConnId -> IncomingBundle -> Remote ConvOrSubConvId -> - Sem r ([LocalConversationUpdate], Maybe UnreachableUsers) + Sem r [LocalConversationUpdate] postMLSCommitBundleToRemoteConv loc qusr c con bundle rConvOrSubId = do -- only local users can send messages to remote conversations lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr @@ -265,11 +274,17 @@ postMLSCommitBundleToRemoteConv loc qusr c con bundle rConvOrSubId = do MLSMessageResponseError e -> rethrowErrors @MLSBundleStaticErrors e MLSMessageResponseProtocolError e -> throw (mlsProtocolError e) MLSMessageResponseProposalFailure e -> throw (MLSProposalFailure e) + MLSMessageResponseUnreachableBackends ds -> throwUnreachableDomains ds MLSMessageResponseUpdates updates unreachables -> do - ups <- for updates $ \update -> do + for_ unreachables $ \us -> + throw . InternalErrorWithDescription $ + "A commit to a remote conversation should not ever return a \ + \non-empty list of users an application message could not be \ + \sent to. The remote end returned: " + <> LT.pack (intercalate ", " (show <$> NE.toList (unreachableUsers us))) + for updates $ \update -> do e <- notifyRemoteConversationAction loc (qualifyAs rConvOrSubId update) con pure (LocalConversationUpdate e update) - pure (ups, unreachables) postMLSMessage :: ( HasProposalEffects r, @@ -390,6 +405,12 @@ postMLSMessageToRemoteConv loc qusr senderClient con msg rConvOrSubId = do MLSMessageResponseProtocolError e -> throw (mlsProtocolError e) MLSMessageResponseProposalFailure e -> throw (MLSProposalFailure e) + MLSMessageResponseUnreachableBackends ds -> + throw . InternalErrorWithDescription $ + "An application or proposal message to a remote conversation should \ + \not ever return a non-empty list of domains a commit could not be \ + \sent to. The remote end returned: " + <> LT.pack (intercalate ", " (show <$> Set.toList (Set.map domainText ds))) MLSMessageResponseUpdates updates unreachables -> do lcus <- for updates $ \update -> do e <- notifyRemoteConversationAction loc (qualifyAs rConvOrSubId update) con diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 327330a5221..7ea2092bb56 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -263,7 +263,7 @@ setFeatureLockStatus FeatureSingletonSelfDeletingMessagesConfig tid status = set setFeatureLockStatus FeatureSingletonGuestLinksConfig tid status = setLockStatusC "guest_links_lock_status" tid status setFeatureLockStatus FeatureSingletonSndFactorPasswordChallengeConfig tid status = setLockStatusC "snd_factor_password_challenge_lock_status" tid status setFeatureLockStatus FeatureSingletonMlsE2EIdConfig tid status = setLockStatusC "mls_e2eid_lock_status" tid status -setFeatureLockStatus FeatureSingletonMlsMigration tid status = setLockStatusC "outlook_cal_integration_lock_status" tid status +setFeatureLockStatus FeatureSingletonMlsMigration tid status = setLockStatusC "mls_migration_lock_status" tid status setFeatureLockStatus FeatureSingletonOutlookCalIntegrationConfig tid status = setLockStatusC "outlook_cal_integration_lock_status" tid status setFeatureLockStatus _ _tid _status = pure () diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index a3d746d79a8..f06b2584151 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -45,6 +45,7 @@ import qualified Data.Text as T import Data.Time import Federator.MockServer hiding (withTempMockFederator) import Imports +import qualified Network.HTTP.Types as HTTP import qualified Network.Wai.Utilities.Error as Wai import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (Second), (#)) @@ -104,6 +105,7 @@ tests s = test s "add user with some non-MLS clients" testAddUserWithProteusClients, test s "send a stale commit" testStaleCommit, test s "add remote user to a conversation" testAddRemoteUser, + test s "add remote users to a conversation (some unreachable)" testAddRemotesSomeUnreachable, test s "return error when commit is locked" testCommitLock, test s "add user to a conversation with proposal + commit" testAddUserBareProposalCommit, test s "post commit that references an unknown proposal" testUnknownProposalRefCommit, @@ -585,6 +587,40 @@ testAddRemoteUser = do event <- assertOne events assertJoinEvent qcnv alice [bob] roleNameWireMember event +testAddRemotesSomeUnreachable :: TestM () +testAddRemotesSomeUnreachable = do + let bobDomain = Domain "bob.example.com" + charlieDomain = Domain "charlie.example.com" + users@[alice, bob, charlie] <- + createAndConnectUsers $ + domainText + <$$> [Nothing, Just bobDomain, Just charlieDomain] + runMLSTest $ do + [alice1, bob1, _charlie1] <- traverse createMLSClient users + (_, qcnv) <- setupMLSGroup alice1 + + commit <- createAddCommit alice1 [bob, charlie] + bundle <- createBundle commit + let unreachable = Set.singleton charlieDomain + (errRaw, _) <- + withTempMockFederator' + ( receiveCommitMockByDomain [bob1] + <|> mlsMockUnreachableFor unreachable + <|> welcomeMockByDomain [bobDomain] + ) + $ localPostCommitBundle (mpSender commit) bundle + + err <- responseJsonError errRaw + liftIO $ do + Wai.label err @?= "federation-unreachable-domains-error" + Wai.code err @?= HTTP.status503 + Wai.message err @?= "The following domains are unreachable: [\"charlie.example.com\"]" + + convAfter <- responseJsonError =<< getConvQualified (qUnqualified alice) qcnv + liftIO $ do + memId (cmSelf (cnvMembers convAfter)) @?= alice + cmOthers (cnvMembers convAfter) @?= [] + testCommitLock :: TestM () testCommitLock = do users <- createAndConnectUsers (replicate 4 Nothing) @@ -1048,10 +1084,9 @@ testAppMessageSomeReachable = do let commitMocks = receiveCommitMockByDomain [bob1, charlie1] <|> welcomeMock - (([event], ftpCommit), _) <- + ([event], _) <- withTempMockFederator' commitMocks $ do - sendAndConsumeCommitBundleFederated commit - liftIO $ ftpCommit @?= mempty + sendAndConsumeCommitBundle commit let unreachables = Set.singleton (Domain "charlie.example.com") let sendMocks = @@ -1060,10 +1095,10 @@ testAppMessageSomeReachable = do withTempMockFederator' sendMocks $ do message <- createApplicationMessage alice1 "hi, bob!" - (_, ftp) <- sendAndConsumeMessage message + (_, failed) <- sendAndConsumeMessage message liftIO $ do assertBool "Event should be member join" $ is _EdMembersJoin (evtData event) - ftp @?= unreachableFromList [charlie] + failed @?= unreachableFromList [charlie] testAppMessageUnreachable :: TestM () testAppMessageUnreachable = do @@ -1082,10 +1117,10 @@ testAppMessageUnreachable = do sendAndConsumeCommitBundle commit message <- createApplicationMessage alice1 "hi, bob!" - (_, ftp) <- sendAndConsumeMessage message + (_, failed) <- sendAndConsumeMessage message liftIO $ do assertBool "Event should be member join" $ is _EdMembersJoin (evtData event) - ftp @?= unreachableFromList [bob] + failed @?= unreachableFromList [bob] testRemoteToRemote :: TestM () testRemoteToRemote = do @@ -2387,7 +2422,9 @@ testRemoteUserJoinSubConv = do cnvmlsEpoch mls @?= Epoch 0 -- bob joins the subconversation - void $ createExternalCommit bob1 Nothing qcs >>= sendAndConsumeCommitBundle + void $ + withTempMockFederator' ("on-mls-message-sent" ~> RemoteMLSMessageOk) $ + createExternalCommit bob1 Nothing qcs >>= sendAndConsumeCommitBundle -- check that bob is now part of the subconversation liftTest $ do @@ -2716,17 +2753,31 @@ testDeleteParentOfSubConv = do traverse_ uploadNewKeyPackage [arthur1] (parentGroupId, qcnv) <- setupMLSGroup alice1 - (qcs, _) <- withTempMockFederator' (receiveCommitMock [bob1]) $ do - void $ createAddCommit alice1 [arthur, bob] >>= sendAndConsumeCommitBundle - createSubConv qcnv alice1 sconv + (qcs, _) <- withTempMockFederator' + ( receiveCommitMock [bob1] + <|> ("on-mls-message-sent" ~> RemoteMLSMessageOk) + ) + $ do + void $ createAddCommit alice1 [arthur, bob] >>= sendAndConsumeCommitBundle + createSubConv qcnv alice1 sconv subGid <- getCurrentGroupId resetGroup arthur1 qcs subGid - void $ createExternalCommit arthur1 Nothing qcs >>= sendAndConsumeCommitBundle + void + $ withTempMockFederator' + ( welcomeMock + <|> ("on-mls-message-sent" ~> RemoteMLSMessageOk) + ) + $ createExternalCommit arthur1 Nothing qcs >>= sendAndConsumeCommitBundle resetGroup bob1 qcs subGid - void $ createExternalCommit bob1 Nothing qcs >>= sendAndConsumeCommitBundle + void + $ withTempMockFederator' + ( welcomeMock + <|> ("on-mls-message-sent" ~> RemoteMLSMessageOk) + ) + $ createExternalCommit bob1 Nothing qcs >>= sendAndConsumeCommitBundle sub' <- responseJsonError @@ -2942,7 +2993,10 @@ testLeaveSubConv isSubConvCreator = do do leaveCommit <- createPendingProposalCommit (head others) mlsBracket (firstLeaver : others) $ \(wsLeaver : wss) -> do - events <- sendAndConsumeCommitBundle leaveCommit + events <- + fst + <$$> withTempMockFederator' ("on-mls-message-sent" ~> RemoteMLSMessageOk) + $ sendAndConsumeCommitBundle leaveCommit liftIO $ events @?= [] WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do wsAssertMLSMessage qsub (cidQualifiedUser . head $ others) (mpMessage leaveCommit) n @@ -2981,7 +3035,9 @@ testLeaveSubConv isSubConvCreator = do traverse_ (uncurry consumeMessage1) (zip others msgs) -- a member commits the pending proposal - void $ createPendingProposalCommit (head others) >>= sendAndConsumeCommitBundle + void $ + withTempMockFederator' ("on-mls-message-sent" ~> RemoteMLSMessageOk) $ + createPendingProposalCommit (head others) >>= sendAndConsumeCommitBundle -- check that only 2 clients are left in the subconv do diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 93ca67d5cd9..a6d57bb66d8 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -143,7 +143,7 @@ remotePostCommitBundle :: Remote ClientIdentity -> Qualified ConvOrSubConvId -> ByteString -> - m ([Event], Maybe UnreachableUsers) + m [Event] remotePostCommitBundle rsender qcs bundle = do client <- view tsFedGalleyClient let msr = @@ -168,20 +168,23 @@ remotePostCommitBundle rsender qcs bundle = do MLSMessageResponseProposalFailure e -> assertFailure $ "proposal failure while receiving commit bundle: " <> displayException e - MLSMessageResponseUpdates _ _ -> pure ([], mempty) + e@(MLSMessageResponseUnreachableBackends _) -> + assertFailure $ + "error while receiving commit bundle: " <> show e + MLSMessageResponseUpdates _ _ -> pure [] postCommitBundle :: HasCallStack => ClientIdentity -> Qualified ConvOrSubConvId -> ByteString -> - TestM ([Event], Maybe UnreachableUsers) + TestM [Event] postCommitBundle sender qcs bundle = do loc <- qualifyLocal () foldQualified loc ( \_ -> - fmap (mmssEvents &&& mmssUnreachableUsers) . responseJsonError + fmap mmssEvents . responseJsonError =<< localPostCommitBundle sender bundle MessagePackage -> MLSTest ([Event], May sendAndConsumeMessage mp = do for_ mp.mpWelcome $ \_ -> liftIO $ assertFailure "use sendAndConsumeCommitBundle" res <- - fmap (mmssEvents Tuple.&&& mmssUnreachableUsers) $ + fmap (mmssEvents Tuple.&&& mmssFailedToSendTo) $ responseJsonError =<< postMessage (mpSender mp) (mpMessage mp) MessagePackage -> MLSTest [Event] -sendAndConsumeCommitBundle = fmap fst . sendAndConsumeCommitBundleFederated - -sendAndConsumeCommitBundleFederated :: - HasCallStack => - MessagePackage -> - MLSTest ([Event], Maybe UnreachableUsers) -sendAndConsumeCommitBundleFederated mp = do +sendAndConsumeCommitBundle mp = do qcs <- getConvId bundle <- createBundle mp resp <- liftTest $ postCommitBundle (mpSender mp) qcs bundle From 27c6a219138449dd46fb7760627882f56891a107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 26 May 2023 16:56:35 +0200 Subject: [PATCH 046/225] Fix: use the mls_migration_lock_status DB column (#3318) - A c/p mistake that is fixed now From ba9a1474ce46a98e532ee39be63f24992558249b Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 1 Jun 2023 17:28:11 +0200 Subject: [PATCH 047/225] Derive group ID from qualified conversation ID and, if applicable, subconversation ID (#3309) Removed federation endpoints - on-new-remote-conversation, - on-new-remote-subconversation, and - on-delete-mls-conversation. Removed effects - Galley.Effects.SubConversationSupply. --- changelog.d/6-federation/FS-1974 | 10 + integration/test/API/Galley.hs | 12 ++ integration/test/Test/MLS.hs | 54 ++++++ .../src/Wire/API/Federation/API/Galley.hs | 36 +--- libs/wire-api/src/Wire/API/MLS/Group.hs | 14 +- .../src/Wire/API/MLS/Group/Serialisation.hs | 82 ++++++++ .../src/Wire/API/MLS/SubConversation.hs | 31 ++-- .../src/Wire/API/Routes/Internal/Galley.hs | 4 +- .../API/Routes/Public/Galley/Conversation.hs | 49 ----- .../Wire/API/Routes/Public/Galley/Feature.hs | 4 +- .../API/Routes/Public/Galley/LegalHold.hs | 10 - .../src/Wire/API/Routes/Public/Galley/MLS.hs | 6 - .../Routes/Public/Galley/TeamConversation.hs | 3 - .../API/MLS/{SubConversation.hs => Group.hs} | 27 ++- libs/wire-api/test/unit/Test/Wire/API/Run.hs | 4 +- libs/wire-api/wire-api.cabal | 3 +- services/galley/default.nix | 4 - services/galley/galley.cabal | 3 - services/galley/src/Galley/API/Action.hs | 72 +------ services/galley/src/Galley/API/Federation.hs | 64 +------ services/galley/src/Galley/API/Internal.hs | 5 +- .../Galley/API/MLS/Commit/InternalCommit.hs | 24 +-- services/galley/src/Galley/API/MLS/Message.hs | 4 +- .../src/Galley/API/MLS/SubConversation.hs | 42 +---- services/galley/src/Galley/API/MLS/Util.hs | 6 + services/galley/src/Galley/API/Update.hs | 17 -- services/galley/src/Galley/App.hs | 2 - .../src/Galley/Cassandra/Conversation.hs | 55 +----- .../galley/src/Galley/Cassandra/Queries.hs | 17 -- .../src/Galley/Cassandra/SubConversation.hs | 11 -- services/galley/src/Galley/Effects.hs | 2 - .../src/Galley/Effects/ConversationStore.hs | 11 +- .../Galley/Effects/SubConversationStore.hs | 3 - .../Galley/Effects/SubConversationSupply.hs | 30 --- .../Effects/SubConversationSupply/Random.hs | 40 ---- services/galley/test/integration/API.hs | 61 +----- services/galley/test/integration/API/MLS.hs | 175 ++---------------- .../galley/test/integration/API/MLS/Mocks.hs | 6 +- .../galley/test/integration/API/MLS/Util.hs | 50 +---- 39 files changed, 264 insertions(+), 789 deletions(-) create mode 100644 changelog.d/6-federation/FS-1974 create mode 100644 libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs rename libs/wire-api/test/unit/Test/Wire/API/MLS/{SubConversation.hs => Group.hs} (60%) delete mode 100644 services/galley/src/Galley/Effects/SubConversationSupply.hs delete mode 100644 services/galley/src/Galley/Effects/SubConversationSupply/Random.hs diff --git a/changelog.d/6-federation/FS-1974 b/changelog.d/6-federation/FS-1974 new file mode 100644 index 00000000000..f3dd1419254 --- /dev/null +++ b/changelog.d/6-federation/FS-1974 @@ -0,0 +1,10 @@ +Derive group ID from qualified conversation ID and, if applicable, +subconversation ID. + +Retire mapping from group IDs to conversation IDs. (group_id_conv_id) + +Remove federation endpoints +- on-new-remote-conversation, +- on-new-remote-subconversation, and +- on-delete-mls-conversation +which were used to synchronise the group to conversation mapping. diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index b10126ab02a..37903ed1b40 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -188,3 +188,15 @@ updateConversationMember user conv target role = do (targetDomain, targetId) <- objQid target req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "members", targetDomain, targetId]) submit "PUT" (req & addJSONObject ["conversation_role" .= role]) + +deleteTeamConv :: + (HasCallStack, MakesValue team, MakesValue conv, MakesValue user) => + team -> + conv -> + user -> + App Response +deleteTeamConv team conv user = do + teamId <- objId team + convId <- objId conv + req <- baseRequest user Galley Versioned (joinHttpPath ["teams", teamId, "conversations", convId]) + submit "DELETE" req diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 5027c628f23..f05406e98e2 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -331,6 +331,60 @@ testJoinSubConv = do createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle +testDeleteParentOfSubConv :: HasCallStack => Domain -> App () +testDeleteParentOfSubConv secondDomain = do + (alice, tid) <- createTeam OwnDomain + bob <- randomUser secondDomain def + connectUsers [alice, bob] + + [alice1, bob1] <- traverse createMLSClient [alice, bob] + traverse_ uploadNewKeyPackage [alice1, bob1] + (_, qcnv) <- createNewGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + sub <- bindResponse (getSubConversation bob qcnv "conference") $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json + resetGroup bob1 sub + + -- bob adds his client to the subconversation + void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle + + -- alice joins with her own client + void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle + + -- bob sends a message to the subconversation + do + mp <- createApplicationMessage bob1 "hello, alice" + void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + resp.status `shouldMatchInt` 201 + + -- alice sends a message to the subconversation + do + mp <- createApplicationMessage bob1 "hello, bob" + void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + resp.status `shouldMatchInt` 201 + + -- alice deletes main conversation + void . bindResponse (deleteTeamConv tid qcnv alice) $ \resp -> do + resp.status `shouldMatchInt` 200 + + -- bob fails to send a message to the subconversation + do + mp <- createApplicationMessage bob1 "hello, alice" + void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + resp.status `shouldMatchInt` 404 + case secondDomain of + OwnDomain -> resp.json %. "label" `shouldMatch` "no-conversation" + OtherDomain -> resp.json %. "label" `shouldMatch` "no-conversation-member" + + -- alice fails to send a message to the subconversation + do + mp <- createApplicationMessage alice1 "hello, bob" + void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + resp.status `shouldMatchInt` 404 + resp.json %. "label" `shouldMatch` "no-conversation" + -- | FUTUREWORK: Don't allow partial adds, not even in the first commit testFirstCommitAllowsPartialAdds :: HasCallStack => App () testFirstCommitAllowsPartialAdds = do 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 a6bbcd873d6..0f1b2aaeea4 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 @@ -57,18 +57,13 @@ type GalleyApi = FedEndpoint "on-conversation-created" (ConversationCreated ConvId) () -- This endpoint is called the first time a user from this backend is -- added to a remote conversation. - :<|> FedEndpoint "on-new-remote-conversation" NewRemoteConversation EmptyResponse - :<|> FedEndpoint "on-new-remote-subconversation" NewRemoteSubConversation EmptyResponse :<|> FedEndpoint "get-conversations" GetConversationsRequest GetConversationsResponse -- used by the backend that owns a conversation to inform this backend of -- changes to the conversation :<|> FedEndpoint "on-conversation-updated" ConversationUpdate () :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-new-remote-conversation", - MakesFederatedCall 'Galley "on-new-remote-subconversation", - MakesFederatedCall 'Galley "on-delete-mls-conversation" + MakesFederatedCall 'Galley "on-mls-message-sent" ] "leave-conversation" LeaveConversationRequest @@ -87,19 +82,14 @@ type GalleyApi = MessageSendResponse :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-new-remote-conversation", - MakesFederatedCall 'Galley "on-new-remote-subconversation" + MakesFederatedCall 'Galley "on-conversation-updated" ] "on-user-deleted-conversations" UserDeletedConversationsNotification EmptyResponse :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-delete-mls-conversation", - MakesFederatedCall 'Galley "on-new-remote-conversation", - MakesFederatedCall 'Galley "on-new-remote-subconversation" + MakesFederatedCall 'Galley "on-mls-message-sent" ] "update-conversation" ConversationUpdateRequest @@ -109,10 +99,7 @@ type GalleyApi = :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-new-remote-conversation", - MakesFederatedCall 'Galley "on-new-remote-subconversation", MakesFederatedCall 'Galley "send-mls-message", - MakesFederatedCall 'Galley "on-delete-mls-conversation", MakesFederatedCall 'Brig "get-mls-clients" ] "send-mls-message" @@ -121,10 +108,7 @@ type GalleyApi = :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "mls-welcome", MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-delete-mls-conversation", MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-new-remote-conversation", - MakesFederatedCall 'Galley "on-new-remote-subconversation", MakesFederatedCall 'Galley "send-mls-commit-bundle", MakesFederatedCall 'Brig "get-mls-clients" ] @@ -147,21 +131,17 @@ type GalleyApi = :<|> FedEndpoint "on-typing-indicator-updated" TypingDataUpdated EmptyResponse :<|> FedEndpoint "get-sub-conversation" GetSubConversationsRequest GetSubConversationsResponse :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-new-remote-subconversation", - MakesFederatedCall 'Galley "on-delete-mls-conversation" + '[ ] "delete-sub-conversation" DeleteSubConversationFedRequest DeleteSubConversationResponse :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-delete-mls-conversation", - MakesFederatedCall 'Galley "on-new-remote-subconversation" + '[ MakesFederatedCall 'Galley "on-mls-message-sent" ] "leave-sub-conversation" LeaveSubConversationRequest LeaveSubConversationResponse - :<|> FedEndpoint "on-delete-mls-conversation" OnDeleteMLSConversationRequest EmptyResponse data TypingDataUpdateRequest = TypingDataUpdateRequest { tdurTypingStatus :: TypingStatus, @@ -540,9 +520,3 @@ data DeleteSubConversationResponse | DeleteSubConversationResponseSuccess deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded DeleteSubConversationResponse) - -newtype OnDeleteMLSConversationRequest = OnDeleteMLSConversationRequest - { odmcGroupIds :: [GroupId] - } - deriving stock (Eq, Show, Generic) - deriving (FromJSON, ToJSON) via (CustomEncoded OnDeleteMLSConversationRequest) diff --git a/libs/wire-api/src/Wire/API/MLS/Group.hs b/libs/wire-api/src/Wire/API/MLS/Group.hs index 31105520009..fcb33a5e36f 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group.hs @@ -17,13 +17,8 @@ module Wire.API.MLS.Group where -import qualified Crypto.Hash as Crypto import qualified Data.Aeson as A -import Data.ByteArray (convert) -import Data.ByteString.Conversion -import Data.Id import Data.Json.Util -import Data.Qualified import Data.Schema import qualified Data.Swagger as S import Imports @@ -50,9 +45,6 @@ instance ToSchema GroupId where <$> unGroupId .= named "GroupId" (Base64ByteString .= fmap fromBase64ByteString (unnamed schema)) --- | Return the group ID associated to a conversation ID. Note that is not --- assumed to be stable over time or even consistent among different backends. -convToGroupId :: Local ConvId -> GroupId -convToGroupId (tUntagged -> qcnv) = - GroupId . convert . Crypto.hash @ByteString @Crypto.SHA256 $ - toByteString' (qUnqualified qcnv) <> toByteString' (qDomain qcnv) +newtype GroupIdGen = GroupIdGen {unGroupIdGen :: Word32} + deriving (Eq, Show, Generic, Ord) + deriving (Arbitrary) via (GenericUniform GroupIdGen) diff --git a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs new file mode 100644 index 00000000000..f735a8a40df --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs @@ -0,0 +1,82 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.MLS.Group.Serialisation + ( convToGroupId, + convToGroupId', + groupIdToConv, + nextGenGroupId, + ) +where + +import Data.Bifunctor +import Data.Binary.Get +import Data.Binary.Put +import Data.ByteString.Conversion +import qualified Data.ByteString.Lazy as L +import Data.Domain +import Data.Id +import Data.Qualified +import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import qualified Data.UUID as UUID +import Imports hiding (cs) +import Web.HttpApiData (FromHttpApiData (parseHeader)) +import Wire.API.MLS.Group +import Wire.API.MLS.SubConversation + +-- | Return the group ID associated to a conversation ID. Note that is not +-- assumed to be stable over time or even consistent among different backends. +convToGroupId :: Qualified ConvOrSubConvId -> GroupIdGen -> GroupId +convToGroupId qcs gen = GroupId . L.toStrict . runPut $ do + let cs = qUnqualified qcs + subId = foldMap unSubConvId cs.subconv + putWord64be 1 -- Version 1 of the GroupId format + putLazyByteString . UUID.toByteString . toUUID $ cs.conv + putWord8 $ fromIntegral (T.length subId) + putByteString $ T.encodeUtf8 subId + maybe (pure ()) (const $ putWord32be (unGroupIdGen gen)) cs.subconv + putLazyByteString . toByteString $ qDomain qcs + +convToGroupId' :: Qualified ConvOrSubConvId -> GroupId +convToGroupId' = flip convToGroupId (GroupIdGen 0) + +groupIdToConv :: GroupId -> Either String (Qualified ConvOrSubConvId, GroupIdGen) +groupIdToConv gid = do + (rem', _, (conv, gen)) <- first (\(_, _, msg) -> msg) $ runGetOrFail readConv (L.fromStrict (unGroupId gid)) + domain <- first displayException . T.decodeUtf8' . L.toStrict $ rem' + pure $ (Qualified conv (Domain domain), gen) + where + readConv = do + version <- getWord64be + unless (version == 1) $ fail "unsupported groupId version" + mUUID <- UUID.fromByteString . L.fromStrict <$> getByteString 16 + uuid <- maybe (fail "invalid conversation UUID in groupId") pure mUUID + n <- getWord8 + if n == 0 + then pure $ (Conv (Id uuid), GroupIdGen 0) + else do + subConvIdBS <- getByteString $ fromIntegral n + subConvId <- either (fail . T.unpack) pure $ parseHeader subConvIdBS + gen <- getWord32be + pure $ (SubConv (Id uuid) (SubConvId subConvId), GroupIdGen gen) + +nextGenGroupId :: GroupId -> Either String GroupId +nextGenGroupId gid = + uncurry convToGroupId + . second (GroupIdGen . succ . unGroupIdGen) + <$> groupIdToConv gid diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index 4082dc5fde0..ed1ff9d1a5e 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -24,10 +24,8 @@ module Wire.API.MLS.SubConversation where import Control.Lens (makePrisms, (?~)) import Control.Lens.Tuple (_1) import Control.Monad.Except -import Crypto.Hash as Crypto import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as A -import Data.ByteArray import Data.ByteString.Conversion import Data.Id import Data.Json.Util @@ -37,7 +35,7 @@ import qualified Data.Swagger as S import qualified Data.Text as T import Data.Time.Clock import GHC.Records -import Imports +import Imports hiding (cs) import Servant (FromHttpApiData (..), ToHttpApiData (toQueryParam)) import Test.QuickCheck import Wire.API.MLS.CipherSuite @@ -52,29 +50,26 @@ import Wire.Arbitrary newtype SubConvId = SubConvId {unSubConvId :: Text} deriving newtype (Eq, ToSchema, Ord, S.ToParamSchema, ToByteString, ToJSON, FromJSON) deriving stock (Generic) - deriving (Arbitrary) via (GenericUniform SubConvId) deriving stock (Show) instance FromHttpApiData SubConvId where parseQueryParam s = do unless (T.length s > 0) $ throwError "The subconversation ID cannot be empty" - unless (T.all isValid s) $ throwError "The subconversation ID contains invalid characters" + unless (T.length s < 256) $ throwError "The subconversation ID cannot be longer than 255 characters" + unless (T.all isValidSubConvChar s) $ throwError "The subconversation ID contains invalid characters" pure (SubConvId s) - where - isValid c = isPrint c && isAscii c && not (isSpace c) instance ToHttpApiData SubConvId where toQueryParam = unSubConvId --- | Compute the inital group ID for a subconversation -initialGroupId :: Local ConvId -> SubConvId -> GroupId -initialGroupId lcnv sconv = - GroupId - . convert - . Crypto.hash @ByteString @Crypto.SHA256 - $ toByteString' (tUnqualified lcnv) - <> toByteString' (tDomain lcnv) - <> toByteString' (unSubConvId sconv) +instance Arbitrary SubConvId where + arbitrary = do + n <- choose (1, 255) + cs <- replicateM n (arbitrary `suchThat` isValidSubConvChar) + pure $ SubConvId (T.pack cs) + +isValidSubConvChar :: Char -> Bool +isValidSubConvChar c = isPrint c && isAscii c && not (isSpace c) data PublicSubConversation = PublicSubConversation { pscParentConvId :: Qualified ConvId, @@ -130,6 +125,10 @@ instance HasField "conv" (ConvOrSubChoice c s) c where getField (Conv c) = c getField (SubConv c _) = c +instance HasField "subconv" (ConvOrSubChoice c s) (Maybe s) where + getField (Conv _) = Nothing + getField (SubConv _ s) = Just s + type ConvOrSubConvId = ConvOrSubChoice ConvId SubConvId makePrisms ''ConvOrSubChoice diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index 331735ab337..3ad80fe567b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -65,9 +65,7 @@ type LegalHoldFeatureStatusChangeErrors = type LegalHoldFeaturesStatusChangeFederatedCalls = '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-new-remote-conversation", - MakesFederatedCall 'Galley "on-new-remote-subconversation" + MakesFederatedCall 'Galley "on-mls-message-sent" ] type IFeatureAPI = diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index cdd91ba959b..98fec9dd478 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -485,8 +485,6 @@ type ConversationAPI = ( Summary "Leave an MLS subconversation" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "leave-sub-conversation" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> CanThrow 'MLSProtocolErrorTag @@ -508,9 +506,6 @@ type ConversationAPI = "delete-subconversation" ( Summary "Delete an MLS subconversation" :> MakesFederatedCall 'Galley "delete-sub-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'MLSNotEnabled @@ -599,10 +594,7 @@ type ConversationAPI = "add-members-to-conversation-unqualified" ( Summary "Add members to an existing conversation (deprecated)" :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> Until 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -626,9 +618,6 @@ type ConversationAPI = ( Summary "Add qualified members to an existing conversation." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> Until 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -653,9 +642,6 @@ type ConversationAPI = ( Summary "Add qualified members to an existing conversation." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> From 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -680,8 +666,6 @@ type ConversationAPI = "join-conversation-by-id-unqualified" ( Summary "Join a conversation by its ID (if link access enabled)" :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -703,8 +687,6 @@ type ConversationAPI = \If the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.\ \Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled." :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow 'CodeNotFound :> CanThrow 'InvalidConversationPassword :> CanThrow 'ConvAccessDenied @@ -861,8 +843,6 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "leave-conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> Until 'V2 :> ZLocalUser :> ZConn @@ -883,8 +863,6 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "leave-conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'RemoveConversationMember) @@ -904,8 +882,6 @@ type ConversationAPI = :> Description "Use `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> ZLocalUser :> ZConn :> CanThrow 'ConvNotFound @@ -930,8 +906,6 @@ type ConversationAPI = :> Description "**Note**: at least one field has to be provided." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> ZLocalUser :> ZConn :> CanThrow 'ConvNotFound @@ -958,8 +932,6 @@ type ConversationAPI = :> Description "Use `/conversations/:domain/:conv/name` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -980,8 +952,6 @@ type ConversationAPI = :> Description "Use `/conversations/:domain/:conv/name` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -1002,8 +972,6 @@ type ConversationAPI = ( Summary "Update conversation name" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -1027,8 +995,6 @@ type ConversationAPI = :> Description "Use `/conversations/:domain/:cnv/message-timer` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) @@ -1050,8 +1016,6 @@ type ConversationAPI = ( Summary "Update the message timer for a conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) @@ -1076,8 +1040,6 @@ type ConversationAPI = :> Description "Use `PUT /conversations/:domain/:cnv/receipt-mode` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Galley "update-conversation" :> ZLocalUser :> ZConn @@ -1100,8 +1062,6 @@ type ConversationAPI = ( Summary "Update receipt mode for a conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Galley "update-conversation" :> ZLocalUser :> ZConn @@ -1126,10 +1086,7 @@ type ConversationAPI = "update-conversation-access-unqualified" ( Summary "Update access modes for a conversation (deprecated)" :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> Until 'V3 :> Description "Use PUT `/conversations/:domain/:cnv/access` instead." :> ZLocalUser @@ -1155,9 +1112,6 @@ type ConversationAPI = ( Summary "Update access modes for a conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> Until 'V3 :> ZLocalUser :> ZConn @@ -1181,10 +1135,7 @@ type ConversationAPI = "update-conversation-access" ( Summary "Update access modes for a conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> From 'V3 :> ZLocalUser :> ZConn diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index e084b87e901..ed21f65875a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -39,9 +39,7 @@ type FeatureAPI = :<|> FeatureStatusGet LegalholdConfig :<|> FeatureStatusPut '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-new-remote-conversation", - MakesFederatedCall 'Galley "on-new-remote-subconversation" + MakesFederatedCall 'Galley "on-mls-message-sent" ] '( 'ActionDenied 'RemoveConversationMember, '( AuthenticationError, diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs index 645878f3758..b29c0024048 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs @@ -65,8 +65,6 @@ type LegalHoldAPI = ( Summary "Delete legal hold service settings" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow AuthenticationError :> CanThrow OperationDenied :> CanThrow 'NotATeamMember @@ -105,8 +103,6 @@ type LegalHoldAPI = ( Summary "Consent to legal hold" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'InvalidOperation :> CanThrow 'TeamMemberNotFound @@ -124,8 +120,6 @@ type LegalHoldAPI = ( Summary "Request legal hold device" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'NotATeamMember :> CanThrow OperationDenied @@ -156,8 +150,6 @@ type LegalHoldAPI = ( Summary "Disable legal hold for user" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow AuthenticationError :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'NotATeamMember @@ -186,8 +178,6 @@ type LegalHoldAPI = ( Summary "Approve legal hold device" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow AuthenticationError :> CanThrow 'AccessDenied :> CanThrow ('ActionDenied 'RemoveConversationMember) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index b0c9dce832c..4dd8fbd56ff 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -38,10 +38,7 @@ type MLSMessagingAPI = :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "send-mls-message" :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Brig "get-mls-clients" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvMemberNotFound :> CanThrow 'ConvNotFound @@ -75,10 +72,7 @@ type MLSMessagingAPI = :> MakesFederatedCall 'Galley "mls-welcome" :> MakesFederatedCall 'Galley "send-mls-commit-bundle" :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> MakesFederatedCall 'Brig "get-mls-clients" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvMemberNotFound :> CanThrow 'ConvNotFound diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs index c814dec30ec..ce5fed146d5 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs @@ -69,10 +69,7 @@ type TeamConversationAPI = "delete-team-conversation" ( Summary "Remove a team conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-delete-mls-conversation" :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "on-new-remote-conversation" - :> MakesFederatedCall 'Galley "on-new-remote-subconversation" :> CanThrow ('ActionDenied 'DeleteConversation) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation diff --git a/libs/wire-api/test/unit/Test/Wire/API/MLS/SubConversation.hs b/libs/wire-api/test/unit/Test/Wire/API/MLS/Group.hs similarity index 60% rename from libs/wire-api/test/unit/Test/Wire/API/MLS/SubConversation.hs rename to libs/wire-api/test/unit/Test/Wire/API/MLS/Group.hs index 6783346756f..c20998b8e4b 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/MLS/Group.hs @@ -15,32 +15,27 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Test.Wire.API.MLS.SubConversation where +module Test.Wire.API.MLS.Group where -import Data.Domain -import Data.Id import Data.Qualified import Imports import Test.QuickCheck import Test.Tasty import Test.Tasty.QuickCheck +import Wire.API.MLS.Group +import Wire.API.MLS.Group.Serialisation import Wire.API.MLS.SubConversation tests :: TestTree tests = testGroup - "Subconversation" - [ testProperty "injectivity of the initial group ID mapping" $ - forAll genIds injectiveInitialGroupId + "Group" + [ testProperty "roundtrip serialise and parse groupId" $ roundtripGroupId ] - where - genIds :: Gen (ConvId, SubConvId, SubConvId) - genIds = do - s1 <- arbitrary - (,,) <$> arbitrary <*> pure s1 <*> arbitrary `suchThat` (/= s1) -injectiveInitialGroupId :: (ConvId, SubConvId, SubConvId) -> Property -injectiveInitialGroupId (cnv, scnv1, scnv2) = do - let domain = Domain "group.example.com" - lcnv = toLocalUnsafe domain cnv - initialGroupId lcnv scnv1 =/= initialGroupId lcnv scnv2 +roundtripGroupId :: Qualified ConvOrSubConvId -> GroupIdGen -> Property +roundtripGroupId convId gen = + let gen' = case qUnqualified convId of + (Conv _) -> GroupIdGen 0 + (SubConv _ _) -> gen + in groupIdToConv (convToGroupId convId gen) === Right (convId, gen') diff --git a/libs/wire-api/test/unit/Test/Wire/API/Run.hs b/libs/wire-api/test/unit/Test/Wire/API/Run.hs index 14382593d05..64f3ae6943a 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Run.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Run.hs @@ -23,7 +23,7 @@ import Test.Tasty import qualified Test.Wire.API.Call.Config as Call.Config import qualified Test.Wire.API.Conversation as Conversation import qualified Test.Wire.API.MLS as MLS -import qualified Test.Wire.API.MLS.SubConversation as SubConversation +import qualified Test.Wire.API.MLS.Group as Group import qualified Test.Wire.API.OAuth as OAuth import qualified Test.Wire.API.RawJson as RawJson import qualified Test.Wire.API.Roundtrip.Aeson as Roundtrip.Aeson @@ -63,7 +63,7 @@ main = Routes.tests, Conversation.tests, MLS.tests, - SubConversation.tests, + Group.tests, Routes.Version.tests, unsafePerformIO Routes.Version.Wai.tests, RawJson.tests, diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 7d95e4f7290..5cac0238a88 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -106,6 +106,7 @@ library Wire.API.MLS.Epoch Wire.API.MLS.Extension Wire.API.MLS.Group + Wire.API.MLS.Group.Serialisation Wire.API.MLS.GroupInfo Wire.API.MLS.HPKEPublicKey Wire.API.MLS.KeyPackage @@ -611,7 +612,7 @@ test-suite wire-api-tests Test.Wire.API.Call.Config Test.Wire.API.Conversation Test.Wire.API.MLS - Test.Wire.API.MLS.SubConversation + Test.Wire.API.MLS.Group Test.Wire.API.OAuth Test.Wire.API.RawJson Test.Wire.API.Roundtrip.Aeson diff --git a/services/galley/default.nix b/services/galley/default.nix index b27031864b6..2699298be3d 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -30,7 +30,6 @@ , currency-codes , data-default , data-timeout -, directory , either , enclosed-exceptions , errors @@ -173,7 +172,6 @@ mkDerivation { imports kan-extensions lens - memory metrics-core metrics-wai mtl @@ -243,7 +241,6 @@ mkDerivation { currency-codes data-default data-timeout - directory errors exceptions extended @@ -251,7 +248,6 @@ mkDerivation { federator filepath galley-types - hex HsOpenSSL hspec http-api-data diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index ee01d270813..fe83b5554d8 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -173,8 +173,6 @@ library Galley.Effects.ServiceStore Galley.Effects.SparAccess Galley.Effects.SubConversationStore - Galley.Effects.SubConversationSupply - Galley.Effects.SubConversationSupply.Random Galley.Effects.TeamFeatureStore Galley.Effects.TeamMemberStore Galley.Effects.TeamNotificationStore @@ -250,7 +248,6 @@ library , imports , kan-extensions , lens >=4.4 - , memory , metrics-core , metrics-wai >=0.4 , mtl >=2.2 diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 5b4555e39ce..c68977e611b 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -80,8 +80,6 @@ import Galley.Types.Conversations.Members import Galley.Types.UserList import Galley.Validation import Imports -import qualified Network.HTTP.Types.Status as Wai -import qualified Network.Wai.Utilities.Error as Wai import Polysemy import Polysemy.Error import Polysemy.Input @@ -364,20 +362,12 @@ performAction tag origUser lconv action = do let cid = convId conv for_ (conv & mlsMetadata <&> cnvmlsGroupId . fst) $ \gidParent -> do sconvs <- E.listSubConversations cid - gidSubs <- for (Map.assocs sconvs) $ \(subid, mlsData) -> do + for_ (Map.assocs sconvs) $ \(subid, mlsData) -> do let gidSub = cnvmlsGroupId mlsData E.deleteSubConversation cid subid - E.deleteGroupIdForSubConversation gidSub deleteGroup gidSub - pure gidSub - E.deleteGroupIdForConversation gidParent deleteGroup gidParent - let odr = OnDeleteMLSConversationRequest ([gidParent] <> gidSubs) - let remotes = bucketRemote (map rmId (Data.convRemoteMembers conv)) - E.runFederatedConcurrently_ remotes $ \_ -> do - void $ fedClient @'Galley @"on-delete-mls-conversation" odr - key <- E.makeKey (tUnqualified lcnv) E.deleteCode key ReusableCode case convTeam conv of @@ -403,14 +393,7 @@ performAction tag origUser lconv action = do SConversationUpdateProtocolTag -> do case (protocolTag (convProtocol (tUnqualified lconv)), action, convTeam (tUnqualified lconv)) of (ProtocolProteusTag, ProtocolMixedTag, Just _) -> do - mls <- E.updateToMixedProtocol lcnv MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - E.runFederatedConcurrently_ (map rmId (convRemoteMembers conv)) $ \_ -> do - void $ - fedClient @'Galley @"on-new-remote-conversation" $ - NewRemoteConversation - { nrcConvId = convId conv, - nrcProtocol = ProtocolMixed mls - } + E.updateToMixedProtocol lcnv MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 pure (mempty, action) (ProtocolProteusTag, ProtocolProteusTag, _) -> noChanges @@ -636,7 +619,6 @@ updateLocalConversation :: Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, - Member SubConversationStore r, Member (Logger (Log.Msg -> Log.Msg)) r, HasConversationActionEffects tag r, SingI tag @@ -677,7 +659,6 @@ updateLocalConversationUnchecked :: Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, - Member SubConversationStore r, Member (Logger (Log.Msg -> Log.Msg)) r, HasConversationActionEffects tag r ) => @@ -755,12 +736,10 @@ addMembersToLocalConversation lcnv users role = do notifyConversationAction :: forall tag r. - ( Member (Error FederationError) r, - Member FederatorAccess r, + ( Member FederatorAccess r, Member ExternalAccess r, Member GundeckAccess r, Member (Input UTCTime) r, - Member SubConversationStore r, Member (Logger (Log.Msg -> Log.Msg)) r ) => Sing tag -> @@ -774,7 +753,6 @@ notifyConversationAction :: notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do now <- input let lcnv = fmap convId lconv - conv = tUnqualified lconv e = conversationActionToEvent tag now quid (tUntagged lcnv) Nothing action let mkUpdate uids = @@ -785,47 +763,7 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do uids (SomeConversationAction tag action) - -- Backends that are seeing this conversation for the first time need to be - -- notified about this conversation and all its subconversations - let newDomains = - Set.difference - (Set.map void (bmRemotes targets)) - (Set.fromList (map (void . rmId) (convRemoteMembers conv))) - newRemotes = - Set.filter (\r -> Set.member (void r) newDomains) - . bmRemotes - $ targets - - subConvs <- Map.assocs <$> E.listSubConversations (convId conv) (update, failedToProcess) <- do - notifyEithers <- - E.runFederatedConcurrentlyEither (toList newRemotes) $ \_ -> do - void $ - fedClient @'Galley @"on-new-remote-conversation" $ - NewRemoteConversation - { nrcConvId = convId conv, - nrcProtocol = convProtocol conv - } - for_ subConvs $ \(mSubId, mlsData) -> - fedClient @'Galley @"on-new-remote-subconversation" - NewRemoteSubConversation - { nrscConvId = convId conv, - nrscSubConvId = mSubId, - nrscMlsData = mlsData - } - - -- For now these users will not be able to join the conversation until - -- queueing and retrying is implemented. - let failedNotifies = lefts notifyEithers - for_ failedNotifies $ - logError - "on-new-remote-conversation" - "An error occurred while communicating with federated server: " - for_ failedNotifies $ \case - -- rethrow invalid-domain errors and mis-configured federation errors - (_, ex@(FederationCallFailure (FederatorClientError (Wai.Error (Wai.Status 422 _) _ _ _)))) -> throw ex - (_, ex@(FederationCallFailure (FederatorClientHTTP2Error (FederatorClientConnectionError _)))) -> throw ex - _ -> pure () updates <- E.runFederatedConcurrentlyEither (toList (bmRemotes targets)) $ \ruids -> do @@ -849,9 +787,7 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do logError "on-conversation-updated" "An error occurred while communicating with federated server: " - let totalFailedToProcess = - failedToAdd (qualifiedFails failedNotifies) - <> toFailedToProcess (qualifiedFails failedUpdates) + let totalFailedToProcess = toFailedToProcess (qualifiedFails failedUpdates) pure (update, totalFailedToProcess) -- notify local participants and bots diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 1539042748b..ee54c9c84cc 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -14,7 +14,6 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} module Galley.API.Federation @@ -25,7 +24,7 @@ module Galley.API.Federation where import Control.Error -import Control.Lens (itraversed, preview, to, (<.>)) +import Control.Lens import Data.Bifunctor import Data.ByteString.Conversion (toByteString') import Data.Domain (Domain) @@ -48,6 +47,7 @@ import Galley.API.MLS.GroupInfo import Galley.API.MLS.Message import Galley.API.MLS.Removal import Galley.API.MLS.SubConversation hiding (leaveSubConversation) +import Galley.API.MLS.Util import Galley.API.MLS.Welcome import qualified Galley.API.Mapping as Mapping import Galley.API.Message @@ -57,12 +57,9 @@ import Galley.API.Util import Galley.App import qualified Galley.Data.Conversation as Data import Galley.Effects -import Galley.Effects.ConversationStore (deleteGroupIds) import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E -import qualified Galley.Effects.SubConversationStore as E -import Galley.Effects.SubConversationSupply import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.UserList (UserList (UserList)) @@ -80,7 +77,6 @@ import qualified System.Logger.Class as Log import Wire.API.Conversation hiding (Member) import qualified Wire.API.Conversation as Public import Wire.API.Conversation.Action -import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley @@ -105,8 +101,6 @@ federationSitemap :: ServerT FederationAPI (Sem GalleyEffects) federationSitemap = Named @"on-conversation-created" onConversationCreated - :<|> Named @"on-new-remote-conversation" onNewRemoteConversation - :<|> Named @"on-new-remote-subconversation" onNewRemoteSubConversation :<|> Named @"get-conversations" getConversations :<|> Named @"on-conversation-updated" onConversationUpdated :<|> Named @"leave-conversation" (callsFed (exposeAnnotations leaveConversation)) @@ -125,7 +119,6 @@ federationSitemap = :<|> Named @"get-sub-conversation" getSubConversationForRemoteUser :<|> Named @"delete-sub-conversation" (callsFed deleteSubConversationForRemoteUser) :<|> Named @"leave-sub-conversation" (callsFed leaveSubConversation) - :<|> Named @"on-delete-mls-conversation" onDeleteMLSConversation onClientRemoved :: ( Member ConversationStore r, @@ -197,40 +190,6 @@ onConversationCreated domain rc = do (EdConversation c) pushConversationEvent Nothing event (qualifyAs loc [qUnqualified . Public.memId $ mem]) [] -onNewRemoteConversation :: - Members - '[ ConversationStore, - SubConversationStore - ] - r => - Domain -> - F.NewRemoteConversation -> - Sem r EmptyResponse -onNewRemoteConversation domain nrc = do - -- update group_id -> conv_id mapping - for_ (preview (to F.nrcProtocol . conversationMLSData) nrc) $ \mls -> - E.setGroupIdForConversation - (cnvmlsGroupId mls) - (Qualified (F.nrcConvId nrc) domain) - - pure EmptyResponse - -onNewRemoteSubConversation :: - Members - '[ ConversationStore, - SubConversationStore - ] - r => - Domain -> - F.NewRemoteSubConversation -> - Sem r EmptyResponse -onNewRemoteSubConversation domain nrsc = do - E.setGroupIdForSubConversation - (cnvmlsGroupId (F.nrscMlsData nrsc)) - (Qualified (F.nrscConvId nrsc) domain) - (F.nrscSubConvId nrsc) - pure EmptyResponse - getConversations :: ( Member ConversationStore r, Member (Input (Local ())) r @@ -590,7 +549,7 @@ sendMLSCommitBundle remoteDomain msr = decodeMLS' (fromBase64ByteString (F.mmsrRawMessage msr)) ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle - qConvOrSub <- E.lookupConvByGroupId ibundle.groupId >>= noteS @'ConvNotFound + qConvOrSub <- getConvFromGroupId ibundle.groupId when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch uncurry F.MLSMessageResponseUpdates . (,mempty) . map lcuUpdate <$> postMLSCommitBundle @@ -637,7 +596,7 @@ sendMLSMessage remoteDomain msr = let sender = toRemoteUnsafe remoteDomain (F.mmsrSender msr) raw <- either (throw . mlsProtocolError) pure $ decodeMLS' (fromBase64ByteString (F.mmsrRawMessage msr)) msg <- noteS @'MLSUnsupportedMessage $ mkIncomingMessage raw - qConvOrSub <- E.lookupConvByGroupId msg.groupId >>= noteS @'ConvNotFound + qConvOrSub <- getConvFromGroupId msg.groupId when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch uncurry F.MLSMessageResponseUpdates . first (map lcuUpdate) <$> postMLSMessage @@ -799,8 +758,7 @@ leaveSubConversation :: ( HasLeaveSubConversationEffects r, Members '[ Input (Local ()), - Resource, - SubConversationSupply + Resource ] r ) => @@ -827,8 +785,7 @@ deleteSubConversationForRemoteUser :: Input Env, MemberStore, Resource, - SubConversationStore, - SubConversationSupply + SubConversationStore ] r ) => @@ -849,15 +806,6 @@ deleteSubConversationForRemoteUser domain DeleteSubConversationFedRequest {..} = lconv <- qualifyLocal dscreqConv deleteLocalSubConversation qusr lconv dscreqSubConv dsc -onDeleteMLSConversation :: - Members '[ConversationStore] r => - Domain -> - OnDeleteMLSConversationRequest -> - Sem r EmptyResponse -onDeleteMLSConversation _domain OnDeleteMLSConversationRequest {..} = do - deleteGroupIds odmcGroupIds - pure EmptyResponse - -------------------------------------------------------------------------------- -- Error handling machinery diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 864ab47c3a5..4b080cbbfe9 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -89,7 +89,8 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error -import Wire.API.MLS.Group +import Wire.API.MLS.Group.Serialisation +import Wire.API.MLS.SubConversation import Wire.API.Provider.Service hiding (Service) import Wire.API.Routes.API import Wire.API.Routes.Internal.Galley @@ -485,5 +486,5 @@ iGetMLSClientListForConv :: ConvId -> Sem r ClientList iGetMLSClientListForConv lusr cnv = do - cm <- E.lookupMLSClients (convToGroupId (qualifyAs lusr cnv)) + cm <- E.lookupMLSClients (convToGroupId' (Conv <$> tUntagged (qualifyAs lusr cnv))) pure $ ClientList (concatMap (Map.keys . snd) (Map.assocs cm)) diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index c0af9359ea4..43d3c8cc285 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -19,7 +19,7 @@ module Galley.API.MLS.Commit.InternalCommit (processInternalCommit) where import Control.Comonad import Control.Error.Util (hush) -import Control.Lens (forOf_, preview) +import Control.Lens import Control.Lens.Extras (is) import Data.Id import Data.List.NonEmpty (NonEmpty, nonEmpty) @@ -36,7 +36,6 @@ import Galley.API.MLS.Util import Galley.Data.Conversation.Types hiding (Conversation) import qualified Galley.Data.Conversation.Types as Data import Galley.Effects -import Galley.Effects.FederatorAccess import Galley.Effects.MemberStore import Galley.Effects.ProposalStore import Galley.Types.Conversations.Members @@ -48,8 +47,6 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.Federation.API -import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit @@ -174,25 +171,6 @@ processInternalCommit senderIdentity con lConvOrSub epoch action commit = do (removeMembers qusr con lConvOrSub) (nonEmpty membersToRemove) - -- if this is a new subconversation, call `on-new-remote-conversation` on all - -- the remote backends involved in the main conversation - forOf_ _SubConv convOrSub $ \(mlsConv, subConv) -> do - when (cnvmlsEpoch (scMLSData subConv) == Epoch 0) $ do - let remoteDomains = - Set.fromList - ( map - (void . rmId) - (mcRemoteMembers mlsConv) - ) - let nrc = - NewRemoteSubConversation - { nrscConvId = mcId mlsConv, - nrscSubConvId = scSubConvId subConv, - nrscMlsData = scMLSData subConv - } - runFederatedConcurrently_ (toList remoteDomains) $ \_ -> do - void $ fedClient @'Galley @"on-new-remote-subconversation" nrc - -- add users to the conversation and send events addEvents <- foldMap (addMembers qusr con lConvOrSub) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 4d626abebe5..6361ed2f7dc 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -134,7 +134,7 @@ postMLSMessageFromLocalUser :: postMLSMessageFromLocalUser lusr c conn smsg = do assertMLSEnabled imsg <- noteS @'MLSUnsupportedMessage $ mkIncomingMessage smsg - cnvOrSub <- lookupConvByGroupId imsg.groupId >>= noteS @'ConvNotFound + cnvOrSub <- getConvFromGroupId imsg.groupId (events, unreachables) <- first (map lcuEvent) <$> postMLSMessage lusr (tUntagged lusr) c cnvOrSub (Just conn) imsg @@ -177,7 +177,7 @@ postMLSCommitBundleFromLocalUser :: postMLSCommitBundleFromLocalUser lusr c conn bundle = do assertMLSEnabled ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle - qConvOrSub <- lookupConvByGroupId ibundle.groupId >>= noteS @'ConvNotFound + qConvOrSub <- getConvFromGroupId ibundle.groupId events <- map lcuEvent <$> postMLSCommitBundle lusr (tUntagged lusr) c qConvOrSub (Just conn) ibundle diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 7179b5c2cd6..fe97707ba53 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -48,12 +48,8 @@ import qualified Galley.Data.Conversation as Data import Galley.Data.Conversation.Types import Galley.Effects import Galley.Effects.FederatorAccess -import qualified Galley.Effects.FederatorAccess as Eff import qualified Galley.Effects.MemberStore as Eff import qualified Galley.Effects.SubConversationStore as Eff -import Galley.Effects.SubConversationSupply (SubConversationSupply) -import qualified Galley.Effects.SubConversationSupply as Eff -import Galley.Types.Conversations.Members import Imports hiding (cs) import Polysemy import Polysemy.Error @@ -68,6 +64,7 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.MLS.Credential +import Wire.API.MLS.Group.Serialisation import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation @@ -130,11 +127,10 @@ getLocalSubConversation qusr lconv sconv = do -- deriving this detemernistically to prevent race condition between -- multiple threads creating the subconversation - let groupId = initialGroupId lconv sconv + let groupId = convToGroupId' $ flip SubConv sconv <$> tUntagged lconv epoch = Epoch 0 suite = cnvmlsCipherSuite mlsMeta Eff.createSubConversation (tUnqualified lconv) sconv suite epoch groupId Nothing - Eff.setGroupIdForSubConversation groupId (tUntagged lconv) sconv let sub = SubConversation { scParentConvId = tUnqualified lconv, @@ -242,8 +238,7 @@ deleteSubConversation :: Input Env, MemberStore, Resource, - SubConversationStore, - SubConversationSupply + SubConversationStore ] r ) => @@ -270,9 +265,7 @@ deleteLocalSubConversation :: Input Env, MemberStore, Resource, - SubConversationStore, - SubConversationSupply, - SubConversationSupply + SubConversationStore ] r ) => @@ -291,7 +284,7 @@ deleteLocalSubConversation qusr lcnvId scnvId dsc = do let cs = cnvmlsCipherSuite mlsMeta - (mlsData, oldGid) <- withCommitLock lConvOrSubId (dscGroupId dsc) (dscEpoch dsc) $ do + withCommitLock lConvOrSubId (dscGroupId dsc) (dscEpoch dsc) $ do sconv <- Eff.getSubConversation cnvId scnvId >>= noteS @'ConvNotFound @@ -300,29 +293,12 @@ deleteLocalSubConversation qusr lcnvId scnvId dsc = do unless (dscEpoch dsc == epoch) $ throwS @'MLSStaleMessage Eff.removeAllMLSClients gid - newGid <- Eff.makeFreshGroupId - - Eff.deleteGroupIdForSubConversation gid - Eff.setGroupIdForSubConversation newGid (tUntagged lcnvId) scnvId + -- swallowing the error and starting with GroupIdGen 0 if nextGenGroupId + let newGid = fromRight (convToGroupId' (flip SubConv scnvId <$> tUntagged lcnvId)) $ nextGenGroupId gid -- the following overwrites any prior information about the subconversation Eff.createSubConversation cnvId scnvId cs (Epoch 0) newGid Nothing - pure (scMLSData sconv, gid) - - -- notify all backends that the subconversation has a new ID - let remotes = bucketRemote (map rmId (convRemoteMembers cnv)) - Eff.runFederatedConcurrently_ remotes $ \_ -> do - void $ - fedClient @'Galley @"on-new-remote-subconversation" - NewRemoteSubConversation - { nrscConvId = cnvId, - nrscSubConvId = scnvId, - nrscMlsData = mlsData - } - fedClient @'Galley @"on-delete-mls-conversation" - (OnDeleteMLSConversationRequest [oldGid]) - deleteRemoteSubConversation :: ( Members '[ ErrorS 'ConvAccessDenied, @@ -388,8 +364,7 @@ leaveSubConversation :: Error FederationError, ErrorS 'MLSStaleMessage, ErrorS 'MLSNotEnabled, - Resource, - SubConversationSupply + Resource ] r, Members LeaveSubConversationStaticErrors r @@ -416,7 +391,6 @@ leaveLocalSubConversation :: ErrorS 'MLSStaleMessage, ErrorS 'MLSNotEnabled, Resource, - SubConversationSupply, MemberStore ] r, diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs index 7091a4989c7..1f41252b398 100644 --- a/services/galley/src/Galley/API/MLS/Util.hs +++ b/services/galley/src/Galley/API/MLS/Util.hs @@ -20,6 +20,7 @@ module Galley.API.MLS.Util where import Control.Comonad import Data.Id import Data.Qualified +import qualified Data.Text as T import Galley.Data.Conversation.Types hiding (Conversation) import qualified Galley.Data.Conversation.Types as Data import Galley.Data.Types @@ -30,6 +31,7 @@ import Galley.Effects.ProposalStore import Galley.Effects.SubConversationStore import Imports import Polysemy +import Polysemy.Error import Polysemy.Resource (Resource, bracket) import Polysemy.TinyLog (TinyLog) import qualified Polysemy.TinyLog as TinyLog @@ -38,6 +40,7 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.MLS.Epoch import Wire.API.MLS.Group +import Wire.API.MLS.Group.Serialisation import Wire.API.MLS.LeafNode import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation @@ -121,3 +124,6 @@ withCommitLock lConvOrSubId gid epoch action = action where ttl = fromIntegral (600 :: Int) -- 10 minutes + +getConvFromGroupId :: Member (Error MLSProtocolError) r => GroupId -> Sem r (Qualified ConvOrSubConvId) +getConvFromGroupId = either (throw . mlsProtocolError . T.pack) (pure . fst) . groupIdToConv diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 3fa5c73f38f..575140e8804 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -331,7 +331,6 @@ updateConversationReceiptMode :: Member (Input (Local ())) r, Member (Input UTCTime) r, Member MemberStore r, - Member SubConversationStore r, Member TinyLog r ) => Local UserId -> @@ -402,7 +401,6 @@ updateConversationReceiptModeUnqualified :: Member (Input (Local ())) r, Member (Input UTCTime) r, Member MemberStore r, - Member SubConversationStore r, Member TinyLog r ) => Local UserId -> @@ -422,7 +420,6 @@ updateConversationMessageTimer :: Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, - Member SubConversationStore r, Member (Logger (Msg -> Msg)) r ) => Local UserId -> @@ -456,7 +453,6 @@ updateConversationMessageTimerUnqualified :: Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, - Member SubConversationStore r, Member (Logger (Msg -> Msg)) r ) => Local UserId -> @@ -701,7 +697,6 @@ updateConversationProtocolWithLocalUser :: Member GundeckAccess r, Member ExternalAccess r, Member FederatorAccess r, - Member SubConversationStore r, Member ConversationStore r ) => Local UserId -> @@ -729,7 +724,6 @@ joinConversationByReusableCode :: ( Member BrigAccess r, Member CodeStore r, Member ConversationStore r, - Member (Error FederationError) r, Member (ErrorS 'CodeNotFound) r, Member (ErrorS 'InvalidConversationPassword) r, Member (ErrorS 'ConvAccessDenied) r, @@ -744,7 +738,6 @@ joinConversationByReusableCode :: Member (Input Opts) r, Member (Input UTCTime) r, Member MemberStore r, - Member SubConversationStore r, Member TeamStore r, Member TeamFeatureStore r, Member (Logger (Msg -> Msg)) r @@ -764,7 +757,6 @@ joinConversationById :: ( Member BrigAccess r, Member FederatorAccess r, Member ConversationStore r, - Member (Error FederationError) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, @@ -775,7 +767,6 @@ joinConversationById :: Member (Input Opts) r, Member (Input UTCTime) r, Member MemberStore r, - Member SubConversationStore r, Member TeamStore r, Member (Logger (Msg -> Msg)) r ) => @@ -790,7 +781,6 @@ joinConversationById lusr zcon cnv = do joinConversation :: ( Member BrigAccess r, Member FederatorAccess r, - Member (Error FederationError) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, @@ -800,7 +790,6 @@ joinConversation :: Member (Input Opts) r, Member (Input UTCTime) r, Member MemberStore r, - Member SubConversationStore r, Member TeamStore r, Member (Logger (Msg -> Msg)) r ) => @@ -1021,7 +1010,6 @@ updateOtherMemberLocalConv :: Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, - Member SubConversationStore r, Member (Logger (Msg -> Msg)) r ) => Local ConvId -> @@ -1049,7 +1037,6 @@ updateOtherMemberUnqualified :: Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, - Member SubConversationStore r, Member (Logger (Msg -> Msg)) r ) => Local UserId -> @@ -1076,7 +1063,6 @@ updateOtherMember :: Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, - Member SubConversationStore r, Member (Logger (Msg -> Msg)) r ) => Local UserId -> @@ -1393,7 +1379,6 @@ updateConversationName :: Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, - Member SubConversationStore r, Member (Logger (Msg -> Msg)) r ) => Local UserId -> @@ -1420,7 +1405,6 @@ updateUnqualifiedConversationName :: Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, - Member SubConversationStore r, Member (Logger (Msg -> Msg)) r ) => Local UserId -> @@ -1443,7 +1427,6 @@ updateLocalConversationName :: Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, - Member SubConversationStore r, Member (Logger (Msg -> Msg)) r ) => Local UserId -> diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index e161d550f55..2ff0b50468a 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -76,7 +76,6 @@ import Galley.Cassandra.TeamFeatures import Galley.Cassandra.TeamNotifications import Galley.Effects import Galley.Effects.FireAndForget (interpretFireAndForget) -import Galley.Effects.SubConversationSupply.Random import Galley.Effects.WaiRoutes.IO import Galley.Env import Galley.External @@ -275,7 +274,6 @@ evalGalley e = . interpretLegalHoldStoreToCassandra lh . interpretCustomBackendStoreToCassandra . randomToIO - . interpretSubConversationSupplyToRandom . interpretSubConversationStoreToCassandra . interpretConversationStoreToCassandra . interpretProposalStoreToCassandra diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 6768fe2e82d..b45fe567967 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -28,7 +28,6 @@ import Cassandra.Util import Control.Error.Util import Control.Monad.Trans.Maybe import Data.ByteString.Conversion -import Data.Domain import Data.Id import qualified Data.Map as Map import Data.Misc @@ -57,7 +56,7 @@ import qualified UnliftIO import Wire.API.Conversation hiding (Conversation, Member) import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite -import Wire.API.MLS.Group +import Wire.API.MLS.Group.Serialisation import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation @@ -75,7 +74,7 @@ createMLSSelfConversation lusr = do ncProtocol = ProtocolCreateMLSTag } meta = ncMetadata nc - gid = convToGroupId . qualifyAs lusr $ cnv + gid = convToGroupId' . fmap Conv . tUntagged . qualifyAs lusr $ cnv -- FUTUREWORK: Stop hard-coding the cipher suite -- -- 'CipherSuite 1' corresponds to @@ -106,7 +105,6 @@ createMLSSelfConversation lusr = do Just gid, Just cs ) - addPrepQuery Cql.insertGroupIdForConversation (gid, cnv, tDomain lusr) (lmems, rmems) <- addMembers cnv (ncUsers nc) pure @@ -125,7 +123,7 @@ createConversation lcnv nc = do (proto, mgid, mep, mcs) = case ncProtocol nc of ProtocolCreateProteusTag -> (ProtocolProteus, Nothing, Nothing, Nothing) ProtocolCreateMLSTag -> - let gid = convToGroupId lcnv + let gid = convToGroupId' $ Conv <$> tUntagged lcnv ep = Epoch 0 cs = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 in ( ProtocolMLS @@ -164,7 +162,6 @@ createConversation lcnv nc = do mcs ) for_ (cnvmTeam meta) $ \tid -> addPrepQuery Cql.insertTeamConv (tid, tUnqualified lcnv) - for_ mgid $ \gid -> addPrepQuery Cql.insertGroupIdForConversation (gid, tUnqualified lcnv, tDomain lcnv) (lmems, rmems) <- addMembers (tUnqualified lcnv) (ncUsers nc) pure Conversation @@ -408,35 +405,6 @@ toConv cid ms remoteMems mconv = do } } -setGroupIdForConversation :: GroupId -> Qualified ConvId -> Client () -setGroupIdForConversation gId conv = - write Cql.insertGroupIdForConversation (params LocalQuorum (gId, qUnqualified conv, qDomain conv)) - -deleteGroupIdForConversation :: GroupId -> Client () -deleteGroupIdForConversation groupId = - retry x5 $ write Cql.deleteGroupId (params LocalQuorum (Identity groupId)) - -lookupConvByGroupId :: GroupId -> Client (Maybe (Qualified ConvOrSubConvId)) -lookupConvByGroupId gId = - toConvOrSubConv <$$> retry x1 (query1 Cql.lookupGroupId (params LocalQuorum (Identity gId))) - where - toConvOrSubConv :: (ConvId, Domain, Maybe SubConvId) -> Qualified ConvOrSubConvId - toConvOrSubConv (convId, domain, mbSubConvId) = - case mbSubConvId of - Nothing -> Qualified (Conv convId) domain - Just subConvId -> Qualified (SubConv convId subConvId) domain - -deleteGroupIds :: - Members - '[ Embed IO, - Input ClientState - ] - r => - [GroupId] -> - Sem r () -deleteGroupIds = - embedClient . UnliftIO.pooledMapConcurrentlyN_ 8 deleteGroupIdForConversation - updateToMixedProtocol :: Members '[ Embed IO, @@ -445,22 +413,15 @@ updateToMixedProtocol :: r => Local ConvId -> CipherSuiteTag -> - Sem r ConversationMLSData + Sem r () updateToMixedProtocol lcnv cs = do - let gid = convToGroupId lcnv + let gid = convToGroupId' $ Conv <$> tUntagged lcnv epoch = Epoch 0 embedClient . retry x5 . batch $ do setType BatchLogged setConsistency LocalQuorum - addPrepQuery Cql.insertGroupIdForConversation (gid, tUnqualified lcnv, tDomain lcnv) addPrepQuery Cql.updateToMixedConv (tUnqualified lcnv, ProtocolMixedTag, gid, epoch, cs) - pure - ConversationMLSData - { cnvmlsGroupId = gid, - cnvmlsEpoch = epoch, - cnvmlsEpochTimestamp = Nothing, - cnvmlsCipherSuite = cs - } + pure () interpretConversationStoreToCassandra :: ( Member (Embed IO) r, @@ -475,7 +436,6 @@ interpretConversationStoreToCassandra = interpret $ \case CreateMLSSelfConversation lusr -> embedClient $ createMLSSelfConversation lusr GetConversation cid -> embedClient $ getConversation cid GetConversationEpoch cid -> embedClient $ getConvEpoch cid - LookupConvByGroupId gId -> embedClient $ lookupConvByGroupId gId GetConversations cids -> localConversations cids GetConversationMetadata cid -> embedClient $ conversationMeta cid GetGroupInfo cid -> embedClient $ getGroupInfo cid @@ -489,10 +449,7 @@ interpretConversationStoreToCassandra = interpret $ \case SetConversationMessageTimer cid value -> embedClient $ updateConvMessageTimer cid value SetConversationEpoch cid epoch -> embedClient $ updateConvEpoch cid epoch DeleteConversation cid -> embedClient $ deleteConversation cid - SetGroupIdForConversation gId cid -> embedClient $ setGroupIdForConversation gId cid - DeleteGroupIdForConversation gId -> embedClient $ deleteGroupIdForConversation gId SetGroupInfo cid gib -> embedClient $ setGroupInfo cid gib AcquireCommitLock gId epoch ttl -> embedClient $ acquireCommitLock gId epoch ttl ReleaseCommitLock gId epoch -> embedClient $ releaseCommitLock gId epoch - DeleteGroupIds gIds -> deleteGroupIds gIds UpdateToMixedProtocol cid cs -> updateToMixedProtocol cid cs diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 7e491df3662..7d95fb03875 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -321,14 +321,6 @@ insertUserConv = "insert into user (user, conv) values (?, ?)" deleteUserConv :: PrepQuery W (UserId, ConvId) () deleteUserConv = "delete from user where user = ? and conv = ?" --- MLS Conversations -------------------------------------------------------- - -insertGroupIdForConversation :: PrepQuery W (GroupId, ConvId, Domain) () -insertGroupIdForConversation = "INSERT INTO group_id_conv_id (group_id, conv_id, domain) VALUES (?, ?, ?)" - -lookupGroupId :: PrepQuery R (Identity GroupId) (ConvId, Domain, Maybe SubConvId) -lookupGroupId = "SELECT conv_id, domain, subconv_id from group_id_conv_id where group_id = ?" - -- MLS SubConversations ----------------------------------------------------- selectSubConversation :: PrepQuery R (ConvId, SubConvId) (CipherSuiteTag, Epoch, Writetime Epoch, GroupId) @@ -346,15 +338,6 @@ selectSubConvGroupInfo = "SELECT public_group_state FROM subconversation WHERE c selectSubConvEpoch :: PrepQuery R (ConvId, SubConvId) (Identity (Maybe Epoch)) selectSubConvEpoch = "SELECT epoch FROM subconversation WHERE conv_id = ? AND subconv_id = ?" -deleteGroupId :: PrepQuery W (Identity GroupId) () -deleteGroupId = "DELETE FROM group_id_conv_id WHERE group_id = ?" - -insertGroupIdForSubConversation :: PrepQuery W (GroupId, ConvId, Domain, SubConvId) () -insertGroupIdForSubConversation = "INSERT INTO group_id_conv_id (group_id, conv_id, domain, subconv_id) VALUES (?, ?, ?, ?)" - -lookupGroupIdForSubConversation :: PrepQuery R (Identity GroupId) (ConvId, Domain, SubConvId) -lookupGroupIdForSubConversation = "SELECT conv_id, domain, subconv_id from group_id_conv_id where group_id = ?" - insertEpochForSubConversation :: PrepQuery W (Epoch, ConvId, SubConvId) () insertEpochForSubConversation = "UPDATE subconversation set epoch = ? WHERE conv_id = ? AND subconv_id = ?" diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index 2f65ce70240..649f20424ed 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -24,7 +24,6 @@ import Cassandra import Cassandra.Util import Data.Id import qualified Data.Map as Map -import Data.Qualified import Data.Time.Clock import Galley.API.MLS.Types import Galley.Cassandra.Conversation.MLS @@ -83,18 +82,10 @@ selectSubConvEpoch :: ConvId -> SubConvId -> Client (Maybe Epoch) selectSubConvEpoch convId subConvId = (runIdentity =<<) <$> retry x5 (query1 Cql.selectSubConvEpoch (params LocalQuorum (convId, subConvId))) -setGroupIdForSubConversation :: GroupId -> Qualified ConvId -> SubConvId -> Client () -setGroupIdForSubConversation groupId qconv sconv = - retry x5 (write Cql.insertGroupIdForSubConversation (params LocalQuorum (groupId, qUnqualified qconv, qDomain qconv, sconv))) - setEpochForSubConversation :: ConvId -> SubConvId -> Epoch -> Client () setEpochForSubConversation cid sconv epoch = retry x5 (write Cql.insertEpochForSubConversation (params LocalQuorum (epoch, cid, sconv))) -deleteGroupId :: GroupId -> Client () -deleteGroupId groupId = - retry x5 $ write Cql.deleteGroupId (params LocalQuorum (Identity groupId)) - deleteSubConversation :: ConvId -> SubConvId -> Client () deleteSubConversation cid sconv = retry x5 $ write Cql.deleteSubConversation (params LocalQuorum (cid, sconv)) @@ -125,9 +116,7 @@ interpretSubConversationStoreToCassandra = interpret $ \case GetSubConversationGroupInfo convId subConvId -> embedClient (selectSubConvGroupInfo convId subConvId) GetSubConversationEpoch convId subConvId -> embedClient (selectSubConvEpoch convId subConvId) SetSubConversationGroupInfo convId subConvId mPgs -> embedClient (updateSubConvGroupInfo convId subConvId mPgs) - SetGroupIdForSubConversation gId cid sconv -> embedClient $ setGroupIdForSubConversation gId cid sconv SetSubConversationEpoch cid sconv epoch -> embedClient $ setEpochForSubConversation cid sconv epoch - DeleteGroupIdForSubConversation groupId -> embedClient $ deleteGroupId groupId ListSubConversations cid -> embedClient $ listSubConversations cid DeleteSubConversation convId subConvId -> embedClient $ deleteSubConversation convId subConvId diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index 61fe40bd02c..94214dc5d32 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -83,7 +83,6 @@ import Galley.Effects.SearchVisibilityStore import Galley.Effects.ServiceStore import Galley.Effects.SparAccess import Galley.Effects.SubConversationStore -import Galley.Effects.SubConversationSupply import Galley.Effects.TeamFeatureStore import Galley.Effects.TeamMemberStore import Galley.Effects.TeamNotificationStore @@ -113,7 +112,6 @@ type GalleyEffects1 = ProposalStore, ConversationStore, SubConversationStore, - SubConversationSupply, Random, CustomBackendStore, LegalHoldStore, diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index d5bb268f991..ca368296d22 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -29,7 +29,6 @@ module Galley.Effects.ConversationStore -- * Read conversation getConversation, getConversationEpoch, - lookupConvByGroupId, getConversations, getConversationMetadata, getGroupInfo, @@ -45,10 +44,7 @@ module Galley.Effects.ConversationStore setConversationMessageTimer, setConversationEpoch, acceptConnectConversation, - setGroupIdForConversation, - deleteGroupIdForConversation, setGroupInfo, - deleteGroupIds, updateToMixedProtocol, -- * Delete conversation @@ -74,7 +70,6 @@ import Wire.API.Conversation hiding (Conversation, Member) import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite (CipherSuiteTag) import Wire.API.MLS.GroupInfo -import Wire.API.MLS.SubConversation data ConversationStore m a where CreateConversationId :: ConversationStore m ConvId @@ -85,7 +80,6 @@ data ConversationStore m a where DeleteConversation :: ConvId -> ConversationStore m () GetConversation :: ConvId -> ConversationStore m (Maybe Conversation) GetConversationEpoch :: ConvId -> ConversationStore m (Maybe Epoch) - LookupConvByGroupId :: GroupId -> ConversationStore m (Maybe (Qualified ConvOrSubConvId)) GetConversations :: [ConvId] -> ConversationStore m [Conversation] GetConversationMetadata :: ConvId -> ConversationStore m (Maybe ConversationMetadata) GetGroupInfo :: ConvId -> ConversationStore m (Maybe GroupInfoData) @@ -101,13 +95,10 @@ data ConversationStore m a where SetConversationReceiptMode :: ConvId -> ReceiptMode -> ConversationStore m () SetConversationMessageTimer :: ConvId -> Maybe Milliseconds -> ConversationStore m () SetConversationEpoch :: ConvId -> Epoch -> ConversationStore m () - SetGroupIdForConversation :: GroupId -> Qualified ConvId -> ConversationStore m () - DeleteGroupIdForConversation :: GroupId -> ConversationStore m () SetGroupInfo :: ConvId -> GroupInfoData -> ConversationStore m () AcquireCommitLock :: GroupId -> Epoch -> NominalDiffTime -> ConversationStore m LockAcquired ReleaseCommitLock :: GroupId -> Epoch -> ConversationStore m () - DeleteGroupIds :: [GroupId] -> ConversationStore m () - UpdateToMixedProtocol :: Local ConvId -> CipherSuiteTag -> ConversationStore m ConversationMLSData + UpdateToMixedProtocol :: Local ConvId -> CipherSuiteTag -> ConversationStore m () makeSem ''ConversationStore diff --git a/services/galley/src/Galley/Effects/SubConversationStore.hs b/services/galley/src/Galley/Effects/SubConversationStore.hs index 4dff138c6ad..8d3e5cb70cd 100644 --- a/services/galley/src/Galley/Effects/SubConversationStore.hs +++ b/services/galley/src/Galley/Effects/SubConversationStore.hs @@ -20,7 +20,6 @@ module Galley.Effects.SubConversationStore where import Data.Id -import Data.Qualified import Galley.API.MLS.Types import Imports import Polysemy @@ -36,9 +35,7 @@ data SubConversationStore m a where GetSubConversationGroupInfo :: ConvId -> SubConvId -> SubConversationStore m (Maybe GroupInfoData) GetSubConversationEpoch :: ConvId -> SubConvId -> SubConversationStore m (Maybe Epoch) SetSubConversationGroupInfo :: ConvId -> SubConvId -> Maybe GroupInfoData -> SubConversationStore m () - SetGroupIdForSubConversation :: GroupId -> Qualified ConvId -> SubConvId -> SubConversationStore m () SetSubConversationEpoch :: ConvId -> SubConvId -> Epoch -> SubConversationStore m () - DeleteGroupIdForSubConversation :: GroupId -> SubConversationStore m () ListSubConversations :: ConvId -> SubConversationStore m (Map SubConvId ConversationMLSData) DeleteSubConversation :: ConvId -> SubConvId -> SubConversationStore m () diff --git a/services/galley/src/Galley/Effects/SubConversationSupply.hs b/services/galley/src/Galley/Effects/SubConversationSupply.hs deleted file mode 100644 index 4b96e0b469c..00000000000 --- a/services/galley/src/Galley/Effects/SubConversationSupply.hs +++ /dev/null @@ -1,30 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 Galley.Effects.SubConversationSupply where - -import Polysemy -import Wire.API.MLS.Group - -data SubConversationSupply m a where - -- | Generate a fresh group ID. This is used for subconversations, but the - -- generator does not depend on a subconversation. - MakeFreshGroupId :: SubConversationSupply m GroupId - -makeSem ''SubConversationSupply diff --git a/services/galley/src/Galley/Effects/SubConversationSupply/Random.hs b/services/galley/src/Galley/Effects/SubConversationSupply/Random.hs deleted file mode 100644 index 31f34175ab8..00000000000 --- a/services/galley/src/Galley/Effects/SubConversationSupply/Random.hs +++ /dev/null @@ -1,40 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 Galley.Effects.SubConversationSupply.Random - ( interpretSubConversationSupplyToRandom, - ) -where - -import qualified Crypto.Hash as Crypto -import Data.ByteArray (convert) -import Galley.Effects.SubConversationSupply -import Imports -import Polysemy -import Wire.API.MLS.Group -import Wire.Sem.Random - -interpretSubConversationSupplyToRandom :: - Member Random r => - Sem (SubConversationSupply ': r) a -> - Sem r a -interpretSubConversationSupplyToRandom = interpret $ \case - MakeFreshGroupId -> freshGroupId - -freshGroupId :: Member Random r => Sem r GroupId -freshGroupId = - GroupId . convert . Crypto.hash @ByteString @Crypto.SHA256 <$> bytes 100 diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 313dadc893d..ecc3eefca3c 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -44,7 +44,7 @@ import Bilge.Assert import Control.Arrow import qualified Control.Concurrent.Async as Async import Control.Exception (throw) -import Control.Lens (at, ix, preview, view, (.~), (?~)) +import Control.Lens hiding ((#), (.=)) import Control.Monad.Trans.Maybe import Data.Aeson hiding (json) import qualified Data.ByteString as BS @@ -81,7 +81,6 @@ import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import TestHelpers import TestSetup -import Util.Options (Endpoint (Endpoint)) import Wire.API.Connection import Wire.API.Conversation import Wire.API.Conversation.Action @@ -92,7 +91,6 @@ import Wire.API.Conversation.Typing import Wire.API.Event.Conversation import Wire.API.Federation.API import qualified Wire.API.Federation.API.Brig as F -import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley import qualified Wire.API.Federation.API.Galley as F import Wire.API.Internal.Notification @@ -185,9 +183,7 @@ tests s = test s "get conversations/:domain/:cnv - remote, not found" testGetQualifiedRemoteConvNotFound, test s "get conversations/:domain/:cnv - remote, not found on remote" testGetQualifiedRemoteConvNotFoundOnRemote, test s "post conversations/list/v2" testBulkGetQualifiedConvs, - test s "add remote members on invalid domain" testAddRemoteMemberInvalidDomain, test s "add remote members when federation isn't enabled" testAddRemoteMemberFederationDisabled, - test s "add remote members when federator is unavailable" testAddRemoteMemberFederationUnavailable, test s "delete conversations/:domain/:cnv/members/:domain/:usr - fail, self conv" deleteMembersQualifiedFailSelf, test s "delete conversations/:domain:/cnv/members/:domain/:usr - fail, 1:1 conv" deleteMembersQualifiedFailO2O, test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with all locals" deleteMembersConvLocalQualifiedOk, @@ -2752,8 +2748,8 @@ testAddRemoteMember = do postQualifiedMembers alice (remoteBob :| []) qconvId mockReply [mkProfile bob (Name "bob")], - "on-new-remote-conversation" ~> EmptyResponse, "on-conversation-updated" ~> () ] @@ -2791,9 +2786,7 @@ testDeleteTeamConversationWithRemoteMembers = do connectWithRemoteUser alice remoteBob - let mock = - ("on-new-remote-conversation" ~> EmptyResponse) - <|> ("on-conversation-updated" ~> ()) + let mock = "on-conversation-updated" ~> () (_, received) <- withTempMockFederator' mock $ do postQualifiedMembers alice (remoteBob :| []) qconvId !!! const 200 === statusCode @@ -2826,9 +2819,8 @@ testDeleteTeamConversationWithUnavailableRemoteMembers = do connectWithRemoteUser alice remoteBob let mock = - ("on-new-remote-conversation" ~> EmptyResponse) - -- Mock an unavailable federation server for the deletion call - <|> (guardRPC "on-conversation-updated" *> throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) + -- Mock an unavailable federation server for the deletion call + (guardRPC "on-conversation-updated" *> throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) <|> (guardRPC "delete-team-conversation" *> throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) (_, received) <- withTempMockFederator' mock $ do postQualifiedMembers alice (remoteBob :| []) qconvId @@ -3046,25 +3038,6 @@ testBulkGetQualifiedConvs = do assertEqual "not founds" expectedNotFound actualNotFound assertEqual "failures" [remoteConvIdCFailure] (crFailed convs) -testAddRemoteMemberInvalidDomain :: TestM () -testAddRemoteMemberInvalidDomain = do - alice <- randomUser - bobId <- randomId - let remoteBob = Qualified bobId (Domain "invalid.example.com") - convId <- decodeConvId <$> postConv alice [] (Just "remote gossip") [] Nothing Nothing - localDomain <- viewFederationDomain - let qconvId = Qualified convId localDomain - - connectWithRemoteUser alice remoteBob - - postQualifiedMembers alice (remoteBob :| []) qconvId - !!! do - const 422 === statusCode - const (Just "/federation/api-version") - === preview (ix "data" . ix "path") . responseJsonUnsafe @Value - const (Just "invalid.example.com") - === preview (ix "data" . ix "domain") . responseJsonUnsafe @Value - -- This test is a safeguard to ensure adding remote members will fail -- on environments where federation isn't configured (such as our production as of May 2021) testAddRemoteMemberFederationDisabled :: TestM () @@ -3086,28 +3059,6 @@ testAddRemoteMemberFederationDisabled = do conv <- responseJsonError =<< getConvQualified alice qconvId randomId - qconvId <- decodeQualifiedConvId <$> postConv alice [] (Just "remote gossip") [] Nothing Nothing - connectWithRemoteUser alice remoteBob - - -- federator endpoint being configured in brig and/or galley, but not being - -- available (i.e. no service listing on that IP/port) can happen due to a - -- misconfiguration of federator. That should give a 500. - -- Port 1 should always be wrong hopefully. - let federatorUnavailable = optFederator ?~ Endpoint "127.0.0.1" 1 - withSettingsOverrides federatorUnavailable $ - postQualifiedMembers alice (remoteBob :| []) qconvId !!! do - const 500 === statusCode - const (Right "federation-not-available") === fmap label . responseJsonEither - - -- in this case, we discover that federation is unavailable too late, and the - -- member has already been added to the conversation - conv <- responseJsonError =<< getConvQualified alice qconvId convId, groupID -- 2) alice creates an MLS group (locally) with bob in it -> commit, welcome -- 3) alice sends commit --- 4) A notifies B about the new conversation +-- 4) deprecated & removed: A notifies B about the new conversation -- 5) A notifies B about bob being in the conversation (Join event) -- 6) B notifies bob about join event -- 7) alice sends welcome @A @@ -828,15 +826,13 @@ testLocalToRemote = do void $ uploadNewKeyPackage bob1 -- step 2 - (groupId, qcnv) <- setupFakeMLSGroup alice1 + (_groupId, qcnv) <- setupFakeMLSGroup alice1 Nothing mp <- createAddCommit alice1 [bob] -- step 10 traverse_ consumeWelcome (mpWelcome mp) -- step 11 message <- createApplicationMessage bob1 "hi" - -- register remote conversation: step 4 - receiveNewRemoteConv (fmap Conv qcnv) groupId -- A notifies B about bob being in the conversation (Join event): step 5 receiveOnConvUpdated qcnv alice bob @@ -869,7 +865,7 @@ testLocalToRemoteNonMember = do void $ uploadNewKeyPackage bob1 -- step 2 - (groupId, qcnv) <- setupFakeMLSGroup alice1 + void $ setupFakeMLSGroup alice1 Nothing mp <- createAddCommit alice1 [bob] -- step 10 @@ -877,9 +873,6 @@ testLocalToRemoteNonMember = do -- step 11 message <- createApplicationMessage bob1 "hi" - -- register remote conversation: step 4 - receiveNewRemoteConv (fmap Conv qcnv) groupId - void $ withTempMockFederator' sendMessageMock $ do galley <- viewGalley @@ -1365,7 +1358,7 @@ propNonExistingConv = do runMLSTest $ do [alice1, bob1] <- traverse createMLSClient [alice, bob] void $ uploadNewKeyPackage bob1 - void $ setupFakeMLSGroup alice1 + void $ setupFakeMLSGroup alice1 Nothing [prop] <- createAddProposals alice1 [bob] postMessage alice1 (mpMessage prop) !!! do @@ -1729,7 +1722,7 @@ sendRemoteMLSWelcome = do [alice, bob] <- createAndConnectUsers [Just "alice.example.com", Nothing] (commit, bob1) <- runMLSTest $ do [alice1, bob1] <- traverse createMLSClient [alice, bob] - void $ setupFakeMLSGroup alice1 + void $ setupFakeMLSGroup alice1 Nothing void $ uploadNewKeyPackage bob1 commit <- createAddCommit alice1 [bob] pure (commit, bob1) @@ -1955,11 +1948,10 @@ testGetGroupInfoOfRemoteConv = do [alice1, bob1] <- traverse createMLSClient [alice, bob] void $ uploadNewKeyPackage bob1 - (groupId, qcnv) <- setupFakeMLSGroup alice1 + (_groupId, qcnv) <- setupFakeMLSGroup alice1 Nothing mp <- createAddCommit alice1 [bob] traverse_ consumeWelcome (mpWelcome mp) - receiveNewRemoteConv (fmap Conv qcnv) groupId receiveOnConvUpdated qcnv alice bob let fakeGroupState = "\xde\xad\xbe\xef" @@ -2047,12 +2039,11 @@ testAddUserToRemoteConvWithBundle = do [alice1, bob1] <- traverse createMLSClient [alice, bob] void $ uploadNewKeyPackage bob1 - (groupId, qcnv) <- setupFakeMLSGroup alice1 + (_groupId, qcnv) <- setupFakeMLSGroup alice1 Nothing mp <- createAddCommit alice1 [bob] traverse_ consumeWelcome (mpWelcome mp) - receiveNewRemoteConv (fmap Conv qcnv) groupId receiveOnConvUpdated qcnv alice bob -- NB. this commit would be rejected by the owning backend, but for the @@ -2317,20 +2308,12 @@ testJoinRemoteSubConv = do -- setup fake group for the subconversation let subId = SubConvId "conference" - (subGroupId, qcnv) <- setupFakeMLSGroup alice1 + (_subGroupId, qcnv) <- setupFakeMLSGroup alice1 (Just subId) let qcs = fmap (flip SubConv subId) qcnv initialCommit <- createPendingProposalCommit alice1 - -- create a fake group ID for the main (we don't need the actual group) - mainGroupId <- fakeGroupId - - -- inform backend about the main conversation - receiveNewRemoteConv (fmap Conv qcnv) mainGroupId receiveOnConvUpdated qcnv alice bob - -- inform backend about the subconversation - receiveNewRemoteConv qcs subGroupId - -- bob joins subconversation let pgs = mpGroupInfo initialCommit let mock = @@ -2369,20 +2352,10 @@ testRemoteSubConvNotificationWhenUserJoins = do State.put s - (_, reqs) <- + void $ withTempMockFederator' (receiveCommitMock [bob1] <|> welcomeMock) $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - do - req <- assertOne $ filter ((== "on-new-remote-conversation") . frRPC) reqs - nrc <- assertOne (toList (Aeson.decode (frBody req))) - liftIO $ nrcConvId nrc @?= qUnqualified qcnv - do - req <- assertOne $ filter ((== "on-new-remote-subconversation") . frRPC) reqs - nrsc <- assertOne (toList (Aeson.decode (frBody req))) - liftIO $ nrscConvId nrsc @?= qUnqualified qcnv - liftIO $ nrscSubConvId nrsc @?= subId - testRemoteUserJoinSubConv :: TestM () testRemoteUserJoinSubConv = do [alice, bob] <- createAndConnectUsers [Nothing, Just "bob.example.com"] @@ -2397,28 +2370,9 @@ testRemoteUserJoinSubConv = do withTempMockFederator' (receiveCommitMock [bob1] <|> welcomeMock) $ sendAndConsumeCommitBundle commit - let mock = - asum - [ "on-new-remote-subconversation" ~> EmptyResponse, - messageSentMock - ] + let mock = messageSentMock let subId = SubConvId "conference" - (qcs, reqs) <- withTempMockFederator' mock $ createSubConv qcnv alice1 subId - psc <- - liftTest $ - responseJsonError - =<< getSubConv (ciUser alice1) qcnv subId >= sendAndConsumeCommitBundle - liftIO $ - assertBool "Unexpected on-new-remote-subconversation" $ - all ((/= "on-new-remote-subconversation") . frRPC) reqs' - testSendMessageSubConv :: TestM () testSendMessageSubConv = do [alice, bob] <- createAndConnectUsers [Nothing, Nothing] @@ -2587,27 +2536,11 @@ testRemoteMemberDeleteSubConv isAMember = do -- Bob is a member of the parent conversation so he's allowed to delete the -- subconversation. - (res, reqs) <- + (res, _reqs) <- withTempMockFederator' deleteMLSConvMock $ do fedGalleyClient <- view tsFedGalleyClient runFedClient @"delete-sub-conversation" fedGalleyClient bobDomain delReq - when isAMember $ do - liftIO $ do - req <- assertOne (filter ((== "on-new-remote-subconversation") . frRPC) reqs) - nrsc <- assertOne (toList (Aeson.decode (frBody req))) - nrscConvId nrsc @?= cnv - nrscSubConvId nrsc @?= scnv - - liftIO $ do - fr <- assertOne (filter ((== "on-delete-mls-conversation") . frRPC) reqs) - frTargetDomain fr @?= bobDomain - frRPC fr @?= "on-delete-mls-conversation" - bdy <- case Aeson.eitherDecode (frBody fr) of - Right b -> pure b - Left e -> assertFailure $ "Could not parse delete-sub-conversation request body: " <> e - odmcGroupIds bdy @?= [groupId] - if isAMember then expectSuccess res else expectFailure ConvNotFound res where expectSuccess :: DeleteSubConversationResponse -> TestM () @@ -2747,7 +2680,7 @@ testDeleteParentOfSubConv = do connectWithRemoteUser aliceUnqualified bob let sconv = SubConvId "conference" - (qcnv, parentGroupId, subGroupId) <- runMLSTest $ do + (qcnv, _parentGroupId, _subGroupId) <- runMLSTest $ do [alice1, arthur1, bob1] <- traverse createMLSClient [alice, arthur, bob] traverse_ uploadNewKeyPackage [arthur1] (parentGroupId, qcnv) <- setupMLSGroup alice1 @@ -2790,79 +2723,14 @@ testDeleteParentOfSubConv = do pure (qcnv, parentGroupId, pscGroupId sub') - (_, freqs) <- withTempMockFederator' deleteMLSConvMock $ do + void $ withTempMockFederator' deleteMLSConvMock $ do deleteTeamConv tid (qUnqualified qcnv) (qUnqualified alice) !!! const 200 === statusCode - req <- assertOne (filter ((== "on-delete-mls-conversation") . frRPC) freqs) - let Just odmc = Aeson.decode (frBody req) - liftIO $ - sort (odmcGroupIds odmc) @?= sort [parentGroupId, subGroupId] - getSubConv (qUnqualified alice) qcnv sconv !!! do const 404 === statusCode -testDeleteRemoteParentOfSubConv :: TestM () -testDeleteRemoteParentOfSubConv = do - [alice, bob] <- createAndConnectUsers [Just "alice.example.com", Nothing] - - runMLSTest $ do - alice1 <- createFakeMLSClient alice - bob1 <- createMLSClient bob - void $ uploadNewKeyPackage bob1 - - -- setup fake group for the subconversation - let subId = SubConvId "conference" - (subGroupId, qcnv) <- setupFakeMLSGroup alice1 - let qcs = fmap (flip SubConv subId) qcnv - initialCommit <- createPendingProposalCommit alice1 - - -- create a fake group ID for the main (we don't need the actual group) - mainGroupId <- fakeGroupId - - -- inform backend about the main conversation - receiveNewRemoteConv (fmap Conv qcnv) mainGroupId - receiveOnConvUpdated qcnv alice bob - - -- inform backend about the subconversation - receiveNewRemoteConv qcs subGroupId - - let pgs = mpGroupInfo initialCommit - let mock = - ("send-mls-commit-bundle" ~> MLSMessageResponseUpdates [] mempty) - <|> queryGroupStateMock (fold pgs) bob - <|> sendMessageMock - void $ withTempMockFederator' mock $ do - -- bob joins subconversation - commit <- createExternalCommit bob1 Nothing qcs - void $ sendAndConsumeCommitBundle commit - - -- bob can send to remote conversation - void $ - withTempMockFederator' sendMessageMock $ do - message <- createApplicationMessage bob1 "hi" - postMessage (mpSender message) (mpMessage message) - !!! const 201 === statusCode - - -- remote notifies about deletion of group - liftTest $ do - client <- view tsFedGalleyClient - let odm = OnDeleteMLSConversationRequest [mainGroupId, subGroupId] - void $ - runFedClient - @"on-delete-mls-conversation" - client - (qDomain alice) - odm - - -- bob's backend has no longer a mapping of the group id - void $ - withTempMockFederator' sendMessageMock $ do - message <- createApplicationMessage bob1 "hi" - postMessage (mpSender message) (mpMessage message) - !!! const 404 === statusCode - testDeleteRemoteSubConv :: Bool -> TestM () testDeleteRemoteSubConv isAMember = do alice <- randomQualifiedUser @@ -3088,21 +2956,14 @@ testLeaveRemoteSubConv = do -- setup fake group for the subconversation let subId = SubConvId "conference" - (subGroupId, qcnv) <- setupFakeMLSGroup alice1 + (_subGroupId, qcnv) <- setupFakeMLSGroup alice1 (Just subId) -- TODO: refactor setupFakeMLSGroup to make it consistent with createSubConv let qcs = fmap (flip SubConv subId) qcnv initialCommit <- createPendingProposalCommit alice1 - -- create a fake group ID for the main (we don't need the actual group) - mainGroupId <- fakeGroupId - -- inform backend about the main conversation - receiveNewRemoteConv (fmap Conv qcnv) mainGroupId receiveOnConvUpdated qcnv alice bob - -- inform backend about the subconversation - receiveNewRemoteConv qcs subGroupId - let pgs = mpGroupInfo initialCommit let mock = ("send-mls-commit-bundle" ~> MLSMessageResponseUpdates [] mempty) diff --git a/services/galley/test/integration/API/MLS/Mocks.hs b/services/galley/test/integration/API/MLS/Mocks.hs index 5e816bb9d3a..c193af43958 100644 --- a/services/galley/test/integration/API/MLS/Mocks.hs +++ b/services/galley/test/integration/API/MLS/Mocks.hs @@ -48,8 +48,6 @@ receiveCommitMock :: [ClientIdentity] -> Mock LByteString receiveCommitMock clients = asum [ "on-conversation-updated" ~> (), - "on-new-remote-conversation" ~> EmptyResponse, - "on-new-remote-subconversation" ~> EmptyResponse, "get-mls-clients" ~> Set.fromList ( map (flip ClientInfo True . ciClient) clients @@ -103,9 +101,7 @@ queryGroupStateMock gs qusr = do deleteMLSConvMock :: Mock LByteString deleteMLSConvMock = asum - [ "on-delete-mls-conversation" ~> EmptyResponse, - "on-new-remote-subconversation" ~> EmptyResponse, - "on-conversation-updated" ~> EmptyResponse + [ "on-conversation-updated" ~> EmptyResponse ] mlsMockUnreachableFor :: Set Domain -> Mock LByteString diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index a6d57bb66d8..71bea0a4a7d 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -76,6 +76,7 @@ import Wire.API.Federation.API.Galley import Wire.API.MLS.CipherSuite import Wire.API.MLS.CommitBundle import Wire.API.MLS.Credential +import Wire.API.MLS.Group.Serialisation import Wire.API.MLS.KeyPackage import Wire.API.MLS.Keys import Wire.API.MLS.LeafNode @@ -516,18 +517,14 @@ createSubConv qcnv creator subId = do setupFakeMLSGroup :: HasCallStack => ClientIdentity -> + Maybe SubConvId -> MLSTest (GroupId, Qualified ConvId) -setupFakeMLSGroup creator = do - groupId <- fakeGroupId +setupFakeMLSGroup creator mSubId = do qcnv <- randomQualifiedId (ciDomain creator) + let groupId = convToGroupId' $ maybe (Conv <$> qcnv) ((<$> qcnv) . flip SubConv) mSubId createGroup creator (fmap Conv qcnv) groupId pure (groupId, qcnv) -fakeGroupId :: MLSTest GroupId -fakeGroupId = - liftIO $ - fmap (GroupId . BS.pack) (replicateM 32 (generate arbitrary)) - claimLocalKeyPackages :: HasCallStack => ClientIdentity -> Local UserId -> MLSTest KeyPackageBundle claimLocalKeyPackages qcid lusr = do brig <- viewBrig @@ -955,45 +952,6 @@ clientKeyPair cid = do Left (_, _, msg) -> liftIO $ assertFailure msg Right (_, _, keys) -> pure keys -receiveNewRemoteConv :: - (MonadReader TestSetup m, MonadIO m) => - Qualified ConvOrSubConvId -> - GroupId -> - m () -receiveNewRemoteConv qcs gid = do - client <- view tsFedGalleyClient - case qUnqualified qcs of - Conv c -> do - let nrc = - NewRemoteConversation c $ - ProtocolMLS - ( ConversationMLSData - gid - (Epoch 1) - (Just (UTCTime (fromGregorian 2020 8 29) 0)) - MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - ) - void $ - runFedClient - @"on-new-remote-conversation" - client - (qDomain qcs) - nrc - SubConv c s -> do - let nrc = - NewRemoteSubConversation c s $ - ConversationMLSData - gid - (Epoch 1) - (Just (UTCTime (fromGregorian 2020 8 29) 0)) - MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - void $ - runFedClient - @"on-new-remote-subconversation" - client - (qDomain qcs) - nrc - receiveOnConvUpdated :: (MonadReader TestSetup m, MonadIO m) => Qualified ConvId -> From 86fb898edf0b3e1f35e455b1df67bca13f936624 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 5 Jun 2023 10:11:57 +0200 Subject: [PATCH 048/225] Fix Capabilities It should be possible for the `Capabilities` structure to contain unknown version, proposal, credential and extension tags. --- libs/wire-api/src/Wire/API/MLS/Capabilities.hs | 9 +++------ libs/wire-api/src/Wire/API/MLS/Validation.hs | 2 +- libs/wire-api/test/resources/key_package1.mls | Bin 0 -> 369 bytes libs/wire-api/test/unit/Test/Wire/API/MLS.hs | 8 ++++++++ 4 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 libs/wire-api/test/resources/key_package1.mls diff --git a/libs/wire-api/src/Wire/API/MLS/Capabilities.hs b/libs/wire-api/src/Wire/API/MLS/Capabilities.hs index 64386ef72e3..589aa7dbcab 100644 --- a/libs/wire-api/src/Wire/API/MLS/Capabilities.hs +++ b/libs/wire-api/src/Wire/API/MLS/Capabilities.hs @@ -20,19 +20,16 @@ module Wire.API.MLS.Capabilities where import Imports import Test.QuickCheck import Wire.API.MLS.CipherSuite -import Wire.API.MLS.Credential -import Wire.API.MLS.ProposalTag -import Wire.API.MLS.ProtocolVersion import Wire.API.MLS.Serialisation import Wire.Arbitrary -- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-7.2-2 data Capabilities = Capabilities - { versions :: [ProtocolVersion], + { versions :: [Word16], ciphersuites :: [CipherSuite], extensions :: [Word16], - proposals :: [ProposalTag], - credentials :: [CredentialTag] + proposals :: [Word16], + credentials :: [Word16] } deriving (Show, Eq, Generic) deriving (Arbitrary) via (GenericUniform Capabilities) diff --git a/libs/wire-api/src/Wire/API/MLS/Validation.hs b/libs/wire-api/src/Wire/API/MLS/Validation.hs index c8f1999e532..f4fd60c56e1 100644 --- a/libs/wire-api/src/Wire/API/MLS/Validation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Validation.hs @@ -121,5 +121,5 @@ validateSource t s = do validateCapabilities :: Capabilities -> Either Text () validateCapabilities caps = - unless (BasicCredentialTag `elem` caps.credentials) $ + unless (fromMLSEnum BasicCredentialTag `elem` caps.credentials) $ Left "missing BasicCredential capability" diff --git a/libs/wire-api/test/resources/key_package1.mls b/libs/wire-api/test/resources/key_package1.mls new file mode 100644 index 0000000000000000000000000000000000000000..bcb18cb6755de6b9869155fb19241b559e3f42b1 GIT binary patch literal 369 zcmZQzWMEXNUAQ~%R&QT?!8xTpi7Kz6F0gMApUC9;+(f}J*eza5OX2bFgZp;a&0A8S z;#z0_k%PKnMPG(*<69Xd$10w?y13LpVkj(>T zGc&L-urjc-fH;f{K#)@T?i84QvFjFtgTr*4IKzmV8ER9y^sGNVb+^-e^W5E5+M&Wm})Cv2w_EPk@Z%V^0BHlPuC;XA_WCspk# z-4*oq-C?gaYus assertFailure $ "Failed to parse identity: " <> T.unpack err Right identity -> identity @?= alice +testParseKeyPackageWithCapabilities :: IO () +testParseKeyPackageWithCapabilities = do + kpData <- BS.readFile "test/resources/key_package1.mls" + case decodeMLS' @KeyPackage kpData of + Left err -> assertFailure (T.unpack err) + Right _ -> pure () + testParseCommit :: IO () testParseCommit = do qcid <- B8.unpack . encodeMLS' <$> randomIdentity From 2a9d0c30aef5218e7f4c1f5fb02a777efea3fee3 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Mon, 5 Jun 2023 11:30:24 +0200 Subject: [PATCH 049/225] [FS-1915] Add conversation id to welcome messages (#3335) * [FS-1915] Include conversation id into welcome message event * Hi ci --- .../add-conv-id-to-welcome-event | 1 + integration/test/Test/MLS.hs | 4 +-- .../src/Wire/API/Federation/API/Galley.hs | 4 ++- services/galley/src/Galley/API/Federation.hs | 4 +-- services/galley/src/Galley/API/MLS/Message.hs | 2 +- services/galley/src/Galley/API/MLS/Welcome.hs | 30 +++++++++++-------- services/galley/test/integration/API/MLS.hs | 13 ++++---- services/galley/test/integration/API/Util.hs | 5 ++-- 8 files changed, 37 insertions(+), 26 deletions(-) create mode 100644 changelog.d/1-api-changes/add-conv-id-to-welcome-event diff --git a/changelog.d/1-api-changes/add-conv-id-to-welcome-event b/changelog.d/1-api-changes/add-conv-id-to-welcome-event new file mode 100644 index 00000000000..ae5e423b297 --- /dev/null +++ b/changelog.d/1-api-changes/add-conv-id-to-welcome-event @@ -0,0 +1 @@ +Replace the placeholder self conversation id with the qualified conversation id for welcome events. diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index f05406e98e2..23bb3693d95 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -291,7 +291,7 @@ testSelfConversation = do alice <- randomUser OwnDomain def creator : others <- traverse createMLSClient (replicate 3 alice) traverse_ uploadNewKeyPackage others - void $ createSelfGroup creator + (_, cnv) <- createSelfGroup creator commit <- createAddCommit creator [alice] welcome <- assertOne (toList commit.welcome) @@ -300,7 +300,7 @@ testSelfConversation = do let isWelcome n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" for_ wss $ \ws -> do n <- awaitMatch 3 isWelcome ws - shouldMatch (nPayload n %. "conversation") (objId alice) + shouldMatch (nPayload n %. "conversation") (objId cnv) shouldMatch (nPayload n %. "from") (objId alice) shouldMatch (nPayload n %. "data") (B8.unpack (Base64.encode welcome)) 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 0f1b2aaeea4..529af23d250 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 @@ -431,7 +431,9 @@ data MLSWelcomeRequest = MLSWelcomeRequest { -- | A serialised welcome message. welcomeMessage :: Base64ByteString, -- | Recipients local to the target backend. - recipients :: [(UserId, ClientId)] + recipients :: [(UserId, ClientId)], + -- | The conversation id, qualified to the owning domain + qualifiedConvId :: Qualified ConvId } deriving stock (Eq, Generic, Show) deriving (Arbitrary) via (GenericUniform MLSWelcomeRequest) diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index ee54c9c84cc..c65f8018ebb 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -617,7 +617,7 @@ mlsSendWelcome :: Domain -> F.MLSWelcomeRequest -> Sem r F.MLSWelcomeResponse -mlsSendWelcome _origDomain req = +mlsSendWelcome _origDomain req = do fmap (either (const MLSWelcomeMLSNotEnabled) (const MLSWelcomeSent)) . runError @(Tagged 'MLSNotEnabled ()) $ do @@ -627,7 +627,7 @@ mlsSendWelcome _origDomain req = welcome <- either (throw . InternalErrorWithDescription . LT.fromStrict) pure $ decodeMLS' (fromBase64ByteString req.welcomeMessage) - sendLocalWelcomes Nothing now welcome (qualifyAs loc req.recipients) + sendLocalWelcomes req.qualifiedConvId Nothing now welcome (qualifyAs loc req.recipients) onMLSMessageSent :: ( Member ExternalAccess r, diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 6361ed2f7dc..b0567d807b4 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -230,7 +230,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle lConvOrSubId = do propagateMessage qusr lConvOrSub conn bundle.rawMessage (tUnqualified lConvOrSub).members >>= mapM_ throwUnreachableUsers - traverse_ (sendWelcomes lConvOrSub conn newClients) bundle.welcome + traverse_ (sendWelcomes lConvOrSubId conn newClients) bundle.welcome pure events postMLSCommitBundleToRemoteConv :: diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index 213ad9a8659..7cc70b49e4f 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -27,7 +27,6 @@ import Data.Json.Util import Data.Qualified import Data.Time import Galley.API.Push -import Galley.Data.Conversation import Galley.Effects.FederatorAccess import Galley.Effects.GundeckAccess import Imports @@ -46,6 +45,7 @@ import Wire.API.Federation.Error import Wire.API.MLS.Credential import Wire.API.MLS.Message import Wire.API.MLS.Serialisation +import Wire.API.MLS.SubConversation import Wire.API.MLS.Welcome import Wire.API.Message @@ -55,51 +55,57 @@ sendWelcomes :: Member P.TinyLog r, Member (Input UTCTime) r ) => - Local x -> + Local ConvOrSubConvId -> Maybe ConnId -> [ClientIdentity] -> RawMLS Welcome -> Sem r () sendWelcomes loc con cids welcome = do now <- input - let (locals, remotes) = partitionQualified loc (map cidQualifiedClient cids) - let msg = mkRawMLS $ mkMessage (MessageWelcome welcome) - sendLocalWelcomes con now msg (qualifyAs loc locals) - sendRemoteWelcomes msg remotes + let qcnv = convFrom <$> tUntagged loc + (locals, remotes) = partitionQualified loc (map cidQualifiedClient cids) + msg = mkRawMLS $ mkMessage (MessageWelcome welcome) + sendLocalWelcomes qcnv con now msg (qualifyAs loc locals) + sendRemoteWelcomes qcnv msg remotes + where + convFrom (Conv c) = c + convFrom (SubConv c _) = c sendLocalWelcomes :: Member GundeckAccess r => + Qualified ConvId -> Maybe ConnId -> UTCTime -> RawMLS Message -> Local [(UserId, ClientId)] -> Sem r () -sendLocalWelcomes con now welcome lclients = do +sendLocalWelcomes qcnv con now welcome lclients = do runMessagePush lclients Nothing $ foldMap (uncurry mkPush) (tUnqualified lclients) where mkPush :: UserId -> ClientId -> MessagePush 'Broadcast mkPush u c = -- FUTUREWORK: use the conversation ID stored in the key package mapping table - let lcnv = qualifyAs lclients (selfConv u) - lusr = qualifyAs lclients u - e = Event (tUntagged lcnv) Nothing (tUntagged lusr) now $ EdMLSWelcome welcome.raw + let lusr = qualifyAs lclients u + e = Event qcnv Nothing (tUntagged lusr) now $ EdMLSWelcome welcome.raw in newMessagePush lclients mempty con defMessageMetadata (u, c) e sendRemoteWelcomes :: ( Member FederatorAccess r, Member P.TinyLog r ) => + Qualified ConvId -> RawMLS Message -> [Remote (UserId, ClientId)] -> Sem r () -sendRemoteWelcomes welcome clients = do +sendRemoteWelcomes qcnv welcome clients = do let msg = Base64ByteString welcome.raw traverse_ handleError <=< runFederatedConcurrentlyEither clients $ \rcpts -> fedClient @'Galley @"mls-welcome" MLSWelcomeRequest { welcomeMessage = msg, - recipients = tUnqualified rcpts + recipients = tUnqualified rcpts, + qualifiedConvId = qcnv } where handleError :: diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 1518f5bf4d3..cbef86588a2 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -329,7 +329,7 @@ testLocalWelcome = do es <- sendAndConsumeCommitBundle commit WS.assertMatchN_ (5 # Second) wss $ - wsAssertMLSWelcome (cidQualifiedUser bob1) welcome + wsAssertMLSWelcome (cidQualifiedUser bob1) qcnv welcome pure es @@ -351,7 +351,7 @@ testAddUserWithBundle = do events <- sendAndConsumeCommitBundle commit for_ (zip bobClients wss) $ \(c, ws) -> WS.assertMatch (5 # Second) ws $ - wsAssertMLSWelcome (cidQualifiedUser c) welcome + wsAssertMLSWelcome (cidQualifiedUser c) qcnv welcome pure events event <- assertOne events @@ -1720,12 +1720,12 @@ sendRemoteMLSWelcome :: TestM () sendRemoteMLSWelcome = do -- Alice is from the originating domain and Bob is local, i.e., on the receiving domain [alice, bob] <- createAndConnectUsers [Just "alice.example.com", Nothing] - (commit, bob1) <- runMLSTest $ do + (commit, bob1, qcid) <- runMLSTest $ do [alice1, bob1] <- traverse createMLSClient [alice, bob] - void $ setupFakeMLSGroup alice1 Nothing + (_, qcid) <- setupFakeMLSGroup alice1 Nothing void $ uploadNewKeyPackage bob1 commit <- createAddCommit alice1 [bob] - pure (commit, bob1) + pure (commit, bob1, qcid) welcome <- assertJust (mpWelcome commit) @@ -1739,11 +1739,12 @@ sendRemoteMLSWelcome = do MLSWelcomeRequest (Base64ByteString welcome) [qUnqualified (cidQualifiedClient bob1)] + qcid -- check that the corresponding event is received liftIO $ do WS.assertMatch_ (5 # WS.Second) wsB $ - wsAssertMLSWelcome bob welcome + wsAssertMLSWelcome bob qcid welcome testBackendRemoveProposalLocalConvLocalLeaverCreator :: TestM () testBackendRemoveProposalLocalConvLocalLeaverCreator = do diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 16fb4bfc5d9..658b2e2e489 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -1672,13 +1672,14 @@ wsAssertOtr' evData conv usr from to txt n = do wsAssertMLSWelcome :: HasCallStack => Qualified UserId -> + Qualified ConvId -> ByteString -> Notification -> IO () -wsAssertMLSWelcome u welcome n = do +wsAssertMLSWelcome u cid welcome n = do let e = List1.head (WS.unpackPayload n) ntfTransient n @?= False - evtConv e @?= fmap selfConv u + evtConv e @?= cid evtType e @?= MLSWelcome evtFrom e @?= u evtData e @?= EdMLSWelcome welcome From 77f9cd63451aae55600915adb82d337c06bbbf80 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 6 Jun 2023 10:47:57 +0200 Subject: [PATCH 050/225] Use serial consistency in commit lock --- services/galley/src/Galley/Cassandra/Conversation/MLS.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/galley/src/Galley/Cassandra/Conversation/MLS.hs b/services/galley/src/Galley/Cassandra/Conversation/MLS.hs index 06e2e65d917..a8170ea1415 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/MLS.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/MLS.hs @@ -24,7 +24,7 @@ module Galley.Cassandra.Conversation.MLS where import Cassandra -import Cassandra.Settings (fromRow) +import Cassandra.Settings import Control.Arrow import Data.Time import Galley.API.MLS.Types @@ -44,6 +44,8 @@ acquireCommitLock groupId epoch ttl = do LocalQuorum (groupId, epoch, round ttl) ) + { serialConsistency = Just LocalSerialConsistency + } pure $ if checkTransSuccess rows then Acquired From a17e265551810cd54f217e2d8728bed45518a6f0 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 7 Jun 2023 15:15:26 +0200 Subject: [PATCH 051/225] Mixed to MLS transition (#3334) * Initial implementation of mixed -> mls transition * Migration criteria check stub * Simple mixed->mls test * Check migration criteria on finalisation * More specific migration criteria error * Test extraneous client removal --- changelog.d/1-api-changes/mixed-to-mls | 1 + integration/test/SetupHelpers.hs | 48 ++++++++++-- integration/test/Test/MLS.hs | 40 ++++++++++ .../src/Wire/API/Federation/API/Galley.hs | 9 ++- libs/wire-api/src/Wire/API/Error/Galley.hs | 3 + .../API/Routes/Public/Galley/Conversation.hs | 18 +++++ .../API/Routes/Public/Galley/LegalHold.hs | 5 ++ .../src/Wire/API/Routes/Public/Galley/MLS.hs | 1 + .../Routes/Public/Galley/TeamConversation.hs | 1 + services/galley/galley.cabal | 1 + services/galley/src/Galley/API/Action.hs | 37 ++++++++- services/galley/src/Galley/API/Federation.hs | 1 + .../galley/src/Galley/API/MLS/Migration.hs | 78 +++++++++++++++++++ services/galley/src/Galley/API/MLS/Removal.hs | 34 ++++++++ services/galley/src/Galley/API/Update.hs | 12 ++- .../src/Galley/Cassandra/Conversation.hs | 13 ++++ .../galley/src/Galley/Cassandra/Queries.hs | 6 +- .../src/Galley/Effects/ConversationStore.hs | 2 + 18 files changed, 295 insertions(+), 15 deletions(-) create mode 100644 changelog.d/1-api-changes/mixed-to-mls create mode 100644 services/galley/src/Galley/API/MLS/Migration.hs diff --git a/changelog.d/1-api-changes/mixed-to-mls b/changelog.d/1-api-changes/mixed-to-mls new file mode 100644 index 00000000000..3eafd6734cc --- /dev/null +++ b/changelog.d/1-api-changes/mixed-to-mls @@ -0,0 +1 @@ +It is now possible to use `PUT /conversation/:domain/:id/protocol` to transition from Mixed to MLS diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index fe79d18a7ac..79b13bb58a6 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -1,7 +1,9 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + module SetupHelpers where -import qualified API.Brig as Public -import qualified API.BrigInternal as Internal +import API.Brig +import API.BrigInternal import API.Galley import Data.Aeson import Data.Default @@ -9,15 +11,15 @@ import Data.Function import GHC.Stack import Testlib.Prelude -randomUser :: (HasCallStack, MakesValue domain) => domain -> Internal.CreateUser -> App Value -randomUser domain cu = bindResponse (Internal.createUser domain cu) $ \resp -> do +randomUser :: (HasCallStack, MakesValue domain) => domain -> CreateUser -> App Value +randomUser domain cu = bindResponse (createUser domain cu) $ \resp -> do resp.status `shouldMatchInt` 201 resp.json -- | returns (user, team id) createTeam :: (HasCallStack, MakesValue domain) => domain -> App (Value, String) createTeam domain = do - res <- Internal.createUser domain def {Internal.team = True} + res <- createUser domain def {team = True} user <- res.json tid <- user %. "team" & asString -- TODO @@ -34,8 +36,8 @@ connectUsers2 :: bob -> App () connectUsers2 alice bob = do - bindResponse (Public.postConnection alice bob) (\resp -> resp.status `shouldMatchInt` 201) - bindResponse (Public.putConnection bob alice "accepted") (\resp -> resp.status `shouldMatchInt` 200) + bindResponse (postConnection alice bob) (\resp -> resp.status `shouldMatchInt` 201) + bindResponse (putConnection bob alice "accepted") (\resp -> resp.status `shouldMatchInt` 200) connectUsers :: HasCallStack => [Value] -> App () connectUsers users = traverse_ (uncurry connectUsers2) $ do @@ -60,3 +62,35 @@ getAllConvs u = do resp.status `shouldMatchInt` 200 resp.json result %. "found" & asList + +-- | Setup a team user, another user, connect the two, create a proteus +-- conversation, upgrade to mixed. Return the two users and the conversation. +simpleMixedConversationSetup :: + (HasCallStack, MakesValue domain) => + domain -> + App (Value, Value, Value) +simpleMixedConversationSetup secondDomain = do + (alice, tid) <- createTeam OwnDomain + bob <- randomUser secondDomain def + connectUsers [alice, bob] + + conv <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + + bindResponse (putConversationProtocol bob conv "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 + + conv' <- getConversation alice conv >>= getJSON 200 + + pure (alice, bob, conv') + +supportMLS :: (HasCallStack, MakesValue u) => u -> App () +supportMLS u = do + prots <- bindResponse (getUserSupportedProtocols u u) $ \resp -> do + resp.status `shouldMatchInt` 200 + prots <- resp.json & asList + traverse asString prots + let prots' = "mls" : prots + bindResponse (putUserSupportedProtocols u prots') $ \resp -> + resp.status `shouldMatchInt` 200 diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 23bb3693d95..fc6c0341a96 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -217,6 +217,46 @@ testMixedProtocolAppMessagesAreDenied secondDomain = do resp.status `shouldMatchInt` 422 resp.json %. "label" `shouldMatch` "mls-unsupported-message" +testMLSProtocolUpgrade :: HasCallStack => Domain -> App () +testMLSProtocolUpgrade secondDomain = do + (alice, bob, conv) <- simpleMixedConversationSetup secondDomain + charlie <- randomUser OwnDomain def + + -- alice creates MLS group and bob joins + [alice1, bob1, charlie1] <- traverse createMLSClient [alice, bob, charlie] + createGroup alice1 conv + void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle + + -- charlie is added to the group + void $ uploadNewKeyPackage charlie1 + void $ createAddCommit alice1 [charlie] >>= sendAndConsumeCommitBundle + + supportMLS alice + bindResponse (putConversationProtocol bob conv "mls") $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-migration-criteria-not-satisfied" + bindResponse (getConversation alice conv) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "protocol" `shouldMatch` "mixed" + + supportMLS bob + + withWebSockets [alice1, bob1] $ \wss -> do + bindResponse (putConversationProtocol bob conv "mls") $ \resp -> do + resp.status `shouldMatchInt` 200 + for_ wss $ \ws -> do + let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" + n <- awaitMatch 3 isMessage ws + msg <- asByteString (nPayload n %. "data") >>= showMessage alice1 + let leafIndexCharlie = 2 + msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexCharlie + msg %. "message.content.sender.External" `shouldMatchInt` 0 + + bindResponse (getConversation alice conv) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "protocol" `shouldMatch` "mls" + testAddUser :: HasCallStack => App () testAddUser = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] 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 529af23d250..e557fd7ea06 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 @@ -63,7 +63,8 @@ type GalleyApi = :<|> FedEndpoint "on-conversation-updated" ConversationUpdate () :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent" + MakesFederatedCall 'Galley "on-mls-message-sent", + MakesFederatedCall 'Brig "get-users-by-ids" ] "leave-conversation" LeaveConversationRequest @@ -89,7 +90,8 @@ type GalleyApi = EmptyResponse :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent" + MakesFederatedCall 'Galley "on-mls-message-sent", + MakesFederatedCall 'Brig "get-users-by-ids" ] "update-conversation" ConversationUpdateRequest @@ -110,7 +112,8 @@ type GalleyApi = MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", MakesFederatedCall 'Galley "send-mls-commit-bundle", - MakesFederatedCall 'Brig "get-mls-clients" + MakesFederatedCall 'Brig "get-mls-clients", + MakesFederatedCall 'Brig "get-users-by-ids" ] "send-mls-commit-bundle" MLSMessageSendRequest diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index b412fa2cdd0..fc58cd7d569 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -88,6 +88,7 @@ data GalleyError | MLSUnexpectedSenderClient | MLSSubConvUnsupportedConvType | MLSSubConvClientNotInParent + | MLSMigrationCriteriaNotSatisfied | -- NoBindingTeamMembers | NoBindingTeam @@ -230,6 +231,8 @@ type instance MapError 'MLSSubConvUnsupportedConvType = 'StaticError 403 "mls-su type instance MapError 'MLSSubConvClientNotInParent = 'StaticError 403 "mls-subconv-join-parent-missing" "MLS client cannot join the subconversation because it is not member of the parent conversation" +type instance MapError 'MLSMigrationCriteriaNotSatisfied = 'StaticError 400 "mls-migration-criteria-not-satisfied" "The migration criteria for mixed to MLS protocol transition are not satisfied for this conversation" + type instance MapError 'NoBindingTeamMembers = 'StaticError 403 "non-binding-team-members" "Both users must be members of the same binding team" type instance MapError 'NoBindingTeam = 'StaticError 403 "no-binding-team" "Operation allowed only on binding teams" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 98fec9dd478..a1f9254b62a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -843,6 +843,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "leave-conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> Until 'V2 :> ZLocalUser :> ZConn @@ -863,6 +864,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "leave-conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'RemoveConversationMember) @@ -882,6 +884,7 @@ type ConversationAPI = :> Description "Use `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow 'ConvNotFound @@ -906,6 +909,7 @@ type ConversationAPI = :> Description "**Note**: at least one field has to be provided." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow 'ConvNotFound @@ -932,6 +936,7 @@ type ConversationAPI = :> Description "Use `/conversations/:domain/:conv/name` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -952,6 +957,7 @@ type ConversationAPI = :> Description "Use `/conversations/:domain/:conv/name` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -972,6 +978,7 @@ type ConversationAPI = ( Summary "Update conversation name" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -995,6 +1002,7 @@ type ConversationAPI = :> Description "Use `/conversations/:domain/:cnv/message-timer` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) @@ -1016,6 +1024,7 @@ type ConversationAPI = ( Summary "Update the message timer for a conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) @@ -1041,6 +1050,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "update-conversation" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationReceiptMode) @@ -1063,6 +1073,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" :> MakesFederatedCall 'Galley "update-conversation" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationReceiptMode) @@ -1087,6 +1098,7 @@ type ConversationAPI = ( Summary "Update access modes for a conversation (deprecated)" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> Until 'V3 :> Description "Use PUT `/conversations/:domain/:cnv/access` instead." :> ZLocalUser @@ -1112,6 +1124,7 @@ type ConversationAPI = ( Summary "Update access modes for a conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> Until 'V3 :> ZLocalUser :> ZConn @@ -1136,6 +1149,7 @@ type ConversationAPI = ( Summary "Update access modes for a conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> From 'V3 :> ZLocalUser :> ZConn @@ -1206,6 +1220,10 @@ type ConversationAPI = :> CanThrow 'ConvInvalidProtocolTransition :> CanThrow ('ActionDenied 'LeaveConversation) :> CanThrow 'InvalidOperation + :> CanThrow 'MLSMigrationCriteriaNotSatisfied + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'TeamNotFound :> ZLocalUser :> ZConn :> "conversations" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs index b29c0024048..60a98c0ea6f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs @@ -65,6 +65,7 @@ type LegalHoldAPI = ( Summary "Delete legal hold service settings" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow AuthenticationError :> CanThrow OperationDenied :> CanThrow 'NotATeamMember @@ -103,6 +104,7 @@ type LegalHoldAPI = ( Summary "Consent to legal hold" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'InvalidOperation :> CanThrow 'TeamMemberNotFound @@ -120,6 +122,7 @@ type LegalHoldAPI = ( Summary "Request legal hold device" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'NotATeamMember :> CanThrow OperationDenied @@ -150,6 +153,7 @@ type LegalHoldAPI = ( Summary "Disable legal hold for user" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow AuthenticationError :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'NotATeamMember @@ -178,6 +182,7 @@ type LegalHoldAPI = ( Summary "Approve legal hold device" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow AuthenticationError :> CanThrow 'AccessDenied :> CanThrow ('ActionDenied 'RemoveConversationMember) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index 4dd8fbd56ff..8a17c071a1c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -73,6 +73,7 @@ type MLSMessagingAPI = :> MakesFederatedCall 'Galley "send-mls-commit-bundle" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Brig "get-mls-clients" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvMemberNotFound :> CanThrow 'ConvNotFound diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs index ce5fed146d5..9eae7ea3ac1 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs @@ -70,6 +70,7 @@ type TeamConversationAPI = ( Summary "Remove a team conversation" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'DeleteConversation) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index a5d17888061..f9985ed7dc5 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -96,6 +96,7 @@ library Galley.API.MLS.IncomingMessage Galley.API.MLS.Keys Galley.API.MLS.Message + Galley.API.MLS.Migration Galley.API.MLS.Propagate Galley.API.MLS.Proposal Galley.API.MLS.Removal diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index c68977e611b..c1d03dba6d3 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -54,7 +54,10 @@ import qualified Data.Set as Set import Data.Singletons import Data.Time.Clock import Galley.API.Error +import Galley.API.MLS.Conversation +import Galley.API.MLS.Migration import Galley.API.MLS.Removal +import Galley.API.Teams.Features.Get import Galley.API.Util import Galley.App import Galley.Data.Conversation @@ -98,6 +101,7 @@ import Wire.API.Federation.API (Component (Galley), fedClient) import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite +import Wire.API.Team.Feature import Wire.API.Team.LegalHold import Wire.API.Team.Member import Wire.API.Unreachable @@ -213,8 +217,24 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con HasConversationActionEffects 'ConversationUpdateProtocolTag r = ( Member ConversationStore r, Member (ErrorS 'ConvInvalidProtocolTransition) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'MLSMigrationCriteriaNotSatisfied) r, Member (Error NoChanges) r, - Member FederatorAccess r + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member BrigAccess r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member SubConversationStore r, + Member TeamFeatureStore r, + Member TeamStore r, + Member TinyLog r ) type family HasConversationActionGalleyErrors (tag :: ConversationActionTag) :: EffectRow where @@ -277,7 +297,11 @@ type family HasConversationActionGalleyErrors (tag :: ConversationActionTag) :: '[ ErrorS ('ActionDenied 'LeaveConversation), ErrorS 'InvalidOperation, ErrorS 'ConvNotFound, - ErrorS 'ConvInvalidProtocolTransition + ErrorS 'ConvInvalidProtocolTransition, + ErrorS 'MLSMigrationCriteriaNotSatisfied, + ErrorS 'NotATeamMember, + ErrorS OperationDenied, + ErrorS 'TeamNotFound ] noChanges :: Member (Error NoChanges) r => Sem r a @@ -395,6 +419,15 @@ performAction tag origUser lconv action = do (ProtocolProteusTag, ProtocolMixedTag, Just _) -> do E.updateToMixedProtocol lcnv MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 pure (mempty, action) + (ProtocolMixedTag, ProtocolMLSTag, Just tid) -> do + mig <- getFeatureStatus @MlsMigrationConfig DontDoAuth tid + now <- input + mlsConv <- mkMLSConversation conv >>= noteS @'ConvInvalidProtocolTransition + ok <- checkMigrationCriteria now mlsConv mig + unless ok $ throwS @'MLSMigrationCriteriaNotSatisfied + removeExtraneousClients origUser lconv + E.updateToMLSProtocol lcnv + pure (mempty, action) (ProtocolProteusTag, ProtocolProteusTag, _) -> noChanges (ProtocolMixedTag, ProtocolMixedTag, _) -> diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index c65f8018ebb..d7fe9171148 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -442,6 +442,7 @@ updateConversation :: Member TinyLog r, Member ConversationStore r, Member SubConversationStore r, + Member TeamFeatureStore r, Member (Input (Local ())) r ) => Domain -> diff --git a/services/galley/src/Galley/API/MLS/Migration.hs b/services/galley/src/Galley/API/MLS/Migration.hs new file mode 100644 index 00000000000..eea8feac440 --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Migration.hs @@ -0,0 +1,78 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 Galley.API.MLS.Migration where + +import Brig.Types.Intra +import Data.Qualified +import qualified Data.Set as Set +import Data.Time +import Galley.API.MLS.Types +import Galley.Effects.BrigAccess +import Galley.Effects.FederatorAccess +import Galley.Types.Conversations.Members +import Imports +import Polysemy +import Wire.API.Federation.API +import Wire.API.Team.Feature +import Wire.API.User + +-- | Similar to @Ap f All@, but short-circuiting. +-- +-- For example: +-- @ +-- ApAll (pure False) <> ApAll (putStrLn "hi" $> True) +-- @ +-- does not print anything. +newtype ApAll f = ApAll {unApAll :: f Bool} + +instance Monad f => Semigroup (ApAll f) where + ApAll a <> ApAll b = ApAll $ a >>= \x -> if x then b else pure False + +instance Monad f => Monoid (ApAll f) where + mempty = ApAll (pure True) + +checkMigrationCriteria :: + ( Member BrigAccess r, + Member FederatorAccess r + ) => + UTCTime -> + MLSConversation -> + WithStatus MlsMigrationConfig -> + Sem r Bool +checkMigrationCriteria now conv ws + | wsStatus ws == FeatureStatusDisabled = pure False + | afterDeadline = pure True + | otherwise = unApAll $ mconcat [localUsersMigrated, remoteUsersMigrated] + where + mig = wsConfig ws + afterDeadline = maybe False (now >=) mig.finaliseRegardlessAfter + + containsMLS = Set.member BaseProtocolMLSTag + + localUsersMigrated = ApAll $ do + localProfiles <- + map accountUser + <$> getUsers (map lmId conv.mcLocalMembers) + pure $ all (containsMLS . userSupportedProtocols) localProfiles + + remoteUsersMigrated = ApAll $ do + remoteProfiles <- fmap (foldMap tUnqualified) + . runFederatedConcurrently (map rmId conv.mcRemoteMembers) + $ \ruids -> + fedClient @'Brig @"get-users-by-ids" (tUnqualified ruids) + pure $ all (containsMLS . profileSupportedProtocols) remoteProfiles diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index e23a3f027b0..9477082c31a 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -17,6 +17,7 @@ module Galley.API.MLS.Removal ( createAndSendRemoveProposals, + removeExtraneousClients, removeClient, removeUser, ) @@ -26,17 +27,20 @@ import Data.Bifunctor import Data.Id import qualified Data.Map as Map import Data.Qualified +import qualified Data.Set as Set import Data.Time import Galley.API.MLS.Conversation import Galley.API.MLS.Keys (getMLSRemovalKey) import Galley.API.MLS.Propagate import Galley.API.MLS.Types +import Galley.Data.Conversation.Types import qualified Galley.Data.Conversation.Types as Data import Galley.Effects import Galley.Effects.MemberStore import Galley.Effects.ProposalStore import Galley.Effects.SubConversationStore import Galley.Env +import Galley.Types.Conversations.Members import Imports hiding (cs) import Polysemy import Polysemy.Input @@ -116,6 +120,8 @@ removeClientsWithClientMapRecursively :: Foldable f ) => Local MLSConversation -> + -- | A function returning the "list" of clients to be removed from either the + -- main conversation or each of its subconversations. (ConvOrSubConv -> f (ClientIdentity, LeafIndex)) -> -- | Originating user. The resulting proposals will appear to be sent by this user. Qualified UserId -> @@ -203,3 +209,31 @@ listSubConversations' cid = do msubs <- for (Map.assocs subs) $ \(subId, _) -> do getSubConversation cid subId pure (catMaybes msubs) + +-- | Send remove proposals for clients of users that are not part of a conversation +removeExtraneousClients :: + ( Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member SubConversationStore r, + Member TinyLog r + ) => + Qualified UserId -> + Local Conversation -> + Sem r () +removeExtraneousClients qusr lconv = do + mMlsConv <- mkMLSConversation (tUnqualified lconv) + for_ mMlsConv $ \mlsConv -> do + let allMembers = + Set.fromList $ + map (tUntagged . qualifyAs lconv . lmId) (mcLocalMembers mlsConv) + <> map (tUntagged . rmId) (mcRemoteMembers mlsConv) + let getClients c = + filter + (\(cid, _) -> cidQualifiedUser cid `Set.notMember` allMembers) + (cmAssocs c.members) + removeClientsWithClientMapRecursively (qualifyAs lconv mlsConv) getClients qusr diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 575140e8804..d116ec0b858 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -689,15 +689,25 @@ updateConversationProtocolWithLocalUser :: Member (ErrorS ('ActionDenied 'LeaveConversation)) r, Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, + Member (ErrorS 'MLSMigrationCriteriaNotSatisfied) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'TeamNotFound) r, Member (Input UTCTime) r, + Member (Input Env) r, Member (Input (Local ())) r, + Member (Input Opts) r, Member BrigAccess r, + Member ConversationStore r, Member MemberStore r, Member TinyLog r, Member GundeckAccess r, Member ExternalAccess r, Member FederatorAccess r, - Member ConversationStore r + Member ProposalStore r, + Member SubConversationStore r, + Member TeamFeatureStore r, + Member TeamStore r ) => Local UserId -> ConnId -> diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index b45fe567967..fdc83dc03ae 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -423,6 +423,18 @@ updateToMixedProtocol lcnv cs = do addPrepQuery Cql.updateToMixedConv (tUnqualified lcnv, ProtocolMixedTag, gid, epoch, cs) pure () +updateToMLSProtocol :: + Members + '[ Embed IO, + Input ClientState + ] + r => + Local ConvId -> + Sem r () +updateToMLSProtocol lcnv = + embedClient . retry x5 $ + write Cql.updateToMLSConv (params LocalQuorum (tUnqualified lcnv, ProtocolMLSTag)) + interpretConversationStoreToCassandra :: ( Member (Embed IO) r, Member (Input ClientState) r, @@ -453,3 +465,4 @@ interpretConversationStoreToCassandra = interpret $ \case AcquireCommitLock gId epoch ttl -> embedClient $ acquireCommitLock gId epoch ttl ReleaseCommitLock gId epoch -> embedClient $ releaseCommitLock gId epoch UpdateToMixedProtocol cid cs -> updateToMixedProtocol cid cs + UpdateToMLSProtocol cid -> updateToMLSProtocol cid diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 7d95fb03875..a1b6cbc93be 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -257,8 +257,10 @@ insertMLSSelfConv = updateToMixedConv :: PrepQuery W (ConvId, ProtocolTag, GroupId, Epoch, CipherSuiteTag) () updateToMixedConv = - fromString $ - "insert into conversation (conv, protocol, group_id, epoch, cipher_suite) values (?, ?, ?, ?, ?)" + "insert into conversation (conv, protocol, group_id, epoch, cipher_suite) values (?, ?, ?, ?, ?)" + +updateToMLSConv :: PrepQuery W (ConvId, ProtocolTag) () +updateToMLSConv = "insert into conversation (conv, protocol) values (?, ?)" updateConvAccess :: PrepQuery W (C.Set Access, C.Set AccessRole, ConvId) () updateConvAccess = "update conversation set access = ?, access_roles_v2 = ? where conv = ?" diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index ca368296d22..f95ba4a5568 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -46,6 +46,7 @@ module Galley.Effects.ConversationStore acceptConnectConversation, setGroupInfo, updateToMixedProtocol, + updateToMLSProtocol, -- * Delete conversation deleteConversation, @@ -99,6 +100,7 @@ data ConversationStore m a where AcquireCommitLock :: GroupId -> Epoch -> NominalDiffTime -> ConversationStore m LockAcquired ReleaseCommitLock :: GroupId -> Epoch -> ConversationStore m () UpdateToMixedProtocol :: Local ConvId -> CipherSuiteTag -> ConversationStore m () + UpdateToMLSProtocol :: Local ConvId -> ConversationStore m () makeSem ''ConversationStore From ad3d5633dc125bcf5b0adb5877d587e41583f2b1 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 12 Jun 2023 09:56:44 +0200 Subject: [PATCH 052/225] Add GET endpoint for MLS 1-1 conversations (#3345) * One2One GET endpoint stub * Make one2one conversation ID protocol-dependent * Create MLS 1-1 conversation object * Add MLS one2one integration tests * Test MLS 1-1 for teammates --- changelog.d/1-api-changes/get-mls-one2one | 1 + integration/integration.cabal | 1 + integration/test/API/Brig.hs | 44 ++++++++++++++++ integration/test/API/BrigInternal.hs | 9 ++++ integration/test/API/Common.hs | 10 ++-- integration/test/API/Galley.hs | 12 +++++ integration/test/SetupHelpers.hs | 8 +++ integration/test/Test/MLS/One2One.hs | 43 ++++++++++++++++ .../src/Galley/Types/Conversations/One2One.hs | 14 ++--- .../src/Wire/API/Conversation/Member.hs | 24 +++++++++ .../API/Routes/Public/Galley/Conversation.hs | 11 ++++ services/brig/test/integration/Util.hs | 2 +- services/galley/src/Galley/API/Action.hs | 4 +- services/galley/src/Galley/API/Create.hs | 3 +- services/galley/src/Galley/API/One2One.hs | 12 ++++- .../src/Galley/API/Public/Conversation.hs | 1 + services/galley/src/Galley/API/Query.hs | 51 +++++++++++++++++++ services/galley/src/Galley/API/Util.hs | 21 ++++++-- services/galley/test/integration/API/Util.hs | 2 +- .../test/unit/Test/Galley/API/One2One.hs | 7 +-- 20 files changed, 258 insertions(+), 22 deletions(-) create mode 100644 changelog.d/1-api-changes/get-mls-one2one create mode 100644 integration/test/Test/MLS/One2One.hs diff --git a/changelog.d/1-api-changes/get-mls-one2one b/changelog.d/1-api-changes/get-mls-one2one new file mode 100644 index 00000000000..b34d49e3c21 --- /dev/null +++ b/changelog.d/1-api-changes/get-mls-one2one @@ -0,0 +1 @@ +Add new endpoint `GET /conversations/one2one/:domain/:uid` to fetch the MLS 1-1 conversation with another user diff --git a/integration/integration.cabal b/integration/integration.cabal index 54b4254745f..cc68a257c74 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -93,6 +93,7 @@ library Test.Demo Test.MLS Test.MLS.KeyPackage + Test.MLS.One2One Test.User Testlib.App Testlib.Assertions diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index da77c21f74a..710314406f1 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -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 -> @@ -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] diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 68ce8666065..639be27d56f 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -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 diff --git a/integration/test/API/Common.hs b/integration/test/API/Common.hs index 88ed7bfe3c7..b2b3dfdc1b0 100644 --- a/integration/test/API/Common.hs +++ b/integration/test/API/Common.hs @@ -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'] diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 37903ed1b40..f3a984fb34f 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -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 diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 79b13bb58a6..ee1b9a995e6 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -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 diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs new file mode 100644 index 00000000000..fb0eda7b36b --- /dev/null +++ b/integration/test/Test/MLS/One2One.hs @@ -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 diff --git a/libs/galley-types/src/Galley/Types/Conversations/One2One.hs b/libs/galley-types/src/Galley/Types/Conversations/One2One.hs index bd2afdc7fd3..cd02c8824d8 100644 --- a/libs/galley-types/src/Galley/Types/Conversations/One2One.hs +++ b/libs/galley-types/src/Galley/Types/Conversations/One2One.hs @@ -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. -- @@ -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) = @@ -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 ] diff --git a/libs/wire-api/src/Wire/API/Conversation/Member.hs b/libs/wire-api/src/Wire/API/Conversation/Member.hs index b6f76ec19e8..c3a41ece88e 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Member.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Member.hs @@ -23,8 +23,10 @@ module Wire.API.Conversation.Member -- * Member Member (..), + defMember, MutedStatus (..), OtherMember (..), + defOtherMember, -- * Member Update MemberUpdate (..), @@ -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" $ @@ -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" $ diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index a1f9254b62a..969af7e49f6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -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 diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 389a1182bfb..d2f70780f8a 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -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) diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index c1d03dba6d3..c4734c57632 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -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, diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 80382af9ed7..43189f0c356 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -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 @@ -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, diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index 8bd2b4d9d1f..b58850b95fb 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -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 -> @@ -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 diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 070d996823d..6341091e356 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -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) diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 43f2d4399ca..7d5912ae588 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -37,6 +37,7 @@ module Galley.API.Query ensureConvAdmin, getMLSSelfConversation, getMLSSelfConversationWithError, + getMLSOne2OneConversation, ) where @@ -58,6 +59,7 @@ import Galley.API.MLS.Keys import Galley.API.MLS.Types import Galley.API.Mapping import qualified Galley.API.Mapping as Mapping +import Galley.API.One2One import Galley.API.Util import qualified Galley.Data.Conversation as Data import Galley.Data.Types (Code (codeConversation)) @@ -85,6 +87,7 @@ import qualified System.Logger.Class as Logger import Wire.API.Conversation hiding (Member) import qualified Wire.API.Conversation as Public import Wire.API.Conversation.Code +import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import qualified Wire.API.Conversation.Role as Public import Wire.API.Error @@ -93,9 +96,13 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Group.Serialisation +import Wire.API.MLS.SubConversation import qualified Wire.API.Provider.Bot as Public import qualified Wire.API.Routes.MultiTablePaging as Public import Wire.API.Team.Feature as Public hiding (setStatus) +import Wire.API.User import Wire.Sem.Paging.Cassandra getBotConversationH :: @@ -734,6 +741,50 @@ getMLSSelfConversation lusr = do cnv <- maybe (E.createMLSSelfConversation lusr) pure mconv conversationView lusr cnv +-- | Get an MLS 1-1 conversation. The conversation object is created on the +-- fly, but not persisted. The conversation will only be stored in the database +-- when its first commit arrives. +getMLSOne2OneConversation :: + ( Member BrigAccess r, + Member (Input Env) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'NotConnected) r, + Member TeamStore r + ) => + Local UserId -> + Qualified UserId -> + Sem r Conversation +getMLSOne2OneConversation lself qother = do + assertMLSEnabled + ensureConnectedOrSameTeam lself [qother] + let convId = one2OneConvId BaseProtocolMLSTag (tUntagged lself) qother + metadata = + ( defConversationMetadata + (tUnqualified lself) + ) + { cnvmType = One2OneConv + } + groupId = convToGroupId' (fmap Conv convId) + mlsData = + ConversationMLSData + { cnvmlsGroupId = groupId, + cnvmlsEpoch = Epoch 0, + cnvmlsEpochTimestamp = Nothing, + cnvmlsCipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + } + let members = + ConvMembers + { cmSelf = defMember (tUntagged lself), + cmOthers = [defOtherMember qother] + } + pure + Conversation + { cnvQualifiedId = convId, + cnvMetadata = metadata, + cnvMembers = members, + cnvProtocol = ProtocolMLS mlsData + } + ------------------------------------------------------------------------------- -- Helpers diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 657207c2101..9d3addbb8ae 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -108,12 +108,27 @@ ensureAccessRole roles users = do let botsExist = any (isJust . User.userService) activated unless (not botsExist || ServiceAccessRole `Set.member` roles) $ throwS @'ConvAccessDenied +-- | Check that the given user is either part of the same team as the other +-- users OR that there is a connection. +ensureConnectedOrSameTeam :: + ( Member BrigAccess r, + Member (ErrorS 'NotConnected) r, + Member TeamStore r + ) => + Local UserId -> + [Qualified UserId] -> + Sem r () +ensureConnectedOrSameTeam lusr others = do + let (locals, remotes) = partitionQualified lusr others + ensureConnectedToLocalsOrSameTeam lusr locals + ensureConnectedToRemotes lusr remotes + -- | Check that the given user is either part of the same team(s) as the other -- users OR that there is a connection. -- -- Team members are always considered connected, so we only check 'ensureConnected' -- for non-team-members of the _given_ user -ensureConnectedOrSameTeam :: +ensureConnectedToLocalsOrSameTeam :: ( Member BrigAccess r, Member (ErrorS 'NotConnected) r, Member TeamStore r @@ -121,8 +136,8 @@ ensureConnectedOrSameTeam :: Local UserId -> [UserId] -> Sem r () -ensureConnectedOrSameTeam _ [] = pure () -ensureConnectedOrSameTeam (tUnqualified -> u) uids = do +ensureConnectedToLocalsOrSameTeam _ [] = pure () +ensureConnectedToLocalsOrSameTeam (tUnqualified -> u) uids = do uTeams <- getUserTeams u -- We collect all the relevant uids from same teams as the origin user sameTeamUids <- forM uTeams $ \team -> diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 658b2e2e489..d92c01eb823 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -2863,7 +2863,7 @@ generateRemoteAndConvId = generateRemoteAndConvIdWithDomain (Domain "far-away.ex generateRemoteAndConvIdWithDomain :: Domain -> Bool -> Local UserId -> TestM (Remote UserId, Qualified ConvId) generateRemoteAndConvIdWithDomain remoteDomain shouldBeLocal lUserId = do other <- Qualified <$> randomId <*> pure remoteDomain - let convId = one2OneConvId (tUntagged lUserId) other + let convId = one2OneConvId BaseProtocolProteusTag (tUntagged lUserId) other isLocal = tDomain lUserId == qDomain convId if shouldBeLocal == isLocal then pure (qTagUnsafe other, convId) diff --git a/services/galley/test/unit/Test/Galley/API/One2One.hs b/services/galley/test/unit/Test/Galley/API/One2One.hs index 40580d29146..9a93da22743 100644 --- a/services/galley/test/unit/Test/Galley/API/One2One.hs +++ b/services/galley/test/unit/Test/Galley/API/One2One.hs @@ -26,6 +26,7 @@ import Imports import Test.Tasty import Test.Tasty.HUnit (Assertion, testCase, (@?=)) import Test.Tasty.QuickCheck +import Wire.API.User tests :: TestTree tests = @@ -35,8 +36,8 @@ tests = testCase "non-collision" one2OneConvIdNonCollision ] -one2OneConvIdSymmetry :: Qualified UserId -> Qualified UserId -> Property -one2OneConvIdSymmetry quid1 quid2 = one2OneConvId quid1 quid2 === one2OneConvId quid2 quid1 +one2OneConvIdSymmetry :: BaseProtocolTag -> Qualified UserId -> Qualified UserId -> Property +one2OneConvIdSymmetry proto quid1 quid2 = one2OneConvId proto quid1 quid2 === one2OneConvId proto quid2 quid1 -- | Make sure that we never get the same conversation ID for a pair of -- (assumingly) distinct qualified user IDs @@ -46,5 +47,5 @@ one2OneConvIdNonCollision = do -- A generator of lists of length 'len' of qualified user ID pairs let gen = vectorOf len arbitrary quids <- nubOrd <$> generate gen - let hashes = nubOrd (fmap (uncurry one2OneConvId) quids) + let hashes = nubOrd (fmap (uncurry (one2OneConvId BaseProtocolProteusTag)) quids) length hashes @?= length quids From 9ba52c541d28f7c4d3daa1b856a3a724e552445f Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 14 Jun 2023 09:23:48 +0200 Subject: [PATCH 053/225] More general MLS 1-1 conversation GET handler (#3349) * More general MLS 1-1 conversation GET handler - Fetch 1-1 conversation from database if present - Get conversation from the other backend when the conversation is supposed to be remote --- integration/test/Test/MLS/One2One.hs | 1 + .../src/Wire/API/Federation/API/Galley.hs | 26 ++++ .../src/Wire/API/Federation/Error.hs | 2 +- services/galley/galley.cabal | 1 + services/galley/src/Galley/API/Federation.hs | 37 +++++- services/galley/src/Galley/API/MLS/One2One.hs | 117 ++++++++++++++++++ services/galley/src/Galley/API/Query.hs | 97 ++++++++++----- 7 files changed, 245 insertions(+), 36 deletions(-) create mode 100644 services/galley/src/Galley/API/MLS/One2One.hs diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index fb0eda7b36b..f7fa4ac4c60 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -28,6 +28,7 @@ testGetMLSOne2One otherDomain = do conv2 %. "type" `shouldMatchInt` 2 conv2 %. "qualified_id" `shouldMatch` convId + conv2 %. "epoch" `shouldMatch` (conv %. "epoch") testGetMLSOne2OneUnconnected :: HasCallStack => Domain -> App () testGetMLSOne2OneUnconnected otherDomain = do 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 e557fd7ea06..65f50624991 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 @@ -145,6 +145,10 @@ type GalleyApi = "leave-sub-conversation" LeaveSubConversationRequest LeaveSubConversationResponse + :<|> FedEndpoint + "get-one2one-conversation" + GetOne2OneConversationRequest + GetOne2OneConversationResponse data TypingDataUpdateRequest = TypingDataUpdateRequest { tdurTypingStatus :: TypingStatus, @@ -189,6 +193,16 @@ data GetConversationsRequest = GetConversationsRequest deriving (Arbitrary) via (GenericUniform GetConversationsRequest) deriving (ToJSON, FromJSON) via (CustomEncoded GetConversationsRequest) +data GetOne2OneConversationRequest = GetOne2OneConversationRequest + { -- The user on the sender's domain + goocSenderUser :: UserId, + -- The user on the receiver's domain + goocReceiverUser :: UserId + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform GetOne2OneConversationRequest) + deriving (ToJSON, FromJSON) via (CustomEncoded GetOne2OneConversationRequest) + data RemoteConvMembers = RemoteConvMembers { rcmSelfRole :: RoleName, rcmOthers :: [OtherMember] @@ -220,6 +234,18 @@ newtype GetConversationsResponse = GetConversationsResponse deriving (Arbitrary) via (GenericUniform GetConversationsResponse) deriving (ToJSON, FromJSON) via (CustomEncoded GetConversationsResponse) +data GetOne2OneConversationResponse + = GetOne2OneConversationOk RemoteConversation + | -- | This is returned when the local backend is asked for a 1-1 conversation + -- that should reside on the other backend. + GetOne2OneConversationBackendMismatch + | -- | This is returned when a 1-1 conversation between two unconnected users + -- is requested. + GetOne2OneConversationNotConnected + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform GetOne2OneConversationResponse) + deriving (ToJSON, FromJSON) via (CustomEncoded GetOne2OneConversationResponse) + -- | A record type describing a new federated conversation -- -- FUTUREWORK: Think about extracting common conversation metadata into a 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 d51fd252f0d..1f300417126 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs @@ -29,7 +29,7 @@ -- corresponding to a failure at the level of the federator client. It -- includes, for example, a failure to reach a remote federator, or an -- error on the remote side. --- * 'FederatorError': this is created by users of the federator client. It +-- * 'FederationError': this is created by users of the federator client. It -- can either wrap a 'FederatorClientError', or be an error that is outside -- the scope of the client, such as when a federated request succeeds with -- an unexpected result. diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index f9985ed7dc5..03157b1e515 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -97,6 +97,7 @@ library Galley.API.MLS.Keys Galley.API.MLS.Message Galley.API.MLS.Migration + Galley.API.MLS.One2One Galley.API.MLS.Propagate Galley.API.MLS.Proposal Galley.API.MLS.Removal diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index d7fe9171148..af31897439c 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -23,7 +23,7 @@ module Galley.API.Federation ) where -import Control.Error +import Control.Error hiding (note) import Control.Lens import Data.Bifunctor import Data.ByteString.Conversion (toByteString') @@ -45,10 +45,12 @@ import Galley.API.Error import Galley.API.MLS.Enabled import Galley.API.MLS.GroupInfo import Galley.API.MLS.Message +import Galley.API.MLS.One2One import Galley.API.MLS.Removal import Galley.API.MLS.SubConversation hiding (leaveSubConversation) import Galley.API.MLS.Util import Galley.API.MLS.Welcome +import Galley.API.Mapping import qualified Galley.API.Mapping as Mapping import Galley.API.Message import Galley.API.Push @@ -62,6 +64,7 @@ import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E import Galley.Options import Galley.Types.Conversations.Members +import Galley.Types.Conversations.One2One import Galley.Types.UserList (UserList (UserList)) import Imports import Polysemy @@ -93,6 +96,7 @@ import Wire.API.MLS.SubConversation import Wire.API.Message import Wire.API.Routes.Named import Wire.API.ServantProto +import Wire.API.User (BaseProtocolTag (..)) type FederationAPI = "federation" :> FedApi 'Galley @@ -119,6 +123,7 @@ federationSitemap = :<|> Named @"get-sub-conversation" getSubConversationForRemoteUser :<|> Named @"delete-sub-conversation" (callsFed deleteSubConversationForRemoteUser) :<|> Named @"leave-sub-conversation" (callsFed leaveSubConversation) + :<|> Named @"get-one2one-conversation" getOne2OneConversation onClientRemoved :: ( Member ConversationStore r, @@ -807,6 +812,36 @@ deleteSubConversationForRemoteUser domain DeleteSubConversationFedRequest {..} = lconv <- qualifyLocal dscreqConv deleteLocalSubConversation qusr lconv dscreqSubConv dsc +getOne2OneConversation :: + ( Member ConversationStore r, + Member (Input (Local ())) r, + Member (Error InternalError) r, + Member BrigAccess r + ) => + Domain -> + GetOne2OneConversationRequest -> + Sem r GetOne2OneConversationResponse +getOne2OneConversation domain (GetOne2OneConversationRequest self other) = + fmap (Imports.fromRight GetOne2OneConversationNotConnected) + . runError @(Tagged 'NotConnected ()) + $ do + lother <- qualifyLocal other + let rself = toRemoteUnsafe domain self + ensureConnectedToRemotes lother [rself] + let getLocal lconv = do + mconv <- E.getConversation (tUnqualified lconv) + fmap GetOne2OneConversationOk $ case mconv of + Nothing -> pure (localMLSOne2OneConversationAsRemote rself lother lconv) + Just conv -> + note + (InternalErrorWithDescription "Unexpected member list in 1-1 conversation") + (conversationToRemote (tDomain lother) rself conv) + foldQualified + lother + getLocal + (const (pure GetOne2OneConversationBackendMismatch)) + (one2OneConvId BaseProtocolMLSTag (tUntagged lother) (tUntagged rself)) + -------------------------------------------------------------------------------- -- Error handling machinery diff --git a/services/galley/src/Galley/API/MLS/One2One.hs b/services/galley/src/Galley/API/MLS/One2One.hs new file mode 100644 index 00000000000..32f6952e454 --- /dev/null +++ b/services/galley/src/Galley/API/MLS/One2One.hs @@ -0,0 +1,117 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.API.MLS.One2One + ( localMLSOne2OneConversation, + localMLSOne2OneConversationAsRemote, + remoteMLSOne2OneConversation, + ) +where + +import Data.Id as Id +import Data.Qualified +import Imports hiding (cs) +import Wire.API.Conversation hiding (Member) +import Wire.API.Conversation.Protocol +import Wire.API.Conversation.Role +import Wire.API.Federation.API.Galley +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Group.Serialisation +import Wire.API.MLS.SubConversation + +-- | Construct a local MLS 1-1 'Conversation' between a local user and another +-- (possibly remote) user. +localMLSOne2OneConversation :: + Local UserId -> + Qualified UserId -> + Local ConvId -> + Conversation +localMLSOne2OneConversation lself qother (tUntagged -> convId) = + let members = + ConvMembers + { cmSelf = defMember (tUntagged lself), + cmOthers = [defOtherMember qother] + } + (metadata, protocol) = localMLSOne2OneConversationMetadata (tUntagged lself) convId + in Conversation + { cnvQualifiedId = convId, + cnvMetadata = metadata, + cnvMembers = members, + cnvProtocol = protocol + } + +-- | Construct a 'RemoteConversation' structure for a local MLS 1-1 +-- conversation to be returned to a remote backend. +localMLSOne2OneConversationAsRemote :: + Remote UserId -> + Local UserId -> + Local ConvId -> + RemoteConversation +localMLSOne2OneConversationAsRemote rself lother lcnv = + let members = + RemoteConvMembers + { rcmSelfRole = roleNameWireMember, + rcmOthers = [defOtherMember (tUntagged lother)] + } + (metadata, protocol) = localMLSOne2OneConversationMetadata (tUntagged rself) (tUntagged lcnv) + in RemoteConversation + { rcnvId = tUnqualified lcnv, + rcnvMetadata = metadata, + rcnvMembers = members, + rcnvProtocol = protocol + } + +localMLSOne2OneConversationMetadata :: + Qualified UserId -> + Qualified ConvId -> + (ConversationMetadata, Protocol) +localMLSOne2OneConversationMetadata self convId = + let metadata = + ( defConversationMetadata + (qUnqualified self) + ) + { cnvmType = One2OneConv + } + groupId = convToGroupId' (fmap Conv convId) + mlsData = + ConversationMLSData + { cnvmlsGroupId = groupId, + cnvmlsEpoch = Epoch 0, + cnvmlsEpochTimestamp = Nothing, + cnvmlsCipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + } + in (metadata, ProtocolMLS mlsData) + +-- | Convert an MLS 1-1 conversation returned by a remote backend into a +-- 'Conversation' to be returned to the client. +remoteMLSOne2OneConversation :: + Local UserId -> + Remote UserId -> + RemoteConversation -> + Conversation +remoteMLSOne2OneConversation lself rother rc = + let members = + ConvMembers + { cmSelf = defMember (tUntagged lself), + cmOthers = [defOtherMember (tUntagged rother)] + } + in Conversation + { cnvQualifiedId = tUntagged (qualifyAs rother (rcnvId rc)), + cnvMetadata = rcnvMetadata rc, + cnvMembers = members, + cnvProtocol = rcnvProtocol rc + } diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 7d5912ae588..6aa1fb5db78 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -56,6 +56,7 @@ import qualified Data.Set as Set import Galley.API.Error import Galley.API.MLS import Galley.API.MLS.Keys +import Galley.API.MLS.One2One import Galley.API.MLS.Types import Galley.API.Mapping import qualified Galley.API.Mapping as Mapping @@ -87,7 +88,6 @@ import qualified System.Logger.Class as Logger import Wire.API.Conversation hiding (Member) import qualified Wire.API.Conversation as Public import Wire.API.Conversation.Code -import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import qualified Wire.API.Conversation.Role as Public import Wire.API.Error @@ -96,9 +96,6 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error -import Wire.API.MLS.CipherSuite -import Wire.API.MLS.Group.Serialisation -import Wire.API.MLS.SubConversation import qualified Wire.API.Provider.Bot as Public import qualified Wire.API.Routes.MultiTablePaging as Public import Wire.API.Team.Feature as Public hiding (setStatus) @@ -741,15 +738,25 @@ getMLSSelfConversation lusr = do cnv <- maybe (E.createMLSSelfConversation lusr) pure mconv conversationView lusr cnv --- | Get an MLS 1-1 conversation. The conversation object is created on the --- fly, but not persisted. The conversation will only be stored in the database --- when its first commit arrives. +-- | Get an MLS 1-1 conversation. If not already existing, the conversation +-- object is created on the fly, but not persisted. The conversation will only +-- be stored in the database when its first commit arrives. +-- +-- For the federated case, we do not make the assumption that the other backend +-- uses the same function to calculate the conversation ID and corresponding +-- group ID, however we /do/ assume that the two backends agree on which of the +-- two is responsible for hosting the conversation. getMLSOne2OneConversation :: ( Member BrigAccess r, + Member ConversationStore r, Member (Input Env) r, + Member (Error FederationError) r, + Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member TeamStore r + Member FederatorAccess r, + Member TeamStore r, + Member P.TinyLog r ) => Local UserId -> Qualified UserId -> @@ -758,32 +765,54 @@ getMLSOne2OneConversation lself qother = do assertMLSEnabled ensureConnectedOrSameTeam lself [qother] let convId = one2OneConvId BaseProtocolMLSTag (tUntagged lself) qother - metadata = - ( defConversationMetadata - (tUnqualified lself) - ) - { cnvmType = One2OneConv - } - groupId = convToGroupId' (fmap Conv convId) - mlsData = - ConversationMLSData - { cnvmlsGroupId = groupId, - cnvmlsEpoch = Epoch 0, - cnvmlsEpochTimestamp = Nothing, - cnvmlsCipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - } - let members = - ConvMembers - { cmSelf = defMember (tUntagged lself), - cmOthers = [defOtherMember qother] - } - pure - Conversation - { cnvQualifiedId = convId, - cnvMetadata = metadata, - cnvMembers = members, - cnvProtocol = ProtocolMLS mlsData - } + foldQualified + lself + (getLocalMLSOne2OneConversation lself qother) + (getRemoteMLSOne2OneConversation lself qother) + convId + +getLocalMLSOne2OneConversation :: + ( Member ConversationStore r, + Member (Error InternalError) r, + Member P.TinyLog r + ) => + Local UserId -> + Qualified UserId -> + Local ConvId -> + Sem r Conversation +getLocalMLSOne2OneConversation lself qother lconv = do + mconv <- E.getConversation (tUnqualified lconv) + case mconv of + Nothing -> pure (localMLSOne2OneConversation lself qother lconv) + Just conv -> conversationView lself conv + +getRemoteMLSOne2OneConversation :: + ( Member (Error InternalError) r, + Member (Error FederationError) r, + Member (ErrorS 'NotConnected) r, + Member FederatorAccess r + ) => + Local UserId -> + Qualified UserId -> + Remote conv -> + Sem r Conversation +getRemoteMLSOne2OneConversation lself qother rconv = do + -- a conversation can only be remote if it is hosted on the other user's domain + rother <- + if qDomain qother == tDomain rconv + then pure (toRemoteUnsafe (tDomain rconv) (qUnqualified qother)) + else throw (InternalErrorWithDescription "Unexpected 1-1 conversation domain") + + resp <- + E.runFederated rconv $ + fedClient @'Galley @"get-one2one-conversation" $ + GetOne2OneConversationRequest (tUnqualified lself) (tUnqualified rother) + case resp of + GetOne2OneConversationOk rc -> + pure (remoteMLSOne2OneConversation lself rother rc) + GetOne2OneConversationBackendMismatch -> + throw (FederationUnexpectedBody "Backend mismatch when retrieving a remote 1-1 conversation") + GetOne2OneConversationNotConnected -> throwS @'NotConnected ------------------------------------------------------------------------------- -- Helpers From 241a58846308d51f002b0b10cab641f0f88402ce Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 15 Jun 2023 12:31:02 +0200 Subject: [PATCH 054/225] add conversation type to group ID serialisation (#3344) --- changelog.d/5-internal/WPB-1925 | 1 + integration/test/API/Galley.hs | 17 ++++++ integration/test/Test/MLS.hs | 29 +++++++++ libs/wire-api/src/Wire/API/Conversation.hs | 2 +- libs/wire-api/src/Wire/API/MLS/Group.hs | 2 + .../src/Wire/API/MLS/Group/Serialisation.hs | 61 +++++++++++++------ .../src/Wire/API/Routes/Internal/Galley.hs | 6 +- .../test/unit/Test/Wire/API/MLS/Group.hs | 21 ++++++- services/galley/src/Galley/API/Action.hs | 2 +- services/galley/src/Galley/API/Internal.hs | 9 +-- services/galley/src/Galley/API/MLS/One2One.hs | 2 +- .../src/Galley/API/MLS/SubConversation.hs | 14 ++++- services/galley/src/Galley/API/MLS/Util.hs | 2 +- .../src/Galley/Cassandra/Conversation.hs | 11 ++-- .../src/Galley/Effects/ConversationStore.hs | 2 +- services/galley/test/integration/API/MLS.hs | 30 +-------- .../galley/test/integration/API/MLS/Util.hs | 2 +- services/galley/test/integration/API/Util.hs | 9 ++- 18 files changed, 144 insertions(+), 78 deletions(-) create mode 100644 changelog.d/5-internal/WPB-1925 diff --git a/changelog.d/5-internal/WPB-1925 b/changelog.d/5-internal/WPB-1925 new file mode 100644 index 00000000000..cc2af9f8948 --- /dev/null +++ b/changelog.d/5-internal/WPB-1925 @@ -0,0 +1 @@ +add conversation type to group ID serialisation diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index f3a984fb34f..43d5b70ceb4 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -1,6 +1,9 @@ module API.Galley where import qualified Data.Aeson as Aeson +import qualified Data.ByteString.Base64 as B64 +import qualified Data.ByteString.Base64.URL as B64U +import qualified Data.ByteString.Char8 as BS import Testlib.Prelude data CreateConv = CreateConv @@ -212,3 +215,17 @@ getMLSOne2OneConversation self other = do baseRequest self Galley Versioned $ joinHttpPath ["conversations", "one2one", domain, uid] submit "GET" req + +getGroupClients :: + (HasCallStack, MakesValue user) => + user -> + String -> + App Response +getGroupClients user groupId = do + req <- + baseRequest + user + Galley + Unversioned + (joinHttpPath ["i", "group", BS.unpack . B64U.encodeUnpadded . B64.decodeLenient $ BS.pack groupId]) + submit "GET" req diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index fc6c0341a96..b534b10706d 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -484,3 +484,32 @@ testRemoveClientsIncomplete = do err <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 409 err %. "label" `shouldMatch` "mls-client-mismatch" + +testAdminRemovesUserFromConv :: HasCallStack => App () +testAdminRemovesUserFromConv = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + void $ createWireClient bob + traverse_ uploadNewKeyPackage [bob1, bob2] + (gid, qcnv) <- createNewGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + events <- createRemoveCommit alice1 [bob1, bob2] >>= sendAndConsumeCommitBundle + + do + event <- assertOne =<< asList (events %. "events") + event %. "qualified_conversation" `shouldMatch` qcnv + event %. "type" `shouldMatch` "conversation.member-leave" + event %. "from" `shouldMatch` objId alice + members <- event %. "data" %. "qualified_user_ids" & asList + bobQid <- bob %. "qualified_id" + shouldMatch members [bobQid] + + convs <- getAllConvs bob + convIds <- traverse (%. "qualified_id") convs + clients <- bindResponse (getGroupClients alice gid) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "client_ids" & asList + void $ assertOne clients + assertBool + "bob is not longer part of conversation after the commit" + (qcnv `notElem` convIds) diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 2155b274808..4695fd6961d 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -598,7 +598,7 @@ data ConvType | SelfConv | One2OneConv | ConnectConv - deriving stock (Eq, Show, Generic) + deriving stock (Eq, Show, Enum, Generic) deriving (Arbitrary) via (GenericUniform ConvType) deriving (FromJSON, ToJSON, S.ToSchema) via Schema ConvType diff --git a/libs/wire-api/src/Wire/API/MLS/Group.hs b/libs/wire-api/src/Wire/API/MLS/Group.hs index fcb33a5e36f..39fcac4f161 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group.hs @@ -22,12 +22,14 @@ import Data.Json.Util import Data.Schema import qualified Data.Swagger as S import Imports +import Servant import Wire.API.MLS.Serialisation import Wire.Arbitrary newtype GroupId = GroupId {unGroupId :: ByteString} deriving (Eq, Show, Generic, Ord) deriving (Arbitrary) via (GenericUniform GroupId) + deriving (FromHttpApiData, ToHttpApiData, S.ToParamSchema) via Base64ByteString deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema GroupId) instance IsString GroupId where diff --git a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs index f735a8a40df..061a72abd87 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs @@ -16,8 +16,9 @@ -- with this program. If not, see . module Wire.API.MLS.Group.Serialisation - ( convToGroupId, - convToGroupId', + ( GroupIdParts (..), + groupIdParts, + convToGroupId, groupIdToConv, nextGenGroupId, ) @@ -36,47 +37,69 @@ import qualified Data.Text.Encoding as T import qualified Data.UUID as UUID import Imports hiding (cs) import Web.HttpApiData (FromHttpApiData (parseHeader)) +import Wire.API.Conversation import Wire.API.MLS.Group import Wire.API.MLS.SubConversation +data GroupIdParts = GroupIdParts + { convType :: ConvType, + qConvId :: Qualified ConvOrSubConvId, + gidGen :: GroupIdGen + } + deriving (Show, Eq) + +groupIdParts :: ConvType -> Qualified ConvOrSubConvId -> GroupIdParts +groupIdParts ct qcs = + GroupIdParts + { convType = ct, + qConvId = qcs, + gidGen = GroupIdGen 0 + } + -- | Return the group ID associated to a conversation ID. Note that is not -- assumed to be stable over time or even consistent among different backends. -convToGroupId :: Qualified ConvOrSubConvId -> GroupIdGen -> GroupId -convToGroupId qcs gen = GroupId . L.toStrict . runPut $ do - let cs = qUnqualified qcs +convToGroupId :: GroupIdParts -> GroupId +convToGroupId parts = GroupId . L.toStrict . runPut $ do + let cs = qUnqualified parts.qConvId subId = foldMap unSubConvId cs.subconv putWord64be 1 -- Version 1 of the GroupId format + putWord32be (fromIntegral $ fromEnum parts.convType) putLazyByteString . UUID.toByteString . toUUID $ cs.conv putWord8 $ fromIntegral (T.length subId) putByteString $ T.encodeUtf8 subId - maybe (pure ()) (const $ putWord32be (unGroupIdGen gen)) cs.subconv - putLazyByteString . toByteString $ qDomain qcs + maybe (pure ()) (const $ putWord32be (unGroupIdGen parts.gidGen)) cs.subconv + putLazyByteString . toByteString $ qDomain parts.qConvId -convToGroupId' :: Qualified ConvOrSubConvId -> GroupId -convToGroupId' = flip convToGroupId (GroupIdGen 0) - -groupIdToConv :: GroupId -> Either String (Qualified ConvOrSubConvId, GroupIdGen) +groupIdToConv :: GroupId -> Either String GroupIdParts groupIdToConv gid = do - (rem', _, (conv, gen)) <- first (\(_, _, msg) -> msg) $ runGetOrFail readConv (L.fromStrict (unGroupId gid)) + (rem', _, (ct, conv, gen)) <- first (\(_, _, msg) -> msg) $ runGetOrFail readConv (L.fromStrict (unGroupId gid)) domain <- first displayException . T.decodeUtf8' . L.toStrict $ rem' - pure $ (Qualified conv (Domain domain), gen) + pure + GroupIdParts + { convType = toEnum $ fromIntegral ct, + qConvId = Qualified conv (Domain domain), + gidGen = gen + } where readConv = do version <- getWord64be + ct <- getWord32be unless (version == 1) $ fail "unsupported groupId version" mUUID <- UUID.fromByteString . L.fromStrict <$> getByteString 16 uuid <- maybe (fail "invalid conversation UUID in groupId") pure mUUID n <- getWord8 if n == 0 - then pure $ (Conv (Id uuid), GroupIdGen 0) + then pure $ (ct, Conv (Id uuid), GroupIdGen 0) else do subConvIdBS <- getByteString $ fromIntegral n subConvId <- either (fail . T.unpack) pure $ parseHeader subConvIdBS gen <- getWord32be - pure $ (SubConv (Id uuid) (SubConvId subConvId), GroupIdGen gen) + pure $ (ct, SubConv (Id uuid) (SubConvId subConvId), GroupIdGen gen) nextGenGroupId :: GroupId -> Either String GroupId -nextGenGroupId gid = - uncurry convToGroupId - . second (GroupIdGen . succ . unGroupIdGen) - <$> groupIdToConv gid +nextGenGroupId gid = convToGroupId . succGen <$> groupIdToConv gid + where + succGen parts = + parts + { gidGen = GroupIdGen (succ $ unGroupIdGen parts.gidGen) + } diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index 07e11ad1b16..8fa4baa9470 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -27,6 +27,7 @@ import Servant hiding (JSON, WithStatus) import qualified Servant hiding (WithStatus) import Servant.Swagger import Wire.API.ApplyMods +import Wire.API.Conversation import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley @@ -216,10 +217,9 @@ type InternalAPIBase = :<|> Named "get-conversation-clients" ( Summary "Get mls conversation client list" - :> ZLocalUser :> CanThrow 'ConvNotFound - :> "conversation" - :> Capture "cnv" ConvId + :> "group" + :> Capture "gid" GroupId :> MultiVerb1 'GET '[Servant.JSON] diff --git a/libs/wire-api/test/unit/Test/Wire/API/MLS/Group.hs b/libs/wire-api/test/unit/Test/Wire/API/MLS/Group.hs index c20998b8e4b..d731b10f5d9 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/MLS/Group.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/MLS/Group.hs @@ -22,6 +22,7 @@ import Imports import Test.QuickCheck import Test.Tasty import Test.Tasty.QuickCheck +import Wire.API.Conversation import Wire.API.MLS.Group import Wire.API.MLS.Group.Serialisation import Wire.API.MLS.SubConversation @@ -33,9 +34,23 @@ tests = [ testProperty "roundtrip serialise and parse groupId" $ roundtripGroupId ] -roundtripGroupId :: Qualified ConvOrSubConvId -> GroupIdGen -> Property -roundtripGroupId convId gen = +roundtripGroupId :: ConvType -> Qualified ConvOrSubConvId -> GroupIdGen -> Property +roundtripGroupId ct convId gen = let gen' = case qUnqualified convId of (Conv _) -> GroupIdGen 0 (SubConv _ _) -> gen - in groupIdToConv (convToGroupId convId gen) === Right (convId, gen') + in groupIdToConv + ( convToGroupId + GroupIdParts + { convType = ct, + qConvId = convId, + gidGen = gen + } + ) + === Right + ( GroupIdParts + { convType = ct, + qConvId = convId, + gidGen = gen' + } + ) diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index c4734c57632..9a0d94c2e93 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -417,7 +417,7 @@ performAction tag origUser lconv action = do SConversationUpdateProtocolTag -> do case (protocolTag (convProtocol (tUnqualified lconv)), action, convTeam (tUnqualified lconv)) of (ProtocolProteusTag, ProtocolMixedTag, Just _) -> do - E.updateToMixedProtocol lcnv MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + E.updateToMixedProtocol lcnv (convType (tUnqualified lconv)) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 pure (mempty, action) (ProtocolMixedTag, ProtocolMLSTag, Just tid) -> do mig <- getFeatureStatus @MlsMigrationConfig DontDoAuth tid diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 5d30424aa94..8d2851a0d0e 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -91,8 +91,6 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error -import Wire.API.MLS.Group.Serialisation -import Wire.API.MLS.SubConversation import Wire.API.Provider.Service hiding (Service) import Wire.API.Routes.API import Wire.API.Routes.Internal.Galley @@ -484,9 +482,8 @@ iGetMLSClientListForConv :: ErrorS 'ConvNotFound ] r => - Local UserId -> - ConvId -> + GroupId -> Sem r ClientList -iGetMLSClientListForConv lusr cnv = do - cm <- E.lookupMLSClients (convToGroupId' (Conv <$> tUntagged (qualifyAs lusr cnv))) +iGetMLSClientListForConv gid = do + cm <- E.lookupMLSClients gid pure $ ClientList (concatMap (Map.keys . snd) (Map.assocs cm)) diff --git a/services/galley/src/Galley/API/MLS/One2One.hs b/services/galley/src/Galley/API/MLS/One2One.hs index 32f6952e454..745bc438e36 100644 --- a/services/galley/src/Galley/API/MLS/One2One.hs +++ b/services/galley/src/Galley/API/MLS/One2One.hs @@ -86,7 +86,7 @@ localMLSOne2OneConversationMetadata self convId = ) { cnvmType = One2OneConv } - groupId = convToGroupId' (fmap Conv convId) + groupId = convToGroupId $ groupIdParts One2OneConv (fmap Conv convId) mlsData = ConversationMLSData { cnvmlsGroupId = groupId, diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index fe97707ba53..6e8ca52fb8c 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -127,7 +127,10 @@ getLocalSubConversation qusr lconv sconv = do -- deriving this detemernistically to prevent race condition between -- multiple threads creating the subconversation - let groupId = convToGroupId' $ flip SubConv sconv <$> tUntagged lconv + let groupId = + convToGroupId + . groupIdParts (Data.convType c) + $ flip SubConv sconv <$> tUntagged lconv epoch = Epoch 0 suite = cnvmlsCipherSuite mlsMeta Eff.createSubConversation (tUnqualified lconv) sconv suite epoch groupId Nothing @@ -294,7 +297,14 @@ deleteLocalSubConversation qusr lcnvId scnvId dsc = do Eff.removeAllMLSClients gid -- swallowing the error and starting with GroupIdGen 0 if nextGenGroupId - let newGid = fromRight (convToGroupId' (flip SubConv scnvId <$> tUntagged lcnvId)) $ nextGenGroupId gid + let newGid = + fromRight + ( convToGroupId $ + groupIdParts + (Data.convType cnv) + (flip SubConv scnvId <$> tUntagged lcnvId) + ) + $ nextGenGroupId gid -- the following overwrites any prior information about the subconversation Eff.createSubConversation cnvId scnvId cs (Epoch 0) newGid Nothing diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs index 1f41252b398..36fb80fbbf8 100644 --- a/services/galley/src/Galley/API/MLS/Util.hs +++ b/services/galley/src/Galley/API/MLS/Util.hs @@ -126,4 +126,4 @@ withCommitLock lConvOrSubId gid epoch action = ttl = fromIntegral (600 :: Int) -- 10 minutes getConvFromGroupId :: Member (Error MLSProtocolError) r => GroupId -> Sem r (Qualified ConvOrSubConvId) -getConvFromGroupId = either (throw . mlsProtocolError . T.pack) (pure . fst) . groupIdToConv +getConvFromGroupId = either (throw . mlsProtocolError . T.pack) (pure . qConvId) . groupIdToConv diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index fdc83dc03ae..aafbfe0d790 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -74,7 +74,7 @@ createMLSSelfConversation lusr = do ncProtocol = ProtocolCreateMLSTag } meta = ncMetadata nc - gid = convToGroupId' . fmap Conv . tUntagged . qualifyAs lusr $ cnv + gid = convToGroupId . groupIdParts meta.cnvmType . fmap Conv . tUntagged . qualifyAs lusr $ cnv -- FUTUREWORK: Stop hard-coding the cipher suite -- -- 'CipherSuite 1' corresponds to @@ -123,7 +123,7 @@ createConversation lcnv nc = do (proto, mgid, mep, mcs) = case ncProtocol nc of ProtocolCreateProteusTag -> (ProtocolProteus, Nothing, Nothing, Nothing) ProtocolCreateMLSTag -> - let gid = convToGroupId' $ Conv <$> tUntagged lcnv + let gid = convToGroupId . groupIdParts meta.cnvmType $ Conv <$> tUntagged lcnv ep = Epoch 0 cs = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 in ( ProtocolMLS @@ -412,10 +412,11 @@ updateToMixedProtocol :: ] r => Local ConvId -> + ConvType -> CipherSuiteTag -> Sem r () -updateToMixedProtocol lcnv cs = do - let gid = convToGroupId' $ Conv <$> tUntagged lcnv +updateToMixedProtocol lcnv ct cs = do + let gid = convToGroupId . groupIdParts ct $ Conv <$> tUntagged lcnv epoch = Epoch 0 embedClient . retry x5 . batch $ do setType BatchLogged @@ -464,5 +465,5 @@ interpretConversationStoreToCassandra = interpret $ \case SetGroupInfo cid gib -> embedClient $ setGroupInfo cid gib AcquireCommitLock gId epoch ttl -> embedClient $ acquireCommitLock gId epoch ttl ReleaseCommitLock gId epoch -> embedClient $ releaseCommitLock gId epoch - UpdateToMixedProtocol cid cs -> updateToMixedProtocol cid cs + UpdateToMixedProtocol cid ct cs -> updateToMixedProtocol cid ct cs UpdateToMLSProtocol cid -> updateToMLSProtocol cid diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index f95ba4a5568..3bdf6808811 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -99,7 +99,7 @@ data ConversationStore m a where SetGroupInfo :: ConvId -> GroupInfoData -> ConversationStore m () AcquireCommitLock :: GroupId -> Epoch -> NominalDiffTime -> ConversationStore m LockAcquired ReleaseCommitLock :: GroupId -> Epoch -> ConversationStore m () - UpdateToMixedProtocol :: Local ConvId -> CipherSuiteTag -> ConversationStore m () + UpdateToMixedProtocol :: Local ConvId -> ConvType -> CipherSuiteTag -> ConversationStore m () UpdateToMLSProtocol :: Local ConvId -> ConversationStore m () makeSem ''ConversationStore diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index cbef86588a2..e668d56c573 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -71,7 +71,6 @@ import Wire.API.Message import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.Version import Wire.API.Unreachable -import Wire.API.User.Client tests :: IO TestSetup -> TestTree tests s = @@ -107,8 +106,7 @@ tests s = test s "return error when commit is locked" testCommitLock, test s "add user to a conversation with proposal + commit" testAddUserBareProposalCommit, test s "post commit that references an unknown proposal" testUnknownProposalRefCommit, - test s "post commit that is not referencing all proposals" testCommitNotReferencingAllProposals, - test s "admin removes user from a conversation" testAdminRemovesUserFromConv + test s "post commit that is not referencing all proposals" testCommitNotReferencingAllProposals ], testGroup "External commit" @@ -728,32 +726,6 @@ testCommitNotReferencingAllProposals = do >= sendAndConsumeCommitBundle - events <- createRemoveCommit alice1 [bob1, bob2] >>= sendAndConsumeCommitBundle - pure (qcnv, events) - - liftIO $ assertOne events >>= assertLeaveEvent qcnv alice [bob] - - do - convs <- getAllConvs (qUnqualified bob) - clients <- getConvClients (qUnqualified alice) (qUnqualified qcnv) - liftIO $ do - assertEqual - ("Expected only one client, got " <> show clients) - (length . clClients $ clients) - 1 - assertBool - "bob is not longer part of conversation after the commit" - (qcnv `notElem` map cnvQualifiedId convs) - testRemoteAppMessage :: TestM () testRemoteAppMessage = do users@[alice, bob] <- createAndConnectUsers [Nothing, Just "bob.example.com"] diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 5cd82e55cef..b045acf7e19 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -521,7 +521,7 @@ setupFakeMLSGroup :: MLSTest (GroupId, Qualified ConvId) setupFakeMLSGroup creator mSubId = do qcnv <- randomQualifiedId (ciDomain creator) - let groupId = convToGroupId' $ maybe (Conv <$> qcnv) ((<$> qcnv) . flip SubConv) mSubId + let groupId = convToGroupId . groupIdParts RegularConv $ maybe (Conv <$> qcnv) ((<$> qcnv) . flip SubConv) mSubId createGroup creator (fmap Conv qcnv) groupId pure (groupId, qcnv) diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index d92c01eb823..5d84ded7119 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -38,6 +38,7 @@ import Data.Aeson hiding (json) import qualified Data.Aeson as A import Data.Aeson.Lens (key, _String) import qualified Data.ByteString as BS +import qualified Data.ByteString.Base64.URL as B64U import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Char8 as C import Data.ByteString.Conversion @@ -1008,15 +1009,13 @@ getConvs u cids = do . zConn "conn" . json (ListConversations (unsafeRange cids)) -getConvClients :: HasCallStack => UserId -> ConvId -> TestM ClientList -getConvClients usr cnv = do +getConvClients :: HasCallStack => GroupId -> TestM ClientList +getConvClients gid = do g <- viewGalley responseJsonError =<< get ( g - . paths ["i", "conversation", toByteString' cnv] - . zUser usr - . zConn "conn" + . paths ["i", "group", B64U.encode $ unGroupId gid] ) getAllConvs :: HasCallStack => UserId -> TestM [Conversation] From 8325c9b3385f56e6d5eaf9f4151a92034e649067 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 15 Jun 2023 14:23:05 +0200 Subject: [PATCH 055/225] alter group ID serialisation (#3353) - 16 bit version - 16 bit conversation type --- changelog.d/5-internal/group-id-subconv-2 | 1 + libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelog.d/5-internal/group-id-subconv-2 diff --git a/changelog.d/5-internal/group-id-subconv-2 b/changelog.d/5-internal/group-id-subconv-2 new file mode 100644 index 00000000000..75eb7947025 --- /dev/null +++ b/changelog.d/5-internal/group-id-subconv-2 @@ -0,0 +1 @@ +change version and conversation type to 16 bit in group ID serialisation diff --git a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs index 061a72abd87..3fb9a7dd3db 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs @@ -62,8 +62,8 @@ convToGroupId :: GroupIdParts -> GroupId convToGroupId parts = GroupId . L.toStrict . runPut $ do let cs = qUnqualified parts.qConvId subId = foldMap unSubConvId cs.subconv - putWord64be 1 -- Version 1 of the GroupId format - putWord32be (fromIntegral $ fromEnum parts.convType) + putWord16be 1 -- Version 1 of the GroupId format + putWord16be (fromIntegral $ fromEnum parts.convType) putLazyByteString . UUID.toByteString . toUUID $ cs.conv putWord8 $ fromIntegral (T.length subId) putByteString $ T.encodeUtf8 subId @@ -82,8 +82,8 @@ groupIdToConv gid = do } where readConv = do - version <- getWord64be - ct <- getWord32be + version <- getWord16be + ct <- getWord16be unless (version == 1) $ fail "unsupported groupId version" mUUID <- UUID.fromByteString . L.fromStrict <$> getByteString 16 uuid <- maybe (fail "invalid conversation UUID in groupId") pure mUUID From 87839302d64c92e291caac91ba62e67a1ad79bca Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 16 Jun 2023 15:03:54 +0200 Subject: [PATCH 056/225] Create subconversations on first commit (#3355) --- changelog.d/5-internal/mls-subconv-creation | 1 + integration/integration.cabal | 1 + integration/test/API/Galley.hs | 15 +++ integration/test/MLS/Util.hs | 9 +- integration/test/Test/MLS.hs | 16 +-- integration/test/Test/MLS/SubConversation.hs | 98 +++++++++++++++ .../Galley/API/MLS/Commit/ExternalCommit.hs | 3 + .../Galley/API/MLS/Commit/InternalCommit.hs | 17 +++ services/galley/src/Galley/API/MLS/Message.hs | 5 +- .../src/Galley/API/MLS/SubConversation.hs | 27 +---- services/galley/src/Galley/API/MLS/Types.hs | 31 +++++ .../src/Galley/Cassandra/SubConversation.hs | 21 ++-- .../Galley/Effects/SubConversationStore.hs | 2 +- services/galley/test/integration/API/MLS.hs | 112 +----------------- 14 files changed, 200 insertions(+), 158 deletions(-) create mode 100644 changelog.d/5-internal/mls-subconv-creation create mode 100644 integration/test/Test/MLS/SubConversation.hs diff --git a/changelog.d/5-internal/mls-subconv-creation b/changelog.d/5-internal/mls-subconv-creation new file mode 100644 index 00000000000..f87217e5d42 --- /dev/null +++ b/changelog.d/5-internal/mls-subconv-creation @@ -0,0 +1 @@ +Subconversations are now created on their first commit diff --git a/integration/integration.cabal b/integration/integration.cabal index cc68a257c74..c9d2c29a178 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -94,6 +94,7 @@ library Test.MLS Test.MLS.KeyPackage Test.MLS.One2One + Test.MLS.SubConversation Test.User Testlib.App Testlib.Assertions diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 43d5b70ceb4..701a01bc530 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -117,6 +117,21 @@ getSubConversation user conv sub = do ] submit "GET" req +deleteSubConversation :: + (HasCallStack, MakesValue user, MakesValue sub) => + user -> + sub -> + App Response +deleteSubConversation user sub = do + (conv, Just subId) <- objSubConv sub + (domain, convId) <- objQid conv + groupId <- sub %. "group_id" & asString + epoch :: Int <- sub %. "epoch" & asIntegral + req <- + baseRequest user Galley Versioned $ + joinHttpPath ["conversations", domain, convId, "subconversations", subId] + submit "DELETE" $ req & addJSONObject ["group_id" .= groupId, "epoch" .= epoch] + getSelfConversation :: (HasCallStack, MakesValue user) => user -> App Response getSelfConversation user = do req <- baseRequest user Galley Versioned "/conversations/mls-self" diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index f142b674759..d0d4b97649f 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -193,9 +193,16 @@ createGroup cid conv = do Nothing -> pure () resetGroup cid conv +createSubConv :: ClientIdentity -> String -> App () +createSubConv cid subId = do + mls <- getMLSState + sub <- getSubConversation cid mls.convId subId >>= getJSON 200 + resetGroup cid sub + void $ createPendingProposalCommit cid >>= sendAndConsumeCommitBundle + resetGroup :: MakesValue conv => ClientIdentity -> conv -> App () resetGroup cid conv = do - convId <- make conv + convId <- objSubConvObject conv groupId <- conv %. "group_id" & asString modifyMLSState $ \s -> s diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index b534b10706d..0205a0fb682 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -351,17 +351,11 @@ testJoinSubConv = do traverse_ uploadNewKeyPackage [bob1, bob2] (_, qcnv) <- createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - - sub <- bindResponse (getSubConversation bob qcnv "conference") $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json - resetGroup bob1 sub + void $ createSubConv bob1 "conference" -- bob adds his first client to the subconversation void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle - sub' <- bindResponse (getSubConversation bob qcnv "conference") $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json + sub' <- getSubConversation bob qcnv "conference" >>= getJSON 200 do tm <- sub' %. "epoch_timestamp" assertBool "Epoch timestamp should not be null" (tm /= Null) @@ -381,11 +375,7 @@ testDeleteParentOfSubConv secondDomain = do traverse_ uploadNewKeyPackage [alice1, bob1] (_, qcnv) <- createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - - sub <- bindResponse (getSubConversation bob qcnv "conference") $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json - resetGroup bob1 sub + void $ createSubConv bob1 "conference" -- bob adds his client to the subconversation void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs new file mode 100644 index 00000000000..0524560e640 --- /dev/null +++ b/integration/test/Test/MLS/SubConversation.hs @@ -0,0 +1,98 @@ +module Test.MLS.SubConversation where + +import API.Galley +import MLS.Util +import SetupHelpers +import Testlib.Prelude + +testJoinSubConv :: App () +testJoinSubConv = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + (_, qcnv) <- createNewGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + createSubConv bob1 "conference" + + -- bob adds his first client to the subconversation + void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle + sub' <- getSubConversation bob qcnv "conference" >>= getJSON 200 + do + tm <- sub' %. "epoch_timestamp" + assertBool "Epoch timestamp should not be null" (tm /= Null) + + -- now alice joins with her own client + void $ + createExternalCommit alice1 Nothing + >>= sendAndConsumeCommitBundle + +testDeleteParentOfSubConv :: HasCallStack => Domain -> App () +testDeleteParentOfSubConv secondDomain = do + (alice, tid) <- createTeam OwnDomain + bob <- randomUser secondDomain def + connectUsers [alice, bob] + + [alice1, bob1] <- traverse createMLSClient [alice, bob] + traverse_ uploadNewKeyPackage [alice1, bob1] + (_, qcnv) <- createNewGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + createSubConv bob1 "conference" + + -- bob adds his client to the subconversation + void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle + + -- alice joins with her own client + void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle + + -- bob sends a message to the subconversation + do + mp <- createApplicationMessage bob1 "hello, alice" + void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + resp.status `shouldMatchInt` 201 + + -- alice sends a message to the subconversation + do + mp <- createApplicationMessage bob1 "hello, bob" + void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + resp.status `shouldMatchInt` 201 + + -- alice deletes main conversation + void . bindResponse (deleteTeamConv tid qcnv alice) $ \resp -> do + resp.status `shouldMatchInt` 200 + + -- bob fails to send a message to the subconversation + do + mp <- createApplicationMessage bob1 "hello, alice" + void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + resp.status `shouldMatchInt` 404 + case secondDomain of + OwnDomain -> resp.json %. "label" `shouldMatch` "no-conversation" + OtherDomain -> resp.json %. "label" `shouldMatch` "no-conversation-member" + + -- alice fails to send a message to the subconversation + do + mp <- createApplicationMessage alice1 "hello, bob" + void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + resp.status `shouldMatchInt` 404 + resp.json %. "label" `shouldMatch` "no-conversation" + +testDeleteSubConversation :: HasCallStack => Domain -> App () +testDeleteSubConversation otherDomain = do + [alice, bob] <- createAndConnectUsers [OwnDomain, otherDomain] + charlie <- randomUser OwnDomain def + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ uploadNewKeyPackage bob1 + (_, qcnv) <- createNewGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + createSubConv alice1 "conference1" + sub1 <- getSubConversation alice qcnv "conference1" >>= getJSON 200 + void $ deleteSubConversation charlie sub1 >>= getBody 403 + void $ deleteSubConversation alice sub1 >>= getBody 200 + + createSubConv alice1 "conference2" + sub2 <- getSubConversation alice qcnv "conference2" >>= getJSON 200 + void $ deleteSubConversation bob sub2 >>= getBody 200 + + sub2' <- getSubConversation alice1 qcnv "conference2" >>= getJSON 200 + sub2 `shouldNotMatch` sub2' diff --git a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs index b4268a650d3..6f650ee6ae7 100644 --- a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs @@ -72,6 +72,9 @@ getExternalCommitData senderIdentity lConvOrSub epoch commit = do curEpoch = cnvmlsEpoch convOrSub.meta groupId = cnvmlsGroupId convOrSub.meta when (epoch /= curEpoch) $ throwS @'MLSStaleMessage + when (epoch == Epoch 0) $ + throw $ + mlsProtocolError "The first commit in a group cannot be external" proposals <- traverse getInlineProposal commit.proposals -- According to the spec, an external commit must contain: diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index 43d3c8cc285..f868a722cda 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -38,6 +38,7 @@ import qualified Galley.Data.Conversation.Types as Data import Galley.Effects import Galley.Effects.MemberStore import Galley.Effects.ProposalStore +import Galley.Effects.SubConversationStore import Galley.Types.Conversations.Members import Imports import Polysemy @@ -165,6 +166,22 @@ processInternalCommit senderIdentity con lConvOrSub epoch action commit = do pure Nothing for_ (unreachableFromList failedAddFetching) throwUnreachableUsers + -- Some types of conversations are created lazily on the first + -- commit. We do that here, with the commit lock held, but before + -- applying changes to the member list. + case convOrSub.id of + SubConv cnv sub | epoch == Epoch 0 -> do + -- create subconversation if it doesn't exist + msub' <- getSubConversation cnv sub + when (isNothing msub') $ + void $ + createSubConversation + cnv + sub + convOrSub.meta.cnvmlsCipherSuite + convOrSub.meta.cnvmlsGroupId + _ -> pure () -- FUTUREWORK: create 1-1 conversation at epoch 0 + -- remove users from the conversation and send events removeEvents <- foldMap diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 344aa214d2b..428b0e59439 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -205,7 +205,9 @@ postMLSCommitBundleToLocalConv qusr c conn bundle lConvOrSubId = do (events, newClients) <- case bundle.sender of SenderMember _index -> do + -- extract added/removed clients from bundle action <- getCommitData senderIdentity lConvOrSub bundle.epoch bundle.commit.value + -- process additions and removals events <- processInternalCommit senderIdentity @@ -445,7 +447,8 @@ fetchConvOrSub qusr convOrSubId = for convOrSubId $ \case SubConv convId sconvId -> do let lconv = qualifyAs convOrSubId convId c <- getMLSConv qusr lconv - subconv <- getSubConversation convId sconvId >>= noteS @'ConvNotFound + msubconv <- getSubConversation convId sconvId + let subconv = fromMaybe (newSubConversationFromParent lconv sconvId (mcMLSData c)) msubconv pure (SubConv c subconv) where getMLSConv :: Qualified UserId -> Local ConvId -> Sem r MLSConversation diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 6e8ca52fb8c..4671a692e0f 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -125,30 +125,9 @@ getLocalSubConversation qusr lconv sconv = do MLSMigrationMixed -> throwS @'MLSSubConvUnsupportedConvType MLSMigrationMLS -> pure () - -- deriving this detemernistically to prevent race condition between + -- deriving this deterministically to prevent race conditions with -- multiple threads creating the subconversation - let groupId = - convToGroupId - . groupIdParts (Data.convType c) - $ flip SubConv sconv <$> tUntagged lconv - epoch = Epoch 0 - suite = cnvmlsCipherSuite mlsMeta - Eff.createSubConversation (tUnqualified lconv) sconv suite epoch groupId Nothing - let sub = - SubConversation - { scParentConvId = tUnqualified lconv, - scSubConvId = sconv, - scMLSData = - ConversationMLSData - { cnvmlsGroupId = groupId, - cnvmlsEpoch = epoch, - cnvmlsEpochTimestamp = Nothing, - cnvmlsCipherSuite = suite - }, - scMembers = mkClientMap [], - scIndexMap = mempty - } - pure sub + pure (newSubConversationFromParent lconv sconv mlsMeta) Just sub -> pure sub pure (toPublicSubConv (tUntagged (qualifyAs lconv sub))) @@ -307,7 +286,7 @@ deleteLocalSubConversation qusr lcnvId scnvId dsc = do $ nextGenGroupId gid -- the following overwrites any prior information about the subconversation - Eff.createSubConversation cnvId scnvId cs (Epoch 0) newGid Nothing + void $ Eff.createSubConversation cnvId scnvId cs newGid deleteRemoteSubConversation :: ( Members diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index eda21c18cfb..0e63e96bdce 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -30,7 +30,9 @@ import Galley.Types.Conversations.Members import Imports import Wire.API.Conversation import Wire.API.Conversation.Protocol +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential +import Wire.API.MLS.Group.Serialisation import Wire.API.MLS.LeafNode import Wire.API.MLS.SubConversation @@ -146,6 +148,35 @@ data SubConversation = SubConversation } deriving (Eq, Show) +newSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> GroupId -> SubConversation +newSubConversation convId subConvId suite groupId = + SubConversation + { scParentConvId = convId, + scSubConvId = subConvId, + scMLSData = + ConversationMLSData + { cnvmlsGroupId = groupId, + cnvmlsEpoch = Epoch 0, + cnvmlsEpochTimestamp = Nothing, + cnvmlsCipherSuite = suite + }, + scMembers = mkClientMap [], + scIndexMap = mempty + } + +newSubConversationFromParent :: + Local ConvId -> + SubConvId -> + ConversationMLSData -> + SubConversation +newSubConversationFromParent lconv sconv mlsMeta = + let groupId = + convToGroupId + . groupIdParts RegularConv + $ flip SubConv sconv <$> tUntagged lconv + suite = cnvmlsCipherSuite mlsMeta + in newSubConversation (tUnqualified lconv) sconv suite groupId + toPublicSubConv :: Qualified SubConversation -> PublicSubConversation toPublicSubConv (Qualified (SubConversation {..}) domain) = let members = map fst (cmAssocs scMembers) diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index 649f20424ed..2b92e2c6b72 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -63,12 +63,19 @@ insertSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> - Epoch -> GroupId -> - Maybe GroupInfoData -> - Client () -insertSubConversation convId subConvId suite epoch groupId mGroupInfo = - retry x5 (write Cql.insertSubConversation (params LocalQuorum (convId, subConvId, suite, epoch, groupId, mGroupInfo))) + Client SubConversation +insertSubConversation convId subConvId suite groupId = do + retry + x5 + ( write + Cql.insertSubConversation + ( params + LocalQuorum + (convId, subConvId, suite, Epoch 0, groupId, Nothing) + ) + ) + pure (newSubConversation convId subConvId suite groupId) updateSubConvGroupInfo :: ConvId -> SubConvId -> Maybe GroupInfoData -> Client () updateSubConvGroupInfo convId subConvId mGroupInfo = @@ -110,8 +117,8 @@ interpretSubConversationStoreToCassandra :: Sem (SubConversationStore ': r) a -> Sem r a interpretSubConversationStoreToCassandra = interpret $ \case - CreateSubConversation convId subConvId suite epoch groupId mGroupInfo -> - embedClient (insertSubConversation convId subConvId suite epoch groupId mGroupInfo) + CreateSubConversation convId subConvId suite groupId -> + embedClient (insertSubConversation convId subConvId suite groupId) GetSubConversation convId subConvId -> embedClient (selectSubConversation convId subConvId) GetSubConversationGroupInfo convId subConvId -> embedClient (selectSubConvGroupInfo convId subConvId) GetSubConversationEpoch convId subConvId -> embedClient (selectSubConvEpoch convId subConvId) diff --git a/services/galley/src/Galley/Effects/SubConversationStore.hs b/services/galley/src/Galley/Effects/SubConversationStore.hs index 8d3e5cb70cd..b70b1167e83 100644 --- a/services/galley/src/Galley/Effects/SubConversationStore.hs +++ b/services/galley/src/Galley/Effects/SubConversationStore.hs @@ -30,7 +30,7 @@ import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation data SubConversationStore m a where - CreateSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> Epoch -> GroupId -> Maybe GroupInfoData -> SubConversationStore m () + CreateSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> GroupId -> SubConversationStore m SubConversation GetSubConversation :: ConvId -> SubConvId -> SubConversationStore m (Maybe SubConversation) GetSubConversationGroupInfo :: ConvId -> SubConvId -> SubConversationStore m (Maybe GroupInfoData) GetSubConversationEpoch :: ConvId -> SubConvId -> SubConversationStore m (Maybe Epoch) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index e668d56c573..6cff58cfe2f 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -210,9 +210,6 @@ tests s = test s "fail to add another client to a subconversation via internal commit" testAddClientSubConvFailure, test s "remove another client from a subconversation" testRemoveClientSubConv, test s "send an application message in a subconversation" testSendMessageSubConv, - test s "reset a subconversation as a creator" (testDeleteSubConv SubConvMember), - test s "reset a subconversation as a conversation member" (testDeleteSubConv ConvMember), - test s "reset a subconversation as a random user" (testDeleteSubConv RandomUser), test s "reset a subconversation and assert no leftover proposals" testJoinDeletedSubConvWithRemoval, test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale, test s "leave a subconversation as a creator" (testLeaveSubConv True), @@ -238,9 +235,7 @@ tests s = "Remote Sender/Local SubConversation" [ test s "get subconversation as a remote member" (testRemoteMemberGetSubConv True), test s "get subconversation as a remote non-member" (testRemoteMemberGetSubConv False), - test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv, - test s "delete subconversation as a remote member" (testRemoteMemberDeleteSubConv True), - test s "delete subconversation as a remote non-member" (testRemoteMemberDeleteSubConv False) + test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv ], testGroup "Remote Sender/Remote SubConversation" @@ -2471,111 +2466,6 @@ testRemoteMemberGetSubConv isAMember = do expectSubConvError _errExpected (GetSubConversationsResponseSuccess _) = liftIO $ assertFailure "Unexpected GetSubConversationsResponseSuccess" expectSubConvError errExpected (GetSubConversationsResponseError err) = liftIO $ err @?= errExpected -testRemoteMemberDeleteSubConv :: HasCallStack => Bool -> TestM () -testRemoteMemberDeleteSubConv isAMember = do - -- alice is local, bob is remote - -- alice creates a local conversation and invites bob - -- bob deletes a subconversation via federated enpdoint - - let bobDomain = Domain "faraway.example.com" - scnv = SubConvId "conference" - [alice, bob] <- createAndConnectUsers [Nothing, Just (domainText bobDomain)] - - (cnv, groupId, epoch) <- runMLSTest $ do - [alice1, bob1] <- traverse createMLSClient [alice, bob] - (_cnvGroupId, qcnv) <- setupMLSGroup alice1 - mp <- createAddCommit alice1 [bob] - - let mock = receiveCommitMock [bob1] <|> welcomeMock - void . withTempMockFederator' mock . sendAndConsumeCommitBundle $ mp - - sub <- - liftTest $ - responseJsonError - =<< getSubConv (qUnqualified alice) qcnv scnv - resetGroup alice1 (fmap (flip SubConv scnv) qcnv) (pscGroupId sub) - - pure (qUnqualified qcnv, pscGroupId sub, pscEpoch sub) - - randUser <- randomId - let delReq = - DeleteSubConversationFedRequest - { dscreqUser = if isAMember then qUnqualified bob else randUser, - dscreqConv = cnv, - dscreqSubConv = scnv, - dscreqGroupId = groupId, - dscreqEpoch = epoch - } - - -- Bob is a member of the parent conversation so he's allowed to delete the - -- subconversation. - (res, _reqs) <- - withTempMockFederator' deleteMLSConvMock $ do - fedGalleyClient <- view tsFedGalleyClient - runFedClient @"delete-sub-conversation" fedGalleyClient bobDomain delReq - - if isAMember then expectSuccess res else expectFailure ConvNotFound res - where - expectSuccess :: DeleteSubConversationResponse -> TestM () - expectSuccess DeleteSubConversationResponseSuccess = pure () - expectSuccess (DeleteSubConversationResponseError err) = - liftIO . assertFailure $ - "Unexpected DeleteSubConversationResponseError: " <> show err - - expectFailure :: GalleyError -> DeleteSubConversationResponse -> TestM () - expectFailure _errExpected DeleteSubConversationResponseSuccess = - liftIO . assertFailure $ - "Unexpected DeleteSubConversationResponseSuccess" - expectFailure errExpected (DeleteSubConversationResponseError err) = - liftIO $ err @?= errExpected - --- | A choice on who is deleting a subconversation -data SubConvDeleterType - = ConvMember - | SubConvMember - | RandomUser - deriving (Eq) - -testDeleteSubConv :: SubConvDeleterType -> TestM () -testDeleteSubConv deleterType = do - [alice, bob] <- createAndConnectUsers [Nothing, Nothing] - randUser <- randomId - let sconv = SubConvId "conference" - qcnv <- runMLSTest $ do - [alice1, bob1] <- traverse createMLSClient [alice, bob] - void $ uploadNewKeyPackage bob1 - (_, qcnv) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - void $ createSubConv qcnv alice1 sconv - pure qcnv - - sub <- - responseJsonError - =<< getSubConv (qUnqualified alice) qcnv sconv - (qUnqualified bob, 200) - SubConvMember -> (qUnqualified alice, 200) - RandomUser -> (randUser, 403) - deleteSubConv deleter qcnv sconv dsc !!! const expectedCode === statusCode - - newSub <- - responseJsonError - =<< getSubConv (qUnqualified alice) qcnv sconv - Date: Fri, 16 Jun 2023 14:04:28 +0200 Subject: [PATCH 057/225] Add /self/supported-protocols to nginz routes (#3358) --- charts/nginz/values.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 1834258a404..7f26b3a3a7e 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -175,6 +175,9 @@ nginx_conf: - path: /self/password envs: - all + - path: /self/supported-protocols + envs: + - all - path: /self/locale envs: - all From 32f4d87422b6bb7ab6ebdf994df0f5f4335d2c90 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Mon, 19 Jun 2023 17:03:10 +0200 Subject: [PATCH 058/225] Remove user & client thresholds from MLS migration (#3364) * Remove user & client thresholds from MLS migration * remove columns from DB * review comments --- changelog.d/5-internal/WPB-485 | 1 + libs/wire-api/src/Wire/API/Team/Feature.hs | 36 ++++--------------- .../schema/src/V84_TeamFeatureMlsMigration.hs | 4 +-- .../src/Galley/Cassandra/TeamFeatures.hs | 18 +++++----- .../test/integration/API/Teams/Feature.hs | 4 +-- 5 files changed, 17 insertions(+), 46 deletions(-) create mode 100644 changelog.d/5-internal/WPB-485 diff --git a/changelog.d/5-internal/WPB-485 b/changelog.d/5-internal/WPB-485 new file mode 100644 index 00000000000..a35171937fd --- /dev/null +++ b/changelog.d/5-internal/WPB-485 @@ -0,0 +1 @@ +Removed user and client threshold fields from mls migration feature. diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 3385c9a56bf..7175f6b5785 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -114,7 +114,6 @@ import Imports import Servant (FromHttpApiData (..), ToHttpApiData (..)) import Test.QuickCheck.Arbitrary (arbitrary) import Test.QuickCheck.Gen (suchThat) -import Test.QuickCheck.Modifiers import Wire.API.Conversation.Protocol (ProtocolTag (ProtocolProteusTag)) import Wire.API.MLS.CipherSuite (CipherSuiteTag (MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519)) import Wire.API.Routes.Named (RenderableSymbol (renderSymbol)) @@ -1044,9 +1043,7 @@ instance IsFeatureConfig MlsE2EIdConfig where data MlsMigrationConfig = MlsMigrationConfig { startTime :: Maybe UTCTime, - finaliseRegardlessAfter :: Maybe UTCTime, - usersThreshold :: Maybe Int, - clientsThreshold :: Maybe Int + finaliseRegardlessAfter :: Maybe UTCTime } deriving stock (Eq, Show, Generic) @@ -1057,45 +1054,24 @@ instance Arbitrary MlsMigrationConfig where arbitrary = do startTime <- fmap fromUTCTimeMillis <$> arbitrary finaliseRegardlessAfter <- fmap fromUTCTimeMillis <$> arbitrary - usersThreshold <- fmap getNonNegative <$> arbitrary - clientsThreshold <- fmap (fmap getNonNegative) $ - case (finaliseRegardlessAfter, usersThreshold) of - (Nothing, Nothing) -> Just <$> arbitrary - _ -> arbitrary pure MlsMigrationConfig { startTime = startTime, - finaliseRegardlessAfter = finaliseRegardlessAfter, - usersThreshold = usersThreshold, - clientsThreshold = clientsThreshold + finaliseRegardlessAfter = finaliseRegardlessAfter } instance ToSchema MlsMigrationConfig where schema = object "MlsMigration" $ - withParser - ( MlsMigrationConfig - <$> startTime .= maybe_ (optField "startTime" utcTimeSchema) - <*> finaliseRegardlessAfter .= maybe_ (optField "finaliseRegardlessAfter" utcTimeSchema) - <*> usersThreshold .= maybe_ (optField "usersThreshold" schema) - <*> clientsThreshold .= maybe_ (optField "clientsThreshold" schema) - ) - checkConfig - where - checkConfig c = do - when - ( isNothing c.finaliseRegardlessAfter - && isNothing c.usersThreshold - && isNothing c.clientsThreshold - ) - $ fail "At least one of finaliseRegardlessAfter, usersThreshold or clientsThreshold must be set" - pure c + MlsMigrationConfig + <$> startTime .= maybe_ (optField "startTime" utcTimeSchema) + <*> finaliseRegardlessAfter .= maybe_ (optField "finaliseRegardlessAfter" utcTimeSchema) instance IsFeatureConfig MlsMigrationConfig where type FeatureSymbol MlsMigrationConfig = "mlsMigration" defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked defValue FeatureTTLUnlimited where - defValue = MlsMigrationConfig Nothing Nothing Nothing Nothing + defValue = MlsMigrationConfig Nothing Nothing featureSingleton = FeatureSingletonMlsMigration objectSchema = field "config" schema diff --git a/services/galley/schema/src/V84_TeamFeatureMlsMigration.hs b/services/galley/schema/src/V84_TeamFeatureMlsMigration.hs index 35f07fb8e2b..92d013510a9 100644 --- a/services/galley/schema/src/V84_TeamFeatureMlsMigration.hs +++ b/services/galley/schema/src/V84_TeamFeatureMlsMigration.hs @@ -31,8 +31,6 @@ migration = Migration 84 "Add feature config for team feature MLS Migration" $ d mls_migration_status int, mls_migration_lock_status int, mls_migration_start_time timestamp, - mls_migration_finalise_regardless_after timestamp, - mls_migration_users_threshold int, - mls_migration_clients_threshold int + mls_migration_finalise_regardless_after timestamp ) |] diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 7ea2092bb56..a171f68215e 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -143,21 +143,19 @@ getFeatureConfig FeatureSingletonMlsMigration tid = do let q = query1 select (params LocalQuorum (Identity tid)) retry x1 q <&> \case Nothing -> Nothing - Just (Nothing, _, _, _, _) -> Nothing - Just (Just fs, startTime, finaliseRegardlessAfter, usersThreshold, clientsThreshold) -> + Just (Nothing, _, _) -> Nothing + Just (Just fs, startTime, finaliseRegardlessAfter) -> Just $ WithStatusNoLock fs MlsMigrationConfig { startTime = startTime, - finaliseRegardlessAfter = finaliseRegardlessAfter, - usersThreshold = fmap fromIntegral usersThreshold, - clientsThreshold = fmap fromIntegral clientsThreshold + finaliseRegardlessAfter = finaliseRegardlessAfter } FeatureTTLUnlimited where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe UTCTime, Maybe UTCTime, Maybe Int32, Maybe Int32) - select = "select mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after, mls_migration_users_threshold, mls_migration_clients_threshold from team_features where team_id = ?" + select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe UTCTime, Maybe UTCTime) + select = "select mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after from team_features where team_id = ?" getFeatureConfig FeatureSingletonExposeInvitationURLsToTeamAdminConfig tid = getTrivialConfigC "expose_invitation_urls_to_team_admin" tid getFeatureConfig FeatureSingletonOutlookCalIntegrationConfig tid = getTrivialConfigC "outlook_cal_integration_status" tid @@ -239,11 +237,11 @@ setFeatureConfig FeatureSingletonMlsMigration tid status = do let statusValue = wssStatus status config = wssConfig status - retry x5 $ write insert (params LocalQuorum (tid, statusValue, config.startTime, config.finaliseRegardlessAfter, fmap fromIntegral config.usersThreshold, fmap fromIntegral config.clientsThreshold)) + retry x5 $ write insert (params LocalQuorum (tid, statusValue, config.startTime, config.finaliseRegardlessAfter)) where - insert :: PrepQuery W (TeamId, FeatureStatus, Maybe UTCTime, Maybe UTCTime, Maybe Int32, Maybe Int32) () + insert :: PrepQuery W (TeamId, FeatureStatus, Maybe UTCTime, Maybe UTCTime) () insert = - "insert into team_features (team_id, mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after, mls_migration_users_threshold, mls_migration_clients_threshold) values (?, ?, ?, ?, ?, ?)" + "insert into team_features (team_id, mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after) values (?, ?, ?, ?)" setFeatureConfig FeatureSingletonExposeInvitationURLsToTeamAdminConfig tid statusNoLock = setFeatureStatusC "expose_invitation_urls_to_team_admin" tid (wssStatus statusNoLock) setFeatureConfig FeatureSingletonOutlookCalIntegrationConfig tid statusNoLock = setFeatureStatusC "outlook_cal_integration_status" tid (wssStatus statusNoLock) diff --git a/services/galley/test/integration/API/Teams/Feature.hs b/services/galley/test/integration/API/Teams/Feature.hs index 1ad2f7c064a..98f5bea53af 100644 --- a/services/galley/test/integration/API/Teams/Feature.hs +++ b/services/galley/test/integration/API/Teams/Feature.hs @@ -1470,8 +1470,6 @@ defaultMlsMigrationConfig = LockStatusLocked MlsMigrationConfig { startTime = fmap fromUTCTimeMillis (readUTCTimeMillis "2029-05-16T10:11:12.123Z"), - finaliseRegardlessAfter = fmap fromUTCTimeMillis (readUTCTimeMillis "2029-10-17T00:00:00.000Z"), - usersThreshold = Just 100, - clientsThreshold = Just 50 + finaliseRegardlessAfter = fmap fromUTCTimeMillis (readUTCTimeMillis "2029-10-17T00:00:00.000Z") } FeatureTTLUnlimited From 7734a7eff4cc75d900b42ca6aacbce2190bbb14d Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 20 Jun 2023 11:10:05 +0200 Subject: [PATCH 059/225] migrate testLocalWelcome (with new originating user) --- integration/test/Test/MLS.hs | 30 +++++++++++++++++++++ services/galley/test/integration/API/MLS.hs | 25 +---------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 0205a0fb682..a16df7fa368 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -503,3 +503,33 @@ testAdminRemovesUserFromConv = do assertBool "bob is not longer part of conversation after the commit" (qcnv `notElem` convIds) + +testLocalWelcome :: HasCallStack => App () +testLocalWelcome = do + users@[alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + + [alice1, bob1] <- traverse createMLSClient users + + void $ uploadNewKeyPackage bob1 + + (_, qcnv) <- createNewGroup alice1 + + commit <- createAddCommit alice1 [bob] + Just welcome <- pure commit.welcome + + es <- withWebSocket bob1 $ \wsBob -> do + es <- sendAndConsumeCommitBundle commit + let isWelcome n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" + + n <- awaitMatch 5 isWelcome wsBob + + shouldMatch (nPayload n %. "conversation") (objId qcnv) + shouldMatch (nPayload n %. "from") (objId alice) + shouldMatch (nPayload n %. "data") (B8.unpack (Base64.encode welcome)) + pure es + + event <- assertOne =<< asList (es %. "events") + event %. "type" `shouldMatch` "conversation.member-join" + event %. "conversation" `shouldMatch` objId qcnv + addedUser <- (event %. "data.users") >>= asList >>= assertOne + objQid addedUser `shouldMatch` objQid bob diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index fe523687c93..76b6538529b 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -83,8 +83,7 @@ tests s = ], testGroup "Welcome" - [ test s "local welcome" testLocalWelcome, - test s "post a remote MLS welcome message" sendRemoteMLSWelcome + [ test s "post a remote MLS welcome message" sendRemoteMLSWelcome ], testGroup "Creation" @@ -307,28 +306,6 @@ testSenderNotInConversation = do liftIO $ Wai.label err @?= "no-conversation" -testLocalWelcome :: TestM () -testLocalWelcome = do - users@[alice, bob] <- createAndConnectUsers [Nothing, Nothing] - runMLSTest $ do - [alice1, bob1] <- traverse createMLSClient users - void $ uploadNewKeyPackage bob1 - (_, qcnv) <- setupMLSGroup alice1 - commit <- createAddCommit alice1 [bob] - welcome <- liftIO $ case mpWelcome commit of - Nothing -> assertFailure "Expected welcome message" - Just w -> pure w - events <- mlsBracket [bob1] $ \wss -> do - es <- sendAndConsumeCommitBundle commit - - WS.assertMatchN_ (5 # Second) wss $ - wsAssertMLSWelcome (cidQualifiedUser bob1) qcnv welcome - - pure es - - event <- assertOne events - liftIO $ assertJoinEvent qcnv alice [bob] roleNameWireMember event - testAddUserWithBundle :: TestM () testAddUserWithBundle = do [alice, bob] <- createAndConnectUsers [Nothing, Nothing] From 354aa6493233917d2b725622d7db2d6a148d155e Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 20 Jun 2023 13:32:23 +0200 Subject: [PATCH 060/225] onMLSMessage: Send single pushes until Ord instance is fixed --- services/galley/src/Galley/API/Federation.hs | 6 ++++-- services/galley/src/Galley/API/MLS/Propagate.hs | 2 +- services/galley/src/Galley/API/MLS/Welcome.hs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 46dc5135991..a29239176b9 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -677,8 +677,10 @@ onMLSMessageSent domain rmm = Event (tUntagged rcnv) (F.rmmSubConversation rmm) (F.rmmSender rmm) (F.rmmTime rmm) $ EdMLSMessage (fromBase64ByteString (F.rmmMessage rmm)) - runMessagePush loc (Just (tUntagged rcnv)) $ - newMessagePush mempty Nothing (F.rmmMetadata rmm) recipients e + -- FUTUREWORK: Send only 1 push, after broken Eq, Ord instances of Recipient is fixed. Find other place via tag [FTRPUSHORD] + for_ recipients $ \(u, c) -> do + runMessagePush loc (Just (tUntagged rcnv)) $ + newMessagePush mempty Nothing (F.rmmMetadata rmm) [(u, c)] e queryGroupInfo :: ( Member ConversationStore r, diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 5160048b6b9..9a8ba3d908a 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -81,7 +81,7 @@ propagateMessage qusr lConvOrSub con msg cm = do sconv = snd (qUnqualified qt) e = Event qcnv sconv qusr now $ EdMLSMessage msg.raw - -- FUTUREWORK: Send only 1 push, after broken Eq, Ord instances of Recipient is fixed + -- FUTUREWORK: Send only 1 push, after broken Eq, Ord instances of Recipient is fixed. Find other place via tag [FTRPUSHORD] for_ (lmems >>= localMemberMLSClients mlsConv) $ \(u, c) -> runMessagePush lConvOrSub (Just qcnv) $ newMessagePush botMap con mm [(u, c)] e diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index 72b4021f671..41ecd040d82 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -87,7 +87,7 @@ sendLocalWelcomes :: Sem r () sendLocalWelcomes qcnv qusr con now welcome lclients = do let e = Event qcnv Nothing qusr now $ EdMLSWelcome welcome.raw - -- FUTUREWORK: Send only 1 push, after broken Eq, Ord instances of Recipient is fixed + -- FUTUREWORK: Send only 1 push, after broken Eq, Ord instances of Recipient is fixed. Find other place via tag [FTRPUSHORD] for_ (tUnqualified lclients) $ \(u, c) -> runMessagePush lclients (Just qcnv) $ newMessagePush mempty con defMessageMetadata [(u, c)] e From 59b399f96079879c5d589122b5669b3328e623c7 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 20 Jun 2023 13:32:51 +0200 Subject: [PATCH 061/225] Adjust tests: welcome is send from committer, not self --- services/brig/test/integration/Federation/End2end.hs | 4 ++-- services/galley/test/integration/API/MLS.hs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index 58355ea46c6..2446d4a88d1 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -902,7 +902,7 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do let e = List1.head (WS.unpackPayload n) ntfTransient n @?= False evtType e @?= MLSWelcome - evtFrom e @?= userQualifiedId alice + evtFrom e @?= userQualifiedId bob evtData e @?= EdMLSWelcome welcome -- verify that alice receives a join event @@ -1107,7 +1107,7 @@ testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 let e = List1.head (WS.unpackPayload n) ntfTransient n @?= False evtType e @?= MLSWelcome - evtFrom e @?= userQualifiedId alice + evtFrom e @?= userQualifiedId bob evtData e @?= EdMLSWelcome welcome -- verify that alice receives a join event diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 76b6538529b..776f8368b26 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -319,9 +319,9 @@ testAddUserWithBundle = do events <- mlsBracket bobClients $ \wss -> do events <- sendAndConsumeCommitBundle commit - for_ (zip bobClients wss) $ \(c, ws) -> + for_ (zip bobClients wss) $ \(_, ws) -> WS.assertMatch (5 # Second) ws $ - wsAssertMLSWelcome (cidQualifiedUser c) qcnv welcome + wsAssertMLSWelcome alice qcnv welcome pure events event <- assertOne events @@ -1689,7 +1689,7 @@ sendRemoteMLSWelcome = do -- check that the corresponding event is received liftIO $ do WS.assertMatch_ (5 # WS.Second) wsB $ - wsAssertMLSWelcome bob qcid welcome + wsAssertMLSWelcome alice qcid welcome testBackendRemoveProposalLocalConvLocalLeaverCreator :: TestM () testBackendRemoveProposalLocalConvLocalLeaverCreator = do From 595ba6630f1142ea3a66b1618ac58db6a69a88fb Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 20 Jun 2023 14:29:39 +0200 Subject: [PATCH 062/225] hi ci From 716661b10e3cc199246024b4f67e321d30d419a1 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Mon, 26 Jun 2023 08:32:16 +0200 Subject: [PATCH 063/225] Added integration test for MLS client removal (#3373) --- integration/test/Test/MLS.hs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index a16df7fa368..108452f0684 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -2,7 +2,7 @@ module Test.MLS where -import API.Brig (claimKeyPackages) +import API.Brig (claimKeyPackages, deleteClient) import API.Galley import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as B8 @@ -304,6 +304,26 @@ testRemoteAddUser = do resp.status `shouldMatchInt` 500 resp.json %. "label" `shouldMatch` "federation-not-implemented" +testRemoteRemoveClient :: HasCallStack => App () +testRemoteRemoveClient = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ uploadNewKeyPackage bob1 + (_, conv) <- createNewGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + withWebSocket alice $ \wsAlice -> do + void $ deleteClient bob bob1.client >>= getBody 200 + let predicate n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" + n <- awaitMatch 5 predicate wsAlice + shouldMatch (nPayload n %. "conversation") (objId conv) + shouldMatch (nPayload n %. "from") (objId bob) + + msg <- asByteString (nPayload n %. "data") >>= showMessage alice1 + let leafIndexBob = 1 + msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob + msg %. "message.content.sender.External" `shouldMatchInt` 0 + testCreateSubConv :: HasCallStack => App () testCreateSubConv = do alice <- randomUser OwnDomain def From 0ac9b077974d62fa13ee19ef2d1249ff76748144 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Tue, 27 Jun 2023 11:29:48 +0200 Subject: [PATCH 064/225] Avoid MLS messages in sender client's event stream (#3379) --- changelog.d/5-internal/dont-return-to-sender | 1 + integration/test/Test/MLS.hs | 28 +++++++++++++++++++ services/galley/src/Galley/API/MLS/Message.hs | 4 +-- .../galley/src/Galley/API/MLS/Propagate.hs | 11 ++++++-- services/galley/src/Galley/API/MLS/Removal.hs | 2 +- services/galley/test/integration/API/MLS.hs | 25 +++++++++-------- 6 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 changelog.d/5-internal/dont-return-to-sender diff --git a/changelog.d/5-internal/dont-return-to-sender b/changelog.d/5-internal/dont-return-to-sender new file mode 100644 index 00000000000..3e3df3c04db --- /dev/null +++ b/changelog.d/5-internal/dont-return-to-sender @@ -0,0 +1 @@ +Avoid including MLS application messages in the sender client's event stream. diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 108452f0684..58a03989457 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -11,6 +11,34 @@ import MLS.Util import SetupHelpers import Testlib.Prelude +testSendMessageNoReturnToSender :: HasCallStack => App () +testSendMessageNoReturnToSender = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + [alice1, alice2, bob1, bob2] <- traverse createMLSClient [alice, alice, bob, bob] + traverse_ uploadNewKeyPackage [alice2, bob1, bob2] + void $ createNewGroup alice1 + void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle + + -- alice1 sends a message to the conversation, all clients but alice1 receive + -- the message + withWebSockets [alice1, alice2, bob1, bob2] $ \(wsSender : wss) -> do + mp <- createApplicationMessage alice1 "hello, bob" + void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + resp.status `shouldMatchInt` 201 + for_ wss $ \ws -> do + n <- awaitMatch 3 (\n -> nPayload n %. "type" `isEqual` "conversation.mls-message-add") ws + nPayload n %. "data" `shouldMatch` T.decodeUtf8 (Base64.encode mp.message) + expectFailure (const $ pure ()) $ + awaitMatch + 3 + ( \n -> + liftM2 + (&&) + (nPayload n %. "type" `isEqual` "conversation.mls-message-add") + (nPayload n %. "data" `isEqual` T.decodeUtf8 (Base64.encode mp.message)) + ) + wsSender + testMixedProtocolUpgrade :: HasCallStack => Domain -> App () testMixedProtocolUpgrade secondDomain = do (alice, tid) <- createTeam OwnDomain diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index c0e6aef628d..6421f62cdc7 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -232,7 +232,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle lConvOrSubId = do storeGroupInfo (tUnqualified lConvOrSub).id bundle.groupInfo - propagateMessage qusr lConvOrSub conn bundle.rawMessage (tUnqualified lConvOrSub).members + propagateMessage qusr (Just c) lConvOrSub conn bundle.rawMessage (tUnqualified lConvOrSub).members >>= mapM_ throwUnreachableUsers for_ bundle.welcome $ \welcome -> @@ -375,7 +375,7 @@ postMLSMessageToLocalConv qusr c con msg convOrSubId = do when ((tUnqualified lConvOrSub).migrationState == MLSMigrationMixed) $ throwS @'MLSUnsupportedMessage - unreachables <- propagateMessage qusr lConvOrSub con msg.rawMessage (tUnqualified lConvOrSub).members + unreachables <- propagateMessage qusr (Just c) lConvOrSub con msg.rawMessage (tUnqualified lConvOrSub).members pure ([], unreachables) postMLSMessageToRemoteConv :: diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 9a8ba3d908a..cd43d1759d0 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -43,6 +43,7 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.MLS.Credential import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation @@ -50,6 +51,8 @@ import Wire.API.Message import Wire.API.Unreachable -- | Propagate a message. +-- The message will not be propagated to the sender client if provided. This is +-- a requirement from Core Crypto and the clients. propagateMessage :: ( Member ExternalAccess r, Member FederatorAccess r, @@ -58,12 +61,13 @@ propagateMessage :: Member TinyLog r ) => Qualified UserId -> + Maybe ClientId -> Local ConvOrSubConv -> Maybe ConnId -> RawMLS Message -> ClientMap -> Sem r (Maybe UnreachableUsers) -propagateMessage qusr lConvOrSub con msg cm = do +propagateMessage qusr mSenderClient lConvOrSub con msg cm = do now <- input @UTCTime let mlsConv = (.conv) <$> lConvOrSub lmems = mcLocalMembers . tUnqualified $ mlsConv @@ -102,13 +106,14 @@ propagateMessage qusr lConvOrSub con msg cm = do rmmMessage = Base64ByteString msg.raw } where + cmWithoutSender = maybe cm (flip cmRemoveClient cm . mkClientIdentity qusr) mSenderClient localMemberMLSClients :: Local x -> LocalMember -> [(UserId, ClientId)] localMemberMLSClients loc lm = let localUserQId = tUntagged (qualifyAs loc localUserId) localUserId = lmId lm in map (\(c, _) -> (localUserId, c)) - (Map.assocs (Map.findWithDefault mempty localUserQId cm)) + (Map.assocs (Map.findWithDefault mempty localUserQId cmWithoutSender)) remoteMemberMLSClients :: RemoteMember -> [(UserId, ClientId)] remoteMemberMLSClients rm = @@ -116,7 +121,7 @@ propagateMessage qusr lConvOrSub con msg cm = do remoteUserId = qUnqualified remoteUserQId in map (\(c, _) -> (remoteUserId, c)) - (Map.assocs (Map.findWithDefault mempty remoteUserQId cm)) + (Map.assocs (Map.findWithDefault mempty remoteUserQId cmWithoutSender)) remotesToQIds = fmap (tUntagged . rmId) diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 9477082c31a..557b4bc9120 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -101,7 +101,7 @@ createAndSendRemoveProposals lConvOrSubConv indices qusr cm = do (publicMessageRef (cnvmlsCipherSuite meta) pmsg) ProposalOriginBackend proposal - propagateMessage qusr lConvOrSubConv Nothing msg cm + propagateMessage qusr Nothing lConvOrSubConv Nothing msg cm removeClientsWithClientMapRecursively :: ( Members diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 776f8368b26..4f25b5e314a 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -967,9 +967,10 @@ testAppMessage = do mlsBracket clients $ \wss -> do (events, _) <- sendAndConsumeMessage message liftIO $ events @?= [] - liftIO $ - WS.assertMatchN_ (5 # WS.Second) wss $ + liftIO $ do + WS.assertMatchN_ (5 # WS.Second) (tail wss) $ wsAssertMLSMessage (fmap Conv qcnv) alice (mpMessage message) + WS.assertNoEvent (2 # WS.Second) [head wss] testAppMessage2 :: TestM () testAppMessage2 = do @@ -992,15 +993,16 @@ testAppMessage2 = do message <- createApplicationMessage bob1 "some text" - mlsBracket (alice1 : clients) $ \wss -> do + mlsBracket (alice1 : clients) $ \[wsAlice1, wsBob1, wsBob2, wsCharlie1] -> do (events, _) <- sendAndConsumeMessage message liftIO $ events @?= [] - -- check that the corresponding event is received - - liftIO $ - WS.assertMatchN_ (5 # WS.Second) wss $ + -- check that the corresponding event is received by everyone except bob1 + -- (the sender) and no message is received by bob1 + liftIO $ do + WS.assertMatchN_ (5 # WS.Second) [wsAlice1, wsBob2, wsCharlie1] $ wsAssertMLSMessage (fmap Conv conversation) bob (mpMessage message) + WS.assertNoEvent (2 # WS.Second) [wsBob1] testAppMessageSomeReachable :: TestM () testAppMessageSomeReachable = do @@ -1829,7 +1831,7 @@ testBackendRemoveProposalLocalConvLocalClient = do WS.assertMatch_ (5 # WS.Second) wsB $ wsAssertClientRemoved (ciClient bob1) - msg <- WS.assertMatch (5 # WS.Second) wsA $ \notification -> do + (msg : _) <- WS.assertMatchN (5 # WS.Second) [wsA, wsC] $ \notification -> do wsAssertBackendRemoveProposal bob (Conv <$> qcnv) idxBob1 notification for_ [alice1, bob2, charlie1] $ @@ -1838,8 +1840,9 @@ testBackendRemoveProposalLocalConvLocalClient = do mp <- createPendingProposalCommit charlie1 events <- sendAndConsumeCommitBundle mp liftIO $ events @?= [] - WS.assertMatchN_ (5 # WS.Second) [wsA, wsC] $ \n -> do + WS.assertMatchN_ (5 # WS.Second) [wsA] $ \n -> do wsAssertMLSMessage (Conv <$> qcnv) charlie (mpMessage mp) n + WS.assertNoEvent (2 # WS.Second) [wsC] testBackendRemoveProposalLocalConvRemoteClient :: TestM () testBackendRemoveProposalLocalConvRemoteClient = do @@ -2700,7 +2703,7 @@ testLeaveSubConv isSubConvCreator = do -- a member commits the pending proposal do leaveCommit <- createPendingProposalCommit (head others) - mlsBracket (firstLeaver : others) $ \(wsLeaver : wss) -> do + mlsBracket (firstLeaver : tail others) $ \(wsLeaver : wss) -> do events <- fst <$$> withTempMockFederator' ("on-mls-message-sent" ~> RemoteMLSMessageOk) @@ -2713,7 +2716,7 @@ testLeaveSubConv isSubConvCreator = do -- send an application message do message <- createApplicationMessage (head others) "some text" - mlsBracket (firstLeaver : others) $ \(wsLeaver : wss) -> do + mlsBracket (firstLeaver : tail others) $ \(wsLeaver : wss) -> do (events, _) <- sendAndConsumeMessage message liftIO $ events @?= [] WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do From 3607c112eec48139a22123d44ad55cce603d8b4c Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 29 Jun 2023 14:21:54 +0200 Subject: [PATCH 065/225] Add supportedProtocols field to mls feature config (#3374) --- .../0-release-notes/supported-protocols | 10 +++ charts/galley/values.yaml | 1 + .../src/Wire/API/Conversation/Protocol.hs | 2 +- libs/wire-api/src/Wire/API/Error/Galley.hs | 4 ++ libs/wire-api/src/Wire/API/Team/Feature.hs | 14 +++- services/galley/galley.cabal | 1 + services/galley/schema/src/Run.hs | 4 +- .../src/V85_TeamFeatureSupportedProtocols.hs | 33 +++++++++ .../galley/src/Galley/API/Teams/Features.hs | 24 ++++++- services/galley/src/Galley/App.hs | 9 +++ services/galley/src/Galley/Cassandra.hs | 2 +- .../src/Galley/Cassandra/TeamFeatures.hs | 16 +++-- .../test/integration/API/Teams/Feature.hs | 69 +++++++++++++------ 13 files changed, 154 insertions(+), 35 deletions(-) create mode 100644 changelog.d/0-release-notes/supported-protocols create mode 100644 services/galley/schema/src/V85_TeamFeatureSupportedProtocols.hs diff --git a/changelog.d/0-release-notes/supported-protocols b/changelog.d/0-release-notes/supported-protocols new file mode 100644 index 00000000000..0de4e14e8af --- /dev/null +++ b/changelog.d/0-release-notes/supported-protocols @@ -0,0 +1,10 @@ +New field for Supported protocols in Galley's MLS feature config + +Galley will refuse to start if the list `supportedProtocols` does not contain +the value of the field `defaultProtocol`. Galley will also refuse to start if +MLS migration is enabled and MLS is not part of `supportedProtocols`. + +The default value for `supportedProtocols` is: +``` +[proteus, mls] +``` diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 31a18e695e1..ecdc71645eb 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -71,6 +71,7 @@ config: defaultProtocol: proteus allowedCipherSuites: [1] defaultCipherSuite: 1 + supportedProtocols: [proteus, mls] # must contain defaultProtocol searchVisibilityInbound: defaults: status: disabled diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index efd1205095b..d99795d197a 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -50,7 +50,7 @@ import Wire.API.MLS.SubConversation import Wire.Arbitrary data ProtocolTag = ProtocolProteusTag | ProtocolMLSTag | ProtocolMixedTag - deriving stock (Eq, Show, Enum, Bounded, Generic) + deriving stock (Eq, Show, Enum, Ord, Bounded, Generic) deriving (Arbitrary) via GenericUniform ProtocolTag data ConversationMLSData = ConversationMLSData diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index fc58cd7d569..1b56146c6d4 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -343,6 +343,7 @@ data TeamFeatureError | LegalHoldWhitelistedOnly | DisableSsoNotImplemented | FeatureLocked + | MLSProtocolMismatch instance IsSwaggerError TeamFeatureError where -- Do not display in Swagger @@ -372,6 +373,8 @@ type instance type instance MapError 'FeatureLocked = 'StaticError 409 "feature-locked" "Feature config cannot be updated (e.g. because it is configured to be locked, or because you need to upgrade your plan)" +type instance MapError 'MLSProtocolMismatch = 'StaticError 400 "mls-protocol-mismatch" "The default protocol needs to be part of the supported protocols" + type instance ErrorEffect TeamFeatureError = Error TeamFeatureError instance Member (Error DynError) r => ServerEffect (Error TeamFeatureError) r where @@ -381,6 +384,7 @@ instance Member (Error DynError) r => ServerEffect (Error TeamFeatureError) r wh LegalHoldWhitelistedOnly -> dynError @(MapError 'LegalHoldWhitelistedOnly) DisableSsoNotImplemented -> dynError @(MapError 'DisableSsoNotImplemented) FeatureLocked -> dynError @(MapError 'FeatureLocked) + MLSProtocolMismatch -> dynError @(MapError 'MLSProtocolMismatch) -------------------------------------------------------------------------------- -- Proposal failure diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 7175f6b5785..519db1f1b94 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -114,7 +114,7 @@ import Imports import Servant (FromHttpApiData (..), ToHttpApiData (..)) import Test.QuickCheck.Arbitrary (arbitrary) import Test.QuickCheck.Gen (suchThat) -import Wire.API.Conversation.Protocol (ProtocolTag (ProtocolProteusTag)) +import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite (CipherSuiteTag (MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519)) import Wire.API.Routes.Named (RenderableSymbol (renderSymbol)) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -912,7 +912,8 @@ data MLSConfig = MLSConfig { mlsProtocolToggleUsers :: [UserId], mlsDefaultProtocol :: ProtocolTag, mlsAllowedCipherSuites :: [CipherSuiteTag], - mlsDefaultCipherSuite :: CipherSuiteTag + mlsDefaultCipherSuite :: CipherSuiteTag, + mlsSupportedProtocols :: [ProtocolTag] } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform MLSConfig) @@ -928,11 +929,18 @@ instance ToSchema MLSConfig where <*> mlsDefaultProtocol .= field "defaultProtocol" schema <*> mlsAllowedCipherSuites .= field "allowedCipherSuites" (array schema) <*> mlsDefaultCipherSuite .= field "defaultCipherSuite" schema + <*> mlsSupportedProtocols .= field "supportedProtocols" (array schema) instance IsFeatureConfig MLSConfig where type FeatureSymbol MLSConfig = "mls" defFeatureStatus = - let config = MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + let config = + MLSConfig + [] + ProtocolProteusTag + [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] + MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + [ProtocolProteusTag, ProtocolMLSTag] in withStatus FeatureStatusDisabled LockStatusUnlocked config FeatureTTLUnlimited featureSingleton = FeatureSingletonMLSConfig objectSchema = field "config" schema diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 2a70d727dd7..05690a27512 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -595,6 +595,7 @@ executable galley-schema V82_MLSSubconversation V83_MLSDraft17 V84_TeamFeatureMlsMigration + V85_TeamFeatureSupportedProtocols hs-source-dirs: schema/src default-extensions: TemplateHaskell diff --git a/services/galley/schema/src/Run.hs b/services/galley/schema/src/Run.hs index adff6cd4e70..f8f41d3b13b 100644 --- a/services/galley/schema/src/Run.hs +++ b/services/galley/schema/src/Run.hs @@ -87,6 +87,7 @@ import qualified V81_TeamFeatureMlsE2EIdUpdate import qualified V82_MLSSubconversation import qualified V83_MLSDraft17 import qualified V84_TeamFeatureMlsMigration +import qualified V85_TeamFeatureSupportedProtocols main :: IO () main = do @@ -159,7 +160,8 @@ main = do V81_TeamFeatureMlsE2EIdUpdate.migration, V82_MLSSubconversation.migration, V83_MLSDraft17.migration, - V84_TeamFeatureMlsMigration.migration + V84_TeamFeatureMlsMigration.migration, + V85_TeamFeatureSupportedProtocols.migration -- When adding migrations here, don't forget to update -- 'schemaVersion' in Galley.Cassandra -- (see also docs/developer/cassandra-interaction.md) diff --git a/services/galley/schema/src/V85_TeamFeatureSupportedProtocols.hs b/services/galley/schema/src/V85_TeamFeatureSupportedProtocols.hs new file mode 100644 index 00000000000..204799085d2 --- /dev/null +++ b/services/galley/schema/src/V85_TeamFeatureSupportedProtocols.hs @@ -0,0 +1,33 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 V85_TeamFeatureSupportedProtocols + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = Migration 85 "Add feature config for supported protocols" $ do + schema' + [r| ALTER TABLE team_features ADD ( + mls_supported_protocols set + ) + |] diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 91740d36a77..d9ea2fcee36 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -360,7 +360,18 @@ instance SetFeatureConfig SearchVisibilityInboundConfig where updateSearchVisibilityInbound $ toTeamStatus tid wsnl persistAndPushEvent tid wsnl -instance SetFeatureConfig MLSConfig +instance SetFeatureConfig MLSConfig where + type SetConfigForTeamConstraints MLSConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) + setConfigForTeam tid wsnl = do + mlsMigrationConfig <- getConfigForTeam @MlsMigrationConfig tid + unless + ( -- default protocol needs to be included in supported protocols + mlsDefaultProtocol (wssConfig wsnl) `elem` mlsSupportedProtocols (wssConfig wsnl) + -- when MLS migration is enabled, MLS needs to be enabled as well + && (wsStatus mlsMigrationConfig == FeatureStatusDisabled || wssStatus wsnl == FeatureStatusEnabled) + ) + $ throw MLSProtocolMismatch + persistAndPushEvent tid wsnl instance SetFeatureConfig ExposeInvitationURLsToTeamAdminConfig @@ -368,4 +379,13 @@ instance SetFeatureConfig OutlookCalIntegrationConfig instance SetFeatureConfig MlsE2EIdConfig -instance SetFeatureConfig MlsMigrationConfig +instance SetFeatureConfig MlsMigrationConfig where + type SetConfigForTeamConstraints MlsMigrationConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) + setConfigForTeam tid wsnl = do + mlsConfig <- getConfigForTeam @MlsMigrationConfig tid + unless + ( -- when MLS migration is enabled, MLS needs to be enabled as well + wssStatus wsnl == FeatureStatusDisabled || wsStatus mlsConfig == FeatureStatusEnabled + ) + $ throw MLSProtocolMismatch + persistAndPushEvent tid wsnl diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 7063260f2c3..b1ad25bc87f 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -107,9 +107,11 @@ import System.Logger.Class import qualified System.Logger.Extended as Logger import qualified UnliftIO.Exception as UnliftIO import Util.Options +import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Federation.Error import Wire.API.Routes.FederationDomainConfig +import Wire.API.Team.Feature import qualified Wire.Sem.Logger import Wire.Sem.Random.IO @@ -157,6 +159,13 @@ validateOptions l o = do (Nothing, Just _) -> error "RabbitMQ config is specified and federator is not, please specify both or none" (Just _, Nothing) -> error "Federator is specified and RabbitMQ config is not, please specify both or none" _ -> pure () + let mlsFlag = o ^. optSettings . setFeatureFlags . Teams.flagMLS . Teams.unDefaults . Teams.unImplicitLockStatus + mlsConfig = wsConfig mlsFlag + migrationStatus = wsStatus $ o ^. optSettings . setFeatureFlags . Teams.flagMlsMigration . Teams.unDefaults + when (migrationStatus == FeatureStatusEnabled && ProtocolMLSTag `notElem` mlsSupportedProtocols mlsConfig) $ + error "For starting MLS migration, MLS must be included in the supportedProtocol list" + unless (mlsDefaultProtocol mlsConfig `elem` mlsSupportedProtocols mlsConfig) $ + error "The list 'settings.featureFlags.mls.supportedProtocols' must include the value in the field 'settings.featureFlags.mls.defaultProtocol'" createEnv :: Metrics -> Opts -> Logger -> IORef FederationDomainConfigs -> IO Env createEnv m o l r = do diff --git a/services/galley/src/Galley/Cassandra.hs b/services/galley/src/Galley/Cassandra.hs index 12a69f0e8d6..92c9bf28e6f 100644 --- a/services/galley/src/Galley/Cassandra.hs +++ b/services/galley/src/Galley/Cassandra.hs @@ -20,4 +20,4 @@ module Galley.Cassandra (schemaVersion) where import Imports schemaVersion :: Int32 -schemaVersion = 84 +schemaVersion = 85 diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index a171f68215e..a1048604fd4 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -105,7 +105,7 @@ getFeatureConfig FeatureSingletonMLSConfig tid = do m <- retry x1 $ query1 select (params LocalQuorum (Identity tid)) pure $ case m of Nothing -> Nothing - Just (status, defaultProtocol, protocolToggleUsers, allowedCipherSuites, defaultCipherSuite) -> + Just (status, defaultProtocol, protocolToggleUsers, allowedCipherSuites, defaultCipherSuite, supportedProtocols) -> WithStatusNoLock <$> status <*> ( MLSConfig @@ -113,13 +113,14 @@ getFeatureConfig FeatureSingletonMLSConfig tid = do <*> defaultProtocol <*> maybe (Just []) (Just . C.fromSet) allowedCipherSuites <*> defaultCipherSuite + <*> maybe (Just []) (Just . C.fromSet) supportedProtocols ) <*> Just FeatureTTLUnlimited where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe ProtocolTag, Maybe (C.Set UserId), Maybe (C.Set CipherSuiteTag), Maybe CipherSuiteTag) + select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe ProtocolTag, Maybe (C.Set UserId), Maybe (C.Set CipherSuiteTag), Maybe CipherSuiteTag, Maybe (C.Set ProtocolTag)) select = "select mls_status, mls_default_protocol, mls_protocol_toggle_users, mls_allowed_ciphersuites, \ - \mls_default_ciphersuite from team_features where team_id = ?" + \mls_default_ciphersuite, mls_supported_protocols from team_features where team_id = ?" getFeatureConfig FeatureSingletonMlsE2EIdConfig tid = do let q = query1 select (params LocalQuorum (Identity tid)) retry x1 q <&> \case @@ -205,7 +206,7 @@ setFeatureConfig FeatureSingletonSndFactorPasswordChallengeConfig tid statusNoLo setFeatureConfig FeatureSingletonSearchVisibilityInboundConfig tid statusNoLock = setFeatureStatusC "search_visibility_status" tid (wssStatus statusNoLock) setFeatureConfig FeatureSingletonMLSConfig tid statusNoLock = do let status = wssStatus statusNoLock - let MLSConfig protocolToggleUsers defaultProtocol allowedCipherSuites defaultCipherSuite = wssConfig statusNoLock + let MLSConfig protocolToggleUsers defaultProtocol allowedCipherSuites defaultCipherSuite supportedProtocols = wssConfig statusNoLock retry x5 $ write insert @@ -216,14 +217,15 @@ setFeatureConfig FeatureSingletonMLSConfig tid statusNoLock = do defaultProtocol, C.Set protocolToggleUsers, C.Set allowedCipherSuites, - defaultCipherSuite + defaultCipherSuite, + C.Set supportedProtocols ) ) where - insert :: PrepQuery W (TeamId, FeatureStatus, ProtocolTag, C.Set UserId, C.Set CipherSuiteTag, CipherSuiteTag) () + insert :: PrepQuery W (TeamId, FeatureStatus, ProtocolTag, C.Set UserId, C.Set CipherSuiteTag, CipherSuiteTag, C.Set ProtocolTag) () insert = "insert into team_features (team_id, mls_status, mls_default_protocol, \ - \mls_protocol_toggle_users, mls_allowed_ciphersuites, mls_default_ciphersuite) values (?, ?, ?, ?, ?, ?)" + \mls_protocol_toggle_users, mls_allowed_ciphersuites, mls_default_ciphersuite, mls_supported_protocols) values (?, ?, ?, ?, ?, ?, ?)" setFeatureConfig FeatureSingletonMlsE2EIdConfig tid status = do let statusValue = wssStatus status vex = verificationExpiration . wssConfig $ status diff --git a/services/galley/test/integration/API/Teams/Feature.hs b/services/galley/test/integration/API/Teams/Feature.hs index 98f5bea53af..84381c303d1 100644 --- a/services/galley/test/integration/API/Teams/Feature.hs +++ b/services/galley/test/integration/API/Teams/Feature.hs @@ -53,7 +53,7 @@ import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit (assertBool, assertFailure, (@?=)) import TestHelpers (eventually, test) import TestSetup -import Wire.API.Conversation.Protocol (ProtocolTag (ProtocolMLSTag, ProtocolProteusTag)) +import Wire.API.Conversation.Protocol import qualified Wire.API.Event.FeatureConfig as FeatureConfig import Wire.API.Internal.Notification (Notification) import Wire.API.MLS.CipherSuite @@ -137,6 +137,7 @@ tests s = ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + [ProtocolProteusTag, ProtocolMLSTag] ) validMLSConfigGen, test s (unpack $ featureNameBS @FileSharingConfig) $ @@ -163,12 +164,17 @@ tests s = validMLSConfigGen :: Gen (WithStatusPatch MLSConfig) validMLSConfigGen = arbitrary - `suchThat` ( \cfg -> case wspConfig cfg of - Just (MLSConfig us _ cTags ctag) -> - sortedAndNoDuplicates us - && sortedAndNoDuplicates cTags - && elem ctag cTags - _ -> True + `suchThat` ( \cfg -> + case wspConfig cfg of + Just (MLSConfig us defProtocol cTags ctag supProtocol) -> + sortedAndNoDuplicates us + && sortedAndNoDuplicates cTags + && elem ctag cTags + && notElem ProtocolMixedTag supProtocol + && elem defProtocol supProtocol + && sortedAndNoDuplicates supProtocol + _ -> True + && Just FeatureStatusEnabled == wspStatus cfg ) where sortedAndNoDuplicates xs = (sort . nub) xs == xs @@ -1046,7 +1052,7 @@ testAllFeatures = do afcSelfDeletingMessages = withStatus FeatureStatusEnabled lockStateSelfDeleting (SelfDeletingMessagesConfig 0) FeatureTTLUnlimited, afcGuestLink = withStatus FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig FeatureTTLUnlimited, afcSndFactorPasswordChallenge = withStatus FeatureStatusDisabled LockStatusLocked SndFactorPasswordChallengeConfig FeatureTTLUnlimited, - afcMLS = withStatus FeatureStatusDisabled LockStatusUnlocked (MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) FeatureTTLUnlimited, + afcMLS = withStatus FeatureStatusDisabled LockStatusUnlocked (MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 [ProtocolProteusTag, ProtocolMLSTag]) FeatureTTLUnlimited, afcSearchVisibilityInboundConfig = withStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityInboundConfig FeatureTTLUnlimited, afcExposeInvitationURLsToTeamAdmin = withStatus FeatureStatusDisabled LockStatusLocked ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited, afcOutlookCalIntegration = withStatus FeatureStatusDisabled LockStatusLocked OutlookCalIntegrationConfig FeatureTTLUnlimited, @@ -1238,31 +1244,42 @@ testMLS = do getForTeamInternal expected getForUser expected - setForTeam :: HasCallStack => WithStatusNoLock MLSConfig -> TestM () - setForTeam wsnl = + setForTeamWithStatusCode :: HasCallStack => Int -> WithStatusNoLock MLSConfig -> TestM () + setForTeamWithStatusCode resStatusCode wsnl = putTeamFeatureFlagWithGalley @MLSConfig galley owner tid wsnl !!! statusCode - === const 200 + === const resStatusCode + + setForTeam :: HasCallStack => WithStatusNoLock MLSConfig -> TestM () + setForTeam = setForTeamWithStatusCode 200 + + setForTeamInternalWithStatusCode :: HasCallStack => (Request -> Request) -> WithStatusNoLock MLSConfig -> TestM () + setForTeamInternalWithStatusCode expect wsnl = + void $ putTeamFeatureFlagInternal @MLSConfig expect tid wsnl setForTeamInternal :: HasCallStack => WithStatusNoLock MLSConfig -> TestM () - setForTeamInternal wsnl = - void $ putTeamFeatureFlagInternal @MLSConfig expect2xx tid wsnl + setForTeamInternal = setForTeamInternalWithStatusCode expect2xx let cipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - let defaultConfig = + defaultConfig = WithStatusNoLock FeatureStatusDisabled - (MLSConfig [] ProtocolProteusTag [cipherSuite] cipherSuite) + (MLSConfig [] ProtocolProteusTag [cipherSuite] cipherSuite [ProtocolProteusTag, ProtocolMLSTag]) FeatureTTLUnlimited - let config2 = + config2 = WithStatusNoLock FeatureStatusEnabled - (MLSConfig [member] ProtocolMLSTag [] cipherSuite) + (MLSConfig [member] ProtocolMLSTag [] cipherSuite [ProtocolProteusTag, ProtocolMLSTag]) FeatureTTLUnlimited - let config3 = + config3 = WithStatusNoLock - FeatureStatusDisabled - (MLSConfig [] ProtocolMLSTag [cipherSuite] cipherSuite) + FeatureStatusEnabled + (MLSConfig [] ProtocolMLSTag [cipherSuite] cipherSuite [ProtocolMLSTag]) + FeatureTTLUnlimited + invalidConfig = + WithStatusNoLock + FeatureStatusEnabled + (MLSConfig [] ProtocolMLSTag [cipherSuite] cipherSuite [ProtocolProteusTag]) FeatureTTLUnlimited getViaEndpoints defaultConfig @@ -1274,6 +1291,12 @@ testMLS = do wsAssertFeatureConfigUpdate @MLSConfig config2 LockStatusUnlocked getViaEndpoints config2 + WS.bracketR cannon member $ \ws -> do + setForTeamWithStatusCode 400 invalidConfig + void . liftIO $ + WS.assertNoEvent (2 # Second) [ws] + getViaEndpoints config2 + WS.bracketR cannon member $ \ws -> do setForTeamInternal config3 void . liftIO $ @@ -1281,6 +1304,12 @@ testMLS = do wsAssertFeatureConfigUpdate @MLSConfig config3 LockStatusUnlocked getViaEndpoints config3 + WS.bracketR cannon member $ \ws -> do + setForTeamInternalWithStatusCode expect4xx invalidConfig + void . liftIO $ + WS.assertNoEvent (2 # Second) [ws] + getViaEndpoints config3 + testExposeInvitationURLsToTeamAdminTeamIdInAllowList :: TestM () testExposeInvitationURLsToTeamAdminTeamIdInAllowList = do owner <- randomUser From 756d83f86aa52d434056a040854f4cc0eb62f4a2 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 4 Jul 2023 08:56:15 +0200 Subject: [PATCH 066/225] WPB-1928: Add members to MLS one2one conversations (#3360) * Add test for happy path * Rename `meta` field of ConvOrSubConv to `mlsMeta` * Make creator field optional in conv metadata * Create MLS 1-1 conversation on commit * Fix connection check for MLS 1-1 conversations * Merge BaseProtocolTag and ProtocolCreateTag * Return empty MLS 1-1 conversation on GET * Check that subconv group ID matches message * Check welcome message on first 1-1 commit --------- Co-authored-by: Paolo Capriotti --- changelog.d/2-features/mls-one-to-one | 1 + integration/test/SetupHelpers.hs | 17 ++++ integration/test/Test/MLS/One2One.hs | 59 +++++++++++- integration/test/Testlib/Assertions.hs | 3 + .../Network/Wire/Client/API/Conversation.hs | 3 +- libs/wire-api/src/Wire/API/Conversation.hs | 44 +++------ libs/wire-api/src/Wire/API/User.hs | 6 ++ .../ConversationList_20Conversation_user.hs | 2 +- .../API/Golden/Generated/Conversation_user.hs | 4 +- .../Wire/API/Golden/Generated/Event_user.hs | 2 +- .../Wire/API/Golden/Generated/NewConv_user.hs | 5 +- .../Golden/Manual/ConversationsResponse.hs | 4 +- services/brig/test/integration/API/OAuth.hs | 4 +- .../brig/test/integration/API/Provider.hs | 2 +- .../brig/test/integration/API/Team/Util.hs | 2 +- .../test/integration/Federation/End2end.hs | 2 +- services/brig/test/integration/Util.hs | 4 +- services/galley/src/Galley/API/Action.hs | 10 +- services/galley/src/Galley/API/Create.hs | 28 +++--- services/galley/src/Galley/API/Federation.hs | 8 +- .../galley/src/Galley/API/MLS/Commit/Core.hs | 6 +- .../Galley/API/MLS/Commit/ExternalCommit.hs | 12 +-- .../Galley/API/MLS/Commit/InternalCommit.hs | 87 +++++++++++++----- .../galley/src/Galley/API/MLS/Conversation.hs | 18 ++++ services/galley/src/Galley/API/MLS/Message.hs | 92 +++++++++++++------ services/galley/src/Galley/API/MLS/One2One.hs | 57 ++++++++---- .../galley/src/Galley/API/MLS/Proposal.hs | 2 +- services/galley/src/Galley/API/MLS/Removal.hs | 2 +- services/galley/src/Galley/API/MLS/Types.hs | 5 +- services/galley/src/Galley/API/MLS/Util.hs | 11 ++- services/galley/src/Galley/API/One2One.hs | 4 +- services/galley/src/Galley/API/Query.hs | 7 +- services/galley/src/Galley/API/Util.hs | 22 ++--- .../src/Galley/Cassandra/Conversation.hs | 17 ++-- .../galley/src/Galley/Cassandra/Queries.hs | 4 +- .../galley/src/Galley/Data/Conversation.hs | 2 +- .../src/Galley/Data/Conversation/Types.hs | 3 +- services/galley/test/integration/API.hs | 26 +++--- .../galley/test/integration/API/Federation.hs | 2 +- services/galley/test/integration/API/MLS.hs | 2 +- .../test/integration/API/MessageTimer.hs | 10 +- services/galley/test/integration/API/Roles.hs | 4 +- services/galley/test/integration/API/Util.hs | 22 ++--- 43 files changed, 413 insertions(+), 214 deletions(-) create mode 100644 changelog.d/2-features/mls-one-to-one diff --git a/changelog.d/2-features/mls-one-to-one b/changelog.d/2-features/mls-one-to-one new file mode 100644 index 00000000000..2f2603aa07b --- /dev/null +++ b/changelog.d/2-features/mls-one-to-one @@ -0,0 +1 @@ +Added support for MSL 1-1 conversations diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index b526b53a961..c13c0b11ddb 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -110,3 +110,20 @@ resetFedConns owndom = do rawlist <- resp.json %. "remotes" & asList (asString . (%. "domain")) `mapM` rawlist deleteFedConn' owndom `mapM_` rdoms + +-- | Create a user on the given domain, such that the 1-1 conversation with +-- 'other' resides on 'convDomain'. This connects the two users as a side-effect. +createMLSOne2OnePartner :: MakesValue user => Domain -> user -> Domain -> App Value +createMLSOne2OnePartner domain other convDomain = loop + where + loop = do + u <- randomUser domain def + connectUsers2 u other + conv <- getMLSOne2OneConversation other u >>= getJSON 200 + + desiredConvDomain <- make convDomain & asString + actualConvDomain <- conv %. "qualified_id.domain" & asString + + if desiredConvDomain == actualConvDomain + then pure u + else loop diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index f7fa4ac4c60..d3894155c76 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -1,6 +1,9 @@ module Test.MLS.One2One where import API.Galley +import qualified Data.ByteString.Base64 as Base64 +import qualified Data.ByteString.Char8 as B8 +import MLS.Util import SetupHelpers import Testlib.Prelude @@ -9,12 +12,8 @@ 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") + shouldBeEmpty (conv %. "members.others") conv %. "members.self.conversation_role" `shouldMatch` "wire_member" conv %. "members.self.qualified_id" `shouldMatch` (alice %. "qualified_id") @@ -42,3 +41,53 @@ testGetMLSOne2OneSameTeam = do (alice, _) <- createTeam OwnDomain bob <- addUserToTeam alice void $ getMLSOne2OneConversation alice bob >>= getJSON 200 + +data One2OneScenario + = -- | Both users are local + One2OneScenarioLocal + | -- | One user is remote, conversation is local + One2OneScenarioLocalConv + | -- | One user is remote, conversation is remote + One2OneScenarioRemoteConv + +instance HasTests x => HasTests (One2OneScenario -> x) where + mkTests m n s f x = + mkTests m (n <> "[domain=own]") s f (x One2OneScenarioLocal) + <> mkTests m (n <> "[domain=other;conv=own]") s f (x One2OneScenarioLocalConv) + <> mkTests m (n <> "[domain=other;conv=other]") s f (x One2OneScenarioRemoteConv) + +one2OneScenarioDomain :: One2OneScenario -> Domain +one2OneScenarioDomain One2OneScenarioLocal = OwnDomain +one2OneScenarioDomain _ = OtherDomain + +one2OneScenarioConvDomain :: One2OneScenario -> Domain +one2OneScenarioConvDomain One2OneScenarioLocal = OwnDomain +one2OneScenarioConvDomain One2OneScenarioLocalConv = OwnDomain +one2OneScenarioConvDomain One2OneScenarioRemoteConv = OtherDomain + +testMLSOne2One :: HasCallStack => One2OneScenario -> App () +testMLSOne2One scenario = do + alice <- randomUser OwnDomain def + let otherDomain = one2OneScenarioDomain scenario + convDomain = one2OneScenarioConvDomain scenario + bob <- createMLSOne2OnePartner otherDomain alice convDomain + [alice1, bob1] <- traverse createMLSClient [alice, bob] + traverse_ uploadNewKeyPackage [bob1] + + conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetGroup alice1 conv + + commit <- createAddCommit alice1 [bob] + withWebSocket bob1 $ \ws -> do + void $ sendAndConsumeCommitBundle commit + + let isWelcome n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" + n <- awaitMatch 3 isWelcome ws + nPayload n %. "data" `shouldMatch` B8.unpack (Base64.encode (fold commit.welcome)) + + withWebSocket bob1 $ \ws -> do + mp <- createApplicationMessage alice1 "hello, world" + void $ sendAndConsumeMessage mp + let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" + n <- awaitMatch 3 isMessage ws + nPayload n %. "data" `shouldMatch` B8.unpack (Base64.encode mp.message) diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index 323a2b4c470..e72532f0887 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -108,6 +108,9 @@ shouldMatchSet a b = do lb <- fmap sort (asList b) la `shouldMatch` lb +shouldBeEmpty :: (MakesValue a, HasCallStack) => a -> App () +shouldBeEmpty a = a `shouldMatch` (mempty :: [Value]) + shouldContainString :: HasCallStack => -- | The actual value diff --git a/libs/api-client/src/Network/Wire/Client/API/Conversation.hs b/libs/api-client/src/Network/Wire/Client/API/Conversation.hs index 280d096dc53..64cc81ef35c 100644 --- a/libs/api-client/src/Network/Wire/Client/API/Conversation.hs +++ b/libs/api-client/src/Network/Wire/Client/API/Conversation.hs @@ -48,6 +48,7 @@ import Wire.API.Conversation.Protocol as M import Wire.API.Conversation.Role (roleNameWireAdmin) import Wire.API.Event.Conversation as M (MemberUpdateData) import Wire.API.Message as M +import qualified Wire.API.User as M postOtrMessage :: MonadSession m => ConvId -> NewOtrMessage -> m ClientMismatch postOtrMessage cnv msg = sessionRequest req rsc readBody @@ -141,6 +142,6 @@ createConv users name = sessionRequest req rsc readBody method POST . path "conversations" . acceptJson - . json (NewConv users [] (name >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin M.ProtocolCreateProteusTag) + . json (NewConv users [] (name >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin M.BaseProtocolProteusTag) $ empty rsc = status201 :| [] diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 4c5be6aa195..ca1515cff92 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -63,8 +63,6 @@ module Wire.API.Conversation maybeRole, -- * create - ProtocolCreateTag (..), - protocolCreateToProtocolTag, NewConv (..), ConvTeamInfo (..), @@ -119,6 +117,7 @@ import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.MultiVerb import Wire.API.Routes.Version import Wire.API.Routes.Versioned +import Wire.API.User import Wire.Arbitrary -------------------------------------------------------------------------------- @@ -127,7 +126,7 @@ import Wire.Arbitrary data ConversationMetadata = ConversationMetadata { cnvmType :: ConvType, -- FUTUREWORK: Make this a qualified user ID. - cnvmCreator :: UserId, + cnvmCreator :: Maybe UserId, cnvmAccess :: [Access], cnvmAccessRoles :: Set AccessRole, cnvmName :: Maybe Text, @@ -141,11 +140,11 @@ data ConversationMetadata = ConversationMetadata deriving (Arbitrary) via (GenericUniform ConversationMetadata) deriving (FromJSON, ToJSON) via Schema ConversationMetadata -defConversationMetadata :: UserId -> ConversationMetadata -defConversationMetadata creator = +defConversationMetadata :: Maybe UserId -> ConversationMetadata +defConversationMetadata mCreator = ConversationMetadata { cnvmType = RegularConv, - cnvmCreator = creator, + cnvmCreator = mCreator, cnvmAccess = [PrivateAccess], cnvmAccessRoles = mempty, cnvmName = Nothing, @@ -194,10 +193,10 @@ conversationMetadataObjectSchema sch = ConversationMetadata <$> cnvmType .= field "type" schema <*> cnvmCreator - .= fieldWithDocModifier + .= optFieldWithDocModifier "creator" (description ?~ "The creator's user ID") - schema + (maybeWithDefault A.Null schema) <*> cnvmAccess .= field "access" (array schema) <*> cnvmAccessRoles .= sch <*> cnvmName .= optField "name" (maybeWithDefault A.Null schema) @@ -243,7 +242,7 @@ data Conversation = Conversation cnvType :: Conversation -> ConvType cnvType = cnvmType . cnvMetadata -cnvCreator :: Conversation -> UserId +cnvCreator :: Conversation -> Maybe UserId cnvCreator = cnvmCreator . cnvMetadata cnvAccess :: Conversation -> [Access] @@ -657,26 +656,6 @@ instance ToSchema ReceiptMode where -------------------------------------------------------------------------------- -- create --- | This is distinct from 'ProtocolTag', which also include ProtocolMixedTag -data ProtocolCreateTag = ProtocolCreateProteusTag | ProtocolCreateMLSTag - deriving stock (Eq, Show, Enum, Bounded, Generic) - deriving (Arbitrary) via GenericUniform ProtocolCreateTag - -instance ToSchema ProtocolCreateTag where - schema = - enum @Text "ProtocolCreateTag" $ - mconcat - [ element "proteus" ProtocolCreateProteusTag, - element "mls" ProtocolCreateMLSTag - ] - -protocolCreateToProtocolTag :: ProtocolCreateTag -> ProtocolTag -protocolCreateToProtocolTag ProtocolCreateProteusTag = ProtocolProteusTag -protocolCreateToProtocolTag ProtocolCreateMLSTag = ProtocolMLSTag - -protocolCreateTagSchema :: ObjectSchema SwaggerDoc ProtocolCreateTag -protocolCreateTagSchema = fmap (fromMaybe ProtocolCreateProteusTag) (optField "protocol" schema) - data NewConv = NewConv { newConvUsers :: [UserId], -- | A list of qualified users, which can include some local qualified users @@ -691,7 +670,7 @@ data NewConv = NewConv -- | Every member except for the creator will have this role newConvUsersRole :: RoleName, -- | The protocol of the conversation. It can be Proteus or MLS (1.0). - newConvProtocol :: ProtocolCreateTag + newConvProtocol :: BaseProtocolTag } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewConv) @@ -750,7 +729,10 @@ newConvSchema sch = .= ( fieldWithDocModifier "conversation_role" (description ?~ usersRoleDesc) schema <|> pure roleNameWireAdmin ) - <*> newConvProtocol .= protocolCreateTagSchema + <*> newConvProtocol + .= fmap + (fromMaybe BaseProtocolProteusTag) + (optField "protocol" schema) where usersDesc = "List of user IDs (excluding the requestor) to be \ diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 433b6210d12..623228fd470 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -119,6 +119,7 @@ module Wire.API.User -- * Protocol preferences BaseProtocolTag (..), + baseProtocolToProtocol, SupportedProtocolUpdate (..), defSupportedProtocols, protocolSetBits, @@ -169,6 +170,7 @@ import Servant (FromHttpApiData (..), ToHttpApiData (..), type (.++)) import qualified Test.QuickCheck as QC import URI.ByteString (serializeURIRef) import qualified Web.Cookie as Web +import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Brig import qualified Wire.API.Error.Brig as E @@ -1635,6 +1637,10 @@ baseProtocolMask :: BaseProtocolTag -> Word32 baseProtocolMask BaseProtocolProteusTag = 1 baseProtocolMask BaseProtocolMLSTag = 2 +baseProtocolToProtocol :: BaseProtocolTag -> ProtocolTag +baseProtocolToProtocol BaseProtocolProteusTag = ProtocolProteusTag +baseProtocolToProtocol BaseProtocolMLSTag = ProtocolMLSTag + instance ToSchema BaseProtocolTag where schema = enum @Text "BaseProtocol" $ diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationList_20Conversation_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationList_20Conversation_user.hs index 26ce7e9ac2f..eb135090f67 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationList_20Conversation_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationList_20Conversation_user.hs @@ -42,7 +42,7 @@ testObject_ConversationList_20Conversation_user_1 = cnvMetadata = ConversationMetadata { cnvmType = RegularConv, - cnvmCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), cnvmAccess = [], cnvmAccessRoles = Set.empty, cnvmName = Just "", diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Conversation_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Conversation_user.hs index 9335944724a..fdd743c733d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Conversation_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Conversation_user.hs @@ -41,7 +41,7 @@ testObject_Conversation_user_1 = cnvMetadata = ConversationMetadata { cnvmType = One2OneConv, - cnvmCreator = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001")), + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))), cnvmAccess = [], cnvmAccessRoles = Set.empty, cnvmName = Just " 0", @@ -75,7 +75,7 @@ testObject_Conversation_user_2 = cnvMetadata = ConversationMetadata { cnvmType = SelfConv, - cnvmCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001")), + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001"))), cnvmAccess = [ InviteAccess, InviteAccess, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs index 7394b74f5a0..f09be3dfd22 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs @@ -148,7 +148,7 @@ testObject_Event_user_8 = cnvMetadata = ConversationMetadata { cnvmType = RegularConv, - cnvmCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001")), + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001"))), cnvmAccess = [InviteAccess, PrivateAccess, LinkAccess, InviteAccess, InviteAccess, InviteAccess, LinkAccess], cnvmAccessRoles = Set.fromList [TeamMemberAccessRole, GuestAccessRole, ServiceAccessRole], diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs index e4a4deca6a2..020437c2dec 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs @@ -27,6 +27,7 @@ import qualified Data.UUID as UUID (fromString) import Imports import Wire.API.Conversation import Wire.API.Conversation.Role +import Wire.API.User testDomain :: Domain testDomain = Domain "testdomain.example.com" @@ -51,7 +52,7 @@ testObject_NewConv_user_1 = newConvMessageTimer = Just (Ms {ms = 3320987366258987}), newConvReceiptMode = Just (ReceiptMode {unReceiptMode = 1}), newConvUsersRole = fromJust (parseRoleName "8tp2gs7b6"), - newConvProtocol = ProtocolCreateProteusTag + newConvProtocol = BaseProtocolProteusTag } testObject_NewConv_user_3 :: NewConv @@ -70,5 +71,5 @@ testObject_NewConv_user_3 = ( parseRoleName "y3otpiwu615lvvccxsq0315jj75jquw01flhtuf49t6mzfurvwe3_sh51f4s257e2x47zo85rif_xyiyfldpan3g4r6zr35rbwnzm0k" ), - newConvProtocol = ProtocolCreateMLSTag + newConvProtocol = BaseProtocolMLSTag } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs index 0e32a61ca41..b2ef39c2552 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs @@ -58,7 +58,7 @@ conv1 = cnvMetadata = ConversationMetadata { cnvmType = One2OneConv, - cnvmCreator = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001")), + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))), cnvmAccess = [], cnvmAccessRoles = Set.empty, cnvmName = Just " 0", @@ -92,7 +92,7 @@ conv2 = cnvMetadata = ConversationMetadata { cnvmType = SelfConv, - cnvmCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001")), + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001"))), cnvmAccess = [ InviteAccess, InviteAccess, diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 477d87219b1..4fa79cd3b71 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -52,7 +52,7 @@ import Text.RawString.QQ import URI.ByteString import Util import Web.FormUrlEncoded -import Wire.API.Conversation (Access (..), Conversation (cnvQualifiedId), ProtocolCreateTag (..)) +import Wire.API.Conversation import qualified Wire.API.Conversation as Conv import Wire.API.Conversation.Code (CreateConversationCodeRequest (CreateConversationCodeRequest)) import qualified Wire.API.Conversation.Role as Role @@ -701,7 +701,7 @@ createTeamConv :: Http ResponseLBS createTeamConv svc mkHeader token tid name = do let tinfo = Conv.ConvTeamInfo tid - let conv = Conv.NewConv [] [] (checked name) (Set.fromList [CodeAccess]) Nothing (Just tinfo) Nothing Nothing Role.roleNameWireAdmin ProtocolCreateProteusTag + let conv = Conv.NewConv [] [] (checked name) (Set.fromList [CodeAccess]) Nothing (Just tinfo) Nothing Nothing Role.roleNameWireAdmin BaseProtocolProteusTag post $ svc . path "conversations" diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 1b5c6d7664e..b22af8838f5 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -1413,7 +1413,7 @@ createConvWithAccessRoles ars g u us = . contentJson . body (RequestBodyLBS (encode conv)) where - conv = NewConv us [] Nothing Set.empty ars Nothing Nothing Nothing roleNameWireAdmin ProtocolCreateProteusTag + conv = NewConv us [] Nothing Set.empty ars Nothing Nothing Nothing roleNameWireAdmin BaseProtocolProteusTag postMessage :: Galley -> diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 44633941969..6d77a809692 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -234,7 +234,7 @@ createTeamConvWithRole role g tid u us mtimer = do mtimer Nothing role - ProtocolCreateProteusTag + BaseProtocolProteusTag r <- post ( g diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index 2446d4a88d1..1351cc0db3e 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -281,7 +281,7 @@ testAddRemoteUsersToLocalConv brig1 galley1 brig2 galley2 = do Nothing Nothing roleNameWireAdmin - ProtocolCreateProteusTag + BaseProtocolProteusTag convId <- fmap cnvQualifiedId . responseJsonError =<< post diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index bd7fcdd1424..66fa096711c 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -735,7 +735,7 @@ createMLSConversation galley zusr c = do Nothing Nothing roleNameWireAdmin - ProtocolCreateMLSTag + BaseProtocolMLSTag post $ galley . path "/conversations" @@ -776,7 +776,7 @@ createConversation galley zusr usersToAdd = do Nothing Nothing roleNameWireAdmin - ProtocolCreateProteusTag + BaseProtocolProteusTag post $ galley . path "/conversations" diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 6e3bc276061..45f28df643d 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -937,7 +937,15 @@ addLocalUsersToRemoteConv :: addLocalUsersToRemoteConv remoteConvId qAdder localUsers = do connStatus <- E.getConnections localUsers (Just [qAdder]) (Just Accepted) let localUserIdsSet = Set.fromList localUsers - connected = Set.fromList $ fmap csv2From connStatus + adder = qUnqualified qAdder + -- If alice@A creates a 1-1 conversation on B, it can appear as if alice is + -- adding herself to a remote conversation. To make sure this is allowed, we + -- always consider a user as connected to themself. + connected = + Set.fromList (fmap csv2From connStatus) + <> if Set.member adder localUserIdsSet + then Set.singleton adder + else mempty unconnected = Set.difference localUserIdsSet connected connectedList = Set.toList connected diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 0769f9c5034..3b7dc4d6460 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -200,12 +200,12 @@ createGroupConversationGeneric lusr conn newConv convCreated = do ensureNoLegalholdConflicts allUsers case newConvProtocol newConv of - ProtocolCreateMLSTag -> do + BaseProtocolMLSTag -> do -- Here we fail early in order to notify users of this misconfiguration assertMLSEnabled unlessM (isJust <$> getMLSRemovalKey) $ throw (InternalErrorWithDescription "No backend removal key is configured (See 'mlsPrivateKeyPaths' in galley's config). Refusing to create MLS conversation.") - ProtocolCreateProteusTag -> pure () + BaseProtocolProteusTag -> pure () lcnv <- traverse (const E.createConversationId) lusr -- FUTUREWORK: Invoke the creating a conversation action only once @@ -300,9 +300,9 @@ createProteusSelfConversation lusr = do create lcnv = do let nc = NewConversation - { ncMetadata = (defConversationMetadata (tUnqualified lusr)) {cnvmType = SelfConv}, + { ncMetadata = (defConversationMetadata (Just (tUnqualified lusr))) {cnvmType = SelfConv}, ncUsers = ulFromLocals [toUserRole (tUnqualified lusr)], - ncProtocol = ProtocolCreateProteusTag + ncProtocol = BaseProtocolProteusTag } c <- E.createConversation lcnv nc conversationCreated lusr c @@ -389,7 +389,7 @@ createLegacyOne2OneConversationUnchecked :: createLegacyOne2OneConversationUnchecked self zcon name mtid other = do lcnv <- localOne2OneConvId self other let meta = - (defConversationMetadata (tUnqualified self)) + (defConversationMetadata (Just (tUnqualified self))) { cnvmType = One2OneConv, cnvmTeam = mtid, cnvmName = fmap fromRange name @@ -397,7 +397,7 @@ createLegacyOne2OneConversationUnchecked self zcon name mtid other = do let nc = NewConversation { ncUsers = ulFromLocals (map (toUserRole . tUnqualified) [self, other]), - ncProtocol = ProtocolCreateProteusTag, + ncProtocol = BaseProtocolProteusTag, ncMetadata = meta } mc <- E.getConversation (tUnqualified lcnv) @@ -453,7 +453,7 @@ createOne2OneConversationLocally lcnv self zcon name mtid other = do Just c -> conversationExisted self c Nothing -> do let meta = - (defConversationMetadata (tUnqualified self)) + (defConversationMetadata (Just (tUnqualified self))) { cnvmType = One2OneConv, cnvmTeam = mtid, cnvmName = fmap fromRange name @@ -462,7 +462,7 @@ createOne2OneConversationLocally lcnv self zcon name mtid other = do NewConversation { ncMetadata = meta, ncUsers = fmap toUserRole (toUserList lcnv [tUntagged self, other]), - ncProtocol = ProtocolCreateProteusTag + ncProtocol = BaseProtocolProteusTag } c <- E.createConversation lcnv nc void $ notifyCreatedConversation self (Just zcon) c @@ -501,7 +501,7 @@ createConnectConversation lusr conn j = do lrecipient <- ensureLocal lusr (cRecipient j) n <- rangeCheckedMaybe (cName j) let meta = - (defConversationMetadata (tUnqualified lusr)) + (defConversationMetadata (Just (tUnqualified lusr))) { cnvmType = ConnectConv, cnvmName = fmap fromRange n } @@ -511,7 +511,7 @@ createConnectConversation lusr conn j = do { -- We add only one member, second one gets added later, -- when the other user accepts the connection request. ncUsers = ulFromLocals (map (toUserRole . tUnqualified) [lusr]), - ncProtocol = ProtocolCreateProteusTag, + ncProtocol = BaseProtocolProteusTag, ncMetadata = meta } E.getConversation (tUnqualified lcnv) @@ -585,8 +585,8 @@ newRegularConversation lusr newConv = do o <- input let uncheckedUsers = newConvMembers lusr newConv users <- case newConvProtocol newConv of - ProtocolCreateProteusTag -> checkedConvSize o uncheckedUsers - ProtocolCreateMLSTag -> do + BaseProtocolProteusTag -> checkedConvSize o uncheckedUsers + BaseProtocolMLSTag -> do unless (null uncheckedUsers) $ throwS @'MLSNonEmptyMemberList pure mempty let nc = @@ -594,7 +594,7 @@ newRegularConversation lusr newConv = do { ncMetadata = ConversationMetadata { cnvmType = RegularConv, - cnvmCreator = tUnqualified lusr, + cnvmCreator = Just (tUnqualified lusr), cnvmAccess = access newConv, cnvmAccessRoles = accessRoles newConv, cnvmName = fmap fromRange (newConvName newConv), @@ -655,7 +655,7 @@ notifyCreatedConversation lusr conn c = do now <- input -- Ask remote server to store conversation membership and notify remote users -- of being added to a conversation - failedToNotify <- registerRemoteConversationMemberships now (tDomain lusr) c + failedToNotify <- registerRemoteConversationMemberships now lusr c let allRemotes = Data.convRemoteMembers c notifiedRemotes = filter diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 416411ff2ac..2ded6f38fe6 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -559,13 +559,14 @@ sendMLSCommitBundle remoteDomain msr = decodeMLS' (fromBase64ByteString (F.mmsrRawMessage msr)) ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle - qConvOrSub <- getConvFromGroupId ibundle.groupId + (ctype, qConvOrSub) <- getConvFromGroupId ibundle.groupId when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch uncurry F.MLSMessageResponseUpdates . (,mempty) . map lcuUpdate <$> postMLSCommitBundle loc (tUntagged sender) (mmsrSenderClient msr) + ctype qConvOrSub Nothing ibundle @@ -606,13 +607,14 @@ sendMLSMessage remoteDomain msr = let sender = toRemoteUnsafe remoteDomain (F.mmsrSender msr) raw <- either (throw . mlsProtocolError) pure $ decodeMLS' (fromBase64ByteString (F.mmsrRawMessage msr)) msg <- noteS @'MLSUnsupportedMessage $ mkIncomingMessage raw - qConvOrSub <- getConvFromGroupId msg.groupId + (ctype, qConvOrSub) <- getConvFromGroupId msg.groupId when (qUnqualified qConvOrSub /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch uncurry F.MLSMessageResponseUpdates . first (map lcuUpdate) <$> postMLSMessage loc (tUntagged sender) (mmsrSenderClient msr) + ctype qConvOrSub Nothing msg @@ -837,7 +839,7 @@ getOne2OneConversation domain (GetOne2OneConversationRequest self other) = let getLocal lconv = do mconv <- E.getConversation (tUnqualified lconv) fmap GetOne2OneConversationOk $ case mconv of - Nothing -> pure (localMLSOne2OneConversationAsRemote rself lother lconv) + Nothing -> pure (localMLSOne2OneConversationAsRemote lconv) Just conv -> note (InternalErrorWithDescription "Unexpected member list in 1-1 conversation") diff --git a/services/galley/src/Galley/API/MLS/Commit/Core.hs b/services/galley/src/Galley/API/MLS/Commit/Core.hs index f0e60188038..4499c619ff3 100644 --- a/services/galley/src/Galley/API/MLS/Commit/Core.hs +++ b/services/galley/src/Galley/API/MLS/Commit/Core.hs @@ -96,15 +96,15 @@ getCommitData :: Sem r ProposalAction getCommitData senderIdentity lConvOrSub epoch commit = do let convOrSub = tUnqualified lConvOrSub - groupId = cnvmlsGroupId convOrSub.meta + groupId = cnvmlsGroupId convOrSub.mlsMeta evalState convOrSub.indexMap $ do creatorAction <- if epoch == Epoch 0 then addProposedClient senderIdentity else mempty - proposals <- traverse (derefOrCheckProposal convOrSub.meta groupId epoch) commit.proposals - action <- applyProposals convOrSub.meta groupId proposals + proposals <- traverse (derefOrCheckProposal convOrSub.mlsMeta groupId epoch) commit.proposals + action <- applyProposals convOrSub.mlsMeta groupId proposals pure (creatorAction <> action) incrementEpoch :: diff --git a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs index 6f650ee6ae7..ef9eba8acbc 100644 --- a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs @@ -69,8 +69,8 @@ getExternalCommitData :: Sem r ExternalCommitAction getExternalCommitData senderIdentity lConvOrSub epoch commit = do let convOrSub = tUnqualified lConvOrSub - curEpoch = cnvmlsEpoch convOrSub.meta - groupId = cnvmlsGroupId convOrSub.meta + curEpoch = cnvmlsEpoch convOrSub.mlsMeta + groupId = cnvmlsGroupId convOrSub.mlsMeta when (epoch /= curEpoch) $ throwS @'MLSStaleMessage when (epoch == Epoch 0) $ throw $ @@ -94,7 +94,7 @@ getExternalCommitData senderIdentity lConvOrSub epoch commit = do evalState convOrSub.indexMap $ do -- process optional removal - propAction <- applyProposals convOrSub.meta groupId proposals + propAction <- applyProposals convOrSub.mlsMeta groupId proposals removedIndex <- case cmAssocs (paRemove propAction) of [(cid, idx)] | cid /= senderIdentity -> @@ -146,8 +146,8 @@ processExternalCommit senderIdentity lConvOrSub epoch action updatePath = do <$> note (mlsProtocolError "External commits need an update path") updatePath - let cs = cnvmlsCipherSuite (tUnqualified lConvOrSub).meta - let groupId = cnvmlsGroupId convOrSub.meta + let cs = cnvmlsCipherSuite (tUnqualified lConvOrSub).mlsMeta + let groupId = cnvmlsGroupId convOrSub.mlsMeta let extra = LeafNodeTBSExtraCommit groupId action.add case validateLeafNode cs (Just senderIdentity) extra leafNode.value of Left errMsg -> @@ -182,7 +182,7 @@ executeExternalCommitAction :: ExternalCommitAction -> Sem r () executeExternalCommitAction lconvOrSub senderIdentity action = do - let mlsMeta = (tUnqualified lconvOrSub).meta + let mlsMeta = (tUnqualified lconvOrSub).mlsMeta -- Remove deprecated sender client from conversation state. for_ action.remove $ \_ -> diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index f868a722cda..fb5fcb02444 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -28,11 +28,14 @@ import Data.Qualified import qualified Data.Set as Set import Data.Tuple.Extra import Galley.API.Action +import Galley.API.Error import Galley.API.MLS.Commit.Core import Galley.API.MLS.Conversation +import Galley.API.MLS.One2One import Galley.API.MLS.Proposal import Galley.API.MLS.Types import Galley.API.MLS.Util +import Galley.API.Util import Galley.Data.Conversation.Types hiding (Conversation) import qualified Galley.Data.Conversation.Types as Data import Galley.Effects @@ -44,6 +47,8 @@ import Imports import Polysemy import Polysemy.Error import Polysemy.Resource (Resource) +import Wire.API.Conversation hiding (Member) +import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error @@ -80,16 +85,16 @@ processInternalCommit senderIdentity con lConvOrSub epoch action commit = do let convOrSub = tUnqualified lConvOrSub qusr = cidQualifiedUser senderIdentity cm = convOrSub.members - ss = csSignatureScheme (cnvmlsCipherSuite convOrSub.meta) + ss = csSignatureScheme (cnvmlsCipherSuite convOrSub.mlsMeta) newUserClients = Map.assocs (paAdd action) -- check all pending proposals are referenced in the commit - allPendingProposals <- getAllPendingProposalRefs (cnvmlsGroupId convOrSub.meta) epoch + allPendingProposals <- getAllPendingProposalRefs (cnvmlsGroupId convOrSub.mlsMeta) epoch let referencedProposals = Set.fromList $ mapMaybe (\x -> preview Proposal._Ref x) commit.proposals unless (all (`Set.member` referencedProposals) allPendingProposals) $ throwS @'MLSCommitMissingReferences - withCommitLock (fmap (.id) lConvOrSub) (cnvmlsGroupId convOrSub.meta) epoch $ do + withCommitLock (fmap (.id) lConvOrSub) (cnvmlsGroupId convOrSub.mlsMeta) epoch $ do -- no client can be directly added to a subconversation when (is _SubConv convOrSub && any ((senderIdentity /=) . fst) (cmAssocs (paAdd action))) $ throw (mlsProtocolError "Add proposals in subconversations are not supported") @@ -178,33 +183,73 @@ processInternalCommit senderIdentity con lConvOrSub epoch action commit = do createSubConversation cnv sub - convOrSub.meta.cnvmlsCipherSuite - convOrSub.meta.cnvmlsGroupId - _ -> pure () -- FUTUREWORK: create 1-1 conversation at epoch 0 - - -- remove users from the conversation and send events - removeEvents <- - foldMap - (removeMembers qusr con lConvOrSub) - (nonEmpty membersToRemove) + convOrSub.mlsMeta.cnvmlsCipherSuite + convOrSub.mlsMeta.cnvmlsGroupId + pure [] + Conv _ + | convOrSub.meta.cnvmType == One2OneConv + && epoch == Epoch 0 -> do + -- create 1-1 conversation with the users as members, set + -- epoch to 0 for now, it will be incremented later + let senderUser = cidQualifiedUser senderIdentity + mlsConv = fmap (.conv) lConvOrSub + lconv = fmap mcConv mlsConv + conv <- case filter ((/= senderUser) . fst) newUserClients of + [(otherUser, _)] -> + createMLSOne2OneConversation + senderUser + otherUser + mlsConv + _ -> + throw + ( mlsProtocolError + "The first commit in a 1-1 conversation should add exactly 1 other user" + ) + -- notify otherUser about being added to this 1-1 conversation + let bm = convBotsAndMembers conv + members <- + note + ( InternalErrorWithDescription + "Unexpected empty member list in MLS 1-1 conversation" + ) + $ nonEmpty (bmQualifiedMembers lconv bm) + (update, _) <- + notifyConversationAction + SConversationJoinTag + senderUser + False + con + lconv + bm + ConversationJoin + { cjUsers = members, + cjRole = roleNameWireMember + } + pure [update] + _ -> do + -- remove users from the conversation and send events + removeEvents <- + foldMap + (removeMembers qusr con lConvOrSub) + (nonEmpty membersToRemove) - -- add users to the conversation and send events - addEvents <- - foldMap (addMembers qusr con lConvOrSub) - . nonEmpty - . map fst - $ newUserClients - pure (addEvents <> removeEvents) + -- add users to the conversation and send events + addEvents <- + foldMap (addMembers qusr con lConvOrSub) + . nonEmpty + . map fst + $ newUserClients + pure (addEvents <> removeEvents) else pure [] -- Remove clients from the conversation state. This includes client removals -- of all types (see Note [client removal]). for_ (Map.assocs (paRemove action)) $ \(qtarget, clients) -> do - removeMLSClients (cnvmlsGroupId convOrSub.meta) qtarget (Map.keysSet clients) + removeMLSClients (cnvmlsGroupId convOrSub.mlsMeta) qtarget (Map.keysSet clients) -- add clients to the conversation state for_ newUserClients $ \(qtarget, newClients) -> do - addMLSClients (cnvmlsGroupId convOrSub.meta) qtarget (Set.fromList (Map.assocs newClients)) + addMLSClients (cnvmlsGroupId convOrSub.mlsMeta) qtarget (Set.fromList (Map.assocs newClients)) -- increment epoch number for_ lConvOrSub incrementEpoch diff --git a/services/galley/src/Galley/API/MLS/Conversation.hs b/services/galley/src/Galley/API/MLS/Conversation.hs index 7d38a776579..1a7ed3d62bc 100644 --- a/services/galley/src/Galley/API/MLS/Conversation.hs +++ b/services/galley/src/Galley/API/MLS/Conversation.hs @@ -17,15 +17,19 @@ module Galley.API.MLS.Conversation ( mkMLSConversation, + newMLSConversation, mcConv, ) where +import Data.Id +import Data.Qualified import Galley.API.MLS.Types import Galley.Data.Conversation.Types as Data import Galley.Effects.MemberStore import Imports import Polysemy +import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol mkMLSConversation :: @@ -47,6 +51,20 @@ mkMLSConversation conv = mcMigrationState = migrationState } +-- | Creates a new MLS conversation with members but no clients. +newMLSConversation :: Local ConvId -> ConversationMetadata -> ConversationMLSData -> MLSConversation +newMLSConversation lcnv meta mlsData = + MLSConversation + { mcId = tUnqualified lcnv, + mcMetadata = meta, + mcMLSData = mlsData, + mcLocalMembers = [], + mcRemoteMembers = [], + mcMembers = mempty, + mcIndexMap = mempty, + mcMigrationState = MLSMigrationMLS + } + mcConv :: MLSConversation -> Data.Conversation mcConv mlsConv = Data.Conversation diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 6421f62cdc7..89000e1adc4 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -46,6 +46,7 @@ import Galley.API.MLS.Commit.InternalCommit import Galley.API.MLS.Conversation import Galley.API.MLS.Enabled import Galley.API.MLS.IncomingMessage +import Galley.API.MLS.One2One import Galley.API.MLS.Propagate import Galley.API.MLS.Proposal import Galley.API.MLS.Types @@ -66,6 +67,7 @@ import Polysemy.Internal import Polysemy.Output import Polysemy.Resource (Resource) import Polysemy.TinyLog +import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley @@ -137,10 +139,10 @@ postMLSMessageFromLocalUser :: postMLSMessageFromLocalUser lusr c conn smsg = do assertMLSEnabled imsg <- noteS @'MLSUnsupportedMessage $ mkIncomingMessage smsg - cnvOrSub <- getConvFromGroupId imsg.groupId + (ctype, cnvOrSub) <- getConvFromGroupId imsg.groupId (events, unreachables) <- first (map lcuEvent) - <$> postMLSMessage lusr (tUntagged lusr) c cnvOrSub (Just conn) imsg + <$> postMLSMessage lusr (tUntagged lusr) c ctype cnvOrSub (Just conn) imsg t <- toUTCTimeMillis <$> input pure $ MLSMessageSendingStatus events t unreachables @@ -154,15 +156,16 @@ postMLSCommitBundle :: Local x -> Qualified UserId -> ClientId -> + ConvType -> Qualified ConvOrSubConvId -> Maybe ConnId -> IncomingBundle -> Sem r [LocalConversationUpdate] -postMLSCommitBundle loc qusr c qConvOrSub conn bundle = +postMLSCommitBundle loc qusr c ctype qConvOrSub conn bundle = foldQualified loc - (postMLSCommitBundleToLocalConv qusr c conn bundle) - (postMLSCommitBundleToRemoteConv loc qusr c conn bundle) + (postMLSCommitBundleToLocalConv qusr c conn bundle ctype) + (postMLSCommitBundleToRemoteConv loc qusr c conn bundle ctype) qConvOrSub postMLSCommitBundleFromLocalUser :: @@ -180,10 +183,10 @@ postMLSCommitBundleFromLocalUser :: postMLSCommitBundleFromLocalUser lusr c conn bundle = do assertMLSEnabled ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle - qConvOrSub <- getConvFromGroupId ibundle.groupId + (ctype, qConvOrSub) <- getConvFromGroupId ibundle.groupId events <- map lcuEvent - <$> postMLSCommitBundle lusr (tUntagged lusr) c qConvOrSub (Just conn) ibundle + <$> postMLSCommitBundle lusr (tUntagged lusr) c ctype qConvOrSub (Just conn) ibundle t <- toUTCTimeMillis <$> input pure $ MLSMessageSendingStatus events t mempty @@ -198,10 +201,11 @@ postMLSCommitBundleToLocalConv :: ClientId -> Maybe ConnId -> IncomingBundle -> + ConvType -> Local ConvOrSubConvId -> Sem r [LocalConversationUpdate] -postMLSCommitBundleToLocalConv qusr c conn bundle lConvOrSubId = do - lConvOrSub <- fetchConvOrSub qusr lConvOrSubId +postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do + lConvOrSub <- fetchConvOrSub qusr bundle.groupId ctype lConvOrSubId senderIdentity <- getSenderIdentity qusr c bundle.sender lConvOrSub (events, newClients) <- case bundle.sender of @@ -258,14 +262,16 @@ postMLSCommitBundleToRemoteConv :: ClientId -> Maybe ConnId -> IncomingBundle -> + ConvType -> Remote ConvOrSubConvId -> Sem r [LocalConversationUpdate] -postMLSCommitBundleToRemoteConv loc qusr c con bundle rConvOrSubId = do +postMLSCommitBundleToRemoteConv loc qusr c con bundle ctype rConvOrSubId = do -- only local users can send messages to remote conversations lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr -- only members may send commit bundles to a remote conversation - flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) ((.conv) <$> rConvOrSubId) + unless (bundle.epoch == Epoch 0 && ctype == One2OneConv) $ + flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) ((.conv) <$> rConvOrSubId) resp <- runFederated rConvOrSubId $ @@ -314,14 +320,15 @@ postMLSMessage :: Local x -> Qualified UserId -> ClientId -> + ConvType -> Qualified ConvOrSubConvId -> Maybe ConnId -> IncomingMessage -> Sem r ([LocalConversationUpdate], Maybe UnreachableUsers) -postMLSMessage loc qusr c qconvOrSub con msg = do +postMLSMessage loc qusr c ctype qconvOrSub con msg = do foldQualified loc - (postMLSMessageToLocalConv qusr c con msg) + (postMLSMessageToLocalConv qusr c con msg ctype) (postMLSMessageToRemoteConv loc qusr c con msg) qconvOrSub @@ -336,7 +343,7 @@ getSenderIdentity :: Sem r ClientIdentity getSenderIdentity qusr c mSender lConvOrSubConv = do let cid = mkClientIdentity qusr c - let epoch = epochNumber . cnvmlsEpoch . (.meta) . tUnqualified $ lConvOrSubConv + let epoch = epochNumber . cnvmlsEpoch . (.mlsMeta) . tUnqualified $ lConvOrSubConv case mSender of SenderMember idx | epoch > 0 -> do cid' <- note (mlsProtocolError "unknown sender leaf index") $ imLookup (tUnqualified lConvOrSubConv).indexMap idx @@ -356,10 +363,11 @@ postMLSMessageToLocalConv :: ClientId -> Maybe ConnId -> IncomingMessage -> + ConvType -> Local ConvOrSubConvId -> Sem r ([LocalConversationUpdate], Maybe UnreachableUsers) -postMLSMessageToLocalConv qusr c con msg convOrSubId = do - lConvOrSub <- fetchConvOrSub qusr convOrSubId +postMLSMessageToLocalConv qusr c con msg ctype convOrSubId = do + lConvOrSub <- fetchConvOrSub qusr msg.groupId ctype convOrSubId for_ msg.sender $ \sender -> void $ getSenderIdentity qusr c sender lConvOrSub @@ -439,23 +447,55 @@ fetchConvOrSub :: forall r. ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, + Member (Error MLSProtocolError) r, Member MemberStore r, Member SubConversationStore r ) => Qualified UserId -> + GroupId -> + ConvType -> Local ConvOrSubConvId -> Sem r (Local ConvOrSubConv) -fetchConvOrSub qusr convOrSubId = for convOrSubId $ \case - Conv convId -> Conv <$> getMLSConv qusr (qualifyAs convOrSubId convId) +fetchConvOrSub qusr groupId ctype convOrSubId = for convOrSubId $ \case + Conv convId -> Conv <$> getMLSConv qusr (Just groupId) ctype (qualifyAs convOrSubId convId) SubConv convId sconvId -> do let lconv = qualifyAs convOrSubId convId - c <- getMLSConv qusr lconv + c <- getMLSConv qusr Nothing ctype lconv msubconv <- getSubConversation convId sconvId - let subconv = fromMaybe (newSubConversationFromParent lconv sconvId (mcMLSData c)) msubconv + subconv <- case msubconv of + Nothing -> pure $ newSubConversationFromParent lconv sconvId (mcMLSData c) + Just subconv -> do + when (groupId /= subconv.scMLSData.cnvmlsGroupId) $ + throw (mlsProtocolError "The message group ID does not match the subconversation") + pure subconv pure (SubConv c subconv) - where - getMLSConv :: Qualified UserId -> Local ConvId -> Sem r MLSConversation - getMLSConv u = - getLocalConvForUser u - >=> mkMLSConversation - >=> noteS @'ConvNotFound + +getMLSConv :: + ( Member (ErrorS 'ConvNotFound) r, + Member (Error MLSProtocolError) r, + Member ConversationStore r, + Member MemberStore r + ) => + Qualified UserId -> + Maybe GroupId -> + ConvType -> + Local ConvId -> + Sem r MLSConversation +getMLSConv u mGroupId ctype lcnv = do + mlsConv <- case ctype of + One2OneConv -> do + mconv <- getConversation (tUnqualified lcnv) + case mconv of + Just conv -> mkMLSConversation conv >>= noteS @'ConvNotFound + Nothing -> + let (meta, mlsData) = localMLSOne2OneConversationMetadata (tUntagged lcnv) + in pure (newMLSConversation lcnv meta mlsData) + _ -> + getLocalConvForUser u lcnv + >>= mkMLSConversation + >>= noteS @'ConvNotFound + -- check that the group ID in the message matches that of the conversation + for_ mGroupId $ \groupId -> + when (groupId /= mlsConv.mcMLSData.cnvmlsGroupId) $ + throw (mlsProtocolError "The message group ID does not match the conversation") + pure mlsConv diff --git a/services/galley/src/Galley/API/MLS/One2One.hs b/services/galley/src/Galley/API/MLS/One2One.hs index 745bc438e36..dc29dc1ca35 100644 --- a/services/galley/src/Galley/API/MLS/One2One.hs +++ b/services/galley/src/Galley/API/MLS/One2One.hs @@ -18,13 +18,20 @@ module Galley.API.MLS.One2One ( localMLSOne2OneConversation, localMLSOne2OneConversationAsRemote, + localMLSOne2OneConversationMetadata, remoteMLSOne2OneConversation, + createMLSOne2OneConversation, ) where import Data.Id as Id import Data.Qualified +import Galley.API.MLS.Types +import qualified Galley.Data.Conversation.Types as Data +import Galley.Effects.ConversationStore +import Galley.Types.UserList import Imports hiding (cs) +import Polysemy import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role @@ -32,58 +39,53 @@ import Wire.API.Federation.API.Galley import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group.Serialisation import Wire.API.MLS.SubConversation +import Wire.API.User -- | Construct a local MLS 1-1 'Conversation' between a local user and another -- (possibly remote) user. localMLSOne2OneConversation :: Local UserId -> - Qualified UserId -> Local ConvId -> Conversation -localMLSOne2OneConversation lself qother (tUntagged -> convId) = +localMLSOne2OneConversation lself (tUntagged -> convId) = let members = ConvMembers { cmSelf = defMember (tUntagged lself), - cmOthers = [defOtherMember qother] + cmOthers = [] } - (metadata, protocol) = localMLSOne2OneConversationMetadata (tUntagged lself) convId + (metadata, mlsData) = localMLSOne2OneConversationMetadata convId in Conversation { cnvQualifiedId = convId, cnvMetadata = metadata, cnvMembers = members, - cnvProtocol = protocol + cnvProtocol = ProtocolMLS mlsData } -- | Construct a 'RemoteConversation' structure for a local MLS 1-1 -- conversation to be returned to a remote backend. localMLSOne2OneConversationAsRemote :: - Remote UserId -> - Local UserId -> Local ConvId -> RemoteConversation -localMLSOne2OneConversationAsRemote rself lother lcnv = +localMLSOne2OneConversationAsRemote lcnv = let members = RemoteConvMembers { rcmSelfRole = roleNameWireMember, - rcmOthers = [defOtherMember (tUntagged lother)] + rcmOthers = [] } - (metadata, protocol) = localMLSOne2OneConversationMetadata (tUntagged rself) (tUntagged lcnv) + (metadata, mlsData) = localMLSOne2OneConversationMetadata (tUntagged lcnv) in RemoteConversation { rcnvId = tUnqualified lcnv, rcnvMetadata = metadata, rcnvMembers = members, - rcnvProtocol = protocol + rcnvProtocol = ProtocolMLS mlsData } localMLSOne2OneConversationMetadata :: - Qualified UserId -> Qualified ConvId -> - (ConversationMetadata, Protocol) -localMLSOne2OneConversationMetadata self convId = + (ConversationMetadata, ConversationMLSData) +localMLSOne2OneConversationMetadata convId = let metadata = - ( defConversationMetadata - (qUnqualified self) - ) + (defConversationMetadata Nothing) { cnvmType = One2OneConv } groupId = convToGroupId $ groupIdParts One2OneConv (fmap Conv convId) @@ -94,7 +96,7 @@ localMLSOne2OneConversationMetadata self convId = cnvmlsEpochTimestamp = Nothing, cnvmlsCipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 } - in (metadata, ProtocolMLS mlsData) + in (metadata, mlsData) -- | Convert an MLS 1-1 conversation returned by a remote backend into a -- 'Conversation' to be returned to the client. @@ -107,7 +109,7 @@ remoteMLSOne2OneConversation lself rother rc = let members = ConvMembers { cmSelf = defMember (tUntagged lself), - cmOthers = [defOtherMember (tUntagged rother)] + cmOthers = [] } in Conversation { cnvQualifiedId = tUntagged (qualifyAs rother (rcnvId rc)), @@ -115,3 +117,20 @@ remoteMLSOne2OneConversation lself rother rc = cnvMembers = members, cnvProtocol = rcnvProtocol rc } + +-- | Create a new record for an MLS 1-1 conversation in the database and add +-- the two members to it. +createMLSOne2OneConversation :: + Member ConversationStore r => + Qualified UserId -> + Qualified UserId -> + Local MLSConversation -> + Sem r Data.Conversation +createMLSOne2OneConversation self other lconv = do + createConversation + (fmap mcId lconv) + Data.NewConversation + { ncMetadata = mcMetadata (tUnqualified lconv), + ncUsers = fmap (,roleNameWireMember) (toUserList lconv [self, other]), + ncProtocol = BaseProtocolMLSTag + } diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs index 21325a84e76..224e0b3c712 100644 --- a/services/galley/src/Galley/API/MLS/Proposal.hs +++ b/services/galley/src/Galley/API/MLS/Proposal.hs @@ -239,7 +239,7 @@ processProposal :: RawMLS Proposal -> Sem r () processProposal qusr lConvOrSub groupId epoch pub prop = do - let mlsMeta = (tUnqualified lConvOrSub).meta + let mlsMeta = (tUnqualified lConvOrSub).mlsMeta -- Check if the epoch number matches that of a conversation unless (epoch == cnvmlsEpoch mlsMeta) $ throwS @'MLSStaleMessage -- Check if the group ID matches that of a conversation diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 557b4bc9120..a578290590a 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -78,7 +78,7 @@ createAndSendRemoveProposals :: ClientMap -> Sem r () createAndSendRemoveProposals lConvOrSubConv indices qusr cm = do - let meta = (tUnqualified lConvOrSubConv).meta + let meta = (tUnqualified lConvOrSubConv).mlsMeta mKeyPair <- getMLSRemovalKey case mKeyPair of Nothing -> do diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index 0e63e96bdce..70da47eea86 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -192,7 +192,10 @@ toPublicSubConv (Qualified (SubConversation {..}) domain) = type ConvOrSubConv = ConvOrSubChoice MLSConversation SubConversation -instance HasField "meta" ConvOrSubConv ConversationMLSData where +instance HasField "meta" ConvOrSubConv ConversationMetadata where + getField x = x.conv.mcMetadata + +instance HasField "mlsMeta" ConvOrSubConv ConversationMLSData where getField (Conv c) = mcMLSData c getField (SubConv _ s) = scMLSData s diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs index 36fb80fbbf8..25d1171e40e 100644 --- a/services/galley/src/Galley/API/MLS/Util.hs +++ b/services/galley/src/Galley/API/MLS/Util.hs @@ -36,10 +36,10 @@ import Polysemy.Resource (Resource, bracket) import Polysemy.TinyLog (TinyLog) import qualified Polysemy.TinyLog as TinyLog import qualified System.Logger as Log +import Wire.API.Conversation hiding (Member) import Wire.API.Error import Wire.API.Error.Galley import Wire.API.MLS.Epoch -import Wire.API.MLS.Group import Wire.API.MLS.Group.Serialisation import Wire.API.MLS.LeafNode import Wire.API.MLS.Proposal @@ -125,5 +125,10 @@ withCommitLock lConvOrSubId gid epoch action = where ttl = fromIntegral (600 :: Int) -- 10 minutes -getConvFromGroupId :: Member (Error MLSProtocolError) r => GroupId -> Sem r (Qualified ConvOrSubConvId) -getConvFromGroupId = either (throw . mlsProtocolError . T.pack) (pure . qConvId) . groupIdToConv +getConvFromGroupId :: + Member (Error MLSProtocolError) r => + GroupId -> + Sem r (ConvType, Qualified ConvOrSubConvId) +getConvFromGroupId gid = case groupIdToConv gid of + Left e -> throw (mlsProtocolError (T.pack e)) + Right parts -> pure (parts.convType, parts.qConvId) diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index b58850b95fb..039ca96f012 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -45,11 +45,11 @@ newConnectConversationWithRemote :: newConnectConversationWithRemote creator users = NewConversation { ncMetadata = - (defConversationMetadata (tUnqualified creator)) + (defConversationMetadata (Just (tUnqualified creator))) { cnvmType = One2OneConv }, ncUsers = fmap toUserRole users, - ncProtocol = ProtocolCreateProteusTag + ncProtocol = BaseProtocolProteusTag } iUpsertOne2OneConversation :: diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 57d0f2234d4..b7d2633e921 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -794,7 +794,7 @@ getMLSOne2OneConversation lself qother = do let convId = one2OneConvId BaseProtocolMLSTag (tUntagged lself) qother foldQualified lself - (getLocalMLSOne2OneConversation lself qother) + (getLocalMLSOne2OneConversation lself) (getRemoteMLSOne2OneConversation lself qother) convId @@ -804,13 +804,12 @@ getLocalMLSOne2OneConversation :: Member P.TinyLog r ) => Local UserId -> - Qualified UserId -> Local ConvId -> Sem r Conversation -getLocalMLSOne2OneConversation lself qother lconv = do +getLocalMLSOne2OneConversation lself lconv = do mconv <- E.getConversation (tUnqualified lconv) case mconv of - Nothing -> pure (localMLSOne2OneConversation lself qother lconv) + Nothing -> pure (localMLSOne2OneConversation lself lconv) Just conv -> conversationView lself conv getRemoteMLSOne2OneConversation :: diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 9d3addbb8ae..be1c9ee41c8 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -712,22 +712,22 @@ runLocalInput = runInputConst . void toConversationCreated :: -- | The time stamp the conversation was created at UTCTime -> - -- | The domain of the user that created the conversation - Domain -> + -- | The user that created the conversation + Local UserId -> -- | The conversation to convert for sending to a remote Galley Data.Conversation -> -- | The resulting information to be sent to a remote Galley ConversationCreated ConvId -toConversationCreated now localDomain Data.Conversation {convMetadata = ConversationMetadata {..}, ..} = +toConversationCreated now lusr Data.Conversation {convMetadata = ConversationMetadata {..}, ..} = ConversationCreated { ccTime = now, - ccOrigUserId = cnvmCreator, + ccOrigUserId = tUnqualified lusr, ccCnvId = convId, ccCnvType = cnvmType, ccCnvAccess = cnvmAccess, ccCnvAccessRoles = cnvmAccessRoles, ccCnvName = cnvmName, - ccNonCreatorMembers = toMembers (filter (\lm -> lmId lm /= cnvmCreator) convLocalMembers) convRemoteMembers, + ccNonCreatorMembers = toMembers (filter (\lm -> lmId lm /= tUnqualified lusr) convLocalMembers) convRemoteMembers, ccMessageTimer = cnvmMessageTimer, ccReceiptMode = cnvmReceiptMode, ccProtocol = convProtocol @@ -739,7 +739,7 @@ toConversationCreated now localDomain Data.Conversation {convMetadata = Conversa Set OtherMember toMembers ls rs = Set.fromList $ - map (localMemberToOther localDomain) ls + map (localMemberToOther (tDomain lusr)) ls <> map remoteMemberToOther rs -- | The function converts a 'ConversationCreated' value to a @@ -791,7 +791,7 @@ fromConversationCreated loc rc@ConversationCreated {..} = { cnvmType = ccCnvType, -- FUTUREWORK: Document this is the same domain as the conversation -- domain - cnvmCreator = ccOrigUserId, + cnvmCreator = Just ccOrigUserId, cnvmAccess = ccCnvAccess, cnvmAccessRoles = ccCnvAccessRoles, cnvmName = ccCnvName, @@ -811,13 +811,13 @@ registerRemoteConversationMemberships :: (Member FederatorAccess r) => -- | The time stamp when the conversation was created UTCTime -> - -- | The domain of the user that created the conversation - Domain -> + -- | The user that created the conversation + Local UserId -> Data.Conversation -> Sem r (Set (Remote UserId)) -registerRemoteConversationMemberships now localDomain c = do +registerRemoteConversationMemberships now lusr c = do let allRemoteMembers = nubOrd (map rmId (Data.convRemoteMembers c)) - rc = toConversationCreated now localDomain c + rc = toConversationCreated now lusr c fmap toSet $ runFederatedConcurrentlyEither allRemoteMembers $ \_ -> fedClient @'Galley @"on-conversation-created" rc where diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index aafbfe0d790..ccb7e757f11 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -59,6 +59,7 @@ import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group.Serialisation import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation +import Wire.API.User createMLSSelfConversation :: Local UserId -> @@ -69,9 +70,9 @@ createMLSSelfConversation lusr = do nc = NewConversation { ncMetadata = - (defConversationMetadata usr) {cnvmType = SelfConv}, + (defConversationMetadata (Just usr)) {cnvmType = SelfConv}, ncUsers = ulFromLocals [toUserRole usr], - ncProtocol = ProtocolCreateMLSTag + ncProtocol = BaseProtocolMLSTag } meta = ncMetadata nc gid = convToGroupId . groupIdParts meta.cnvmType . fmap Conv . tUntagged . qualifyAs lusr $ cnv @@ -121,8 +122,8 @@ createConversation :: Local ConvId -> NewConversation -> Client Conversation createConversation lcnv nc = do let meta = ncMetadata nc (proto, mgid, mep, mcs) = case ncProtocol nc of - ProtocolCreateProteusTag -> (ProtocolProteus, Nothing, Nothing, Nothing) - ProtocolCreateMLSTag -> + BaseProtocolProteusTag -> (ProtocolProteus, Nothing, Nothing, Nothing) + BaseProtocolMLSTag -> let gid = convToGroupId . groupIdParts meta.cnvmType $ Conv <$> tUntagged lcnv ep = Epoch 0 cs = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 @@ -156,7 +157,7 @@ createConversation lcnv nc = do cnvmTeam meta, cnvmMessageTimer meta, cnvmReceiptMode meta, - protocolCreateToProtocolTag (ncProtocol nc), + baseProtocolToProtocol (ncProtocol nc), mgid, mep, mcs @@ -191,10 +192,9 @@ conversationMeta conv = <$> retry x1 (query1 Cql.selectConv (params LocalQuorum (Identity conv))) where toConvMeta (t, mc, a, r, r', n, i, _, mt, rm, _, _, _, _, _) = do - c <- mc let mbAccessRolesV2 = Set.fromList . Cql.fromSet <$> r' accessRoles = maybeRole t $ parseAccessRoles r mbAccessRolesV2 - pure $ ConversationMetadata t c (defAccess t a) accessRoles n i mt rm + pure $ ConversationMetadata t mc (defAccess t a) accessRoles n i mt rm getGroupInfo :: ConvId -> Client (Maybe GroupInfoData) getGroupInfo cid = do @@ -381,7 +381,6 @@ toConv :: Maybe Conversation toConv cid ms remoteMems mconv = do (cty, muid, acc, role, roleV2, nme, ti, del, timer, rm, ptag, mgid, mep, mts, mcs) <- mconv - uid <- muid let mbAccessRolesV2 = Set.fromList . Cql.fromSet <$> roleV2 accessRoles = maybeRole cty $ parseAccessRoles role mbAccessRolesV2 proto <- toProtocol ptag mgid mep (writetimeToUTC <$> mts) mcs @@ -395,7 +394,7 @@ toConv cid ms remoteMems mconv = do convMetadata = ConversationMetadata { cnvmType = cty, - cnvmCreator = uid, + cnvmCreator = muid, cnvmAccess = defAccess cty acc, cnvmAccessRoles = accessRoles, cnvmName = nme, diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index a1b6cbc93be..6190aeb8c9f 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -227,7 +227,7 @@ selectReceiptMode = "select receipt_mode from conversation where conv = ?" isConvDeleted :: PrepQuery R (Identity ConvId) (Identity (Maybe Bool)) isConvDeleted = "select deleted from conversation where conv = ?" -insertConv :: PrepQuery W (ConvId, ConvType, UserId, C.Set Access, C.Set AccessRole, Maybe Text, Maybe TeamId, Maybe Milliseconds, Maybe ReceiptMode, ProtocolTag, Maybe GroupId, Maybe Epoch, Maybe CipherSuiteTag) () +insertConv :: PrepQuery W (ConvId, ConvType, Maybe UserId, C.Set Access, C.Set AccessRole, Maybe Text, Maybe TeamId, Maybe Milliseconds, Maybe ReceiptMode, ProtocolTag, Maybe GroupId, Maybe Epoch, Maybe CipherSuiteTag) () insertConv = "insert into conversation (conv, type, creator, access, access_roles_v2, name, team, message_timer, receipt_mode, protocol, group_id, epoch, cipher_suite) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" insertMLSSelfConv :: @@ -235,7 +235,7 @@ insertMLSSelfConv :: W ( ConvId, ConvType, - UserId, + Maybe UserId, C.Set Access, C.Set AccessRole, Maybe Text, diff --git a/services/galley/src/Galley/Data/Conversation.hs b/services/galley/src/Galley/Data/Conversation.hs index 519e8608a6a..bf644d0de26 100644 --- a/services/galley/src/Galley/Data/Conversation.hs +++ b/services/galley/src/Galley/Data/Conversation.hs @@ -86,7 +86,7 @@ convAccessData c = (Set.fromList (convAccess c)) (convAccessRoles c) -convCreator :: Conversation -> UserId +convCreator :: Conversation -> Maybe UserId convCreator = cnvmCreator . convMetadata convName :: Conversation -> Maybe Text diff --git a/services/galley/src/Galley/Data/Conversation/Types.hs b/services/galley/src/Galley/Data/Conversation/Types.hs index beacb1b30b2..9dcf413fd40 100644 --- a/services/galley/src/Galley/Data/Conversation/Types.hs +++ b/services/galley/src/Galley/Data/Conversation/Types.hs @@ -24,6 +24,7 @@ import Imports import Wire.API.Conversation hiding (Conversation) import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role +import Wire.API.User -- | Internal conversation type, corresponding directly to database schema. -- Should never be sent to users (and therefore doesn't have 'FromJSON' or @@ -41,7 +42,7 @@ data Conversation = Conversation data NewConversation = NewConversation { ncMetadata :: ConversationMetadata, ncUsers :: UserList (UserId, RoleName), - ncProtocol :: ProtocolCreateTag + ncProtocol :: BaseProtocolTag } data MLSMigrationState diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 7ce920ba490..5af5b766d0e 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -342,7 +342,7 @@ postProteusConvOk = do rsp <- postConv alice [bob, jane] (Just nameMaxSize) [] Nothing Nothing (Request -> Request) -> UserId -> [Qualified UserId] -> m ResponseLBS postConvHelper g zusr newUsers = do - let conv = NewConv [] newUsers (checked "gossip") (Set.fromList []) Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolCreateProteusTag + let conv = NewConv [] newUsers (checked "gossip") (Set.fromList []) Nothing Nothing Nothing Nothing roleNameWireAdmin BaseProtocolProteusTag post $ g . path "/conversations" . zUser zusr . zConn "conn" . zType "access" . json conv postSelfConvOk :: TestM () @@ -2502,8 +2502,8 @@ postSelfConvOk = do let alice = qUnqualified qalice m <- postSelfConv alice do diff --git a/services/galley/test/integration/API/Roles.hs b/services/galley/test/integration/API/Roles.hs index 2eabd8deb96..e0f01338a24 100644 --- a/services/galley/test/integration/API/Roles.hs +++ b/services/galley/test/integration/API/Roles.hs @@ -97,7 +97,7 @@ handleConversationRoleAdmin = do let role = roleNameWireAdmin cid <- WS.bracketR3 c alice bob chuck $ \(wsA, wsB, wsC) -> do rsp <- postConvWithRole alice [bob, chuck] (Just "gossip") [] Nothing Nothing role - void $ assertConvWithRole rsp RegularConv alice qalice [qbob, qchuck] (Just "gossip") Nothing role + void $ assertConvWithRole rsp RegularConv (Just alice) qalice [qbob, qchuck] (Just "gossip") Nothing role let cid = decodeConvId rsp qcid = Qualified cid localDomain -- Make sure everyone gets the correct event @@ -138,7 +138,7 @@ handleConversationRoleMember = do let role = roleNameWireMember cid <- WS.bracketR3 c alice bob chuck $ \(wsA, wsB, wsC) -> do rsp <- postConvWithRole alice [bob, chuck] (Just "gossip") [] Nothing Nothing role - void $ assertConvWithRole rsp RegularConv alice qalice [qbob, qchuck] (Just "gossip") Nothing role + void $ assertConvWithRole rsp RegularConv (Just alice) qalice [qbob, qchuck] (Just "gossip") Nothing role let cid = decodeConvId rsp qcid = Qualified cid localDomain -- Make sure everyone gets the correct event diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 9e0dbc707bf..a0700117f41 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -623,7 +623,7 @@ createTeamConvAccessRaw u tid us name acc role mtimer convRole = do g <- viewGalley let tinfo = ConvTeamInfo tid let conv = - NewConv us [] (name >>= checked) (fromMaybe (Set.fromList []) acc) role (Just tinfo) mtimer Nothing (fromMaybe roleNameWireAdmin convRole) ProtocolCreateProteusTag + NewConv us [] (name >>= checked) (fromMaybe (Set.fromList []) acc) role (Just tinfo) mtimer Nothing (fromMaybe roleNameWireAdmin convRole) BaseProtocolProteusTag post ( g . path "/conversations" @@ -659,7 +659,7 @@ createMLSTeamConv lusr c tid users name access role timer convRole = do newConvMessageTimer = timer, newConvUsersRole = fromMaybe roleNameWireAdmin convRole, newConvReceiptMode = Nothing, - newConvProtocol = ProtocolCreateMLSTag + newConvProtocol = BaseProtocolMLSTag } r <- post @@ -690,7 +690,7 @@ createOne2OneTeamConv :: UserId -> UserId -> Maybe Text -> TeamId -> TestM Respo createOne2OneTeamConv u1 u2 n tid = do g <- viewGalley let conv = - NewConv [u2] [] (n >>= checked) mempty Nothing (Just $ ConvTeamInfo tid) Nothing Nothing roleNameWireAdmin ProtocolCreateProteusTag + NewConv [u2] [] (n >>= checked) mempty Nothing (Just $ ConvTeamInfo tid) Nothing Nothing roleNameWireAdmin BaseProtocolProteusTag post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv postConv :: @@ -704,12 +704,12 @@ postConv :: postConv u us name a r mtimer = postConvWithRole u us name a r mtimer roleNameWireAdmin defNewProteusConv :: NewConv -defNewProteusConv = NewConv [] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolCreateProteusTag +defNewProteusConv = NewConv [] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin BaseProtocolProteusTag defNewMLSConv :: NewConv defNewMLSConv = defNewProteusConv - { newConvProtocol = ProtocolCreateMLSTag + { newConvProtocol = BaseProtocolMLSTag } postConvQualified :: @@ -759,7 +759,7 @@ postConvWithRemoteUsers = postConvWithRemoteUsersGeneric $ mockReply () postTeamConv :: TeamId -> UserId -> [UserId] -> Maybe Text -> [Access] -> Maybe (Set AccessRole) -> Maybe Milliseconds -> TestM ResponseLBS postTeamConv tid u us name a r mtimer = do g <- viewGalley - let conv = NewConv us [] (name >>= checked) (Set.fromList a) r (Just (ConvTeamInfo tid)) mtimer Nothing roleNameWireAdmin ProtocolCreateProteusTag + let conv = NewConv us [] (name >>= checked) (Set.fromList a) r (Just (ConvTeamInfo tid)) mtimer Nothing roleNameWireAdmin BaseProtocolProteusTag post $ g . path "/conversations" . zUser u . zConn "conn" . zType "access" . json conv deleteTeamConv :: (HasGalley m, MonadIO m, MonadHttp m) => TeamId -> ConvId -> UserId -> m ResponseLBS @@ -797,7 +797,7 @@ postConvWithRole u members name access arole timer role = postConvWithReceipt :: UserId -> [UserId] -> Maybe Text -> [Access] -> Maybe (Set AccessRole) -> Maybe Milliseconds -> ReceiptMode -> TestM ResponseLBS postConvWithReceipt u us name a r mtimer rcpt = do g <- viewGalley - let conv = NewConv us [] (name >>= checked) (Set.fromList a) r Nothing mtimer (Just rcpt) roleNameWireAdmin ProtocolCreateProteusTag + let conv = NewConv us [] (name >>= checked) (Set.fromList a) r Nothing mtimer (Just rcpt) roleNameWireAdmin BaseProtocolProteusTag post $ g . path "/conversations" . zUser u . zConn "conn" . zType "access" . json conv postSelfConv :: UserId -> TestM ResponseLBS @@ -808,7 +808,7 @@ postSelfConv u = do postO2OConv :: UserId -> UserId -> Maybe Text -> TestM ResponseLBS postO2OConv u1 u2 n = do g <- viewGalley - let conv = NewConv [u2] [] (n >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolCreateProteusTag + let conv = NewConv [u2] [] (n >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin BaseProtocolProteusTag post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv postConnectConv :: UserId -> UserId -> Text -> Text -> Maybe Text -> TestM ResponseLBS @@ -1607,7 +1607,7 @@ assertConv :: HasCallStack => Response (Maybe Lazy.ByteString) -> ConvType -> - UserId -> + Maybe UserId -> Qualified UserId -> [Qualified UserId] -> Maybe Text -> @@ -1619,7 +1619,7 @@ assertConvWithRole :: HasCallStack => Response (Maybe Lazy.ByteString) -> ConvType -> - UserId -> + Maybe UserId -> Qualified UserId -> [Qualified UserId] -> Maybe Text -> @@ -2440,7 +2440,7 @@ mkProteusConv cnvId creator selfRole otherMembers = cnvId ( ConversationMetadata RegularConv - creator + (Just creator) [] (Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole]) (Just "federated gossip") From b9457438bc10dd9225c37d9097801820581e30a3 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 7 Jul 2023 16:04:42 +0200 Subject: [PATCH 067/225] Fix import --- services/galley/src/Galley/App.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 13e65eeff66..f29e4fcd567 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -110,7 +110,6 @@ import Util.Options import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Federation.Error -import Wire.API.Routes.FederationDomainConfig import Wire.API.Team.Feature import qualified Wire.Sem.Logger import Wire.Sem.Random.IO From f1822161c871776d583221382ba976b085b560c5 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 10 Jul 2023 12:29:53 +0200 Subject: [PATCH 068/225] Make use of deduplication for MLS (#3413) --- hack/bin/performance.py | 15 ++++++++------- hack/python/wire/api.py | 7 ++++++- services/galley/src/Galley/API/Federation.hs | 6 ++---- services/galley/src/Galley/API/MLS/Propagate.hs | 6 ++---- services/galley/src/Galley/API/MLS/Welcome.hs | 6 ++---- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/hack/bin/performance.py b/hack/bin/performance.py index 0337e366045..bd39b835a3f 100755 --- a/hack/bin/performance.py +++ b/hack/bin/performance.py @@ -75,8 +75,8 @@ def simplify_body(content): else: return content except Exception: - b = b64encode(content).decode("utf8") - return {"base64data": b, "comment": "binary blob has been replaced"} + b = b64encode(content).decode("utf8")[:1000] + return {"base64data": b, "comment": "binary blob has been replaced (and truncated)"} def simplify_response(response): @@ -323,7 +323,7 @@ def save(res, path): if not os.path.exists(d): os.makedirs(d) save_json_file(b, path) - print(f"Saving to {path}") + # print(f"Saving to {path}") return b @@ -816,13 +816,14 @@ def main_send(basedir): msg = random_msg() log.log("message_send_begin") + msg = create_application_message(admin_client_state, msg)["message"] + tbefore = time.time() res_test_msg = save( - api.mls_send_message( - ctx, create_application_message(admin_client_state, msg)["message"], - client=client_id - ), + api.mls_send_message(ctx, msg, client=client_id), j(ud, "res_test_msg.json"), ) + tafter = time.time() + print(f'Message sending took {tafter-tbefore}') log.log("message_send_end") simple_expect_status(201, res_test_msg) diff --git a/hack/python/wire/api.py b/hack/python/wire/api.py index 6e6c2169298..aea4ce10e8f 100644 --- a/hack/python/wire/api.py +++ b/hack/python/wire/api.py @@ -4,6 +4,7 @@ """ from base64 import b64encode +import time import json import random import requests @@ -134,13 +135,17 @@ def mls_welcome(ctx, user, welcome): def mls_post_commit_bundle(ctx, client, commit_bundle): url = ctx.mkurl("galley", f"/mls/commit-bundles") - return ctx.request( + tbefore = time.time() + res = ctx.request( "POST", url, headers={"Content-Type": "message/mls"}, client=client, data=commit_bundle, ) + tafter = time.time() + print(f'posting commit bundle took {tafter-tbefore:.0f} seconds') + return res def mls_send_message(ctx, msg, **kwargs): diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 2ded6f38fe6..faec60f7d5b 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -680,10 +680,8 @@ onMLSMessageSent domain rmm = Event (tUntagged rcnv) (F.rmmSubConversation rmm) (F.rmmSender rmm) (F.rmmTime rmm) $ EdMLSMessage (fromBase64ByteString (F.rmmMessage rmm)) - -- FUTUREWORK: Send only 1 push, after broken Eq, Ord instances of Recipient is fixed. Find other place via tag [FTRPUSHORD] - for_ recipients $ \(u, c) -> do - runMessagePush loc (Just (tUntagged rcnv)) $ - newMessagePush mempty Nothing (F.rmmMetadata rmm) [(u, c)] e + runMessagePush loc (Just (tUntagged rcnv)) $ + newMessagePush mempty Nothing (F.rmmMetadata rmm) recipients e queryGroupInfo :: ( Member ConversationStore r, diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index cd43d1759d0..85a69b83bd9 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -85,10 +85,8 @@ propagateMessage qusr mSenderClient lConvOrSub con msg cm = do sconv = snd (qUnqualified qt) e = Event qcnv sconv qusr now $ EdMLSMessage msg.raw - -- FUTUREWORK: Send only 1 push, after broken Eq, Ord instances of Recipient is fixed. Find other place via tag [FTRPUSHORD] - for_ (lmems >>= localMemberMLSClients mlsConv) $ \(u, c) -> - runMessagePush lConvOrSub (Just qcnv) $ - newMessagePush botMap con mm [(u, c)] e + runMessagePush lConvOrSub (Just qcnv) $ + newMessagePush botMap con mm (lmems >>= localMemberMLSClients mlsConv) e -- send to remotes unreachableFromList . concat diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index 41ecd040d82..4b77d6e08b1 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -87,10 +87,8 @@ sendLocalWelcomes :: Sem r () sendLocalWelcomes qcnv qusr con now welcome lclients = do let e = Event qcnv Nothing qusr now $ EdMLSWelcome welcome.raw - -- FUTUREWORK: Send only 1 push, after broken Eq, Ord instances of Recipient is fixed. Find other place via tag [FTRPUSHORD] - for_ (tUnqualified lclients) $ \(u, c) -> - runMessagePush lclients (Just qcnv) $ - newMessagePush mempty con defMessageMetadata [(u, c)] e + runMessagePush lclients (Just qcnv) $ + newMessagePush mempty con defMessageMetadata (tUnqualified lclients) e sendRemoteWelcomes :: ( Member FederatorAccess r, From 99abc0f8cc81658cc78375014199c3445fd33bf7 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 13 Jul 2023 09:50:40 +0200 Subject: [PATCH 069/225] fix interdependency between feature configs (#3424) --- .../galley/src/Galley/API/Teams/Features.hs | 2 +- .../test/integration/API/Teams/Feature.hs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index d9ea2fcee36..7daa81b4156 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -382,7 +382,7 @@ instance SetFeatureConfig MlsE2EIdConfig instance SetFeatureConfig MlsMigrationConfig where type SetConfigForTeamConstraints MlsMigrationConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) setConfigForTeam tid wsnl = do - mlsConfig <- getConfigForTeam @MlsMigrationConfig tid + mlsConfig <- getConfigForTeam @MLSConfig tid unless ( -- when MLS migration is enabled, MLS needs to be enabled as well wssStatus wsnl == FeatureStatusDisabled || wsStatus mlsConfig == FeatureStatusEnabled diff --git a/services/galley/test/integration/API/Teams/Feature.hs b/services/galley/test/integration/API/Teams/Feature.hs index 84381c303d1..d5d5e053143 100644 --- a/services/galley/test/integration/API/Teams/Feature.hs +++ b/services/galley/test/integration/API/Teams/Feature.hs @@ -1191,9 +1191,27 @@ testNonTrivialConfigNoTTL defaultCfg = do -- unlock feature setLockStatus LockStatusUnlocked + let defaultMLSConfig = + WithStatusNoLock + { wssStatus = FeatureStatusEnabled, + wssConfig = + MLSConfig + { mlsProtocolToggleUsers = [], + mlsDefaultProtocol = ProtocolMLSTag, + mlsAllowedCipherSuites = [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519], + mlsDefaultCipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519, + mlsSupportedProtocols = [ProtocolProteusTag, ProtocolMLSTag] + }, + wssTTL = FeatureTTLUnlimited + } + config2 <- liftIO $ generate arbitrary <&> (forgetLock . setTTL FeatureTTLUnlimited) config3 <- liftIO $ generate arbitrary <&> (forgetLock . setTTL FeatureTTLUnlimited) + putTeamFeatureFlagWithGalley @MLSConfig galley owner tid defaultMLSConfig + !!! statusCode + === const 200 + WS.bracketR cannon member $ \ws -> do setForTeam config2 void . liftIO $ From 870e5f7f2c692809abb26a69f7d93697e4405009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Ivankovi=C4=87?= <33936733+Veki301@users.noreply.github.com> Date: Tue, 25 Jul 2023 10:09:55 +0200 Subject: [PATCH 070/225] WIP: helm chart for outlook-integration --- charts/outlook-addin/Chart.yaml | 4 ++ .../outlook-addin/templates/deployment.yaml | 39 +++++++++++++++++++ charts/outlook-addin/templates/ingress.yaml | 15 +++++++ charts/outlook-addin/templates/service.yaml | 12 ++++++ charts/outlook-addin/values.yaml | 5 +++ 5 files changed, 75 insertions(+) create mode 100644 charts/outlook-addin/Chart.yaml create mode 100644 charts/outlook-addin/templates/deployment.yaml create mode 100644 charts/outlook-addin/templates/ingress.yaml create mode 100644 charts/outlook-addin/templates/service.yaml create mode 100644 charts/outlook-addin/values.yaml diff --git a/charts/outlook-addin/Chart.yaml b/charts/outlook-addin/Chart.yaml new file mode 100644 index 00000000000..02c87c184a1 --- /dev/null +++ b/charts/outlook-addin/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +name: outlook-addin +version: 4.35.0 +description: Helm chart for outlook addin for Wire diff --git a/charts/outlook-addin/templates/deployment.yaml b/charts/outlook-addin/templates/deployment.yaml new file mode 100644 index 00000000000..08d1e9ee29e --- /dev/null +++ b/charts/outlook-addin/templates/deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: outlook-addin + labels: + app: outlook-addin +spec: + replicas: 3 + selector: + matchLabels: + app: outlook-addin + template: + metadata: + labels: + app: outlook-addin + spec: + containers: + - name: outlook-addin + image: { { .Values.containerImage } } + ports: + - name: http + containerPort: 80 + env: + - name: BASE_URL + value: "{{ .Values.baseUrl }}" + - name: CLIENT_ID + value: "{{ .Values.clientId }}" + - name: WIRE_API_BASE_URL + value: "{{ .Values.wireApiBaseUrl }}" + - name: WIRE_AUTHORIZATION_ENDPOINT + value: "{{ .Values.wireAuthorizationEndpoint }}" + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http diff --git a/charts/outlook-addin/templates/ingress.yaml b/charts/outlook-addin/templates/ingress.yaml new file mode 100644 index 00000000000..67678176378 --- /dev/null +++ b/charts/outlook-addin/templates/ingress.yaml @@ -0,0 +1,15 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: outlook-addin-ingress +spec: + rules: + - host: https://outlook.zhica.eu + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: outlook-addin + port: + number: 8080 diff --git a/charts/outlook-addin/templates/service.yaml b/charts/outlook-addin/templates/service.yaml new file mode 100644 index 00000000000..a496331e031 --- /dev/null +++ b/charts/outlook-addin/templates/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: outlook-addin +spec: + selector: + app: outlook-addin + ports: + - name: http + port: 8080 + targetPort: http + type: ClusterIP diff --git a/charts/outlook-addin/values.yaml b/charts/outlook-addin/values.yaml new file mode 100644 index 00000000000..6a0242de68f --- /dev/null +++ b/charts/outlook-addin/values.yaml @@ -0,0 +1,5 @@ +baseUrl: "https://outlook.zhica.eu" +clientId: "" +containerImage: "quay.io/wire/outlook-addin:0.1.3" +wireApiBaseUrl: "https://nginz-https.zhica.eu" +wireAuthorizationEndpoint: "https://webapp.zhica.eu/auth" From b09e8577723821489a744d037b8c0360a5b81d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Ivankovi=C4=87?= <33936733+Veki301@users.noreply.github.com> Date: Wed, 26 Jul 2023 09:23:34 +0200 Subject: [PATCH 071/225] chart for outlook-addin with working ingress and certificates (cert-manager or own) --- charts/outlook-addin/templates/_helpers.tpl | 82 +++++++++++++++++++ .../outlook-addin/templates/deployment.yaml | 13 +-- charts/outlook-addin/templates/ingress.yaml | 25 +++++- .../templates/secret-or-certificate.yaml | 34 ++++++++ charts/outlook-addin/templates/service.yaml | 9 +- charts/outlook-addin/values.yaml | 13 ++- 6 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 charts/outlook-addin/templates/_helpers.tpl create mode 100644 charts/outlook-addin/templates/secret-or-certificate.yaml diff --git a/charts/outlook-addin/templates/_helpers.tpl b/charts/outlook-addin/templates/_helpers.tpl new file mode 100644 index 00000000000..c2f40c04c95 --- /dev/null +++ b/charts/outlook-addin/templates/_helpers.tpl @@ -0,0 +1,82 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "outlook.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "outlook.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "outlook.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "outlook.labels" -}} +helm.sh/chart: {{ include "outlook.chart" . }} +{{ include "outlook.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "outlook.selectorLabels" -}} +app.kubernetes.io/name: {{ include "outlook.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* Allow KubeVersion to be overridden. */}} +{{- define "kubeVersion" -}} + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} +{{- end -}} + +{{/* Get Ingress API Version */}} +{{- define "ingress.apiVersion" -}} + {{- if and (.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" (include "kubeVersion" .)) -}} + {{- print "networking.k8s.io/v1" -}} + {{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -}} + {{- print "networking.k8s.io/v1beta1" -}} + {{- else -}} + {{- print "extensions/v1beta1" -}} + {{- end -}} +{{- end -}} + +{{/* Check Ingress stability */}} +{{- define "ingress.isStable" -}} + {{- eq (include "ingress.apiVersion" .) "networking.k8s.io/v1" -}} +{{- end -}} + +{{/* Check Ingress supports pathType */}} +{{/* pathType was added to networking.k8s.io/v1beta1 in Kubernetes 1.18 */}} +{{- define "ingress.supportsPathType" -}} + {{- or (eq (include "ingress.isStable" .) "true") (and (eq (include "ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" (include "kubeVersion" .))) -}} +{{- end -}} + +{{- define "ingress.FieldNotAnnotation" -}} + {{- (semverCompare ">= 1.27-0" (include "kubeVersion" .)) -}} +{{- end -}} diff --git a/charts/outlook-addin/templates/deployment.yaml b/charts/outlook-addin/templates/deployment.yaml index 08d1e9ee29e..37600f7ad55 100644 --- a/charts/outlook-addin/templates/deployment.yaml +++ b/charts/outlook-addin/templates/deployment.yaml @@ -1,28 +1,29 @@ +--- apiVersion: apps/v1 kind: Deployment metadata: - name: outlook-addin + name: {{ include "outlook.fullname" . }} labels: - app: outlook-addin + {{- include "outlook.labels" . | nindent 4 }} spec: replicas: 3 selector: matchLabels: - app: outlook-addin + app: {{ include "outlook.fullname" . }} template: metadata: labels: - app: outlook-addin + app: {{ include "outlook.fullname" . }} spec: containers: - - name: outlook-addin + - name: {{ include "outlook.fullname" . }} image: { { .Values.containerImage } } ports: - name: http containerPort: 80 env: - name: BASE_URL - value: "{{ .Values.baseUrl }}" + value: "https://{{ .Values.host }}" - name: CLIENT_ID value: "{{ .Values.clientId }}" - name: WIRE_API_BASE_URL diff --git a/charts/outlook-addin/templates/ingress.yaml b/charts/outlook-addin/templates/ingress.yaml index 67678176378..87419a10c23 100644 --- a/charts/outlook-addin/templates/ingress.yaml +++ b/charts/outlook-addin/templates/ingress.yaml @@ -1,15 +1,32 @@ -apiVersion: networking.k8s.io/v1 +{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}} +{{- $ingressFieldNotAnnotation := eq (include "ingress.FieldNotAnnotation" .) "true" -}} +{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}} +apiVersion: {{ include "ingress.apiVersion" . }} kind: Ingress -metadata: outlook-addin-ingress +metadata: + name: "{{ include "outlook.fullname" . }}" + labels: + {{- include "outlook.labels" . | nindent 4 }} + annotations: + {{- if not $ingressFieldNotAnnotation }} + kubernetes.io/ingress.class: "{{ .Values.config.ingressClass }}" + {{- end }} spec: + {{- if $ingressFieldNotAnnotation }} + ingressClassName: "{{ .Values.config.ingressClass }}" + {{- end }} + tls: + - hosts: + - "{{ .Values.host }}" + secretName: "{{ include "outlook.fullname" . }}" rules: - - host: https://outlook.zhica.eu + - host: "{{ .Values.host }}" http: paths: - path: / pathType: Prefix backend: service: - name: outlook-addin + name: {{ include "outlook.fullname" . }} port: number: 8080 diff --git a/charts/outlook-addin/templates/secret-or-certificate.yaml b/charts/outlook-addin/templates/secret-or-certificate.yaml new file mode 100644 index 00000000000..5199985ed9d --- /dev/null +++ b/charts/outlook-addin/templates/secret-or-certificate.yaml @@ -0,0 +1,34 @@ +{{- if .Values.tls.issuerRef -}} +{{- if or .Values.tls.key .Values.tls.crt }} +{{- fail "ingress.issuer and ingress.{crt,key} are mutually exclusive" -}} +{{- end -}} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: "{{ include "outlook.fullname" . }}" + labels: + {{- include "outlook.labels" . | nindent 4 }} +spec: + dnsNames: + - {{ .Values.host }} + secretName: "{{ include "outlook.fullname" . }}" + issuerRef: + {{- toYaml .Values.tls.issuerRef | nindent 4 }} + privateKey: + rotationPolicy: Always + algorithm: ECDSA + size: 384 +{{- else if and .Values.tls.key .Values.tls.crt -}} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ include "outlook.fullname" . }}" + labels: + {{- include "outlook.labels" . | nindent 4 }} +type: kubernetes.io/tls +data: + tls.key: {{ required "tls.key is required" .Values.tls.key | b64enc }} + tls.crt: {{ required "tls.crt is required" .Values.tls.crt | b64enc }} +{{- else -}} +{{- fail "must specify tls.key and tls.crt , or tls.issuerRef" -}} +{{- end -}} \ No newline at end of file diff --git a/charts/outlook-addin/templates/service.yaml b/charts/outlook-addin/templates/service.yaml index a496331e031..254430f197e 100644 --- a/charts/outlook-addin/templates/service.yaml +++ b/charts/outlook-addin/templates/service.yaml @@ -1,12 +1,17 @@ +--- apiVersion: v1 kind: Service metadata: - name: outlook-addin + name: {{ include "outlook.fullname" . }} + labels: + {{- include "outlook.labels" . | nindent 4 }} spec: selector: - app: outlook-addin + app: {{ include "outlook.fullname" . }} ports: - name: http port: 8080 targetPort: http type: ClusterIP + + diff --git a/charts/outlook-addin/values.yaml b/charts/outlook-addin/values.yaml index 6a0242de68f..10220b546f5 100644 --- a/charts/outlook-addin/values.yaml +++ b/charts/outlook-addin/values.yaml @@ -1,5 +1,10 @@ -baseUrl: "https://outlook.zhica.eu" -clientId: "" containerImage: "quay.io/wire/outlook-addin:0.1.3" -wireApiBaseUrl: "https://nginz-https.zhica.eu" -wireAuthorizationEndpoint: "https://webapp.zhica.eu/auth" +ingressClass: nginx +#host: "outlook.example.com" +#wireApiBaseUrl: "https://nginz-https.example.com" +#wireAuthorizationEndpoint: "https://webapp.example.com/auth" +#tls: +# issuerRef: +# name: letsencrypt-http01 +# Should probably comment here +#clientId: "" From 64d488d9fcdeef46f002537882bda7350d8826d4 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 27 Jul 2023 10:32:08 +0200 Subject: [PATCH 072/225] Welcome messages should not be sent to creator (#3392) * Do not send welcome message to sender --- changelog.d/3-bug-fixes/sender-welcome | 1 + integration/test/Test/MLS.hs | 7 +++---- services/galley/src/Galley/API/MLS/Message.hs | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 changelog.d/3-bug-fixes/sender-welcome diff --git a/changelog.d/3-bug-fixes/sender-welcome b/changelog.d/3-bug-fixes/sender-welcome new file mode 100644 index 00000000000..22503e2aab9 --- /dev/null +++ b/changelog.d/3-bug-fixes/sender-welcome @@ -0,0 +1 @@ +Welcome messages are not sent anymore to the creator of an MLS group on the first commit diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 58a03989457..7cd2b466a12 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -111,13 +111,12 @@ testMixedProtocolAddUsers secondDomain = do traverse_ uploadNewKeyPackage [bob1] - withWebSockets [alice, bob] $ \wss -> do + withWebSocket bob $ \ws -> do mp <- createAddCommit alice1 [bob] welcome <- assertJust "should have welcome" mp.welcome void $ sendAndConsumeCommitBundle mp - for_ wss $ \ws -> do - n <- awaitMatch 3 (\n -> nPayload n %. "type" `isEqual` "conversation.mls-welcome") ws - nPayload n %. "data" `shouldMatch` T.decodeUtf8 (Base64.encode welcome) + n <- awaitMatch 3 (\n -> nPayload n %. "type" `isEqual` "conversation.mls-welcome") ws + nPayload n %. "data" `shouldMatch` T.decodeUtf8 (Base64.encode welcome) testMixedProtocolUserLeaves :: HasCallStack => Domain -> App () testMixedProtocolUserLeaves secondDomain = do diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 89000e1adc4..3db183b2b2d 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -221,7 +221,10 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do bundle.epoch action bundle.commit.value - pure (events, cmIdentities (paAdd action)) + -- the sender client is included in the Add action on the first commit, + -- but it doesn't need to get a welcome message, so we filter it out here + let newClients = filter ((/=) senderIdentity) (cmIdentities (paAdd action)) + pure (events, newClients) SenderExternal _ -> throw (mlsProtocolError "Unexpected sender") SenderNewMemberProposal -> throw (mlsProtocolError "Unexpected sender") SenderNewMemberCommit -> do From ce82b91a292d4469b8b7dd3e38fe87ba25fa6997 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 27 Jul 2023 12:50:48 +0000 Subject: [PATCH 073/225] change status of federation-denied back to 400 --- services/federator/src/Federator/Validation.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/federator/src/Federator/Validation.hs b/services/federator/src/Federator/Validation.hs index 2a7de661bac..4973c8dcde8 100644 --- a/services/federator/src/Federator/Validation.hs +++ b/services/federator/src/Federator/Validation.hs @@ -87,7 +87,7 @@ validationErrorStatus :: ValidationError -> HTTP.Status -- the FederationDenied case is handled differently, because it may be caused -- by wrong input in the original request, so we let this error propagate to the -- client -validationErrorStatus (FederationDenied _) = HTTP.status422 +validationErrorStatus (FederationDenied _) = HTTP.status400 validationErrorStatus _ = HTTP.status403 -- | Validates an already-parsed domain against the allow list (stored in From 9b0edf109e62e7616ebe57107e865c21d72eb5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Ivankovi=C4=87?= <33936733+Veki301@users.noreply.github.com> Date: Tue, 1 Aug 2023 09:13:12 +0200 Subject: [PATCH 074/225] add README.md --- charts/outlook-addin/README.md | 177 +++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 charts/outlook-addin/README.md diff --git a/charts/outlook-addin/README.md b/charts/outlook-addin/README.md new file mode 100644 index 00000000000..2aed4a217e9 --- /dev/null +++ b/charts/outlook-addin/README.md @@ -0,0 +1,177 @@ +# How to install Outlook AddIn for Wire-Server + +WIP: Some of these configurations are subject to change down the line. This documentation will be updated accordingly as they happen. + +This document assumes you already have an instance of wire-server running. If you don't, follow this [documentation](https://docker.com) + +## Set up OAuth with wire-server + +To use OAuth, first you will need to enable it by editing `values/wire-server/values.yaml` as follows: + +``` +brig: + # ... + config: + # ... + optSettings: + # ... + setOAuthEnabled: true +``` + +Then you will need to generate a key using "OKP" (Octet Key Pair) and the "Ed25519" curve with OpenSSL that will be used as JWK (JSON Web Key) in the wire-server helm chart. This key will be used to sign and verify [OAuth](https://docs.wire.com/developer/reference/oauth.html#setting-up-public-and-private-keys) access tokens. + +``` +openssl genpkey -algorithm Ed25519 -out private_key.pem +``` + +You can find a `generate_jwk.py` in this chart which you can use to generate the JWK in JSON format that can be used in your wire-server helm chart. Use it in `brig` and `nginz` namespaces in `values/wire-server/secrets.yaml` like shown below. + +``` +brig: + secrets: + oauthJwkKeyPair: | + { + "kty": "OKP", + "crv": "Ed25519", + "x": "...", + "d": "...", + "kid": "..." + } +``` + +``` +# values.yaml or secrets.yaml +nginz: + secrets: + oAuth: + publicKeys: | + { + "kty": "OKP", + "crv": "Ed25519", + "x": "...", + "kid": "..." + } +``` + +Now redeploy wire-server chart: + +``` +d helm upgrade --install wire-server charts/wire-server --values values/wire-server/values.yaml --values/wire-server/secrets.yaml +``` + +## Outlook integration feature flag + +By default, outlook addin as a feature is disabled for all teams. To change this make the following changes in your configuration in `galley` namespace: + +``` +outlookCalIntegration: + config: + # ... + settings: + # ... + featureFlags: + # ... + defaults: + status: disabled + lockStatus: locked +``` + +Redeploy wire-server for these changes to take effect. + +NOTE: As of the time of writing `outlookCalIntegration` is not a typo! (at least not in this documentation) + +If you have an existing team in your wire-server that did not have this feature flag enabled prior to this. You will need to enable that feature flag through [Backoffice API](https://github.com/wireapp/wire-server/tree/05778a2b14ac5aaffca937d6e2cdd9b7b5f3106d/charts/backoffice). + +NOTE: As of the time of writing Backoffice API endpoint for enabling this feature flag is not working as intended so please follow this manual on how to do it with curl on the machine wire-server is running on. + +### How to manually enable outlookCalIntegration feature flag for a team + +You will need your `teamId` (you can find it in TeamSettings under Customization tab). +List all your pods in your Kubernetes cluster with: + +``` +d kubectl get pods -owide +``` + +Copy the name of one of your galley pods and run: + +``` +d kubectl exec -it galley_pod_name /bin/bash +``` + +In the new terminal type: + +``` +curl -v -XPATCH 'http://localhost:8080/i/teams/your_teamID/features/outlookCalIntegration' -H 'content-type: application/json;charset=utf-8' -d '{"status": "enabled", "lockStatus": "unlocked"}' +``` + +Do this for all the teams you want to enable the feature for. + +## Create new client service for OAuth in Brig + +List all your pods in your Kubernetes cluster with: + +``` +d kubectl get pods -owide +``` + +Copy the name of one of your brig pods and run: + +``` +d kubectl exec -it brig_pod_name /bin/bash +``` + +In the new terminal type: + +``` +curl -s -X POST localhost:8080/i/oauth/clients \ + -H "Content-Type: application/json" \ + -d '{ + "application_name":"Wire Microsoft Outlook Calendar Add-in", + "redirect_url":"https://outlook.your_domain.com/callback.html" + }' +``` + +You will get back a response in JSON format that should look like: + +``` +{"client_id":"b2b3...","client_secret":"9ee60..."} +``` + +Write down your client_id as it will be needed later. + +## Deploying Wire Outlook AddIn + +Create a new `values.yaml` file in `values/outlook-addin` directory (create the directory too if missing). +Append the following configuration (change the example.com with your domain). + +``` +host: "outlook.example.com" # this entry has to be without https://!!! +wireApiBaseUrl: "https://nginz-https.example.com" +wireAuthorizationEndpoint: "https://webapp.example.com/auth" +tls: + issuerRef: + name: letsencrypt-http01 +# Should probably comment here +clientId: "" +``` + +As of the time of writing nginz used by wire-server is not set up to whitelist outlook subdomain for CORS requests. So please edit `charts/wire-server/charts/nginz/values.yaml` and find under `nginx_conf`: + +``` + allowlisted_origins: + - webapp + - teams + - account + - outlook # add outlook entry so your addin doesnt get CORS blocked +``` + +Now deploy outlook addin chart with: + +``` +d helm upgrade --install outlook-addin charts/outlook-addin --values values/outlook-addin/values.yaml +``` + +## Install Wire AddIn in Microsoft Outlook + +After deploying `outlook-addin` you will be able to find `manifest.xml` file on https://outlook.your.domain.com/manifest.xml which you can use to install the addin in your outlook. You can find instructions and screenshots how to do it [here](https://github.com/tlebon/outlook-addin/blob/staging/README.md#how-to-install-the-add-in-in-ms-outlook). From 07974659160f106bbe5cee442c85da0d1b3ec891 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 3 Aug 2023 09:46:53 +0200 Subject: [PATCH 075/225] MLS conversation limits (#3468) * Remove member limit for MLS conversations * Restore HardTruncationLimit to 2000 * Fix client dir path in performance script --- changelog.d/2-features/mls-conv-limits | 1 + hack/python/wire/mlscli.py | 7 ++++++- libs/wire-api/src/Wire/API/Team/Member.hs | 3 +-- services/galley/src/Galley/API/Action.hs | 4 ++-- services/galley/src/Galley/API/Update.hs | 5 +++-- services/galley/src/Galley/API/Util.hs | 11 ++++++----- services/galley/src/Galley/Data/Conversation/Types.hs | 3 +++ 7 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 changelog.d/2-features/mls-conv-limits diff --git a/changelog.d/2-features/mls-conv-limits b/changelog.d/2-features/mls-conv-limits new file mode 100644 index 00000000000..6a499746c4d --- /dev/null +++ b/changelog.d/2-features/mls-conv-limits @@ -0,0 +1 @@ +Remove conversation size limit for MLS conversations diff --git a/hack/python/wire/mlscli.py b/hack/python/wire/mlscli.py index 1e0b0f09f08..42b71b459a5 100644 --- a/hack/python/wire/mlscli.py +++ b/hack/python/wire/mlscli.py @@ -40,10 +40,12 @@ def mlscli(state, client_identity, args, stdin=None): else: args_substd.append(arg) + basedir = os.path.join(cdir, cid2str(state.client_identity) ) + os.makedirs(basedir, exist_ok=True) all_args = [ "mls-test-cli", "--store", - os.path.join(cdir, cid2str(state.client_identity), "store"), + os.path.join(basedir, "store"), ] + args_substd # TODO: maybe add cwd=cdir, not sure if necessary @@ -154,6 +156,9 @@ def restore_backup_into(client_dir): os.system(f'cp -r /tmp/client_state_backup {client_dir}') return ClientState.load(client_dir) + def __repr__(self): + values = ', '.join(f'{k}={str(getattr(self, k))}' for k in self.saveable_attrs.keys()) + return f'{self.__class__.__name__}({values})' def key_package_file(state, ref): return os.path.join(state.client_dir, cid2str(state.client_identity), ref.hex()) diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index 785e1885c6b..74006175133 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -271,8 +271,7 @@ instance ToSchema (TeamMember' tag) => ToSchema (TeamMemberList' tag) where <*> _teamMemberListType .= fieldWithDocModifier "hasMore" (description ?~ "true if 'members' doesn't contain all team members") schema --- TODO: Revert this to 2000 before mergin 'mls' to the develop branch -type HardTruncationLimit = (100000 :: Nat) +type HardTruncationLimit = (2000 :: Nat) hardTruncationLimit :: Integral a => a hardTruncationLimit = fromIntegral $ natVal (Proxy @HardTruncationLimit) diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 3ac2cb0a721..00b5b059ae3 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -64,7 +64,7 @@ import Galley.API.Teams.Features.Get import Galley.API.Util import Galley.Data.Conversation import qualified Galley.Data.Conversation as Data -import Galley.Data.Conversation.Types (mlsMetadata) +import Galley.Data.Conversation.Types import Galley.Data.Scope (Scope (ReusableCode)) import Galley.Data.Services import Galley.Effects @@ -455,7 +455,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do let newMembers = ulNewMembers lconv conv . toUserList lconv $ invited lusr <- ensureLocal lconv qusr - ensureMemberLimit (toList (convLocalMembers conv)) newMembers + ensureMemberLimit (convProtocolTag conv) (toList (convLocalMembers conv)) newMembers ensureAccess conv InviteAccess checkLocals lusr (convTeam conv) (ulLocals newMembers) checkRemotes lusr (ulRemotes newMembers) diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 016ce7bba72..c38401ec436 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -92,6 +92,7 @@ import qualified Galley.API.Query as Query import Galley.API.Util import Galley.App import qualified Galley.Data.Conversation as Data +import qualified Galley.Data.Conversation.Types as Data import Galley.Data.Services as Data import Galley.Data.Types hiding (Conversation) import Galley.Effects @@ -804,7 +805,7 @@ joinConversation lusr zcon conv access = do ensureConversationAccess (tUnqualified lusr) conv access ensureGroupConversation conv -- FUTUREWORK: remote users? - ensureMemberLimit (toList $ Data.convLocalMembers conv) [tUnqualified lusr] + ensureMemberLimit (Data.convProtocolTag conv) (toList $ Data.convLocalMembers conv) [tUnqualified lusr] getUpdateResult $ do -- NOTE: When joining conversations, all users become members -- as this is our desired behavior for these types of conversations @@ -1597,7 +1598,7 @@ addBot lusr zcon b = do ensureActionAllowed SAddConversationMember self unless (any ((== b ^. addBotId) . botMemId) bots) $ do let botId = qualifyAs lusr (botUserId (b ^. addBotId)) - ensureMemberLimit (toList $ Data.convLocalMembers c) [tUntagged botId] + ensureMemberLimit (Data.convProtocolTag c) (toList $ Data.convLocalMembers c) [tUntagged botId] pure (bots, users) rmBotH :: diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 34c41b4fb51..97063670ee9 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -54,7 +54,7 @@ import Galley.Effects.MemberStore import Galley.Effects.TeamStore import Galley.Intra.Push import Galley.Options -import Galley.Types.Conversations.Members (LocalMember (..), RemoteMember (..), localMemberToOther, remoteMemberQualify, remoteMemberToOther) +import Galley.Types.Conversations.Members import Galley.Types.Conversations.Roles import Galley.Types.Teams import Galley.Types.UserList @@ -84,8 +84,7 @@ import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util import Wire.API.Team.Member import Wire.API.Team.Role -import Wire.API.User (VerificationAction) -import qualified Wire.API.User as User +import Wire.API.User hiding (userId) import Wire.API.User.Auth.ReAuth type JSON = Media "application" "json" @@ -107,7 +106,7 @@ ensureAccessRole roles users = do activated <- lookupActivatedUsers (fst <$> users) let guestsExist = length activated /= length users unless (not guestsExist || GuestAccessRole `Set.member` roles) $ throwS @'ConvAccessDenied - let botsExist = any (isJust . User.userService) activated + let botsExist = any (isJust . userService) activated unless (not botsExist || ServiceAccessRole `Set.member` roles) $ throwS @'ConvAccessDenied -- | Check that the given user is either part of the same team as the other @@ -995,10 +994,12 @@ ensureMemberLimit :: Member (Input Opts) r ) ) => + ProtocolTag -> [LocalMember] -> f a -> Sem r () -ensureMemberLimit old new = do +ensureMemberLimit ProtocolMLSTag _ _ = pure () +ensureMemberLimit _ old new = do o <- input let maxSize = fromIntegral (o ^. optSettings . setMaxConvSize) when (length old + length new > maxSize) $ diff --git a/services/galley/src/Galley/Data/Conversation/Types.hs b/services/galley/src/Galley/Data/Conversation/Types.hs index 9dcf413fd40..b7f3624b988 100644 --- a/services/galley/src/Galley/Data/Conversation/Types.hs +++ b/services/galley/src/Galley/Data/Conversation/Types.hs @@ -39,6 +39,9 @@ data Conversation = Conversation } deriving (Show) +convProtocolTag :: Conversation -> ProtocolTag +convProtocolTag = protocolTag . convProtocol + data NewConversation = NewConversation { ncMetadata :: ConversationMetadata, ncUsers :: UserList (UserId, RoleName), From 656244d305b6dad51b554ce32d06e2389654c038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Ivankovi=C4=87?= <33936733+Veki301@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:45:47 +0200 Subject: [PATCH 076/225] add CORS, disclaimer for hardcoded domains in linked document --- charts/outlook-addin/README.md | 1 + charts/outlook-addin/templates/ingress.yaml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/charts/outlook-addin/README.md b/charts/outlook-addin/README.md index 2aed4a217e9..c0acd0b7109 100644 --- a/charts/outlook-addin/README.md +++ b/charts/outlook-addin/README.md @@ -175,3 +175,4 @@ d helm upgrade --install outlook-addin charts/outlook-addin --values values/outl ## Install Wire AddIn in Microsoft Outlook After deploying `outlook-addin` you will be able to find `manifest.xml` file on https://outlook.your.domain.com/manifest.xml which you can use to install the addin in your outlook. You can find instructions and screenshots how to do it [here](https://github.com/tlebon/outlook-addin/blob/staging/README.md#how-to-install-the-add-in-in-ms-outlook). +NOTE: Links in the outlined documents are hardcoded for a testing/prod environment, any reference to zinfra.io or wire.com in it should be treated as example.com. diff --git a/charts/outlook-addin/templates/ingress.yaml b/charts/outlook-addin/templates/ingress.yaml index 87419a10c23..f006d3dc0e6 100644 --- a/charts/outlook-addin/templates/ingress.yaml +++ b/charts/outlook-addin/templates/ingress.yaml @@ -11,6 +11,8 @@ metadata: {{- if not $ingressFieldNotAnnotation }} kubernetes.io/ingress.class: "{{ .Values.config.ingressClass }}" {{- end }} + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: "{{ required "Must specify allowOrigin" .Values.allowOrigin }}" spec: {{- if $ingressFieldNotAnnotation }} ingressClassName: "{{ .Values.config.ingressClass }}" From 73433b45c6469b51e94cf772be27eeb5f0383700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Ivankovi=C4=87?= <33936733+Veki301@users.noreply.github.com> Date: Thu, 10 Aug 2023 10:42:48 +0200 Subject: [PATCH 077/225] fix: bad link --- charts/outlook-addin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/outlook-addin/README.md b/charts/outlook-addin/README.md index c0acd0b7109..276857e6ec8 100644 --- a/charts/outlook-addin/README.md +++ b/charts/outlook-addin/README.md @@ -2,7 +2,7 @@ WIP: Some of these configurations are subject to change down the line. This documentation will be updated accordingly as they happen. -This document assumes you already have an instance of wire-server running. If you don't, follow this [documentation](https://docker.com) +This document assumes you already have an instance of wire-server running. If you don't, follow this [documentation](https://github.com/wireapp/wire-server-deploy/blob/master/offline/docs.md) ## Set up OAuth with wire-server From 17b45f6d069e886dbcedd5aaa03338ffc80fa570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Ivankovi=C4=87?= <33936733+Veki301@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:13:44 +0200 Subject: [PATCH 078/225] add generate_jwk.py script for outlook-addin --- charts/outlook-addin/generate_jwk.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 charts/outlook-addin/generate_jwk.py diff --git a/charts/outlook-addin/generate_jwk.py b/charts/outlook-addin/generate_jwk.py new file mode 100644 index 00000000000..64ff106fe52 --- /dev/null +++ b/charts/outlook-addin/generate_jwk.py @@ -0,0 +1,23 @@ +import json +from jwcrypto import jwk +import base64 + +def pem_to_jwk(pem_key, is_private=True): + key = jwk.JWK.from_pem(pem_key) + if is_private: + key_dict = key.export(as_dict=True, private_key=True) + else: + key_dict = key.export(as_dict=True, private_key=False) + return key_dict + +def convert_to_pem(base64_key): + pem_key = base64.b64decode(base64_key) + return pem_key + +with open("private_key.pem", "rb") as f: + private_key_pem = f.read() + private_key_b64 = base64.b64encode(private_key_pem).decode('utf-8') + private_jwk = pem_to_jwk(convert_to_pem(private_key_b64), is_private=True) + +print("Private JWK:") +print(json.dumps(private_jwk, indent=2)) From ed15a09eb53e9c343e9be459ea24d2071bf808fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Ivankovi=C4=87?= <33936733+Veki301@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:21:30 +0200 Subject: [PATCH 079/225] fix: typos --- charts/outlook-addin/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/charts/outlook-addin/README.md b/charts/outlook-addin/README.md index 276857e6ec8..474d06c2a3a 100644 --- a/charts/outlook-addin/README.md +++ b/charts/outlook-addin/README.md @@ -64,16 +64,17 @@ d helm upgrade --install wire-server charts/wire-server --values values/wire-ser By default, outlook addin as a feature is disabled for all teams. To change this make the following changes in your configuration in `galley` namespace: ``` -outlookCalIntegration: +galley: config: # ... settings: # ... featureFlags: # ... - defaults: - status: disabled - lockStatus: locked + outlookCalIntegration: + defaults: + status: enabled + lockStatus: unlocked ``` Redeploy wire-server for these changes to take effect. From 7486925535cd80436934f157ca5abfba08d723ba Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 14 Aug 2023 14:14:15 +0200 Subject: [PATCH 080/225] Linter fixes --- integration/test/Test/MLS.hs | 6 +-- integration/test/Test/MLS/One2One.hs | 4 +- .../src/Wire/API/Conversation/Protocol.hs | 2 +- .../src/Wire/API/Event/Conversation.hs | 2 +- libs/wire-api/src/Wire/API/MLS/Epoch.hs | 2 +- .../src/Wire/API/MLS/Group/Serialisation.hs | 8 ++-- libs/wire-api/src/Wire/API/MLS/GroupInfo.hs | 4 +- libs/wire-api/src/Wire/API/MLS/LeafNode.hs | 2 +- .../API/Golden/Manual/ConversationEvent.hs | 2 +- .../Wire/API/Golden/Manual/SubConversation.hs | 2 +- services/brig/test/unit/Main.hs | 43 ------------------- services/brig/test/unit/Run.hs | 12 +++--- .../Galley/API/MLS/Commit/ExternalCommit.hs | 4 +- .../galley/src/Galley/API/MLS/Migration.hs | 2 +- services/galley/src/Galley/API/MLS/One2One.hs | 2 +- .../src/Galley/API/MLS/SubConversation.hs | 8 ++-- .../src/Galley/Cassandra/SubConversation.hs | 4 +- services/galley/test/integration/API/MLS.hs | 18 ++++---- 18 files changed, 42 insertions(+), 85 deletions(-) delete mode 100644 services/brig/test/unit/Main.hs diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 7cd2b466a12..b447739c444 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -4,9 +4,9 @@ module Test.MLS where import API.Brig (claimKeyPackages, deleteClient) import API.Galley -import qualified Data.ByteString.Base64 as Base64 -import qualified Data.ByteString.Char8 as B8 -import qualified Data.Text.Encoding as T +import Data.ByteString.Base64 qualified as Base64 +import Data.ByteString.Char8 qualified as B8 +import Data.Text.Encoding qualified as T import MLS.Util import SetupHelpers import Testlib.Prelude diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index d3894155c76..a7cf895498e 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -1,8 +1,8 @@ module Test.MLS.One2One where import API.Galley -import qualified Data.ByteString.Base64 as Base64 -import qualified Data.ByteString.Char8 as B8 +import Data.ByteString.Base64 qualified as Base64 +import Data.ByteString.Char8 qualified as B8 import MLS.Util import SetupHelpers import Testlib.Prelude diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index d99795d197a..86f19ee90dc 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -39,7 +39,7 @@ import Control.Arrow import Control.Lens (Traversal', makePrisms, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Schema -import qualified Data.Swagger as S +import Data.Swagger qualified as S import Data.Time.Clock import Imports import Wire.API.Conversation.Action.Tag diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index 7a20dbfb5d5..6e31d4525ea 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -82,7 +82,7 @@ import URI.ByteString () import Wire.API.Conversation import Wire.API.Conversation.Code (ConversationCode (..), ConversationCodeInfo) import Wire.API.Conversation.Protocol (ProtocolUpdate (unProtocolUpdate)) -import qualified Wire.API.Conversation.Protocol as P +import Wire.API.Conversation.Protocol qualified as P import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.MLS.SubConversation diff --git a/libs/wire-api/src/Wire/API/MLS/Epoch.hs b/libs/wire-api/src/Wire/API/MLS/Epoch.hs index 0a0bf206a67..a12b65179f8 100644 --- a/libs/wire-api/src/Wire/API/MLS/Epoch.hs +++ b/libs/wire-api/src/Wire/API/MLS/Epoch.hs @@ -19,7 +19,7 @@ module Wire.API.MLS.Epoch where -import qualified Data.Aeson as A +import Data.Aeson qualified as A import Data.Binary import Data.Schema import Imports diff --git a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs index 3fb9a7dd3db..9a52f1a0879 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs @@ -28,13 +28,13 @@ import Data.Bifunctor import Data.Binary.Get import Data.Binary.Put import Data.ByteString.Conversion -import qualified Data.ByteString.Lazy as L +import Data.ByteString.Lazy qualified as L import Data.Domain import Data.Id import Data.Qualified -import qualified Data.Text as T -import qualified Data.Text.Encoding as T -import qualified Data.UUID as UUID +import Data.Text qualified as T +import Data.Text.Encoding qualified as T +import Data.UUID qualified as UUID import Imports hiding (cs) import Web.HttpApiData (FromHttpApiData (parseHeader)) import Wire.API.Conversation diff --git a/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs b/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs index 77cf2036627..2c0182f0dd4 100644 --- a/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs +++ b/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs @@ -24,8 +24,8 @@ where import Data.Binary.Get import Data.Binary.Put -import qualified Data.ByteString.Lazy as LBS -import qualified Data.Swagger as S +import Data.ByteString.Lazy qualified as LBS +import Data.Swagger qualified as S import GHC.Records import Imports import Wire.API.MLS.CipherSuite diff --git a/libs/wire-api/src/Wire/API/MLS/LeafNode.hs b/libs/wire-api/src/Wire/API/MLS/LeafNode.hs index 9e362bd6c72..a41da43b08a 100644 --- a/libs/wire-api/src/Wire/API/MLS/LeafNode.hs +++ b/libs/wire-api/src/Wire/API/MLS/LeafNode.hs @@ -28,7 +28,7 @@ module Wire.API.MLS.LeafNode where import Data.Binary -import qualified Data.Swagger as S +import Data.Swagger qualified as S import GHC.Records import Imports import Test.QuickCheck diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs index 4579fbe3e0a..ba1c36fbba2 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationEvent.hs @@ -23,7 +23,7 @@ import Data.Qualified (Qualified (..)) import Data.Time import Data.UUID qualified as UUID import Imports -import qualified Wire.API.Conversation.Protocol as P +import Wire.API.Conversation.Protocol qualified as P import Wire.API.Event.Conversation import Wire.API.MLS.SubConversation diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs index b2af34d1835..f885593fa42 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs @@ -26,7 +26,7 @@ import Data.Id import Data.Qualified import Data.Time.Calendar import Data.Time.Clock -import qualified Data.UUID as UUID +import Data.UUID qualified as UUID import Imports import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential diff --git a/services/brig/test/unit/Main.hs b/services/brig/test/unit/Main.hs deleted file mode 100644 index 64092fef3b5..00000000000 --- a/services/brig/test/unit/Main.hs +++ /dev/null @@ -1,43 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 Run - ( main, - ) -where - -import Imports -import Test.Brig.Calling qualified -import Test.Brig.Calling.Internal qualified -import Test.Brig.InternalNotification qualified -import Test.Brig.MLS qualified -import Test.Brig.Roundtrip qualified -import Test.Brig.User.Search.Index.Types qualified -import Test.Tasty - -main :: IO () -main = - defaultMain $ - testGroup - "Tests" - [ Test.Brig.User.Search.Index.Types.tests, - Test.Brig.Calling.tests, - Test.Brig.Calling.Internal.tests, - Test.Brig.Roundtrip.tests, - Test.Brig.MLS.tests, - Test.Brig.InternalNotification.tests - ] diff --git a/services/brig/test/unit/Run.hs b/services/brig/test/unit/Run.hs index 6ab5658fca1..64092fef3b5 100644 --- a/services/brig/test/unit/Run.hs +++ b/services/brig/test/unit/Run.hs @@ -21,12 +21,12 @@ module Run where import Imports -import qualified Test.Brig.Calling -import qualified Test.Brig.Calling.Internal -import qualified Test.Brig.InternalNotification -import qualified Test.Brig.MLS -import qualified Test.Brig.Roundtrip -import qualified Test.Brig.User.Search.Index.Types +import Test.Brig.Calling qualified +import Test.Brig.Calling.Internal qualified +import Test.Brig.InternalNotification qualified +import Test.Brig.MLS qualified +import Test.Brig.Roundtrip qualified +import Test.Brig.User.Search.Index.Types qualified import Test.Tasty main :: IO () diff --git a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs index ef9eba8acbc..907e9ecb36d 100644 --- a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs @@ -23,9 +23,9 @@ where import Control.Comonad import Control.Lens (forOf_) -import qualified Data.Map as Map +import Data.Map qualified as Map import Data.Qualified -import qualified Data.Set as Set +import Data.Set qualified as Set import Galley.API.MLS.Commit.Core import Galley.API.MLS.Proposal import Galley.API.MLS.Removal diff --git a/services/galley/src/Galley/API/MLS/Migration.hs b/services/galley/src/Galley/API/MLS/Migration.hs index eea8feac440..b59fa410655 100644 --- a/services/galley/src/Galley/API/MLS/Migration.hs +++ b/services/galley/src/Galley/API/MLS/Migration.hs @@ -19,7 +19,7 @@ module Galley.API.MLS.Migration where import Brig.Types.Intra import Data.Qualified -import qualified Data.Set as Set +import Data.Set qualified as Set import Data.Time import Galley.API.MLS.Types import Galley.Effects.BrigAccess diff --git a/services/galley/src/Galley/API/MLS/One2One.hs b/services/galley/src/Galley/API/MLS/One2One.hs index dc29dc1ca35..6d6688e0e6c 100644 --- a/services/galley/src/Galley/API/MLS/One2One.hs +++ b/services/galley/src/Galley/API/MLS/One2One.hs @@ -27,7 +27,7 @@ where import Data.Id as Id import Data.Qualified import Galley.API.MLS.Types -import qualified Galley.Data.Conversation.Types as Data +import Galley.Data.Conversation.Types qualified as Data import Galley.Effects.ConversationStore import Galley.Types.UserList import Imports hiding (cs) diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 4671a692e0f..cb849571ca6 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -33,7 +33,7 @@ where import Control.Arrow import Data.Id -import qualified Data.Map as Map +import Data.Map qualified as Map import Data.Qualified import Data.Time.Clock import Galley.API.MLS @@ -44,12 +44,12 @@ import Galley.API.MLS.Types import Galley.API.MLS.Util import Galley.API.Util import Galley.App (Env) -import qualified Galley.Data.Conversation as Data +import Galley.Data.Conversation qualified as Data import Galley.Data.Conversation.Types import Galley.Effects import Galley.Effects.FederatorAccess -import qualified Galley.Effects.MemberStore as Eff -import qualified Galley.Effects.SubConversationStore as Eff +import Galley.Effects.MemberStore qualified as Eff +import Galley.Effects.SubConversationStore qualified as Eff import Imports hiding (cs) import Polysemy import Polysemy.Error diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index 2b92e2c6b72..8a9a0287c1f 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -23,11 +23,11 @@ where import Cassandra import Cassandra.Util import Data.Id -import qualified Data.Map as Map +import Data.Map qualified as Map import Data.Time.Clock import Galley.API.MLS.Types import Galley.Cassandra.Conversation.MLS -import qualified Galley.Cassandra.Queries as Cql +import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store (embedClient) import Galley.Effects.SubConversationStore (SubConversationStore (..)) import Imports hiding (cs) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 4848e9071ee..6b873d6c4ce 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -27,28 +27,28 @@ import Bilge.Assert import Cassandra hiding (Set) import Control.Lens (view) import Control.Lens.Extras -import qualified Control.Monad.State as State +import Control.Monad.State qualified as State import Crypto.Error -import qualified Crypto.PubKey.Ed25519 as Ed25519 -import qualified Data.Aeson as Aeson +import Crypto.PubKey.Ed25519 qualified as Ed25519 +import Data.Aeson qualified as Aeson import Data.Domain import Data.Id import Data.Json.Util hiding ((#)) import Data.List1 hiding (head) -import qualified Data.Map as Map +import Data.Map qualified as Map import Data.Qualified import Data.Range -import qualified Data.Set as Set +import Data.Set qualified as Set import Data.Singletons -import qualified Data.Text as T +import Data.Text qualified as T import Data.Time import Federator.MockServer hiding (withTempMockFederator) import Imports -import qualified Network.HTTP.Types as HTTP -import qualified Network.Wai.Utilities.Error as Wai +import Network.HTTP.Types qualified as HTTP +import Network.Wai.Utilities.Error qualified as Wai import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (Second), (#)) -import qualified Test.Tasty.Cannon as WS +import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit import TestHelpers import TestSetup From 882e3b1f8900755e2f8c2970669cc532f780d7bc Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 15 Aug 2023 09:25:22 +0200 Subject: [PATCH 081/225] Add nfcg RPC to commit mock --- services/galley/test/integration/API/MLS/Mocks.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/galley/test/integration/API/MLS/Mocks.hs b/services/galley/test/integration/API/MLS/Mocks.hs index cd87946da5a..b40cc361bcb 100644 --- a/services/galley/test/integration/API/MLS/Mocks.hs +++ b/services/galley/test/integration/API/MLS/Mocks.hs @@ -37,6 +37,7 @@ import Data.Set qualified as Set import Federator.MockServer import Imports import Wire.API.Error.Galley +import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Common (EmptyResponse (..)) import Wire.API.Federation.API.Galley import Wire.API.MLS.Credential @@ -47,6 +48,7 @@ receiveCommitMock :: [ClientIdentity] -> Mock LByteString receiveCommitMock clients = asum [ "on-conversation-updated" ~> (), + "get-not-fully-connected-backends" ~> NonConnectedBackends mempty, "get-mls-clients" ~> Set.fromList ( map (flip ClientInfo True . ciClient) clients From a75e1d8232c5c4e4020ed7e537a15be810c1a3cb Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 15 Aug 2023 11:48:15 +0200 Subject: [PATCH 082/225] Fix assertion in unreachable test --- services/galley/test/integration/API/MLS.hs | 14 ++++---------- services/galley/test/integration/API/MLS/Mocks.hs | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 6b873d6c4ce..0101a43f4c4 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -44,7 +44,6 @@ import Data.Text qualified as T import Data.Time import Federator.MockServer hiding (withTempMockFederator) import Imports -import Network.HTTP.Types qualified as HTTP import Network.Wai.Utilities.Error qualified as Wai import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (Second), (#)) @@ -569,19 +568,14 @@ testAddRemotesSomeUnreachable = do commit <- createAddCommit alice1 [bob, charlie] bundle <- createBundle commit let unreachable = Set.singleton charlieDomain - (errRaw, _) <- - withTempMockFederator' + void + $ withTempMockFederator' ( receiveCommitMockByDomain [bob1] <|> mockUnreachableFor unreachable <|> welcomeMockByDomain [bobDomain] ) - $ localPostCommitBundle (mpSender commit) bundle - - err <- responseJsonError errRaw - liftIO $ do - Wai.label err @?= "federation-unreachable-domains-error" - Wai.code err @?= HTTP.status503 - Wai.message err @?= "The following domains are unreachable: [\"charlie.example.com\"]" + $ localPostCommitBundle (mpSender commit) bundle + Mock LByteString receiveCommitMock clients = asum - [ "on-conversation-updated" ~> (), + [ "on-conversation-updated" ~> EmptyResponse, "get-not-fully-connected-backends" ~> NonConnectedBackends mempty, "get-mls-clients" ~> Set.fromList From e754f930a12f4b5a2f9c822236917e4ee06ee278 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 15 Aug 2023 15:18:05 +0200 Subject: [PATCH 083/225] Add mls-test-cli to integration nix packages --- nix/wire-server.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 0f4241bb223..9292333937b 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -269,6 +269,7 @@ let brig-templates background-worker pkgs.nginz + pkgs.mls-test-cli integration-dynamic-backends-db-schemas integration-dynamic-backends-brig-index integration-dynamic-backends-sqs From 13de4e4f772564c08ed6bb499042efbb2a698014 Mon Sep 17 00:00:00 2001 From: Owen Harvey Date: Wed, 16 Aug 2023 23:33:02 +1000 Subject: [PATCH 084/225] WPB-3611: Notifying remote domains about defederation. (#3487) * WPB-3611: Notifying remote domains about defederation. During defederation, remaining federation members will now be notified about the defederation by a new notification type "federation.connectionRemoved" that carries the domains that were involved in the defederation. This allows federation members to clean up their conversations where users from both domains are present. Additionally, this is sent out to all of their local clients so they can also clean up. Normal notifications are suppressed for these operations, similar to "federation.delete" handling. Changes: - Added a new federation event type. - Added a new federation endpoint in galley. This route cleans up local conversations and notifies local clients of the defederation using the new event type. - Updated background-worker to send notifications to federation members. - Added tests for the new notifications, and that local conversations are cleaned up as expected. --------- Co-authored-by: Leif Battermann Co-authored-by: Leif Battermann --- changelog.d/1-api-changes/WPB-3611 | 5 + changelog.d/6-federation/WPB-3611 | 1 + integration/integration.cabal | 1 + integration/test/API/GalleyInternal.hs | 9 + integration/test/Test/Conversation.hs | 12 +- integration/test/Test/Defederation.hs | 79 ++++++ integration/test/Testlib/Cannon.hs | 141 ++++++++++ libs/schema-profunctor/src/Data/Schema.hs | 19 ++ .../src/Wire/API/Federation/API/Galley.hs | 1 + .../wire-api/src/Wire/API/Event/Federation.hs | 69 +++-- .../wire-api/src/Wire/API/FederationUpdate.hs | 1 + .../src/Wire/BackgroundWorker/Env.hs | 6 + .../src/Wire/Defederation.hs | 93 ++++--- .../Wire/BackendNotificationPusherSpec.hs | 40 +++ .../background-worker/test/Test/Wire/Util.hs | 1 + services/brig/src/Brig/API/Internal.hs | 12 + services/galley/src/Galley/API/Federation.hs | 105 ++++++- services/galley/src/Galley/API/Internal.hs | 67 +++-- .../Galley/Cassandra/Conversation/Members.hs | 24 +- .../galley/src/Galley/Cassandra/Queries.hs | 13 + .../Effects/DefederationNotifications.hs | 2 + .../galley/src/Galley/Effects/MemberStore.hs | 10 + services/galley/src/Galley/Intra/Effects.hs | 73 +++-- services/galley/test/integration/API.hs | 256 +++++++++++++++++- services/galley/test/integration/API/Util.hs | 48 +++- .../galley/test/integration/Federation.hs | 3 +- 26 files changed, 986 insertions(+), 105 deletions(-) create mode 100644 changelog.d/1-api-changes/WPB-3611 create mode 100644 changelog.d/6-federation/WPB-3611 create mode 100644 integration/test/Test/Defederation.hs diff --git a/changelog.d/1-api-changes/WPB-3611 b/changelog.d/1-api-changes/WPB-3611 new file mode 100644 index 00000000000..95fa03edec4 --- /dev/null +++ b/changelog.d/1-api-changes/WPB-3611 @@ -0,0 +1,5 @@ +Added a new notification event type, "federation.connectionRemoved" +This event contains a pair of domains that are no longer federating, and is used to inform other federation members of the change. +This notification is sent twice to local clients of federation members who receive this notification. Once before and once after cleaning up local conversaions where users from both domains are present. + +Added a new Galley federation endpoint "/federation/on-connection-removed" to receive the connection removed notification. \ No newline at end of file diff --git a/changelog.d/6-federation/WPB-3611 b/changelog.d/6-federation/WPB-3611 new file mode 100644 index 00000000000..4d485843b0f --- /dev/null +++ b/changelog.d/6-federation/WPB-3611 @@ -0,0 +1 @@ +Defederating from a remote server will now inform your remaining federation members, allowing them to clean up their local conversations and inform their clients. \ No newline at end of file diff --git a/integration/integration.cabal b/integration/integration.cabal index c44937120fe..912cb5dbb3c 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -103,6 +103,7 @@ library Test.Brig Test.Client Test.Conversation + Test.Defederation Test.Demo Test.Federator Test.Notifications diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index 8e78beb8b81..bfdff81b50a 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -51,3 +51,12 @@ getFederationStatus user domains = submit "GET" $ req & addJSONObject ["domains" .= domainList] + +deleteFederationDomain :: + ( HasCallStack + ) => + String -> + App Response +deleteFederationDomain domain = do + req <- rawBaseRequest OwnDomain Galley Unversioned $ joinHttpPath ["i", "federation", domain] + submit "DELETE" req diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 26c687fa487..7f974103b15 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -218,7 +218,17 @@ testDefederationGroupConversation = do r.status `shouldMatchInt` 404 -- assert federation.delete event is sent twice - void $ awaitNMatches 2 3 (\n -> nPayload n %. "type" `isEqual` "federation.delete") ws + void $ + awaitNMatches + 2 + 3 + ( \n -> do + correctType <- nPayload n %. "type" `isEqual` "federation.delete" + if correctType + then nPayload n %. "domain" `isEqual` domainB + else pure False + ) + ws -- assert no conversation.delete event is sent to uA eventPayloads <- diff --git a/integration/test/Test/Defederation.hs b/integration/test/Test/Defederation.hs new file mode 100644 index 00000000000..5712e33088c --- /dev/null +++ b/integration/test/Test/Defederation.hs @@ -0,0 +1,79 @@ +module Test.Defederation where + +import API.BrigInternal +-- import API.BrigInternal qualified as Internal +-- import API.Galley (defProteus, getConversation, postConversation, qualifiedUsers) +-- import Control.Applicative +-- import Data.Aeson qualified as Aeson +import GHC.Stack +import SetupHelpers +import Testlib.Prelude + +testDefederationRemoteNotifications :: HasCallStack => App () +testDefederationRemoteNotifications = do + let remoteDomain = "example.example.com" + -- Setup federation between OtherDomain and the remote domain + bindResponse (createFedConn OtherDomain $ object ["domain" .= remoteDomain, "search_policy" .= "full_search"]) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- Setup a remote user we can get notifications for. + user <- randomUser OtherDomain def + + withWebSocket user $ \ws -> do + -- Defederate from a domain that doesn't exist. This won't do anything to the databases + -- But it will send out notifications that we can wait on. + -- Begin the whole process at Brig, the same as an operator would. + void $ deleteFedConn OwnDomain remoteDomain + void $ awaitNMatches 2 3 (\n -> nPayload n %. "type" `isEqual` "federation.connectionRemoved") ws + +-- FUTUREWORK: temporarily disabled, enable when fixed on CI +-- testDefederationNonFullyConnectedGraph :: HasCallStack => App () +-- testDefederationNonFullyConnectedGraph = do +-- let setFederationConfig = +-- setField "optSettings.setFederationStrategy" "allowDynamic" +-- >=> removeField "optSettings.setFederationDomainConfigs" +-- >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) +-- startDynamicBackends +-- [ def {dbBrig = setFederationConfig}, +-- def {dbBrig = setFederationConfig}, +-- def {dbBrig = setFederationConfig} +-- ] +-- $ \dynDomains -> do +-- domains@[domainA, domainB, domainC] <- pure dynDomains +-- connectAllDomainsAndWaitToSync 1 domains +-- [uA, uB, uC] <- createAndConnectUsers [domainA, domainB, domainC] +-- -- create group conversation owned by domainA with users from domainB and domainC +-- convId <- bindResponse (postConversation uA (defProteus {qualifiedUsers = [uB, uC]})) $ \r -> do +-- r.status `shouldMatchInt` 201 +-- r.json %. "qualified_id" + +-- -- check conversation exists on all backends +-- for [uB, uC] objQidObject >>= checkConv convId uA + +-- -- one of the 2 non-conversation-owning domains (domainB and domainC) +-- -- defederate from the other non-conversation-owning domain +-- void $ Internal.deleteFedConn domainB domainC + +-- -- assert that clients from domainA receive federation.connectionRemoved events +-- -- Notifications being delivered at least n times is what we want to ensure here, +-- -- however they are often delivered more than once, so check that it doesn't happen +-- -- hundreds of times. +-- let isConnectionRemoved n = do +-- correctType <- nPayload n %. "type" `isEqual` "federation.connectionRemoved" +-- if correctType +-- then do +-- domsV <- nPayload n %. "domains" & asList +-- domsStr <- for domsV asString <&> sort +-- pure $ domsStr == sort [domainB, domainC] +-- else pure False +-- void $ awaitNToMMatches 2 10 20 isConnectionRemoved wsA + +-- retryT $ checkConv convId uA [] +-- where +-- checkConv :: Value -> Value -> [Value] -> App () +-- checkConv convId user expectedOtherMembers = do +-- bindResponse (getConversation user convId) $ \r -> do +-- r.status `shouldMatchInt` 200 +-- members <- r.json %. "members.others" & asList +-- qIds <- for members (\m -> m %. "qualified_id") +-- qIds `shouldMatchSet` expectedOtherMembers diff --git a/integration/test/Testlib/Cannon.hs b/integration/test/Testlib/Cannon.hs index 418823a9ab3..daf14ae4bf6 100644 --- a/integration/test/Testlib/Cannon.hs +++ b/integration/test/Testlib/Cannon.hs @@ -27,8 +27,13 @@ module Testlib.Cannon awaitNMatchesResult, awaitNMatches, awaitMatch, + awaitAtLeastNMatchesResult, + awaitAtLeastNMatches, + awaitNToMMatchesResult, + awaitNToMMatches, nPayload, printAwaitResult, + printAwaitAtLeastResult, ) where @@ -225,6 +230,14 @@ data AwaitResult = AwaitResult nonMatches :: [Value] } +data AwaitAtLeastResult = AwaitAtLeastResult + { success :: Bool, + nMatchesExpectedMin :: Int, + nMatchesExpectedMax :: Maybe Int, + matches :: [Value], + nonMatches :: [Value] + } + prettyAwaitResult :: AwaitResult -> App String prettyAwaitResult r = do matchesS <- for r.matches prettyJSON @@ -240,9 +253,29 @@ prettyAwaitResult r = do ] ) +prettyAwaitAtLeastResult :: AwaitAtLeastResult -> App String +prettyAwaitAtLeastResult r = do + matchesS <- for r.matches prettyJSON + nonMatchesS <- for r.nonMatches prettyJSON + pure $ + "AwaitAtLeastResult\n" + <> indent + 4 + ( unlines + [ "min expected:" <> show r.nMatchesExpectedMin, + "max expected:" <> show r.nMatchesExpectedMax, + "success: " <> show (r.success), + "matches:\n" <> unlines matchesS, + "non-matches:\n" <> unlines nonMatchesS + ] + ) + printAwaitResult :: AwaitResult -> App () printAwaitResult = prettyAwaitResult >=> liftIO . putStrLn +printAwaitAtLeastResult :: AwaitAtLeastResult -> App () +printAwaitAtLeastResult = prettyAwaitAtLeastResult >=> liftIO . putStrLn + awaitAnyEvent :: MonadIO m => Int -> WebSocket -> m (Maybe Value) awaitAnyEvent tSecs = liftIO . timeout (tSecs * 1000 * 1000) . atomically . readTChan . wsChan @@ -293,6 +326,74 @@ awaitNMatchesResult nExpected tSecs checkMatch ws = go nExpected [] [] } refill = mapM_ (liftIO . atomically . writeTChan (wsChan ws)) +awaitAtLeastNMatchesResult :: + HasCallStack => + -- | Minimum number of matches + Int -> + -- | Timeout in seconds + Int -> + -- | Selection function. Exceptions are *not* caught. + (Value -> App Bool) -> + WebSocket -> + App AwaitAtLeastResult +awaitAtLeastNMatchesResult nExpected tSecs checkMatch ws = go 0 [] [] + where + go nSeen nonMatches matches = do + mEvent <- awaitAnyEvent tSecs ws + case mEvent of + Just event -> + do + isMatch <- checkMatch event + if isMatch + then go (nSeen + 1) nonMatches (event : matches) + else go nSeen (event : nonMatches) matches + Nothing -> do + refill nonMatches + pure $ + AwaitAtLeastResult + { success = nSeen >= nExpected, + nMatchesExpectedMin = nExpected, + nMatchesExpectedMax = Nothing, + matches = reverse matches, + nonMatches = reverse nonMatches + } + refill = mapM_ (liftIO . atomically . writeTChan (wsChan ws)) + +awaitNToMMatchesResult :: + HasCallStack => + -- | Minimum number of matches + Int -> + -- | Maximum number of matches + Int -> + -- | Timeout in seconds + Int -> + -- | Selection function. Exceptions are *not* caught. + (Value -> App Bool) -> + WebSocket -> + App AwaitAtLeastResult +awaitNToMMatchesResult nMin nMax tSecs checkMatch ws = go 0 [] [] + where + go nSeen nonMatches matches = do + mEvent <- awaitAnyEvent tSecs ws + case mEvent of + Just event -> + do + isMatch <- checkMatch event + if isMatch + then go (nSeen + 1) nonMatches (event : matches) + else go nSeen (event : nonMatches) matches + Nothing -> do + refill nonMatches + pure $ + AwaitAtLeastResult + { success = nMin <= nSeen && nSeen <= nMax, + nMatchesExpectedMin = nMin, + nMatchesExpectedMax = pure nMax, + matches = reverse matches, + nonMatches = reverse nonMatches + } + refill = mapM_ (liftIO . atomically . writeTChan (wsChan ws)) + awaitNMatches :: HasCallStack => -- | Number of matches @@ -312,6 +413,46 @@ awaitNMatches nExpected tSecs checkMatch ws = do details <- ("Details:\n" <>) <$> prettyAwaitResult res assertFailure $ unlines [msgHeader, details] +awaitAtLeastNMatches :: + HasCallStack => + -- | Minumum number of matches + Int -> + -- | Timeout in seconds + Int -> + -- | Selection function. Should not throw any exceptions + (Value -> App Bool) -> + WebSocket -> + App [Value] +awaitAtLeastNMatches nExpected tSecs checkMatch ws = do + res <- awaitAtLeastNMatchesResult nExpected tSecs checkMatch ws + if res.success + then pure res.matches + else do + let msgHeader = "Expected " <> show nExpected <> " matching events, but got " <> show (length res.matches) <> "." + details <- ("Details:\n" <>) <$> prettyAwaitAtLeastResult res + assertFailure $ unlines [msgHeader, details] + +awaitNToMMatches :: + HasCallStack => + -- | Minimum Number of matches + Int -> + -- | Maximum Number of matches + Int -> + -- | Timeout in seconds + Int -> + -- | Selection function. Should not throw any exceptions + (Value -> App Bool) -> + WebSocket -> + App [Value] +awaitNToMMatches nMin nMax tSecs checkMatch ws = do + res <- awaitNToMMatchesResult nMin nMax tSecs checkMatch ws + if res.success + then pure res.matches + else do + let msgHeader = "Expected between" <> show nMin <> " to " <> show nMax <> " matching events, but got " <> show (length res.matches) <> "." + details <- ("Details:\n" <>) <$> prettyAwaitAtLeastResult res + assertFailure $ unlines [msgHeader, details] + awaitMatch :: HasCallStack => -- | Timeout in seconds diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index 6ff30f7ed38..548ed8bfbe0 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -62,6 +62,7 @@ module Data.Schema fieldOverF, fieldWithDocModifierF, array, + pair, set, nonEmptyArray, map_, @@ -463,6 +464,24 @@ array sch = SchemaP (SchemaDoc s) (SchemaIn r) (SchemaOut w) s = mkArray (schemaDoc sch) w x = A.Array . V.fromList <$> mapM (schemaOut sch) x +-- | A schema for a JSON pair. +-- This is serialised as JSON array of exactly 2 elements +-- of the same type. Any more or less is an error. +pair :: + (HasArray ndoc doc, HasName ndoc) => + ValueSchema ndoc a -> + ValueSchema doc (a, a) +pair sch = SchemaP (SchemaDoc s) (SchemaIn r) (SchemaOut w) + where + name = maybe "pair" ("pair of " <>) (getName (schemaDoc sch)) + r = A.withArray (T.unpack name) $ \arr -> do + l <- mapM (schemaIn sch) $ V.toList arr + case l of + [a, b] -> pure (a, b) + _ -> fail $ "Expected exactly 2 elements, but got " <> show (length l) + s = mkArray (schemaDoc sch) + w (a, b) = A.Array . V.fromList <$> mapM (schemaOut sch) [a, b] + set :: (HasArray ndoc doc, HasName ndoc, Ord a) => ValueSchema ndoc a -> 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 7400abaa4da..fdd17aa6f68 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 @@ -131,6 +131,7 @@ type GalleyApi = TypingDataUpdateRequest TypingDataUpdateResponse :<|> FedEndpoint "on-typing-indicator-updated" TypingDataUpdated EmptyResponse + :<|> FedEndpoint "on-connection-removed" Domain EmptyResponse data TypingDataUpdateRequest = TypingDataUpdateRequest { tdurTypingStatus :: TypingStatus, diff --git a/libs/wire-api/src/Wire/API/Event/Federation.hs b/libs/wire-api/src/Wire/API/Event/Federation.hs index 17d5120c0ce..cc38b1e2795 100644 --- a/libs/wire-api/src/Wire/API/Event/Federation.hs +++ b/libs/wire-api/src/Wire/API/Event/Federation.hs @@ -1,9 +1,12 @@ +{-# LANGUAGE TemplateHaskell #-} + module Wire.API.Event.Federation ( Event (..), - EventType (..), ) where +import Control.Arrow ((&&&)) +import Control.Lens (makePrisms) import Data.Aeson (FromJSON, ToJSON) import Data.Aeson qualified as A import Data.Aeson.KeyMap qualified as KeyMap @@ -12,41 +15,71 @@ import Data.Json.Util (ToJSONObject (toJSONObject)) import Data.Schema import Data.Swagger qualified as S import Imports +import Test.QuickCheck.Gen import Wire.Arbitrary -data Event = Event - { _eventType :: EventType, - _eventDomain :: Domain - } - deriving (Eq, Show, Ord, Generic) +data Event + = FederationDelete Domain + | FederationConnectionRemoved (Domain, Domain) + deriving stock (Eq, Show, Generic) + +$(makePrisms ''Event) instance Arbitrary Event where arbitrary = - Event - <$> arbitrary - <*> arbitrary + oneof + [ FederationDelete <$> arbitrary, + FederationConnectionRemoved <$> arbitrary + ] data EventType - = FederationDelete - deriving (Eq, Show, Ord, Generic) + = FederationTypeDelete + | FederationTypeConnectionRemoved + deriving (Eq, Show, Ord, Enum, Bounded, Generic) deriving (Arbitrary) via (GenericUniform EventType) deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema EventType instance ToSchema EventType where schema = - enum @Text "EventType" $ + enum @Text "FederationEventType" $ mconcat - [ element "federation.delete" FederationDelete + [ element "federation.delete" FederationTypeDelete, + element "federation.connectionRemoved" FederationTypeConnectionRemoved ] +eventType :: Event -> EventType +eventType (FederationDelete _) = FederationTypeDelete +eventType (FederationConnectionRemoved _) = FederationTypeConnectionRemoved + +taggedEventDataSchema :: ObjectSchema SwaggerDoc (EventType, Event) +taggedEventDataSchema = + bind + (fst .= field "type" schema) + -- The fields we need to look at change based on the event + -- type, so we need to dispatch here to get monadic-ish behaviour. + -- + -- federation.delete is expecting a "domain" field that contains a bare domain string. + -- federation.connectionRemoved is expecting a "domains" field that contains exactly a pair of domains in a list + ( snd .= dispatch dataSchema + ) + where + dataSchema :: EventType -> ObjectSchema SwaggerDoc Event + dataSchema FederationTypeDelete = tag _FederationDelete deleteSchema + dataSchema FederationTypeConnectionRemoved = tag _FederationConnectionRemoved connectionRemovedSchema + +-- These schemas have different fields they are targeting. +deleteSchema :: ObjectSchema SwaggerDoc Domain +deleteSchema = field "domain" schema + +connectionRemovedSchema :: ObjectSchema SwaggerDoc (Domain, Domain) +connectionRemovedSchema = field "domains" (pair schema) + +-- Schemas for the events, as they have different structures. eventObjectSchema :: ObjectSchema SwaggerDoc Event -eventObjectSchema = - Event - <$> _eventType .= field "type" schema - <*> _eventDomain .= field "domain" schema +eventObjectSchema = snd <$> (eventType &&& id) .= taggedEventDataSchema instance ToSchema Event where - schema = object "Event" eventObjectSchema + schema = object "FederationEvent" eventObjectSchema instance ToJSONObject Event where toJSONObject = diff --git a/libs/wire-api/src/Wire/API/FederationUpdate.hs b/libs/wire-api/src/Wire/API/FederationUpdate.hs index bd5d3ec6a8d..0bafee435af 100644 --- a/libs/wire-api/src/Wire/API/FederationUpdate.hs +++ b/libs/wire-api/src/Wire/API/FederationUpdate.hs @@ -3,6 +3,7 @@ module Wire.API.FederationUpdate SyncFedDomainConfigsCallback (..), emptySyncFedDomainConfigsCallback, deleteFederationRemoteGalley, + fetch, ) where diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index e6f9b93edee..2f3e0130aef 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -12,6 +12,7 @@ import Data.Map.Strict qualified as Map import Data.Metrics qualified as Metrics import HTTP2.Client.Manager import Imports +import Network.AMQP (Channel) import Network.AMQP.Extended import Network.HTTP.Client import Network.RabbitMqAdmin qualified as RabbitMqAdmin @@ -49,6 +50,10 @@ data Env = Env remoteDomains :: IORef FederationDomainConfigs, remoteDomainsChan :: Chan FederationDomainConfigs, backendNotificationMetrics :: BackendNotificationMetrics, + -- This is needed so that the defederation worker can push + -- connection-removed notifications into the notifications channels. + -- This allows us to reuse existing code. This only pushes. + notificationChannel :: MVar Channel, statuses :: IORef (Map Worker IsWorking) } @@ -95,6 +100,7 @@ mkEnv opts = do ] metrics <- Metrics.metrics backendNotificationMetrics <- mkBackendNotificationMetrics + notificationChannel <- mkRabbitMqChannelMVar logger $ demoteOpts opts.rabbitmq pure (Env {..}, syncThread) initHttp2Manager :: IO Http2Manager diff --git a/services/background-worker/src/Wire/Defederation.hs b/services/background-worker/src/Wire/Defederation.hs index b87e112c267..6f1efbb09bc 100644 --- a/services/background-worker/src/Wire/Defederation.hs +++ b/services/background-worker/src/Wire/Defederation.hs @@ -7,6 +7,8 @@ import Control.Monad.Catch import Control.Retry import Data.Aeson qualified as A import Data.ByteString.Conversion +import Data.Domain +import Data.Text (unpack) import Data.Text.Encoding import Imports import Network.AMQP qualified as Q @@ -14,9 +16,11 @@ import Network.AMQP.Extended import Network.AMQP.Lifted qualified as QL import Network.HTTP.Client import Network.HTTP.Types +import Servant.Client (BaseUrl (..), ClientEnv, Scheme (Http), mkClientEnv) import System.Logger.Class qualified as Log import Util.Options import Wire.API.Federation.BackendNotifications +import Wire.API.Routes.FederationDomainConfig qualified as Fed import Wire.BackgroundWorker.Env import Wire.BackgroundWorker.Util @@ -47,42 +51,67 @@ deleteFederationDomainInner' go (msg, envelope) = do Log.msg (Log.val "Failed to delete federation domain") . Log.field "error" err +mkBrigEnv :: AppT IO ClientEnv +mkBrigEnv = do + Endpoint brigHost brigPort <- asks brig + mkClientEnv + <$> asks httpManager + <*> pure (BaseUrl Http (unpack brigHost) (fromIntegral brigPort) "") + +getRemoteDomains :: AppT IO [Domain] +getRemoteDomains = do + ref <- asks remoteDomains + fmap Fed.domain . Fed.remotes <$> readIORef ref + +callGalleyDelete :: + ( MonadReader Env m, + MonadMask m, + ToByteString a, + RabbitMQEnvelope e, + MonadIO m + ) => + MVar () -> + e -> + a -> + m () +callGalleyDelete runningFlag envelope domain = do + env <- ask + -- Jittered exponential backoff with 10ms as starting delay and 60s as max + -- delay. When 60 is reached, every retry will happen after 60s. + let policy = capDelay 60_000_000 $ fullJitterBackoff 10000 + manager = httpManager env + recovering policy httpHandlers $ \_ -> + bracket_ (takeMVar runningFlag) (putMVar runningFlag ()) $ do + -- Non 2xx responses will throw an exception + -- So we are relying on that to be caught by recovering + resp <- liftIO $ httpLbs (req env domain) manager + let code = statusCode $ responseStatus resp + if code >= 200 && code <= 299 + then do + liftIO $ ack envelope + else -- ensure that the message is requeued + -- This message was able to be parsed but something + -- else in our stack failed and we should try again. + liftIO $ reject envelope True + +req :: ToByteString a => Env -> a -> Request +req env dom = + defaultRequest + { method = methodDelete, + secure = False, + host = galley env ^. epHost . to encodeUtf8, + port = galley env ^. epPort . to fromIntegral, + path = "/i/federation/" <> toByteString' dom, + requestHeaders = ("Accept", "application/json") : requestHeaders defaultRequest, + responseTimeout = defederationTimeout env + } + -- What should we do with non-recoverable (unparsable) errors/messages? -- should we deadletter, or do something else? -- Deadlettering has a privacy implication -- FUTUREWORK. -deleteFederationDomainInner :: (RabbitMQEnvelope e) => MVar () -> (Q.Message, e) -> AppT IO () +deleteFederationDomainInner :: RabbitMQEnvelope e => MVar () -> (Q.Message, e) -> AppT IO () deleteFederationDomainInner runningFlag (msg, envelope) = - deleteFederationDomainInner' (const callGalley) (msg, envelope) - where - callGalley domain = do - env <- ask - -- Jittered exponential backoff with 10ms as starting delay and 60s as max - -- delay. When 60 is reached, every retry will happen after 60s. - let policy = capDelay 60_000_000 $ fullJitterBackoff 10000 - manager = httpManager env - recovering policy httpHandlers $ \_ -> - bracket_ (takeMVar runningFlag) (putMVar runningFlag ()) $ do - -- Non 2xx responses will throw an exception - -- So we are relying on that to be caught by recovering - resp <- liftIO $ httpLbs (req env domain) manager - let code = statusCode $ responseStatus resp - if code >= 200 && code <= 299 - then do - liftIO $ ack envelope - else -- ensure that the message is requeued - -- This message was able to be parsed but something - -- else in our stack failed and we should try again. - liftIO $ reject envelope True - req env dom = - defaultRequest - { method = methodDelete, - secure = False, - host = galley env ^. epHost . to encodeUtf8, - port = galley env ^. epPort . to fromIntegral, - path = "/i/federation/" <> toByteString' dom, - requestHeaders = ("Accept", "application/json") : requestHeaders defaultRequest, - responseTimeout = defederationTimeout env - } + deleteFederationDomainInner' (const $ callGalleyDelete runningFlag envelope) (msg, envelope) startDefederator :: IORef (Maybe (Q.ConsumerTag, MVar ())) -> Q.Channel -> AppT IO () startDefederator consumerRef chan = do diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index daacceeab32..5a78b5d55c8 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -91,6 +91,44 @@ spec = do getVectorWith env.backendNotificationMetrics.pushedCounter getCounter `shouldReturn` [(domainText targetDomain, 1)] + it "should push on-connection-removed notifications" $ do + let returnSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) + let origDomain = Domain "origin.example.com" + targetDomain = Domain "target.example.com" + defederatedDomain = Domain "defederated.example.com" + let notif = + BackendNotification + { targetComponent = Galley, + ownDomain = origDomain, + path = "/on-connection-removed", + body = RawJson $ Aeson.encode defederatedDomain + } + envelope <- newMockEnvelope + let msg = + Q.newMsg + { Q.msgBody = Aeson.encode notif, + Q.msgContentType = Just "application/json" + } + runningFlag <- newMVar () + (env, fedReqs) <- + withTempMockFederator [] returnSuccess . runTestAppT $ do + wait =<< pushNotification runningFlag targetDomain (msg, envelope) + ask + + readIORef envelope.acks `shouldReturn` 1 + readIORef envelope.rejections `shouldReturn` [] + fedReqs + `shouldBe` [ FederatedRequest + { frTargetDomain = targetDomain, + frOriginDomain = origDomain, + frComponent = Galley, + frRPC = "on-connection-removed", + frBody = Aeson.encode defederatedDomain + } + ] + getVectorWith env.backendNotificationMetrics.pushedCounter getCounter + `shouldReturn` [(domainText targetDomain, 1)] + it "should reject invalid notifications" $ do let returnSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) envelope <- newMockEnvelope @@ -183,6 +221,7 @@ spec = do httpManager <- newManager defaultManagerSettings remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan + notificationChannel <- newEmptyMVar let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined @@ -204,6 +243,7 @@ spec = do httpManager <- newManager defaultManagerSettings remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan + notificationChannel <- newEmptyMVar let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index 470dd3992a2..d88db07780a 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -21,6 +21,7 @@ testEnv = do httpManager <- newManager defaultManagerSettings remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan + notificationChannel <- newEmptyMVar let federatorInternal = Endpoint "localhost" 0 rabbitmqAdminClient = undefined rabbitmqVHost = undefined diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 321b68c60d6..cd2393132ee 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -343,6 +343,8 @@ deleteFederationRemote dom = do lift . wrapClient . Data.deleteFederationRemote $ dom assertNoDomainsFromConfigFiles dom env <- ask + ownDomain <- viewFederationDomain + remoteDomains <- fmap domain . remotes <$> getFederationRemotes for_ (env ^. rabbitmqChannel) $ \chan -> liftIO . withMVar chan $ \chan' -> do -- ensureQueue uses routingKey internally ensureQueue chan' defederationQueue @@ -355,6 +357,16 @@ deleteFederationRemote dom = do Q.msgDeliveryMode = pure Q.Persistent, Q.msgContentType = pure "application/json" } + -- Send a notification to remaining federation servers, telling them + -- that we are defederating from a given domain, and that they should + -- clean up their conversations and notify clients. + -- Just to be safe! + for_ (filter (/= dom) remoteDomains) $ \remoteDomain -> do + ensureQueue chan' $ domainText remoteDomain + liftIO + $ enqueue chan' ownDomain remoteDomain Q.Persistent + . void + $ fedQueueClient @'Galley @"on-connection-removed" dom -- Drop the notification queue for the domain. -- This will also drop all of the messages in the queue -- as we will no longer be able to communicate with this diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index c1a475c6a54..f939de7357e 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -1,4 +1,3 @@ -{-# OPTIONS -Wno-redundant-constraints #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -21,20 +20,27 @@ module Galley.API.Federation where +import Bilge.Retry (httpHandlers) +import Cassandra (ClientState, Consistency (LocalQuorum), Page (hasMore, nextPage, result), PrepQuery, QueryParams, R, Tuple, paginate, paramsP) import Control.Error +import Control.Exception (throwIO) import Control.Lens +import Control.Retry (capDelay, fullJitterBackoff, recovering) import Data.Bifunctor import Data.ByteString.Conversion (toByteString') import Data.Domain (Domain) import Data.Id import Data.Json.Util +import Data.List.NonEmpty qualified as N import Data.Map qualified as Map import Data.Map.Lens (toMapOf) +import Data.Proxy (Proxy (Proxy)) import Data.Qualified -import Data.Range (Range (fromRange)) +import Data.Range (Range (fromRange), toRange) import Data.Set qualified as Set import Data.Singletons (SingI (..), demote, sing) import Data.Tagged +import Data.Text qualified as T import Data.Text.Lazy qualified as LT import Data.Time.Clock import Galley.API.Action @@ -50,13 +56,17 @@ import Galley.API.Message import Galley.API.Push import Galley.API.Util import Galley.App +import Galley.Cassandra.Queries qualified as Q +import Galley.Cassandra.Store import Galley.Data.Conversation qualified as Data import Galley.Effects import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.ConversationStore qualified as E +import Galley.Effects.DefederationNotifications import Galley.Effects.FireAndForget qualified as E import Galley.Effects.MemberStore qualified as E import Galley.Effects.ProposalStore (ProposalStore) +import Galley.Env import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.UserList (UserList (UserList)) @@ -69,8 +79,10 @@ import Polysemy.Resource import Polysemy.TinyLog import Polysemy.TinyLog qualified as P import Servant (ServerT) -import Servant.API +import Servant.API hiding (QueryParams) +import Servant.Client (BaseUrl (BaseUrl), Scheme (Http), mkClientEnv) import System.Logger.Class qualified as Log +import Util.Options (Endpoint (..)) import Wire.API.Conversation hiding (Member) import Wire.API.Conversation qualified as Public import Wire.API.Conversation.Action @@ -83,6 +95,7 @@ import Wire.API.Federation.API.Common (EmptyResponse (..)) import Wire.API.Federation.API.Galley import Wire.API.Federation.API.Galley qualified as F import Wire.API.Federation.Error +import Wire.API.FederationUpdate (fetch) import Wire.API.MLS.CommitBundle import Wire.API.MLS.Credential import Wire.API.MLS.Message @@ -91,6 +104,7 @@ import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.MLS.Welcome import Wire.API.Message +import Wire.API.Routes.FederationDomainConfig (domain, remotes) import Wire.API.Routes.Named import Wire.API.ServantProto @@ -116,6 +130,7 @@ federationSitemap = :<|> Named @"on-client-removed" (callsFed (exposeAnnotations onClientRemoved)) :<|> Named @"update-typing-indicator" (callsFed (exposeAnnotations updateTypingIndicator)) :<|> Named @"on-typing-indicator-updated" onTypingIndicatorUpdated + :<|> Named @"on-connection-removed" (onFederationConnectionRemoved (toRange (Proxy @500))) onClientRemoved :: ( Member ConversationStore r, @@ -223,7 +238,6 @@ onConversationUpdated requestingDomain cu = do leaveConversation :: ( Member ConversationStore r, Member (Error InternalError) r, - Member (Error FederationError) r, Member ExternalAccess r, Member FederatorAccess r, Member GundeckAccess r, @@ -368,8 +382,7 @@ sendMessage originDomain msr = do throwErr = throw . InvalidPayload . LT.pack onUserDeleted :: - ( Member (Error FederationError) r, - Member ConversationStore r, + ( Member ConversationStore r, Member FederatorAccess r, Member FireAndForget r, Member ExternalAccess r, @@ -545,7 +558,6 @@ sendMLSCommitBundle :: Member (Error FederationError) r, Member (Error InternalError) r, Member FederatorAccess r, - Member BackendNotificationQueueAccess r, Member GundeckAccess r, Member (Input (Local ())) r, Member (Input Env) r, @@ -633,9 +645,6 @@ instance mlsSendWelcome :: ( Member BrigAccess r, Member (Error InternalError) r, - Member GundeckAccess r, - Member ExternalAccess r, - Member P.TinyLog r, Member (Input Env) r, Member (Input (Local ())) r, Member (Input UTCTime) r @@ -760,6 +769,78 @@ onTypingIndicatorUpdated origDomain TypingDataUpdated {..} = do pushTypingIndicatorEvents tudOrigUserId tudTime tudUsersInConv Nothing qcnv tudTypingStatus pure EmptyResponse +-- Since we already have the origin domain where the defederation event started, +-- all it needs to carry in addition is the domain it is defederating from. This +-- is all the information that we need to cleanup the database and notify clients. +onFederationConnectionRemoved :: + forall r. + ( Member (Input Env) r, + Member (Embed IO) r, + Member (Input ClientState) r, + Member MemberStore r, + Member DefederationNotifications r + ) => + Range 1 1000 Int32 -> + Domain -> + Domain -> + Sem r EmptyResponse +onFederationConnectionRemoved range originDomain targetDomain = do + fedDomains <- getFederationDomains + let federatedWithBoth = all (`elem` fedDomains) [originDomain, targetDomain] + when federatedWithBoth $ do + sendOnConnectionRemovedNotifications originDomain targetDomain + cleanupRemovedConnections originDomain targetDomain range + sendOnConnectionRemovedNotifications originDomain targetDomain + pure EmptyResponse + +getFederationDomains :: + ( Member (Input Env) r, + Member (Embed IO) r + ) => + Sem r [Domain] +getFederationDomains = do + Endpoint (T.unpack -> h) (fromIntegral -> p) <- inputs _brig + mgr <- inputs _manager + liftIO $ recovering policy httpHandlers $ \_ -> do + resp <- fetch $ mkClientEnv mgr $ BaseUrl Http h p "" + either throwIO (pure . fmap domain . remotes) resp + where + policy = capDelay 60_000_000 $ fullJitterBackoff 200_000 + +-- for all conversations owned by backend C, only if there are users from both A and B, +-- remove users from A and B from those conversations +-- This is similar to Galley.API.Internal.deleteFederationDomain +-- However it has some important differences, such as we only remove from our conversations +-- where users for both domains are in the same conversation. +cleanupRemovedConnections :: + forall r. + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member MemberStore r + ) => + Domain -> + Domain -> + Range 1 1000 Int32 -> + Sem r () +cleanupRemovedConnections domainA domainB (fromRange -> maxPage) = do + runPaginated Q.selectConvIdsByRemoteDomain (paramsP LocalQuorum (Identity domainA) maxPage) $ \convIds -> + -- `nub $ sort` is a small performance boost, it will drop duplicate convIds from the page results. + -- However we can certainly still process a conversation more than once if it is in multiple pages. + for_ (nub $ sort convIds) $ \(runIdentity -> convId) -> do + -- Check if users from domain B are in the conversation + b <- isJust <$> E.checkConvForRemoteDomain convId domainB + when b $ do + -- Users from both domains exist, delete all of them from the conversation. + E.removeRemoteDomain convId domainA + E.removeRemoteDomain convId domainB + where + runPaginated :: (Tuple p, Tuple a) => PrepQuery R p a -> QueryParams p -> ([a] -> Sem r b) -> Sem r b + runPaginated q ps f = go f <=< embedClient $ paginate q ps + go :: ([a] -> Sem r b) -> Page a -> Sem r b + go f page + | hasMore page = f (result page) >> embedClient (nextPage page) >>= go f + | otherwise = f $ result page + -------------------------------------------------------------------------------- -- Utilities -------------------------------------------------------------------------------- @@ -780,3 +861,7 @@ logFederationError lc e = \ a user from a local conversation: " <> displayException e ) + +-- Build the map, keyed by conversations to the list of members +insertIntoMap :: (ConvId, a) -> Map ConvId (N.NonEmpty a) -> Map ConvId (N.NonEmpty a) +insertIntoMap (cnvId, user) m = Map.alter (pure . maybe (pure user) (N.cons user)) cnvId m diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index dc4176dc049..a21cf229c40 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -27,6 +27,7 @@ module Galley.API.Internal where import Bilge.Retry +import Cassandra (ClientState, Consistency (LocalQuorum), Page (hasMore, nextPage, result), paginate, paramsP) import Control.Exception.Safe (catchAny) import Control.Lens hiding (Getter, Setter, (.=)) import Control.Retry @@ -45,6 +46,7 @@ import Galley.API.Clients qualified as Clients import Galley.API.Create qualified as Create import Galley.API.CustomBackend qualified as CustomBackend import Galley.API.Error +import Galley.API.Federation (insertIntoMap) import Galley.API.LegalHold (unsetTeamLegalholdWhitelistedH) import Galley.API.LegalHold.Conflicts import Galley.API.MLS.Removal @@ -58,6 +60,8 @@ import Galley.API.Teams.Features import Galley.API.Update qualified as Update import Galley.API.Util import Galley.App +import Galley.Cassandra.Queries qualified as Q +import Galley.Cassandra.Store (embedClient) import Galley.Data.Conversation qualified as Data import Galley.Data.Conversation.Types import Galley.Effects @@ -78,7 +82,7 @@ import Galley.Options import Galley.Queue qualified as Q import Galley.Types.Bot (AddBot, RemoveBot) import Galley.Types.Bot.Service -import Galley.Types.Conversations.Members (RemoteMember (rmId)) +import Galley.Types.Conversations.Members (RemoteMember (RemoteMember, rmId)) import Galley.Types.UserList import Imports hiding (head) import Network.AMQP qualified as Q @@ -115,7 +119,7 @@ import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Routes.MultiTablePaging (mtpHasMore, mtpPagingState, mtpResults) import Wire.API.Team.Feature hiding (setStatus) import Wire.API.Team.Member -import Wire.Sem.Paging +import Wire.Sem.Paging (Paging (pageItems, pageState)) import Wire.Sem.Paging.Cassandra internalAPI :: API InternalAPI GalleyEffects @@ -321,7 +325,7 @@ internalSitemap = unsafeCallsFed @'Galley @"on-client-removed" $ unsafeCallsFed capture "domain" .&. accept "application" "json" - delete "/i/federation/:domain" (continue internalDeleteFederationDomainH) $ + delete "/i/federation/:domain" (continue . internalDeleteFederationDomainH $ toRange (Proxy @500)) $ capture "domain" .&. accept "application" "json" @@ -493,10 +497,6 @@ guardLegalholdPolicyConflictsH glh = do mapError @LegalholdConflicts (const $ Tagged @'MissingLegalholdConsent ()) $ guardLegalholdPolicyConflicts (glhProtectee glh) (glhUserClients glh) --- Build the map, keyed by conversations to the list of members -insertIntoMap :: (ConvId, a) -> Map ConvId (N.NonEmpty a) -> Map ConvId (N.NonEmpty a) -insertIntoMap (cnvId, user) m = Map.alter (pure . maybe (pure user) (N.cons user)) cnvId m - -- Bundle all of the deletes together for easy calling -- Errors & exceptions are thrown to IO to stop the message being ACKed, eventually timing it -- out so that it can be redelivered. @@ -505,17 +505,19 @@ deleteFederationDomain :: Member (P.Logger (Msg -> Msg)) r, Member (Error FederationError) r, Member MemberStore r, + Member (Input ClientState) r, Member ConversationStore r, Member (Embed IO) r, Member CodeStore r, Member TeamStore r, Member (Error InternalError) r ) => + Range 1 1000 Int32 -> Domain -> Sem r () -deleteFederationDomain d = do - deleteFederationDomainRemoteUserFromLocalConversations d - deleteFederationDomainLocalUserFromRemoteConversation d +deleteFederationDomain range d = do + deleteFederationDomainRemoteUserFromLocalConversations range d + deleteFederationDomainLocalUserFromRemoteConversation range d deleteFederationDomainOneOnOne d internalDeleteFederationDomainH :: @@ -525,19 +527,21 @@ internalDeleteFederationDomainH :: Member MemberStore r, Member ConversationStore r, Member (Embed IO) r, + Member (Input ClientState) r, Member CodeStore r, Member TeamStore r, Member DefederationNotifications r, Member (Error InternalError) r ) => + Range 1 1000 Int32 -> Domain ::: JSON -> Sem r Response -internalDeleteFederationDomainH (domain ::: _) = do +internalDeleteFederationDomainH range (domain ::: _) = do -- We have to send the same event twice. -- Once before and once after defederation work. -- https://wearezeta.atlassian.net/wiki/spaces/ENGINEERIN/pages/809238539/Use+case+Stopping+to+federate+with+a+domain sendDefederationNotifications domain - deleteFederationDomain domain + deleteFederationDomain range domain sendDefederationNotifications domain pure (empty & setStatus status200) @@ -547,15 +551,24 @@ deleteFederationDomainRemoteUserFromLocalConversations :: ( Member (Input Env) r, Member (P.Logger (Msg -> Msg)) r, Member (Error FederationError) r, + Member (Input ClientState) r, + Member (Embed IO) r, Member MemberStore r, Member ConversationStore r, Member CodeStore r, Member TeamStore r ) => + Range 1 1000 Int32 -> Domain -> Sem r () -deleteFederationDomainRemoteUserFromLocalConversations dom = do - remoteUsers <- E.getRemoteMembersByDomain dom +deleteFederationDomainRemoteUserFromLocalConversations (fromRange -> maxPage) dom = do + remoteUsers <- + mkConvMem <$$> do + page <- + embedClient $ + paginate Q.selectRemoteMembersByDomain $ + paramsP LocalQuorum (Identity dom) maxPage + getPaginatedData page env <- input let lCnvMap = foldr insertIntoMap mempty remoteUsers localDomain = env ^. Galley.App.options . optSettings . setFederationDomain @@ -574,6 +587,7 @@ deleteFederationDomainRemoteUserFromLocalConversations dom = do getConversation cnvId >>= maybe (pure () {- conv already gone, nothing to do -}) (delConv localDomain rUsers) where + mkConvMem (convId, usr, role) = (convId, RemoteMember (toRemoteUnsafe dom usr) role) delConv :: Domain -> N.NonEmpty RemoteMember -> @@ -602,12 +616,21 @@ deleteFederationDomainRemoteUserFromLocalConversations dom = do -- Remove local members from remote conversations deleteFederationDomainLocalUserFromRemoteConversation :: ( Member (Error InternalError) r, + Member (Input ClientState) r, + Member (Embed IO) r, Member MemberStore r ) => + Range 1 1000 Int32 -> Domain -> Sem r () -deleteFederationDomainLocalUserFromRemoteConversation dom = do - remoteConvs <- foldr insertIntoMap mempty <$> E.getLocalMembersByDomain dom +deleteFederationDomainLocalUserFromRemoteConversation (fromRange -> maxPage) dom = do + remoteConvs <- + foldr insertIntoMap mempty <$> do + page <- + embedClient $ + paginate Q.selectLocalMembersByDomain $ + paramsP LocalQuorum (Identity dom) maxPage + getPaginatedData page for_ (Map.toList remoteConvs) $ \(cnv, lUsers) -> do -- All errors, either exceptions or Either e, get thrown into IO mapError @NoChanges (const (InternalErrorWithDescription "No Changes: Could not remove a local member from a remote conversation.")) $ do @@ -629,3 +652,15 @@ deleteFederationDomainOneOnOne dom = do void . liftIO . recovering policy httpHandlers $ \_ -> deleteFederationRemoteGalley dom c where mkClientEnv mgr (Endpoint h p) = ClientEnv mgr (BaseUrl Http (unpack h) (fromIntegral p) "") Nothing defaultMakeClientRequest + +getPaginatedData :: + ( Member (Input ClientState) r, + Member (Embed IO) r + ) => + Page a -> + Sem r [a] +getPaginatedData page + | hasMore page = + (result page <>) <$> do + getPaginatedData <=< embedClient $ nextPage page + | otherwise = pure $ result page diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index fe6c18a4fdc..34787ef69b1 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -208,14 +208,32 @@ lookupRemoteMembers conv = do lookupRemoteMembersByDomain :: Domain -> Client [(ConvId, RemoteMember)] lookupRemoteMembersByDomain dom = do - fmap (fmap mkConvMem) . retry x1 $ query Cql.selectRemoteMembersByDomain (params LocalQuorum (Identity dom)) + mkConvMem <$$$> retry x1 $ query Cql.selectRemoteMembersByDomain (params LocalQuorum (Identity dom)) where mkConvMem (convId, usr, role) = (convId, RemoteMember (toRemoteUnsafe dom usr) role) +lookupRemoteMembersByConvAndDomain :: ConvId -> Domain -> Client [RemoteMember] +lookupRemoteMembersByConvAndDomain conv dom = do + mkMem <$$$> retry x1 $ query Cql.selectRemoteMembersByConvAndDomain (params LocalQuorum (conv, dom)) + where + mkMem (usr, role) = RemoteMember (toRemoteUnsafe dom usr) role + lookupLocalMembersByDomain :: Domain -> Client [(ConvId, UserId)] lookupLocalMembersByDomain dom = do retry x1 $ query Cql.selectLocalMembersByDomain (params LocalQuorum (Identity dom)) +removeRemoteDomain :: ConvId -> Domain -> Client () +removeRemoteDomain convId dom = do + retry x1 $ write Cql.removeRemoteDomain $ params LocalQuorum (convId, dom) + +selectConvIdsByRemoteDomain :: Domain -> Client [ConvId] +selectConvIdsByRemoteDomain dom = do + runIdentity <$$$> retry x1 $ query Cql.selectConvIdsByRemoteDomain $ params LocalQuorum $ Identity dom + +checkConvForRemoteDomain :: ConvId -> Domain -> Client (Maybe ConvId) +checkConvForRemoteDomain convId dom = do + runIdentity <$$$> retry x1 $ query1 Cql.checkConvForRemoteDomain $ params LocalQuorum (convId, dom) + member :: ConvId -> UserId -> @@ -409,4 +427,8 @@ interpretMemberStoreToCassandra = interpret $ \case RemoveMLSClients lcnv quid cs -> embedClient $ removeMLSClients lcnv quid cs LookupMLSClients lcnv -> embedClient $ lookupMLSClients lcnv GetRemoteMembersByDomain dom -> embedClient $ lookupRemoteMembersByDomain dom + GetRemoteMembersByConvAndDomain conv dom -> embedClient $ lookupRemoteMembersByConvAndDomain conv dom GetLocalMembersByDomain dom -> embedClient $ lookupLocalMembersByDomain dom + RemoveRemoteDomain convId dom -> embedClient $ removeRemoteDomain convId dom + SelectConvIdsByRemoteDomain dom -> embedClient $ selectConvIdsByRemoteDomain dom + CheckConvForRemoteDomain convId dom -> embedClient $ checkConvForRemoteDomain convId dom diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index e04507c944e..4a03725ae4c 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -381,11 +381,24 @@ selectRemoteMembers = "select user_remote_domain, user_remote_id, conversation_r updateRemoteMemberConvRoleName :: PrepQuery W (RoleName, ConvId, Domain, UserId) () updateRemoteMemberConvRoleName = "update member_remote_user set conversation_role = ? where conv = ? and user_remote_domain = ? and user_remote_id = ?" +removeRemoteDomain :: PrepQuery W (ConvId, Domain) () +removeRemoteDomain = "delete from member_remote_user where conv = ? and user_remote_domain = ?" + -- Used when removing a federation domain, so that we can quickly list all of the affected remote users and conversations -- This returns local conversation IDs and remote users selectRemoteMembersByDomain :: PrepQuery R (Identity Domain) (ConvId, UserId, RoleName) selectRemoteMembersByDomain = "select conv, user_remote_id, conversation_role from member_remote_user where user_remote_domain = ?" +selectRemoteMembersByConvAndDomain :: PrepQuery R (ConvId, Domain) (UserId, RoleName) +selectRemoteMembersByConvAndDomain = "select user_remote_id, conversation_role from member_remote_user where conv = ? and user_remote_domain = ?" + +selectConvIdsByRemoteDomain :: PrepQuery R (Identity Domain) (Identity ConvId) +selectConvIdsByRemoteDomain = "select conv from member_remote_user where user_remote_domain = ?" + +-- Return a single element, as this is being used as a SQL exists analog +checkConvForRemoteDomain :: PrepQuery R (ConvId, Domain) (Identity ConvId) +checkConvForRemoteDomain = "select conv from member_remote_user where conv = ? and user_remote_domain = ? limit 1" + -- local user with remote conversations insertUserRemoteConv :: PrepQuery W (UserId, Domain, ConvId) () diff --git a/services/galley/src/Galley/Effects/DefederationNotifications.hs b/services/galley/src/Galley/Effects/DefederationNotifications.hs index 2c8cf12b987..aaef53bc794 100644 --- a/services/galley/src/Galley/Effects/DefederationNotifications.hs +++ b/services/galley/src/Galley/Effects/DefederationNotifications.hs @@ -3,6 +3,7 @@ module Galley.Effects.DefederationNotifications ( DefederationNotifications (..), sendDefederationNotifications, + sendOnConnectionRemovedNotifications, ) where @@ -11,5 +12,6 @@ import Polysemy data DefederationNotifications m a where SendDefederationNotifications :: Domain -> DefederationNotifications m () + SendOnConnectionRemovedNotifications :: Domain -> Domain -> DefederationNotifications m () makeSem ''DefederationNotifications diff --git a/services/galley/src/Galley/Effects/MemberStore.hs b/services/galley/src/Galley/Effects/MemberStore.hs index c8542a71f3e..604804aab66 100644 --- a/services/galley/src/Galley/Effects/MemberStore.hs +++ b/services/galley/src/Galley/Effects/MemberStore.hs @@ -36,8 +36,13 @@ module Galley.Effects.MemberStore checkLocalMemberRemoteConv, selectRemoteMembers, getRemoteMembersByDomain, + getRemoteMembersByConvAndDomain, getLocalMembersByDomain, + -- * Conversation checks + selectConvIdsByRemoteDomain, + checkConvForRemoteDomain, + -- * Update members setSelfMember, setOtherMember, @@ -48,6 +53,7 @@ module Galley.Effects.MemberStore -- * Delete members deleteMembers, deleteMembersInRemoteConversation, + removeRemoteDomain, ) where @@ -86,7 +92,11 @@ data MemberStore m a where GroupId -> MemberStore m (Map (Qualified UserId) (Set (ClientId, KeyPackageRef))) GetRemoteMembersByDomain :: Domain -> MemberStore m [(ConvId, RemoteMember)] + GetRemoteMembersByConvAndDomain :: ConvId -> Domain -> MemberStore m [RemoteMember] GetLocalMembersByDomain :: Domain -> MemberStore m [(ConvId, UserId)] + RemoveRemoteDomain :: ConvId -> Domain -> MemberStore m () + SelectConvIdsByRemoteDomain :: Domain -> MemberStore m [ConvId] + CheckConvForRemoteDomain :: ConvId -> Domain -> MemberStore m (Maybe ConvId) makeSem ''MemberStore diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs index e2fab6ac770..481c4ab5301 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/services/galley/src/Galley/Intra/Effects.hs @@ -26,11 +26,12 @@ where import Cassandra (ClientState, Consistency (LocalQuorum), Page (hasMore, nextPage, result), paginate, paramsP) import Control.Lens ((.~)) +import Data.Id (ProviderId, ServiceId, UserId) import Data.Range (Range (fromRange)) import Galley.API.Error import Galley.API.Util (localBotsAndUsers) import Galley.Cassandra.Conversation.Members (toMember) -import Galley.Cassandra.Queries (selectAllMembers) +import Galley.Cassandra.Queries (MemberStatus, selectAllMembers) import Galley.Cassandra.Store (embedClient) import Galley.Effects.BotAccess (BotAccess (..)) import Galley.Effects.BrigAccess (BrigAccess (..)) @@ -46,12 +47,15 @@ import Galley.Intra.Spar import Galley.Intra.Team import Galley.Intra.User import Galley.Monad +import Galley.Types.Conversations.Members (LocalMember) import Imports import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P import UnliftIO qualified +import Wire.API.Conversation (MutedStatus) +import Wire.API.Conversation.Role (RoleName) import Wire.API.Event.Federation qualified as Federation import Wire.API.Team.Member (ListType (ListComplete)) @@ -138,6 +142,7 @@ interpretGundeckAccess = interpret $ \case PushSlowly ps -> embedApp $ G.pushSlowly ps interpretDefederationNotifications :: + forall r a. ( Member (Embed IO) r, Member (Input Env) r, Member (Input ClientState) r, @@ -147,25 +152,47 @@ interpretDefederationNotifications :: Sem (DefederationNotifications ': r) a -> Sem r a interpretDefederationNotifications = interpret $ \case - SendDefederationNotifications domain -> do - maxPage <- inputs $ fromRange . currentFanoutLimit . _options -- This is based on the limits in removeIfLargeFanout - page <- embedClient $ paginate selectAllMembers (paramsP LocalQuorum () maxPage) - void $ sendNotificationPage page - where - pushEvents results = do - let (bots, mems) = localBotsAndUsers results - recipients = Intra.recipient <$> mems - event = Intra.FederationEvent $ Federation.Event Federation.FederationDelete domain - for_ (Intra.newPush ListComplete Nothing event recipients) $ \p -> do - -- Futurework: Transient or not? - -- RouteAny is used as it will wake up mobile clients - -- and notify them of the changes to federation state. - push1 $ p & Intra.pushRoute .~ Intra.RouteAny - deliverAsync (bots `zip` repeat (G.pushEventJson event)) - sendNotificationPage page = do - let res = result page - mems = mapMaybe toMember res - pushEvents mems - when (hasMore page) $ do - page' <- embedClient $ nextPage page - sendNotificationPage page' + SendDefederationNotifications domain -> + getPage + >>= void . sendNotificationPage (Federation.FederationDelete domain) + SendOnConnectionRemovedNotifications domainA domainB -> + getPage + >>= void . sendNotificationPage (Federation.FederationConnectionRemoved (domainA, domainB)) + where + getPage :: Sem r (Page PageType) + getPage = do + maxPage <- inputs (fromRange . currentFanoutLimit . _options) -- This is based on the limits in removeIfLargeFanout + embedClient $ paginate selectAllMembers (paramsP LocalQuorum () maxPage) + pushEvents :: Federation.Event -> [LocalMember] -> Sem r () + pushEvents eventData results = do + let (bots, mems) = localBotsAndUsers results + recipients = Intra.recipient <$> mems + event = Intra.FederationEvent eventData + for_ (Intra.newPush ListComplete Nothing event recipients) $ \p -> do + -- Futurework: Transient or not? + -- RouteAny is used as it will wake up mobile clients + -- and notify them of the changes to federation state. + push1 $ p & Intra.pushRoute .~ Intra.RouteAny + deliverAsync (bots `zip` repeat (G.pushEventJson event)) + sendNotificationPage :: Federation.Event -> Page PageType -> Sem r () + sendNotificationPage eventData page = do + let res = result page + mems = mapMaybe toMember res + pushEvents eventData mems + when (hasMore page) $ do + page' <- embedClient $ nextPage page + sendNotificationPage eventData page' + +type PageType = + ( UserId, + Maybe ServiceId, + Maybe ProviderId, + Maybe MemberStatus, + Maybe MutedStatus, + Maybe Text, + Maybe Bool, + Maybe Text, + Maybe Bool, + Maybe Text, + Maybe RoleName + ) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 664a2c80337..c753e81a5fc 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -272,7 +272,17 @@ tests s = -- As a lot of these tests are waiting on specific notifications to come through in a specified -- order, these tests will cause them to fail. -- See the Tasty docs on patterns. https://hackage.haskell.org/package/tasty-1.4.3#patterns - after AllFinish "$0 !~ /delete federation notifications/" $ test s "delete federation notifications" API.testDefederationNotifications + after AllFinish "$0 !~ /federation notifications/" $ + testGroup + "federation notifications" + -- Run these tests in order by having them wait on each other. + -- The names need to be distint enough so that there isn't a loop with the regexes + [ test s "delete federation notifications" testDefederationNotifications, + after AllFinish "$0 ~ /delete federation notifications/" $ test s "connection removed notifications normal" testConnectionRemovedNotifications, + after AllFinish "$0 ~ /connection removed notifications normal/" $ test s "connection removed notifications no-op" testConnectionRemovedNotificationsNoop, + after AllFinish "$0 ~ /connection removed notifications no-op/" $ test s "connection removed notifications domain A bias" testConnectionRemovedNotificationsNoopDomainA, + after AllFinish "$0 ~ /connection removed notifications domain A bias/" $ test s "connection removed notifications domain B bias" testConnectionRemovedNotificationsNoopDomainB + ] ] rb1, rb2, rb3, rb4 :: Remote Backend rb1 = @@ -4487,4 +4497,248 @@ testDefederationNotifications = do cmOthers (cnvMembers conv2)) @?= sort [bob, charlie] +-- Testing defederation notifications. The important thing to note for all +-- of this is that when defederating from a remote domain only _2_ notifications +-- are sent, and both are identical. One notification is at the start of +-- defederation, and one is sent at the end of defederation. No other +-- notifications about users being removed from conversations, or conversations +-- being deleted are sent. We are do not want to DOS either our local clients, +-- nor our own services. +-- There are four tests here. + +-- * A normal run where we have users from both remote domains in a conversation. Both remote users should be removed. + +-- * A no-op run where we have no remote users in the conversation. The conversation remains unchanged. + +-- * A domain A biased run where we have a conversation with a remote member from domain A, but none from domain B. The conversation remains unchanged. + +-- * A domain B biased run where we have a conversation with a remote member from domain B, but none from domain A. The conversation remains unchanged. + +testConnectionRemovedNotifications :: TestM () +testConnectionRemovedNotifications = do + -- alice, bob are in a team + (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 + + -- charlie is a local guest + charlie <- randomQualifiedUser + connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) + + let remoteDomain1 = Domain "far-away.example.com" + remoteDomain2 = Domain "far-away-2.example.com" + remoteDomain3 = Domain "far-away-3.example.com" + -- dee and erin are remote guests + dee <- Qualified <$> randomId <*> pure remoteDomain1 + erin <- Qualified <$> randomId <*> pure remoteDomain2 + -- frank is a remote we are going to keep around. + frank <- Qualified <$> randomId <*> pure remoteDomain3 + + -- Set up the federation + addFederation remoteDomain1 !!! const 200 === statusCode + addFederation remoteDomain2 !!! const 200 === statusCode + addFederation remoteDomain3 !!! const 200 === statusCode + + connectWithRemoteUser (qUnqualified alice) dee + connectWithRemoteUser (qUnqualified alice) erin + connectWithRemoteUser (qUnqualified alice) frank + + -- they are all in a local conversation + conv <- + responseJsonError + =<< postConvWithRemoteUsers + (qUnqualified alice) + Nothing + defNewProteusConv + { newConvQualifiedUsers = [bob, charlie, dee, erin, frank], + newConvTeam = Just (ConvTeamInfo tid) + } + do + -- conversation access role changes to team only + (_, reqs) <- withTempMockFederator' (mockReply ()) $ do + -- Remove the connection + connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode + -- First notification to local clients + WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ + wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 + -- Second notification to local clients + WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ + wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 + -- dee, erin, and frank's remotes don't receive a notification + WS.assertNoEvent (5 # Second) [wsD, wsE, wsF] + -- There should be not requests out to the federtaion domain + liftIO $ reqs @?= [] + + -- only alice, bob, charlie, and frank remain + conv2 <- + responseJsonError + =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) + cmOthers (cnvMembers conv2)) @?= sort [bob, charlie, frank] + +testConnectionRemovedNotificationsNoop :: TestM () +testConnectionRemovedNotificationsNoop = do + -- alice, bob are in a team + (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 + + -- charlie is a local guest + charlie <- randomQualifiedUser + connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) + + let remoteDomain1 = Domain "far-away.example.com" + remoteDomain2 = Domain "far-away-2.example.com" + + -- Setup federation + addFederation remoteDomain1 !!! const 200 === statusCode + addFederation remoteDomain2 !!! const 200 === statusCode + + -- they are all in a local conversation + conv <- + responseJsonError + =<< postConvWithRemoteUsers + (qUnqualified alice) + Nothing + defNewProteusConv + { newConvQualifiedUsers = [bob, charlie], + newConvTeam = Just (ConvTeamInfo tid) + } + do + -- conversation access role changes to team only + (_, reqs) <- withTempMockFederator' (mockReply ()) $ do + -- Remove the connection + connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode + -- First notification to local clients + WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ + wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 + -- Second notification to local clients + WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ + wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 + -- There should be not requests out to the federtaion domain + liftIO $ reqs @?= [] + + -- only alice, bob, and charlie remain + conv2 <- + responseJsonError + =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) + cmOthers (cnvMembers conv2)) @?= sort [bob, charlie] + +testConnectionRemovedNotificationsNoopDomainA :: TestM () +testConnectionRemovedNotificationsNoopDomainA = do + -- alice, bob are in a team + (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 + + -- charlie is a local guest + charlie <- randomQualifiedUser + connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) + + let remoteDomain1 = Domain "far-away.example.com" + remoteDomain2 = Domain "far-away-2.example.com" + + -- Setup federation + addFederation remoteDomain1 !!! const 200 === statusCode + addFederation remoteDomain2 !!! const 200 === statusCode + + -- dee is a remote guest + dee <- Qualified <$> randomId <*> pure remoteDomain1 + + connectWithRemoteUser (qUnqualified alice) dee + + -- they are all in a local conversation + conv <- + responseJsonError + =<< postConvWithRemoteUsers + (qUnqualified alice) + Nothing + defNewProteusConv + { newConvQualifiedUsers = [bob, charlie, dee], + newConvTeam = Just (ConvTeamInfo tid) + } + do + -- conversation access role changes to team only + (_, reqs) <- withTempMockFederator' (mockReply ()) $ do + -- Remove the connection + connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode + -- First notification to local clients + WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ + wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 + -- Second notification to local clients + WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ + wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 + -- dee and remote doesn't receive a notification + WS.assertNoEvent (5 # Second) [wsD] + -- There should be not requests out to the federtaion domain + liftIO $ reqs @?= [] + + -- alice, bob, charlie, and dee remain + conv2 <- + responseJsonError + =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) + cmOthers (cnvMembers conv2)) @?= sort [bob, charlie, dee] + +testConnectionRemovedNotificationsNoopDomainB :: TestM () +testConnectionRemovedNotificationsNoopDomainB = do + -- alice, bob are in a team + (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 + + -- charlie is a local guest + charlie <- randomQualifiedUser + connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) + + let remoteDomain1 = Domain "far-away.example.com" + remoteDomain2 = Domain "far-away-2.example.com" + + -- Setup federation + addFederation remoteDomain1 !!! const 200 === statusCode + addFederation remoteDomain2 !!! const 200 === statusCode + + -- erin is a remote guest + erin <- Qualified <$> randomId <*> pure remoteDomain2 + + connectWithRemoteUser (qUnqualified alice) erin + + -- they are all in a local conversation + conv <- + responseJsonError + =<< postConvWithRemoteUsers + (qUnqualified alice) + Nothing + defNewProteusConv + { newConvQualifiedUsers = [bob, charlie, erin], + newConvTeam = Just (ConvTeamInfo tid) + } + do + -- conversation access role changes to team only + (_, reqs) <- withTempMockFederator' (mockReply ()) $ do + -- Remove the connection + connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode + -- First notification to local clients + WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ + wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 + -- Second notification to local clients + WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ + wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 + -- erin's remote doesn't receive a notification + WS.assertNoEvent (5 # Second) [wsE] + -- There should be not requests out to the federtaion domain + liftIO $ reqs @?= [] + + -- alice, bob, charlie, and erin remain + conv2 <- + responseJsonError + =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) + cmOthers (cnvMembers conv2)) @?= sort [bob, charlie, erin] + -- @END diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index d260f74d6f6..87276bd4358 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -129,6 +129,7 @@ import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.API.Message import Wire.API.Message.Proto qualified as Proto +import Wire.API.Routes.FederationDomainConfig (FederationDomainConfig (..)) import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi @@ -147,6 +148,7 @@ import Wire.API.User.Auth hiding (Access) import Wire.API.User.Client import Wire.API.User.Client qualified as Client import Wire.API.User.Client.Prekey +import Wire.API.User.Search (FederatedUserSearchPolicy (..)) ------------------------------------------------------------------------------- -- API Operations @@ -1406,6 +1408,31 @@ deleteFederation dom = do delete $ g . paths ["/i/federation", toByteString' dom] +addFederation :: + (MonadHttp m, HasBrig m, MonadIO m) => + Domain -> + m ResponseLBS +addFederation dom = do + b <- viewBrig + post $ + b + . paths ["/i/federation/remotes"] + . json (FederationDomainConfig dom FullSearch) + +connectionRemovedFederation :: + (MonadHttp m, HasGalley m, MonadIO m) => + Domain -> + Domain -> + m ResponseLBS +connectionRemovedFederation origin target = do + g <- viewGalley + post $ + g + . paths ["federation", "on-connection-removed"] + . content "application/json" + . header "Wire-Origin-Domain" (toByteString' origin) + . json target + putQualifiedAccessUpdate :: (MonadHttp m, HasGalley m, MonadIO m) => UserId -> @@ -1785,8 +1812,25 @@ assertFederationDeletedEvent :: Fed.Event -> IO () assertFederationDeletedEvent dom e = do - Fed._eventType e @?= Fed.FederationDelete - Fed._eventDomain e @?= dom + e @?= Fed.FederationDelete dom + +wsAssertFederationConnectionRemoved :: + HasCallStack => + Domain -> + Domain -> + Notification -> + IO () +wsAssertFederationConnectionRemoved domA domB n = do + ntfTransient n @?= False + assertFederationConnectionRemovedEvent domA domB $ List1.head (WS.unpackPayload n) + +assertFederationConnectionRemovedEvent :: + Domain -> + Domain -> + Fed.Event -> + IO () +assertFederationConnectionRemovedEvent domA domB e = do + e @?= Fed.FederationConnectionRemoved (domA, domB) -- FUTUREWORK: See if this one can be implemented in terms of: -- diff --git a/services/galley/test/integration/Federation.hs b/services/galley/test/integration/Federation.hs index 56dd7704dd1..6e3d853f7d1 100644 --- a/services/galley/test/integration/Federation.hs +++ b/services/galley/test/integration/Federation.hs @@ -16,6 +16,7 @@ import Data.Id import Data.List.NonEmpty import Data.List1 qualified as List1 import Data.Qualified +import Data.Range (toRange) import Data.Set qualified as Set import Data.Singletons import Data.Time (getCurrentTime) @@ -164,7 +165,7 @@ deleteFederationDomains old new = do deletedDomains = Set.difference prev curr env <- ask -- Call into the galley code - for_ deletedDomains $ liftIO . evalGalleyToIO env . deleteFederationDomain + for_ deletedDomains $ liftIO . evalGalleyToIO env . deleteFederationDomain (toRange $ Proxy @500) constHandlers :: (MonadIO m) => [RetryStatus -> Handler m Bool] constHandlers = [const $ Handler $ (\(_ :: SomeException) -> pure True)] From 48ac13cf4ca49c7f7b460c3b57e89d7b729f571e Mon Sep 17 00:00:00 2001 From: fisx Date: Thu, 17 Aug 2023 12:48:17 +0200 Subject: [PATCH 085/225] Add `repair-brig-clients-table` to clean up after the fix in #3504. (#3507) Co-authored-by: Sven Tennie --- cabal.project | 1 + changelog.d/5-internal/wpb-3888 | 1 + nix/local-haskell-packages.nix | 1 + tools/db/repair-brig-clients-table/.ormolu | 1 + tools/db/repair-brig-clients-table/README.md | 12 +++ .../db/repair-brig-clients-table/default.nix | 38 ++++++++ .../repair-brig-clients-table.cabal | 81 ++++++++++++++++ .../db/repair-brig-clients-table/src/Main.hs | 58 ++++++++++++ .../repair-brig-clients-table/src/Options.hs | 93 +++++++++++++++++++ .../db/repair-brig-clients-table/src/Work.hs | 87 +++++++++++++++++ 10 files changed, 373 insertions(+) create mode 100644 changelog.d/5-internal/wpb-3888 create mode 120000 tools/db/repair-brig-clients-table/.ormolu create mode 100644 tools/db/repair-brig-clients-table/README.md create mode 100644 tools/db/repair-brig-clients-table/default.nix create mode 100644 tools/db/repair-brig-clients-table/repair-brig-clients-table.cabal create mode 100644 tools/db/repair-brig-clients-table/src/Main.hs create mode 100644 tools/db/repair-brig-clients-table/src/Options.hs create mode 100644 tools/db/repair-brig-clients-table/src/Work.hs diff --git a/cabal.project b/cabal.project index a6334c5db40..c2d8ae65d22 100644 --- a/cabal.project +++ b/cabal.project @@ -46,6 +46,7 @@ packages: , tools/db/migrate-sso-feature-flag/ , tools/db/move-team/ , tools/db/repair-handles/ + , tools/db/repair-brig-clients-table/ , tools/db/service-backfill/ , tools/fedcalls/ , tools/rex/ diff --git a/changelog.d/5-internal/wpb-3888 b/changelog.d/5-internal/wpb-3888 new file mode 100644 index 00000000000..d18f4de6508 --- /dev/null +++ b/changelog.d/5-internal/wpb-3888 @@ -0,0 +1 @@ +Added `/tools/db/repair-brig-clients-table` to clean up after the fix in #3504 \ No newline at end of file diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index 0770af43b6d..dea24d5abe5 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -49,6 +49,7 @@ inconsistencies = hself.callPackage ../tools/db/inconsistencies/default.nix { inherit gitignoreSource; }; migrate-sso-feature-flag = hself.callPackage ../tools/db/migrate-sso-feature-flag/default.nix { inherit gitignoreSource; }; move-team = hself.callPackage ../tools/db/move-team/default.nix { inherit gitignoreSource; }; + repair-brig-clients-table = hself.callPackage ../tools/db/repair-brig-clients-table/default.nix { inherit gitignoreSource; }; repair-handles = hself.callPackage ../tools/db/repair-handles/default.nix { inherit gitignoreSource; }; service-backfill = hself.callPackage ../tools/db/service-backfill/default.nix { inherit gitignoreSource; }; fedcalls = hself.callPackage ../tools/fedcalls/default.nix { inherit gitignoreSource; }; diff --git a/tools/db/repair-brig-clients-table/.ormolu b/tools/db/repair-brig-clients-table/.ormolu new file mode 120000 index 00000000000..ffc2ca9745e --- /dev/null +++ b/tools/db/repair-brig-clients-table/.ormolu @@ -0,0 +1 @@ +../../../.ormolu \ No newline at end of file diff --git a/tools/db/repair-brig-clients-table/README.md b/tools/db/repair-brig-clients-table/README.md new file mode 100644 index 00000000000..16cb7a126f1 --- /dev/null +++ b/tools/db/repair-brig-clients-table/README.md @@ -0,0 +1,12 @@ +context: +- https://github.com/wireapp/wire-server/pull/3504 +- https://wearezeta.atlassian.net/browse/WPB-3888 + +Connects to brig database. + +Set up port-forwarding to brig database (hacky, slow, maybe dangerous), or run from a machine with access to those databases (preferred approach). Refer to ../service-backfill/ for an example. Then: + +```sh +# assuming local port forwarding cassandra_galley on 2021 and cassandra_spar on 2022: +./dist/repair-brig-clients-table --cassandra-host-brig localhost --cassandra-port-brig 2022 --cassandra-keyspace-brig spar +``` diff --git a/tools/db/repair-brig-clients-table/default.nix b/tools/db/repair-brig-clients-table/default.nix new file mode 100644 index 00000000000..ea9dcfe43c7 --- /dev/null +++ b/tools/db/repair-brig-clients-table/default.nix @@ -0,0 +1,38 @@ +# WARNING: GENERATED FILE, DO NOT EDIT. +# This file is generated by running hack/bin/generate-local-nix-packages.sh and +# must be regenerated whenever local packages are added or removed, or +# dependencies are added or removed. +{ mkDerivation +, base +, cassandra-util +, conduit +, gitignoreSource +, imports +, lens +, lib +, optparse-applicative +, time +, tinylog +, types-common +}: +mkDerivation { + pname = "repair-brig-clients-table"; + version = "1.0.0"; + src = gitignoreSource ./.; + isLibrary = false; + isExecutable = true; + executableHaskellDepends = [ + base + cassandra-util + conduit + imports + lens + optparse-applicative + time + tinylog + types-common + ]; + description = "Removes and reports entries from brig.clients that have been accidentally upserted."; + license = lib.licenses.agpl3Only; + mainProgram = "repair-brig-clients-table"; +} diff --git a/tools/db/repair-brig-clients-table/repair-brig-clients-table.cabal b/tools/db/repair-brig-clients-table/repair-brig-clients-table.cabal new file mode 100644 index 00000000000..5347b11d73a --- /dev/null +++ b/tools/db/repair-brig-clients-table/repair-brig-clients-table.cabal @@ -0,0 +1,81 @@ +cabal-version: 1.12 +name: repair-brig-clients-table +version: 1.0.0 +synopsis: + Removes and reports entries from brig.clients that have been accidentally upserted. + +category: Network +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2023 Wire Swiss GmbH +license: AGPL-3 +build-type: Simple + +executable repair-brig-clients-table + main-is: Main.hs + other-modules: + Options + Paths_repair_brig_clients_table + Work + + hs-source-dirs: src + default-extensions: + NoImplicitPrelude + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + DuplicateRecordFields + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + OverloadedRecordDot + OverloadedStrings + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns + + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T + -rtsopts -Wredundant-constraints -Wunused-packages + + build-depends: + base + , cassandra-util + , conduit + , imports + , lens + , optparse-applicative + , time + , tinylog + , types-common + + default-language: GHC2021 diff --git a/tools/db/repair-brig-clients-table/src/Main.hs b/tools/db/repair-brig-clients-table/src/Main.hs new file mode 100644 index 00000000000..30da3ef880d --- /dev/null +++ b/tools/db/repair-brig-clients-table/src/Main.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Main + ( main, + ) +where + +import Cassandra as C +import Cassandra.Settings as C +import Control.Lens hiding ((.=)) +import Imports +import Options as O +import Options.Applicative +import System.Logger qualified as Log +import Work + +main :: IO () +main = do + s <- execParser (info (helper <*> settingsParser) desc) + lgr <- initLogger + bc <- initCas (s ^. setCasBrig) lgr -- Brig's Cassandra + runCommand (s ^. setDryRun) lgr bc + where + desc = + header "repair-brig-clients-table" + <> progDesc "Removes and reports entries from brig.clients that have been accidentally upserted." + <> fullDesc + initLogger = + Log.new + . Log.setOutput Log.StdOut + . Log.setFormat Nothing + . Log.setBufSize 0 + $ Log.defSettings + initCas cas l = + C.init + . C.setLogger (C.mkLogger l) + . C.setContacts (cas ^. cHosts) [] + . C.setPortNumber (fromIntegral $ cas ^. cPort) + . C.setKeyspace (cas ^. cKeyspace) + . C.setProtocolVersion C.V4 + $ C.defSettings diff --git a/tools/db/repair-brig-clients-table/src/Options.hs b/tools/db/repair-brig-clients-table/src/Options.hs new file mode 100644 index 00000000000..123c3943511 --- /dev/null +++ b/tools/db/repair-brig-clients-table/src/Options.hs @@ -0,0 +1,93 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Options + ( setCasBrig, + setDryRun, + cHosts, + cPort, + cKeyspace, + settingsParser, + ) +where + +import Cassandra qualified as C +import Control.Lens +import Data.Text.Strict.Lens +import Imports +import Options.Applicative + +data MigratorSettings = MigratorSettings + { _setCasBrig :: !CassandraSettings, + _setDryRun :: !Bool + } + deriving (Show) + +data CassandraSettings = CassandraSettings + { _cHosts :: !String, + _cPort :: !Word16, + _cKeyspace :: !C.Keyspace + } + deriving (Show) + +makeLenses ''MigratorSettings + +makeLenses ''CassandraSettings + +settingsParser :: Parser MigratorSettings +settingsParser = + MigratorSettings + <$> cassandraSettingsParser "brig" + <*> dryRunParser + +dryRunParser :: Parser Bool +dryRunParser = + flag False True $ + ( long ("dry-run") + <> help ("Just detect offending rows, don't change the db") + <> showDefault + ) + +cassandraSettingsParser :: String -> Parser CassandraSettings +cassandraSettingsParser ks = + CassandraSettings + <$> strOption + ( long ("cassandra-host-" ++ ks) + <> metavar "HOST" + <> help ("Cassandra Host for: " ++ ks) + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long ("cassandra-port-" ++ ks) + <> metavar "PORT" + <> help ("Cassandra Port for: " ++ ks) + <> value 9042 + <> showDefault + ) + <*> ( C.Keyspace . view packed + <$> strOption + ( long ("cassandra-keyspace-" ++ ks) + <> metavar "STRING" + <> help ("Cassandra Keyspace for: " ++ ks) + <> value (ks ++ "_test") + <> showDefault + ) + ) diff --git a/tools/db/repair-brig-clients-table/src/Work.hs b/tools/db/repair-brig-clients-table/src/Work.hs new file mode 100644 index 00000000000..41eca357c92 --- /dev/null +++ b/tools/db/repair-brig-clients-table/src/Work.hs @@ -0,0 +1,87 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Work where + +import Cassandra +import Data.Conduit +import Data.Conduit.Internal (zipSources) +import Data.Conduit.List qualified as C +import Data.Id +import Data.Time.Clock +import Imports +import System.Logger (Logger) +import System.Logger qualified as Log + +runCommand :: Bool -> Logger -> ClientState -> IO () +runCommand dryRun l brig = + runConduit $ + zipSources + (C.sourceList [(1 :: Int32) ..]) + (transPipe (runClient brig) getClients) + .| C.mapM + ( \(i, rows) -> do + Log.info l (Log.field "number of clients processed: " (show (i * pageSize))) + pure rows + ) + .| C.mapM_ (\rows -> runClient brig (mapM_ (filterReportRemove dryRun l) rows)) + +pageSize :: Int32 +pageSize = 1000 + +type ClientRow = + ( UserId, -- user + Text, -- client + Maybe (Cassandra.Set Int32), -- capabilities + Maybe Int32, -- class + Maybe Text, -- cookie + Maybe Text, -- label + Maybe UTCTime, -- last_active + Maybe Text, -- model + Maybe UTCTime, -- tstamp + Maybe Int32 -- type + ) + +getClients :: ConduitM () [ClientRow] Client () +getClients = paginateC cql (paramsP LocalQuorum () pageSize) x5 + where + cql :: PrepQuery R () ClientRow + cql = "select user, client, capabilities, class, cookie, label, last_active, model, tstamp, type from clients" + +filterReportRemove :: Bool -> Logger -> ClientRow -> Client () +filterReportRemove dryRun l row@(user, client, Nothing, Nothing, Nothing, Nothing, Just _lastActive, Nothing, Nothing, Nothing) = do + Log.info l (Log.msg $ "*** bad row in brig.clients: " <> show row) + if dryRun + then do + Log.info l (Log.msg $ "would run: " <> rmqs) + else do + Log.info l (Log.msg $ "running: " <> rmqs) + rm user client + Log.info l (Log.msg @Text "removed!") + where + rm :: MonadClient m => UserId -> Text -> m () + rm uid cid = + retry x5 $ write rmq (params LocalQuorum (uid, cid)) + + rmq :: PrepQuery W (UserId, Text) () + rmq = fromString rmqs + + rmqs :: String + rmqs = "delete from clients where user = ? and client = ?" +filterReportRemove _ _ _ = pure () From 5e2abc9d799fba7ca3b69868df9bff1a4a2504f2 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 17 Aug 2023 21:06:21 +0200 Subject: [PATCH 086/225] Reject stale application messages (#3438) --- changelog.d/2-features/mls-stale-app-messages | 1 + integration/test/Test/MLS.hs | 27 +++++++++++++++++++ services/galley/src/Galley/API/MLS/Message.hs | 14 +++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 changelog.d/2-features/mls-stale-app-messages diff --git a/changelog.d/2-features/mls-stale-app-messages b/changelog.d/2-features/mls-stale-app-messages new file mode 100644 index 00000000000..5005ccbac9d --- /dev/null +++ b/changelog.d/2-features/mls-stale-app-messages @@ -0,0 +1 @@ +MLS application messages for older epochs are now rejected diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index b447739c444..2d333f40876 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -39,6 +39,33 @@ testSendMessageNoReturnToSender = do ) wsSender +testStaleApplicationMessage :: HasCallStack => Domain -> App () +testStaleApplicationMessage otherDomain = do + [alice, bob, charlie, dave, eve] <- + createAndConnectUsers [OwnDomain, otherDomain, OwnDomain, OwnDomain, OwnDomain] + [alice1, bob1, charlie1] <- traverse createMLSClient [alice, bob, charlie] + traverse_ uploadNewKeyPackage [bob1, charlie1] + void $ createNewGroup alice1 + + -- alice adds bob first + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + -- bob prepares some application messages + [msg1, msg2] <- replicateM 2 $ createApplicationMessage bob1 "hi alice" + + -- alice adds charlie and dave with different commits + void $ createAddCommit alice1 [charlie] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 [dave] >>= sendAndConsumeCommitBundle + + -- bob's application messages still go through + void $ postMLSMessage bob1 msg1.message >>= getJSON 201 + + -- alice adds eve + void $ createAddCommit alice1 [eve] >>= sendAndConsumeCommitBundle + + -- bob's application messages are now rejected + void $ postMLSMessage bob1 msg2.message >>= getJSON 409 + testMixedProtocolUpgrade :: HasCallStack => Domain -> App () testMixedProtocolUpgrade secondDomain = do (alice, tid) <- createTeam OwnDomain diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index a04628fc347..38251f475b5 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -373,6 +373,7 @@ postMLSMessageToLocalConv :: Sem r ([LocalConversationUpdate], Maybe UnreachableUsers) postMLSMessageToLocalConv qusr c con msg ctype convOrSubId = do lConvOrSub <- fetchConvOrSub qusr msg.groupId ctype convOrSubId + let convOrSub = tUnqualified lConvOrSub for_ msg.sender $ \sender -> void $ getSenderIdentity qusr c sender lConvOrSub @@ -382,12 +383,23 @@ postMLSMessageToLocalConv qusr c con msg ctype convOrSubId = do IncomingMessageContentPublic pub -> case pub.content of FramedContentCommit _commit -> throwS @'MLSUnsupportedMessage FramedContentApplicationData _ -> throwS @'MLSUnsupportedMessage + -- proposal message FramedContentProposal prop -> processProposal qusr lConvOrSub msg.groupId msg.epoch pub prop IncomingMessageContentPrivate -> do - when ((tUnqualified lConvOrSub).migrationState == MLSMigrationMixed) $ + -- application message: + + -- reject all application messages if the conv is in mixed state + when (convOrSub.migrationState == MLSMigrationMixed) $ throwS @'MLSUnsupportedMessage + -- reject application messages older than 2 epochs + let epochInt :: Epoch -> Integer + epochInt = fromIntegral . epochNumber + when + (epochInt msg.epoch < epochInt convOrSub.mlsMeta.cnvmlsEpoch - 2) + $ throwS @'MLSStaleMessage + unreachables <- propagateMessage qusr (Just c) lConvOrSub con msg.rawMessage (tUnqualified lConvOrSub).members pure ([], unreachables) From 02bbf2ada43184271781ee49b7e7ace402a169e2 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 18 Aug 2023 14:08:24 +0200 Subject: [PATCH 087/225] Fix bug: federatorInternal host not set for background-worker (#3516) --- integration/test/Testlib/ModService.hs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 796af0cf33f..0de729cd80b 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -161,7 +161,7 @@ startDynamicBackend resource staticPorts beOverrides = do >=> setKeyspace srv >=> setEsIndex srv >=> setFederationSettings srv - >=> setAwsAdnQueuesConfigs srv + >=> setAwsConfigs srv >=> setLogLevel srv ) defaultServiceOverridesToMap @@ -176,20 +176,18 @@ startDynamicBackend resource staticPorts beOverrides = do in Map.insert resource.berDomain (setFederatorPorts resource $ updateServiceMap ports templateBackend) sm ) where - setAwsAdnQueuesConfigs :: Service -> Value -> App Value - setAwsAdnQueuesConfigs = \case + setAwsConfigs :: Service -> Value -> App Value + setAwsConfigs = \case Brig -> setField "aws.userJournalQueue" resource.berAwsUserJournalQueue >=> setField "aws.prekeyTable" resource.berAwsPrekeyTable >=> setField "internalEvents.queueName" resource.berBrigInternalEvents >=> setField "emailSMS.email.sesQueue" resource.berEmailSMSSesQueue >=> setField "emailSMS.general.emailSender" resource.berEmailSMSEmailSender - >=> setField "rabbitmq.vHost" resource.berVHost Cargohold -> setField "aws.s3Bucket" resource.berAwsS3Bucket Gundeck -> setField "aws.queueName" resource.berAwsQueueName Galley -> setField "journal.queueName" resource.berGalleyJournal - >=> setField "rabbitmq.vHost" resource.berVHost _ -> pure setFederationSettings :: Service -> Value -> App Value @@ -197,21 +195,24 @@ startDynamicBackend resource staticPorts beOverrides = do \case Brig -> setField "optSettings.setFederationDomain" resource.berDomain - >=> setField - "optSettings.setFederationDomainConfigs" - ([] :: [Value]) + >=> setField "optSettings.setFederationDomainConfigs" ([] :: [Value]) >=> setField "federatorInternal.port" resource.berFederatorInternal >=> setField "federatorInternal.host" ("127.0.0.1" :: String) + >=> setField "rabbitmq.vHost" resource.berVHost Cargohold -> setField "settings.federationDomain" resource.berDomain + >=> setField "federator.host" ("127.0.0.1" :: String) >=> setField "federator.port" resource.berFederatorInternal Galley -> setField "settings.federationDomain" resource.berDomain >=> setField "settings.featureFlags.classifiedDomains.config.domains" [resource.berDomain] + >=> setField "federator.host" ("127.0.0.1" :: String) >=> setField "federator.port" resource.berFederatorInternal + >=> setField "rabbitmq.vHost" resource.berVHost Gundeck -> setField "settings.federationDomain" resource.berDomain BackgroundWorker -> setField "federatorInternal.port" resource.berFederatorInternal + >=> setField "federatorInternal.host" ("127.0.0.1" :: String) >=> setField "rabbitmq.vHost" resource.berVHost _ -> pure From 13e3f09b23c77dac7a75c0c6877056ef3a1aafca Mon Sep 17 00:00:00 2001 From: Owen Harvey Date: Fri, 18 Aug 2023 23:09:18 +1000 Subject: [PATCH 088/225] WPB-3916: Filtering out duplicate members when sending defederation notifications (#3515) --- .../duplicate-member-notifications | 1 + integration/test/Test/Defederation.hs | 108 +++++++++--------- services/galley/src/Galley/Intra/Effects.hs | 40 +++++-- .../galley/src/Galley/Intra/Push/Internal.hs | 2 +- 4 files changed, 90 insertions(+), 61 deletions(-) create mode 100644 changelog.d/3-bug-fixes/duplicate-member-notifications diff --git a/changelog.d/3-bug-fixes/duplicate-member-notifications b/changelog.d/3-bug-fixes/duplicate-member-notifications new file mode 100644 index 00000000000..120b5bc7ebf --- /dev/null +++ b/changelog.d/3-bug-fixes/duplicate-member-notifications @@ -0,0 +1 @@ +Defederation notifications, federation.delete and federation.connectionRemoved, now deduplicate the user list so that we don't send them more notifications than required. \ No newline at end of file diff --git a/integration/test/Test/Defederation.hs b/integration/test/Test/Defederation.hs index 5712e33088c..73399d96280 100644 --- a/integration/test/Test/Defederation.hs +++ b/integration/test/Test/Defederation.hs @@ -1,10 +1,10 @@ module Test.Defederation where import API.BrigInternal --- import API.BrigInternal qualified as Internal --- import API.Galley (defProteus, getConversation, postConversation, qualifiedUsers) --- import Control.Applicative --- import Data.Aeson qualified as Aeson +import API.BrigInternal qualified as Internal +import API.Galley (defProteus, getConversation, postConversation, qualifiedUsers) +import Control.Applicative +import Data.Aeson qualified as Aeson import GHC.Stack import SetupHelpers import Testlib.Prelude @@ -26,54 +26,60 @@ testDefederationRemoteNotifications = do void $ deleteFedConn OwnDomain remoteDomain void $ awaitNMatches 2 3 (\n -> nPayload n %. "type" `isEqual` "federation.connectionRemoved") ws --- FUTUREWORK: temporarily disabled, enable when fixed on CI --- testDefederationNonFullyConnectedGraph :: HasCallStack => App () --- testDefederationNonFullyConnectedGraph = do --- let setFederationConfig = --- setField "optSettings.setFederationStrategy" "allowDynamic" --- >=> removeField "optSettings.setFederationDomainConfigs" --- >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) --- startDynamicBackends --- [ def {dbBrig = setFederationConfig}, --- def {dbBrig = setFederationConfig}, --- def {dbBrig = setFederationConfig} --- ] --- $ \dynDomains -> do --- domains@[domainA, domainB, domainC] <- pure dynDomains --- connectAllDomainsAndWaitToSync 1 domains --- [uA, uB, uC] <- createAndConnectUsers [domainA, domainB, domainC] --- -- create group conversation owned by domainA with users from domainB and domainC --- convId <- bindResponse (postConversation uA (defProteus {qualifiedUsers = [uB, uC]})) $ \r -> do --- r.status `shouldMatchInt` 201 --- r.json %. "qualified_id" +testDefederationNonFullyConnectedGraph :: HasCallStack => App () +testDefederationNonFullyConnectedGraph = do + let setFederationConfig = + setField "optSettings.setFederationStrategy" "allowDynamic" + >=> removeField "optSettings.setFederationDomainConfigs" + >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) + startDynamicBackends + [ def {dbBrig = setFederationConfig}, + def {dbBrig = setFederationConfig}, + def {dbBrig = setFederationConfig} + ] + $ \dynDomains -> do + domains@[domainA, domainB, domainC] <- pure dynDomains + connectAllDomainsAndWaitToSync 1 domains --- -- check conversation exists on all backends --- for [uB, uC] objQidObject >>= checkConv convId uA + -- create a few extra users and connections to make sure that does not lead to any extra `connectionRemoved` notifications + [uA, uA2, _, _, uB, uC] <- createAndConnectUsers [domainA, domainA, domainA, domainA, domainB, domainC] >>= traverse objQidObject --- -- one of the 2 non-conversation-owning domains (domainB and domainC) --- -- defederate from the other non-conversation-owning domain --- void $ Internal.deleteFedConn domainB domainC + -- create group conversation owned by domainA with users from domainB and domainC + convId <- bindResponse (postConversation uA (defProteus {qualifiedUsers = [uA2, uB, uC]})) $ \r -> do + r.status `shouldMatchInt` 201 + r.json %. "qualified_id" --- -- assert that clients from domainA receive federation.connectionRemoved events --- -- Notifications being delivered at least n times is what we want to ensure here, --- -- however they are often delivered more than once, so check that it doesn't happen --- -- hundreds of times. --- let isConnectionRemoved n = do --- correctType <- nPayload n %. "type" `isEqual` "federation.connectionRemoved" --- if correctType --- then do --- domsV <- nPayload n %. "domains" & asList --- domsStr <- for domsV asString <&> sort --- pure $ domsStr == sort [domainB, domainC] --- else pure False --- void $ awaitNToMMatches 2 10 20 isConnectionRemoved wsA + -- check conversation exists on all backends + checkConv convId uA [uB, uC, uA2] + checkConv convId uB [uA, uC, uA2] + checkConv convId uC [uA, uB, uA2] --- retryT $ checkConv convId uA [] --- where --- checkConv :: Value -> Value -> [Value] -> App () --- checkConv convId user expectedOtherMembers = do --- bindResponse (getConversation user convId) $ \r -> do --- r.status `shouldMatchInt` 200 --- members <- r.json %. "members.others" & asList --- qIds <- for members (\m -> m %. "qualified_id") --- qIds `shouldMatchSet` expectedOtherMembers + withWebSocket uA $ \wsA -> do + -- one of the 2 non-conversation-owning domains (domainB and domainC) + -- defederate from the other non-conversation-owning domain + void $ Internal.deleteFedConn domainB domainC + + -- assert that clients from domainA receive federation.connectionRemoved events + -- Notifications being delivered exactly twice + void $ awaitNMatches 2 20 (isConnectionRemoved [domainB, domainC]) wsA + + -- remote members should be removed from local conversation eventually + retryT $ checkConv convId uA [uA2] + where + isConnectionRemoved :: [String] -> Value -> App Bool + isConnectionRemoved domains n = do + correctType <- nPayload n %. "type" `isEqual` "federation.connectionRemoved" + if correctType + then do + domsV <- nPayload n %. "domains" & asList + domsStr <- for domsV asString <&> sort + pure $ domsStr == sort domains + else pure False + + checkConv :: Value -> Value -> [Value] -> App () + checkConv convId user expectedOtherMembers = do + bindResponse (getConversation user convId) $ \r -> do + r.status `shouldMatchInt` 200 + members <- r.json %. "members.others" & asList + qIds <- for members (\m -> m %. "qualified_id") + qIds `shouldMatchSet` expectedOtherMembers diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs index 481c4ab5301..e6639c4ce96 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/services/galley/src/Galley/Intra/Effects.hs @@ -28,6 +28,7 @@ import Cassandra (ClientState, Consistency (LocalQuorum), Page (hasMore, nextPag import Control.Lens ((.~)) import Data.Id (ProviderId, ServiceId, UserId) import Data.Range (Range (fromRange)) +import Data.Set qualified as Set import Galley.API.Error import Galley.API.Util (localBotsAndUsers) import Galley.Cassandra.Conversation.Members (toMember) @@ -141,6 +142,13 @@ interpretGundeckAccess = interpret $ \case Push ps -> embedApp $ G.push ps PushSlowly ps -> embedApp $ G.pushSlowly ps +-- FUTUREWORK: +-- This functions uses an in-memory set for tracking UserIds that we have already +-- sent notifications to. This set will only grow throughout the lifttime of this +-- function, and may cause memory & performance problems with millions of users. +-- How we are tracking which users have already been sent 0, 1, or 2 defederation +-- messages should be rethought to be more fault tollerant, e.g. this method doesn't +-- handle the server crashing and restarting. interpretDefederationNotifications :: forall r a. ( Member (Embed IO) r, @@ -154,34 +162,48 @@ interpretDefederationNotifications :: interpretDefederationNotifications = interpret $ \case SendDefederationNotifications domain -> getPage - >>= void . sendNotificationPage (Federation.FederationDelete domain) + >>= void . sendNotificationPage mempty (Federation.FederationDelete domain) SendOnConnectionRemovedNotifications domainA domainB -> getPage - >>= void . sendNotificationPage (Federation.FederationConnectionRemoved (domainA, domainB)) + >>= void . sendNotificationPage mempty (Federation.FederationConnectionRemoved (domainA, domainB)) where getPage :: Sem r (Page PageType) getPage = do maxPage <- inputs (fromRange . currentFanoutLimit . _options) -- This is based on the limits in removeIfLargeFanout + -- selectAllMembers will return duplicate members when they are in more than one chat + -- however we need the full row to build out the bot members to send notifications + -- to them. We have to do the duplicate filtering here. embedClient $ paginate selectAllMembers (paramsP LocalQuorum () maxPage) - pushEvents :: Federation.Event -> [LocalMember] -> Sem r () - pushEvents eventData results = do + pushEvents :: Set UserId -> Federation.Event -> [LocalMember] -> Sem r (Set UserId) + pushEvents seenRecipients eventData results = do let (bots, mems) = localBotsAndUsers results recipients = Intra.recipient <$> mems event = Intra.FederationEvent eventData - for_ (Intra.newPush ListComplete Nothing event recipients) $ \p -> do + filteredRecipients = + -- Deduplicate by UserId the page of recipients that we are working on + nubBy (\a b -> a._recipientUserId == b._recipientUserId) + -- Sort the remaining recipients by their IDs + $ + sortBy (\a b -> a._recipientUserId `compare` b._recipientUserId) + -- Filter out any recipient that we have already seen in a previous page + $ + filter (\r -> r._recipientUserId `notElem` seenRecipients) recipients + for_ (Intra.newPush ListComplete Nothing event filteredRecipients) $ \p -> do -- Futurework: Transient or not? -- RouteAny is used as it will wake up mobile clients -- and notify them of the changes to federation state. push1 $ p & Intra.pushRoute .~ Intra.RouteAny deliverAsync (bots `zip` repeat (G.pushEventJson event)) - sendNotificationPage :: Federation.Event -> Page PageType -> Sem r () - sendNotificationPage eventData page = do + -- Add the users to the set of users we've sent messages to. + pure $ seenRecipients <> Set.fromList ((._recipientUserId) <$> filteredRecipients) + sendNotificationPage :: Set UserId -> Federation.Event -> Page PageType -> Sem r () + sendNotificationPage seenRecipients eventData page = do let res = result page mems = mapMaybe toMember res - pushEvents eventData mems + seenRecipients' <- pushEvents seenRecipients eventData mems when (hasMore page) $ do page' <- embedClient $ nextPage page - sendNotificationPage eventData page' + sendNotificationPage seenRecipients' eventData page' type PageType = ( UserId, diff --git a/services/galley/src/Galley/Intra/Push/Internal.hs b/services/galley/src/Galley/Intra/Push/Internal.hs index 971c9e283a7..0f6deec5619 100644 --- a/services/galley/src/Galley/Intra/Push/Internal.hs +++ b/services/galley/src/Galley/Intra/Push/Internal.hs @@ -60,7 +60,7 @@ data RecipientBy user = Recipient { _recipientUserId :: user, _recipientClients :: RecipientClients } - deriving stock (Functor, Foldable, Traversable, Show) + deriving stock (Functor, Foldable, Traversable, Show, Ord, Eq) makeLenses ''RecipientBy From 71e0769b74a90b9167022fdd755a9ff9451d8b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Ivankovi=C4=87?= <33936733+Veki301@users.noreply.github.com> Date: Mon, 21 Aug 2023 10:08:21 +0200 Subject: [PATCH 089/225] fix: bad config, missing config values refactor: documentation now outlines how to deploy using own certificates aswell --- charts/outlook-addin/README.md | 20 ++++++++++++++++---- charts/outlook-addin/values.yaml | 8 +++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/charts/outlook-addin/README.md b/charts/outlook-addin/README.md index 474d06c2a3a..6fe0d2263d1 100644 --- a/charts/outlook-addin/README.md +++ b/charts/outlook-addin/README.md @@ -150,10 +150,6 @@ Append the following configuration (change the example.com with your domain). host: "outlook.example.com" # this entry has to be without https://!!! wireApiBaseUrl: "https://nginz-https.example.com" wireAuthorizationEndpoint: "https://webapp.example.com/auth" -tls: - issuerRef: - name: letsencrypt-http01 -# Should probably comment here clientId: "" ``` @@ -167,12 +163,28 @@ As of the time of writing nginz used by wire-server is not set up to whitelist o - outlook # add outlook entry so your addin doesnt get CORS blocked ``` +### Certificates + +If you are using cert-manager just make the following configuration in values.yaml: + +``` +tls: + issuerRef: + name: letsencrypt-http01 # letsencrypt-http01 is a default config in wire-server, change if needed in your instance +``` + Now deploy outlook addin chart with: ``` d helm upgrade --install outlook-addin charts/outlook-addin --values values/outlook-addin/values.yaml ``` +If you are using your own provided certificates, deploy the addin with this command: + +``` +d helm upgrade --install outlook-addin charts/outlook-addin --values values/outlook-addin/values.yaml --set-file tls.crt=/path/to/tls.crt --set-file tls.key=/path/to/tls.key +``` + ## Install Wire AddIn in Microsoft Outlook After deploying `outlook-addin` you will be able to find `manifest.xml` file on https://outlook.your.domain.com/manifest.xml which you can use to install the addin in your outlook. You can find instructions and screenshots how to do it [here](https://github.com/tlebon/outlook-addin/blob/staging/README.md#how-to-install-the-add-in-in-ms-outlook). diff --git a/charts/outlook-addin/values.yaml b/charts/outlook-addin/values.yaml index 10220b546f5..621b65eea41 100644 --- a/charts/outlook-addin/values.yaml +++ b/charts/outlook-addin/values.yaml @@ -1,10 +1,12 @@ containerImage: "quay.io/wire/outlook-addin:0.1.3" -ingressClass: nginx +allowOrigin: "https://webapp.example.com, https://nginz-https.example.com" +config: + ingressClass: nginx #host: "outlook.example.com" #wireApiBaseUrl: "https://nginz-https.example.com" #wireAuthorizationEndpoint: "https://webapp.example.com/auth" #tls: # issuerRef: # name: letsencrypt-http01 -# Should probably comment here -#clientId: "" +# clientId is obtained after registering outlook service with wire OAuth +# clientId: "" From 52d69cace7a77c7560855239b5d543181d6294a4 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 21 Aug 2023 14:38:52 +0200 Subject: [PATCH 090/225] integration: Add test to verify behaviour with offline backends (#3501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * background-worker: Make push backoff times configurable * brig/getFederationStatus: Always return NonConnectedBackends as empty when fed policy is AllowAll * integration: Use separate vHosts for backendA and B. * integration/RunServices: Add hack to make federation work * integration: Add test to verify behaviour with offline backends * helm-var-integration: Workaround bug with federation * integration-test.sh: Run new integration test suite first --------- Co-authored-by: Marko Dimjašević --- charts/background-worker/values.yaml | 3 +- deploy/dockerephemeral/init_vhosts.sh | 2 + hack/bin/integration-test.sh | 2 +- hack/helm_vars/common.yaml.gotmpl | 4 + hack/helm_vars/wire-server/values.yaml.gotmpl | 10 +- hack/helmfile.yaml | 12 ++ integration/default.nix | 4 + integration/integration.cabal | 4 + integration/test/API/Brig.hs | 6 + integration/test/API/Galley.hs | 46 ++++++- integration/test/Notifications.hs | 72 ++++++++++ integration/test/SetupHelpers.hs | 10 +- integration/test/Test/Federation.hs | 130 ++++++++++++++++++ integration/test/Testlib/Assertions.hs | 44 +++--- integration/test/Testlib/Cannon.hs | 8 +- integration/test/Testlib/HTTP.hs | 12 +- integration/test/Testlib/JSON.hs | 25 ++++ integration/test/Testlib/ModService.hs | 2 + integration/test/Testlib/RunServices.hs | 22 ++- integration/test/Testlib/Types.hs | 10 +- .../background-worker.integration.yaml | 5 +- .../src/Wire/BackendNotificationPusher.hs | 4 +- .../src/Wire/BackgroundWorker/Env.hs | 2 + .../src/Wire/BackgroundWorker/Options.hs | 19 ++- .../Wire/BackendNotificationPusherSpec.hs | 3 + .../background-worker/test/Test/Wire/Util.hs | 2 + services/brig/src/Brig/API/Federation.hs | 10 +- 27 files changed, 422 insertions(+), 51 deletions(-) create mode 100644 integration/test/Notifications.hs create mode 100644 integration/test/Test/Federation.hs diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml index 6fcdb5e05be..fcae0115bfc 100644 --- a/charts/background-worker/values.yaml +++ b/charts/background-worker/values.yaml @@ -24,7 +24,8 @@ config: vHost: / adminPort: 15672 backendNotificationPusher: - remotesRefreshInterval: 60 # seconds + pushBackoffMinWait: 10000 # in microseconds, so 10ms + pushBackoffMaxWait: 300000000 # microseconds, so 300s serviceAccount: # When setting this to 'false', either make sure that a service account named diff --git a/deploy/dockerephemeral/init_vhosts.sh b/deploy/dockerephemeral/init_vhosts.sh index a7f9bd7c4a1..4c169ba4431 100755 --- a/deploy/dockerephemeral/init_vhosts.sh +++ b/deploy/dockerephemeral/init_vhosts.sh @@ -6,6 +6,8 @@ exec_until_ready() { echo 'Creating RabbitMQ resources' +exec_until_ready "curl -u $RABBITMQ_USERNAME:$RABBITMQ_PASSWORD -X PUT http://rabbitmq:15672/api/vhosts/backendA" +exec_until_ready "curl -u $RABBITMQ_USERNAME:$RABBITMQ_PASSWORD -X PUT http://rabbitmq:15672/api/vhosts/backendB" exec_until_ready "curl -u $RABBITMQ_USERNAME:$RABBITMQ_PASSWORD -X PUT http://rabbitmq:15672/api/vhosts/d1.example.com" exec_until_ready "curl -u $RABBITMQ_USERNAME:$RABBITMQ_PASSWORD -X PUT http://rabbitmq:15672/api/vhosts/d2.example.com" exec_until_ready "curl -u $RABBITMQ_USERNAME:$RABBITMQ_PASSWORD -X PUT http://rabbitmq:15672/api/vhosts/d3.example.com" diff --git a/hack/bin/integration-test.sh b/hack/bin/integration-test.sh index 27f85d0275e..841a3172fd8 100755 --- a/hack/bin/integration-test.sh +++ b/hack/bin/integration-test.sh @@ -11,7 +11,7 @@ UPLOAD_LOGS=${UPLOAD_LOGS:-0} echo "Running integration tests on wire-server with parallelism=${HELM_PARALLELISM} ..." CHART=wire-server -tests=(stern galley cargohold gundeck federator spar brig integration) +tests=(integration stern galley cargohold gundeck federator spar brig) cleanup() { if (( CLEANUP_LOCAL_FILES > 0 )); then diff --git a/hack/helm_vars/common.yaml.gotmpl b/hack/helm_vars/common.yaml.gotmpl index b5748c96012..010aa42dadb 100644 --- a/hack/helm_vars/common.yaml.gotmpl +++ b/hack/helm_vars/common.yaml.gotmpl @@ -5,3 +5,7 @@ federationDomain2: {{ requiredEnv "FEDERATION_DOMAIN_2" }} ingressChart: {{ requiredEnv "INGRESS_CHART" }} rabbitmqUsername: guest rabbitmqPassword: guest + +dynBackendDomain1: dynamic-backend-1.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster.local +dynBackendDomain2: dynamic-backend-2.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster.local +dynBackendDomain3: dynamic-backend-3.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster.local \ No newline at end of file diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index ba7fb2f042a..4bd06d953fe 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -85,6 +85,13 @@ brig: search_policy: full_search - domain: federation-test-helper.{{ .Release.Namespace }}.svc.cluster.local search_policy: full_search + # Remove these after fixing https://wearezeta.atlassian.net/browse/WPB-3796 + - domain: dyn-backend-1 + search_policy: full_search + - domain: dyn-backend-2 + search_policy: full_search + - domain: dyn-backend-3 + search_policy: full_search setFederationStrategy: allowAll setFederationDomainConfigsUpdateFreq: 10 set2FACodeGenerationDelaySecs: 5 @@ -312,7 +319,8 @@ background-worker: imagePullPolicy: {{ .Values.imagePullPolicy }} config: backendNotificationPusher: - remotesRefreshInterval: 1 + pushBackoffMinWait: 1000 # 1ms + pushBackoffMaxWait: 500000 # 0.5s secrets: rabbitmq: username: {{ .Values.rabbitmqUsername }} diff --git a/hack/helmfile.yaml b/hack/helmfile.yaml index d9568f2e5a8..b40cd73d623 100644 --- a/hack/helmfile.yaml +++ b/hack/helmfile.yaml @@ -129,6 +129,12 @@ releases: value: {{ .Values.federationDomain1 }} - name: brig.config.optSettings.setFederationDomainConfigs[0].domain value: {{ .Values.federationDomain2 }} + - name: brig.config.optSettings.setFederationDomainConfigs[2].domain + value: {{ .Values.dynBackendDomain1 }} + - name: brig.config.optSettings.setFederationDomainConfigs[3].domain + value: {{ .Values.dynBackendDomain2 }} + - name: brig.config.optSettings.setFederationDomainConfigs[4].domain + value: {{ .Values.dynBackendDomain3 }} needs: - 'databases-ephemeral' @@ -147,5 +153,11 @@ releases: value: {{ .Values.federationDomain2 }} - name: brig.config.optSettings.setFederationDomainConfigs[0].domain value: {{ .Values.federationDomain1 }} + - name: brig.config.optSettings.setFederationDomainConfigs[2].domain + value: {{ .Values.dynBackendDomain1 }} + - name: brig.config.optSettings.setFederationDomainConfigs[3].domain + value: {{ .Values.dynBackendDomain2 }} + - name: brig.config.optSettings.setFederationDomainConfigs[4].domain + value: {{ .Values.dynBackendDomain3 }} needs: - 'databases-ephemeral' diff --git a/integration/default.nix b/integration/default.nix index 600ceb7ddc1..e02e43a5c6f 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -35,6 +35,7 @@ , network-uri , optparse-applicative , process +, proto-lens , random , raw-strings-qq , retry @@ -53,6 +54,7 @@ , uuid , vector , websockets +, wire-message-proto-lens , yaml }: mkDerivation { @@ -92,6 +94,7 @@ mkDerivation { network-uri optparse-applicative process + proto-lens random raw-strings-qq retry @@ -110,6 +113,7 @@ mkDerivation { uuid vector websockets + wire-message-proto-lens yaml ]; license = lib.licenses.agpl3Only; diff --git a/integration/integration.cabal b/integration/integration.cabal index 912cb5dbb3c..8e29915dbb2 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -96,6 +96,7 @@ library API.GundeckInternal API.Nginz MLS.Util + Notifications RunAllTests SetupHelpers Test.AssetDownload @@ -105,6 +106,7 @@ library Test.Conversation Test.Defederation Test.Demo + Test.Federation Test.Federator Test.Notifications Test.Presence @@ -156,6 +158,7 @@ library , network-uri , optparse-applicative , process + , proto-lens , random , raw-strings-qq , retry @@ -174,4 +177,5 @@ library , uuid , vector , websockets + , wire-message-proto-lens , yaml diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index af7d16487d4..6fd779b9f31 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -32,6 +32,12 @@ getClient u cli = do joinHttpPath ["clients", c] submit "GET" req +deleteUser :: (HasCallStack, MakesValue user) => user -> App Response +deleteUser user = do + req <- baseRequest user Brig Versioned "/self" + submit "DELETE" $ + req & addJSONObject ["password" .= defPassword] + data AddClient = AddClient { ctype :: String, internal :: Bool, diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 3b2446f47df..d03ba1322c9 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -1,6 +1,16 @@ +{-# LANGUAGE OverloadedLabels #-} + module API.Galley where +import Control.Lens hiding ((.=)) +import Control.Monad.Reader import Data.Aeson qualified as Aeson +import Data.ByteString.Lazy qualified as LBS +import Data.ProtoLens qualified as Proto +import Data.ProtoLens.Labels () +import Data.UUID qualified as UUID +import Numeric.Lens +import Proto.Otr as Proto import Testlib.Prelude data CreateConv = CreateConv @@ -152,6 +162,32 @@ postMLSCommitBundle cid msg = do req <- baseRequest cid Galley Versioned "/mls/commit-bundles" submit "POST" (addMLS msg req) +postProteusMessage :: (HasCallStack, MakesValue user, MakesValue conv) => user -> conv -> QualifiedNewOtrMessage -> App Response +postProteusMessage user conv msgs = do + convDomain <- objDomain conv + convId <- objId conv + let bytes = Proto.encodeMessage msgs + req <- baseRequest user Galley Versioned ("/conversations/" <> convDomain <> "/" <> convId <> "/proteus/messages") + submit "POST" (addProtobuf bytes req) + +mkProteusRecipient :: (HasCallStack, MakesValue user, MakesValue client) => user -> client -> String -> App Proto.QualifiedUserEntry +mkProteusRecipient user client msg = do + userDomain <- objDomain user + userId <- LBS.toStrict . UUID.toByteString . fromJust . UUID.fromString <$> objId user + clientId <- (^?! hex) <$> objId client + pure $ + Proto.defMessage + & #domain .~ fromString userDomain + & #entries + .~ [ Proto.defMessage + & #user . #uuid .~ userId + & #clients + .~ [ Proto.defMessage + & #client . #client .~ clientId + & #text .~ fromString msg + ] + ] + getGroupInfo :: (HasCallStack, MakesValue user, MakesValue conv) => user -> @@ -167,7 +203,15 @@ getGroupInfo user conv = do submit "GET" req addMembers :: (HasCallStack, MakesValue user, MakesValue conv) => user -> conv -> [Value] -> App Response -addMembers usr qcnv qUsers = do +addMembers usr qcnv newMembers = do (convDomain, convId) <- objQid qcnv + qUsers <- mapM objQidObject newMembers req <- baseRequest usr Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "members"]) submit "POST" (req & addJSONObject ["qualified_users" .= qUsers]) + +removeMember :: (HasCallStack, MakesValue remover, MakesValue conv, MakesValue removed) => remover -> conv -> removed -> App Response +removeMember remover qcnv removed = do + (convDomain, convId) <- objQid qcnv + (removedDomain, removedId) <- objQid removed + req <- baseRequest remover Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "members", removedDomain, removedId]) + submit "DELETE" req diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs new file mode 100644 index 00000000000..0364bee7ceb --- /dev/null +++ b/integration/test/Notifications.hs @@ -0,0 +1,72 @@ +module Notifications where + +import API.Gundeck +import Control.Monad.Extra +import Testlib.Prelude +import UnliftIO.Concurrent + +awaitNotifications :: + (HasCallStack, MakesValue user, MakesValue client) => + user -> + client -> + Maybe String -> + -- | Timeout in seconds + Int -> + -- | Max no. of notifications + Int -> + -- | Selection function. Should not throw any exceptions + (Value -> App Bool) -> + App [Value] +awaitNotifications user client since0 tSecs n selector = + assertAwaitResult =<< go tSecs since0 (AwaitResult False n [] []) + where + go 0 _ res = pure res + go timeRemaining since res0 = do + notifs <- bindResponse (getNotifications user client (GetNotifications since Nothing)) $ \resp -> asList (resp.json %. "notifications") + lastNotifId <- case notifs of + [] -> pure since + _ -> Just <$> objId (last notifs) + (matching, notMatching) <- partitionM selector notifs + let matchesSoFar = res0.matches <> matching + res = + res0 + { matches = matchesSoFar, + nonMatches = res0.nonMatches <> notMatching, + success = length matchesSoFar >= res0.nMatchesExpected + } + if res.success + then pure res + else do + threadDelay (1_000_000) + go (timeRemaining - 1) lastNotifId res + +awaitNotification :: + (HasCallStack, MakesValue user, MakesValue client, MakesValue lastNotifId) => + user -> + client -> + Maybe lastNotifId -> + Int -> + (Value -> App Bool) -> + App Value +awaitNotification user client lastNotifId tSecs selector = do + since0 <- mapM objId lastNotifId + head <$> awaitNotifications user client since0 tSecs 1 selector + +isDeleteUserNotif :: MakesValue a => a -> App Bool +isDeleteUserNotif n = + nPayload n %. "type" `isEqual` "user.delete" + +isNewMessageNotif :: MakesValue a => a -> App Bool +isNewMessageNotif n = fieldEquals n "payload.0.type" "conversation.otr-message-add" + +isMemberJoinNotif :: MakesValue a => a -> App Bool +isMemberJoinNotif n = fieldEquals n "payload.0.type" "conversation.member-join" + +isConvLeaveNotif :: MakesValue a => a -> App Bool +isConvLeaveNotif n = fieldEquals n "payload.0.type" "conversation.member-leave" + +isNotifConv :: (MakesValue conv, MakesValue a) => conv -> a -> App Bool +isNotifConv conv n = fieldEquals n "payload.0.qualified_conversation" (objQidObject conv) + +isNotifForUser :: (MakesValue user, MakesValue a) => user -> a -> App Bool +isNotifForUser user n = fieldEquals n "payload.0.data.qualified_user_ids.0" (objQidObject user) diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index e2766e1735c..f6ba6f46768 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -1,6 +1,6 @@ module SetupHelpers where -import API.Brig qualified as Public +import API.Brig qualified as Brig import API.BrigInternal qualified as Internal import API.Galley import Control.Concurrent (threadDelay) @@ -25,6 +25,10 @@ randomUser domain cu = bindResponse (Internal.createUser domain cu) $ \resp -> d resp.status `shouldMatchInt` 201 resp.json +deleteUser :: (HasCallStack, MakesValue user) => user -> App () +deleteUser user = bindResponse (Brig.deleteUser user) $ \resp -> do + resp.status `shouldMatchInt` 200 + -- | returns (user, team id) createTeam :: (HasCallStack, MakesValue domain) => domain -> App (Value, String) createTeam domain = do @@ -45,8 +49,8 @@ connectUsers :: bob -> App () connectUsers alice bob = do - bindResponse (Public.postConnection alice bob) (\resp -> resp.status `shouldMatchInt` 201) - bindResponse (Public.putConnection bob alice "accepted") (\resp -> resp.status `shouldMatchInt` 200) + bindResponse (Brig.postConnection alice bob) (\resp -> resp.status `shouldMatchInt` 201) + bindResponse (Brig.putConnection bob alice "accepted") (\resp -> resp.status `shouldMatchInt` 200) createAndConnectUsers :: (HasCallStack, MakesValue domain) => [domain] -> App [Value] createAndConnectUsers domains = do diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs new file mode 100644 index 00000000000..2636827be65 --- /dev/null +++ b/integration/test/Test/Federation.hs @@ -0,0 +1,130 @@ +{-# LANGUAGE OverloadedLabels #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +module Test.Federation where + +import API.Brig qualified as API +import API.Galley +import Control.Lens +import Control.Monad.Codensity +import Control.Monad.Reader +import Data.ProtoLens qualified as Proto +import Data.ProtoLens.Labels () +import Notifications +import Numeric.Lens +import Proto.Otr qualified as Proto +import Proto.Otr_Fields qualified as Proto +import SetupHelpers +import Testlib.Prelude +import Testlib.ResourcePool + +testNotificationsForOfflineBackends :: HasCallStack => App () +testNotificationsForOfflineBackends = do + resourcePool <- asks (.resourcePool) + -- `delUser` will eventually get deleted. + [delUser, otherUser] <- createAndConnectUsers [OwnDomain, OtherDomain] + delClient <- objId $ bindResponse (API.addClient delUser def) $ getJSON 201 + otherClient <- objId $ bindResponse (API.addClient otherUser def) $ getJSON 201 + + -- We call it 'downBackend' because it is down for the most of this test + -- except for setup and assertions. Perhaps there is a better name. + runCodensity (acquireResources 1 resourcePool) $ \[downBackend] -> do + (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) <- runCodensity (startDynamicBackend downBackend mempty mempty) $ \_ -> do + downUser1 <- randomUser downBackend.berDomain def + downUser2 <- randomUser downBackend.berDomain def + downClient1 <- objId $ bindResponse (API.addClient downUser1 def) $ getJSON 201 + connectUsers delUser downUser1 + connectUsers delUser downUser2 + connectUsers otherUser downUser1 + upBackendConv <- bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, downUser1]})) $ getJSON 201 + downBackendConv <- bindResponse (postConversation downUser1 (defProteus {qualifiedUsers = [otherUser, delUser]})) $ getJSON 201 + pure (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) + + -- Even when a participating backend is down, messages to conversations + -- owned by other backends should go. + successfulMsgForOtherUser <- mkProteusRecipient otherUser otherClient "success message for other user" + successfulMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "success message for down user" + let successfulMsg = + Proto.defMessage @Proto.QualifiedNewOtrMessage + & #sender . Proto.client .~ (delClient ^?! hex) + & #recipients .~ [successfulMsgForOtherUser, successfulMsgForDownUser] + & #reportAll .~ Proto.defMessage + bindResponse (postProteusMessage delUser upBackendConv successfulMsg) assertSuccess + + -- When conversation owning backend is down, messages will fail to be sent. + failedMsgForOtherUser <- mkProteusRecipient otherUser otherClient "failed message for other user" + failedMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "failed message for down user" + let failedMsg = + Proto.defMessage @Proto.QualifiedNewOtrMessage + & #sender . Proto.client .~ (delClient ^?! hex) + & #recipients .~ [failedMsgForOtherUser, failedMsgForDownUser] + & #reportAll .~ Proto.defMessage + bindResponse (postProteusMessage delUser downBackendConv failedMsg) $ \resp -> + -- Due to the way federation breaks in local env vs K8s, it can return 521 + -- (local) or 533 (K8s). + resp.status `shouldMatchOneOf` [Number 521, Number 533] + + -- Conversation creation with people from down backend should fail + bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, downUser1]})) $ \resp -> + resp.status `shouldMatchInt` 533 + + -- Adding users to an up backend conversation should work even when one of + -- the participating backends is down + otherUser2 <- randomUser OtherDomain def + connectUsers delUser otherUser2 + bindResponse (addMembers delUser upBackendConv [otherUser2]) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- Adding users from down backend to a conversation should also fail + bindResponse (addMembers delUser upBackendConv [downUser2]) $ \resp -> + resp.status `shouldMatchInt` 533 + + -- Removing users from an up backend conversation should work even when one + -- of the participating backends is down. + bindResponse (removeMember delUser upBackendConv otherUser2) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- User deletions should eventually make it to the other backend. + deleteUser delUser + do + newMsgNotif <- awaitNotification otherUser otherClient noValue 1 isNewMessageNotif + newMsgNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv + newMsgNotif %. "payload.0.data.text" `shouldMatchBase64` "success message for other user" + + memberJoinNotif <- awaitNotification otherUser otherClient (Just newMsgNotif) 1 isMemberJoinNotif + memberJoinNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv + asListOf objQidObject (memberJoinNotif %. "payload.0.data.users") `shouldMatch` mapM objQidObject [otherUser2] + + delUserDeletedNotif <- nPayload $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDeleteUserNotif + objQid delUserDeletedNotif `shouldMatch` objQid delUser + + runCodensity (startDynamicBackend downBackend mempty mempty) $ \_ -> do + newMsgNotif <- awaitNotification downUser1 downClient1 noValue 5 isNewMessageNotif + newMsgNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv + newMsgNotif %. "payload.0.data.text" `shouldMatchBase64` "success message for down user" + + -- FUTUREWORK: Uncomment after fixing this bug: https://wearezeta.atlassian.net/browse/WPB-3664 + -- memberJoinNotif <- awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isMemberJoinNotif + -- memberJoinNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv + -- asListOf objQidObject (memberJoinNotif %. "payload.0.data.users") `shouldMatch` mapM objQidObject [downUser2] + + let isDelUserLeaveDownConvNotif = + allPreds + [ isConvLeaveNotif, + isNotifConv downBackendConv, + isNotifForUser delUser + ] + void $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isDelUserLeaveDownConvNotif + void $ awaitNotification otherUser otherClient noValue 1 isDelUserLeaveDownConvNotif + + -- FUTUREWORK: Uncomment after fixing this bug: https://wearezeta.atlassian.net/browse/WPB-3664 + -- void $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 (allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser otherUser]) + -- void $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 (allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser delUser]) + + delUserDeletedNotif <- nPayload $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isDeleteUserNotif + objQid delUserDeletedNotif `shouldMatch` objQid delUser + +allPreds :: (Applicative f) => [a -> f Bool] -> a -> f Bool +allPreds [] _ = pure True +allPreds [p] x = p x +allPreds (p1 : ps) x = (&&) <$> p1 x <*> allPreds ps x diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index 1d7a624d0a2..ef2b2c46eb0 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -4,11 +4,13 @@ module Testlib.Assertions where import Control.Exception as E import Control.Monad.Reader -import Data.Aeson (Value) +import Data.ByteString.Base64 qualified as B64 import Data.Char import Data.Foldable import Data.List import Data.Map qualified as Map +import Data.Text qualified as Text +import Data.Text.Encoding qualified as Text import GHC.Stack as Stack import System.FilePath import Testlib.JSON @@ -49,6 +51,17 @@ a `shouldMatch` b = do pb <- prettyJSON xb assertFailure $ "Actual:\n" <> pa <> "\nExpected:\n" <> pb +shouldMatchBase64 :: + (MakesValue a, MakesValue b, HasCallStack) => + -- | The actual value, in base64 + a -> + -- | The expected value, in plain text + b -> + App () +a `shouldMatchBase64` b = do + xa <- Text.decodeUtf8 . B64.decodeLenient . Text.encodeUtf8 . Text.pack <$> asString a + xa `shouldMatch` b + shouldNotMatch :: (MakesValue a, MakesValue b, HasCallStack) => -- | The actual value @@ -107,6 +120,19 @@ shouldMatchSet a b = do lb <- fmap sort (asList b) la `shouldMatch` lb +shouldMatchOneOf :: + (MakesValue a, MakesValue b, HasCallStack) => + a -> + b -> + App () +shouldMatchOneOf a b = do + lb <- asList b + xa <- make a + unless (xa `elem` lb) $ do + pa <- prettyJSON a + pb <- prettyJSON b + assertFailure $ "Expected:\n" <> pa <> "\n to match at least one of:\n" <> pb + shouldContainString :: HasCallStack => -- | The actual value @@ -118,22 +144,6 @@ super `shouldContainString` sub = do unless (sub `isInfixOf` super) $ do assertFailure $ "String:\n" <> show super <> "\nDoes not contain:\n" <> show sub -liftP2 :: - (MakesValue a, MakesValue b, HasCallStack) => - (Value -> Value -> c) -> - a -> - b -> - App c -liftP2 f a b = do - f <$> make a <*> make b - -isEqual :: - (MakesValue a, MakesValue b, HasCallStack) => - a -> - b -> - App Bool -isEqual = liftP2 (==) - printFailureDetails :: AssertionFailure -> IO String printFailureDetails (AssertionFailure stack mbResponse msg) = do s <- prettierCallStack stack diff --git a/integration/test/Testlib/Cannon.hs b/integration/test/Testlib/Cannon.hs index daf14ae4bf6..4fefd3dfe3f 100644 --- a/integration/test/Testlib/Cannon.hs +++ b/integration/test/Testlib/Cannon.hs @@ -22,6 +22,7 @@ module Testlib.Cannon ( WebSocket (..), WSConnect (..), ToWSConnect (..), + AwaitResult (..), withWebSocket, withWebSockets, awaitNMatchesResult, @@ -31,6 +32,7 @@ module Testlib.Cannon awaitAtLeastNMatches, awaitNToMMatchesResult, awaitNToMMatches, + assertAwaitResult, nPayload, printAwaitResult, printAwaitAtLeastResult, @@ -406,10 +408,14 @@ awaitNMatches :: App [Value] awaitNMatches nExpected tSecs checkMatch ws = do res <- awaitNMatchesResult nExpected tSecs checkMatch ws + assertAwaitResult res + +assertAwaitResult :: HasCallStack => AwaitResult -> App [Value] +assertAwaitResult res = do if res.success then pure res.matches else do - let msgHeader = "Expected " <> show nExpected <> " matching events, but got " <> show (length res.matches) <> "." + let msgHeader = "Expected " <> show res.nMatchesExpected <> " matching events, but got " <> show (length res.matches) <> "." details <- ("Details:\n" <>) <$> prettyAwaitResult res assertFailure $ unlines [msgHeader, details] diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index f31bf2a6b4c..4168c709e6a 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -52,12 +52,19 @@ addBody body contentType req = addMLS :: ByteString -> HTTP.Request -> HTTP.Request addMLS bytes req = req - { HTTP.requestBody = HTTP.RequestBodyLBS (L.fromStrict bytes), + { HTTP.requestBody = HTTP.RequestBodyBS bytes, HTTP.requestHeaders = (fromString "Content-Type", fromString "message/mls") : HTTP.requestHeaders req } +addProtobuf :: ByteString -> HTTP.Request -> HTTP.Request +addProtobuf bytes req = + req + { HTTP.requestBody = HTTP.RequestBodyBS bytes, + HTTP.requestHeaders = (fromString "Content-Type", fromString "application/x-protobuf") : HTTP.requestHeaders req + } + addHeader :: String -> String -> HTTP.Request -> HTTP.Request addHeader name value req = req {HTTP.requestHeaders = (CI.mk . C8.pack $ name, C8.pack value) : HTTP.requestHeaders req} @@ -93,6 +100,9 @@ getJSON status resp = withResponse resp $ \r -> do r.status `shouldMatch` status r.json +assertSuccess :: Response -> App () +assertSuccess resp = withResponse resp $ \r -> r.status `shouldMatchRange` (200, 299) + onFailureAddResponse :: HasCallStack => Response -> App a -> App a onFailureAddResponse r m = App $ do e <- ask diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index 3c3d2132efa..a5c932d8741 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -1,6 +1,7 @@ module Testlib.JSON where import Control.Monad +import Control.Monad.Catch import Control.Monad.IO.Class import Control.Monad.Trans.Maybe import Data.Aeson hiding ((.=)) @@ -120,6 +121,30 @@ asBool x = App Value (%.) x k = lookupField x k >>= assertField x k +isEqual :: + (MakesValue a, MakesValue b, HasCallStack) => + a -> + b -> + App Bool +isEqual = liftP2 (==) + +liftP2 :: + (MakesValue a, MakesValue b, HasCallStack) => + (Value -> Value -> c) -> + a -> + b -> + App c +liftP2 f a b = do + f <$> make a <*> make b + +fieldEquals :: (MakesValue a, MakesValue b) => a -> String -> b -> App Bool +fieldEquals a fieldSelector b = do + ma <- lookupField a fieldSelector `catchAll` const (pure Nothing) + case ma of + Nothing -> pure False + Just f -> + f `isEqual` b + assertField :: (HasCallStack, MakesValue a) => a -> String -> Maybe Value -> App Value assertField x k Nothing = assertFailureWithJSON x $ "Field \"" <> k <> "\" is missing from object:" assertField _ _ (Just x) = pure x diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 0de729cd80b..44606a6f238 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -188,6 +188,8 @@ startDynamicBackend resource staticPorts beOverrides = do Gundeck -> setField "aws.queueName" resource.berAwsQueueName Galley -> setField "journal.queueName" resource.berGalleyJournal + >=> setField "rabbitmq.vHost" resource.berVHost + BackgroundWorker -> setField "rabbitmq.vHost" resource.berVHost _ -> pure setFederationSettings :: Service -> Value -> App Value diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index 01966efe720..3991a607b59 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -5,6 +5,7 @@ module Testlib.RunServices where import Control.Concurrent import Control.Monad.Codensity (lowerCodensity) import Data.Map qualified as Map +import SetupHelpers import System.Directory import System.Environment (getArgs) import System.Exit (exitWith) @@ -34,7 +35,7 @@ backendA = berEmailSMSSesQueue = "integration-brig-events", berEmailSMSEmailSender = "backend-integration@wire.com", berGalleyJournal = "integration-team-events.fifo", - berVHost = "/", + berVHost = "backendA", berNginzSslPort = 8443 } @@ -74,7 +75,7 @@ backendB = -- FUTUREWORK: set up vhosts in dev/ci for example.com and b.example.com -- in case we want backendA and backendB to federate with a third backend -- (because otherwise both queues will overlap) - berVHost = "/", + berVHost = "backendB", berNginzSslPort = 9443 } @@ -137,17 +138,12 @@ main = do runAppWithEnv env $ do lowerCodensity $ do - let fedConfig = - def - { dbBrig = - setField - "optSettings.setFederationDomainConfigs" - [ object ["domain" .= backendA.berDomain, "search_policy" .= "full_search"], - object ["domain" .= backendB.berDomain, "search_policy" .= "full_search"] - ] - } _modifyEnv <- traverseConcurrentlyCodensity - (\(res, staticPorts, overrides) -> startDynamicBackend res staticPorts overrides) - [(backendA, staticPortsA, fedConfig), (backendB, staticPortsB, fedConfig)] + ( \(res, staticPorts) -> + -- We add the 'fullSerachWithAll' overrrides is a hack to get + -- around https://wearezeta.atlassian.net/browse/WPB-3796 + startDynamicBackend res staticPorts fullSearchWithAll + ) + [(backendA, staticPortsA), (backendB, staticPortsB)] liftIO run diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 1e0aa03ab16..25ed9c640ed 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -29,6 +29,7 @@ import Network.HTTP.Types qualified as HTTP import Network.URI import Testlib.Env import Testlib.Printing +import UnliftIO (MonadUnliftIO) import Prelude data Response = Response @@ -107,14 +108,11 @@ newtype App a = App {unApp :: ReaderT Env IO a} MonadCatch, MonadThrow, MonadReader Env, - MonadBase IO + MonadBase IO, + MonadUnliftIO, + MonadBaseControl IO ) -instance MonadBaseControl IO App where - type StM App a = StM (ReaderT Env IO) a - liftBaseWith f = App (liftBaseWith (\g -> f (g . unApp))) - restoreM = App . restoreM - runAppWithEnv :: Env -> App a -> IO a runAppWithEnv e m = runReaderT (unApp m) e diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml index 03e95748914..9762cc70825 100644 --- a/services/background-worker/background-worker.integration.yaml +++ b/services/background-worker/background-worker.integration.yaml @@ -1,4 +1,4 @@ -logLevel: Info +logLevel: Debug backgroundWorker: host: 0.0.0.0 @@ -23,4 +23,5 @@ rabbitmq: adminPort: 15672 backendNotificationPusher: - remotesRefreshInterval: 1 \ No newline at end of file + pushBackoffMinWait: 1000 + pushBackoffMaxWait: 1000000 diff --git a/services/background-worker/src/Wire/BackendNotificationPusher.hs b/services/background-worker/src/Wire/BackendNotificationPusher.hs index 0b8bd91c807..f52f165dbbd 100644 --- a/services/background-worker/src/Wire/BackendNotificationPusher.hs +++ b/services/background-worker/src/Wire/BackendNotificationPusher.hs @@ -23,6 +23,7 @@ import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Client import Wire.API.Routes.FederationDomainConfig import Wire.BackgroundWorker.Env +import Wire.BackgroundWorker.Options import Wire.BackgroundWorker.Util startPushingNotifications :: @@ -36,11 +37,12 @@ startPushingNotifications runningFlag chan domain = do pushNotification :: RabbitMQEnvelope e => MVar () -> Domain -> (Q.Message, e) -> AppT IO (Async ()) pushNotification runningFlag targetDomain (msg, envelope) = do + cfg <- asks (.backendNotificationsConfig) -- Jittered exponential backoff with 10ms as starting delay and 300s as max -- delay. When 300s is reached, every retry will happen after 300s. -- -- FUTUREWORK: Pull these numbers into config.s - let policy = capDelay 300_000_000 $ fullJitterBackoff 10000 + let policy = capDelay cfg.pushBackoffMaxWait $ fullJitterBackoff cfg.pushBackoffMinWait logErrr willRetry (SomeException e) rs = do Log.err $ Log.msg (Log.val "Exception occurred while pushing notification") diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index 2f3e0130aef..86a5b99ed57 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -54,6 +54,7 @@ data Env = Env -- connection-removed notifications into the notifications channels. -- This allows us to reuse existing code. This only pushes. notificationChannel :: MVar Channel, + backendNotificationsConfig :: BackendNotificationsConfig, statuses :: IORef (Map Worker IsWorking) } @@ -101,6 +102,7 @@ mkEnv opts = do metrics <- Metrics.metrics backendNotificationMetrics <- mkBackendNotificationMetrics notificationChannel <- mkRabbitMqChannelMVar logger $ demoteOpts opts.rabbitmq + let backendNotificationsConfig = opts.backendNotificationPusher pure (Env {..}, syncThread) initHttp2Manager :: IO Http2Manager diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index 1778dcf905f..7cac93318db 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -14,8 +14,25 @@ data Opts = Opts rabbitmq :: !RabbitMqAdminOpts, galley :: !Endpoint, brig :: !Endpoint, - defederationTimeout :: Maybe Int -- Seconds, Nothing for no timeout + -- | Seconds, Nothing for no timeout + defederationTimeout :: Maybe Int, + backendNotificationPusher :: BackendNotificationsConfig } deriving (Show, Generic) instance FromJSON Opts + +data BackendNotificationsConfig = BackendNotificationsConfig + { -- | Minimum amount of time (in microseconds) to wait before doing the first + -- retry in pushing a notification. Futher retries are done in a jittered + -- exponential way. + -- https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + pushBackoffMinWait :: Int, + -- | Upper limit on amount of time (in microseconds) to wait before retrying + -- any notification. This exists to ensure that exponential back-off doesn't + -- cause wait times to be very big. + pushBackoffMaxWait :: Int + } + deriving (Show, Generic) + +instance FromJSON BackendNotificationsConfig diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index 5a78b5d55c8..d7a275cf285 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -45,6 +45,7 @@ import Wire.API.RawJson import Wire.API.Routes.FederationDomainConfig import Wire.BackendNotificationPusher import Wire.BackgroundWorker.Env +import Wire.BackgroundWorker.Options import Wire.BackgroundWorker.Util spec :: Spec @@ -231,6 +232,7 @@ spec = do defederationTimeout = responseTimeoutNone galley = Endpoint "localhost" 8085 brig = Endpoint "localhost" 8082 + backendNotificationsConfig = BackendNotificationsConfig 1000 500000 backendNotificationMetrics <- mkBackendNotificationMetrics domains <- runAppT Env {..} getRemoteDomains @@ -253,6 +255,7 @@ spec = do defederationTimeout = responseTimeoutNone galley = Endpoint "localhost" 8085 brig = Endpoint "localhost" 8082 + backendNotificationsConfig = BackendNotificationsConfig 1000 500000 backendNotificationMetrics <- mkBackendNotificationMetrics domainsThread <- async $ runAppT Env {..} getRemoteDomains diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index d88db07780a..51e8ce51b38 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -10,6 +10,7 @@ import Util.Options import Wire.API.Routes.FederationDomainConfig import Wire.BackgroundWorker.Env hiding (federatorInternal, galley) import Wire.BackgroundWorker.Env qualified as E +import Wire.BackgroundWorker.Options import Wire.BackgroundWorker.Util testEnv :: IO Env @@ -29,6 +30,7 @@ testEnv = do galley = Endpoint "localhost" 8085 brig = Endpoint "localhost" 8082 defederationTimeout = responseTimeoutNone + backendNotificationsConfig = BackendNotificationsConfig 1000 500000 pure Env {..} runTestAppT :: AppT IO a -> Int -> IO a diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index bb9ea753595..ece2d3c737e 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -33,10 +33,12 @@ import Brig.Data.Connection qualified as Data import Brig.Data.User qualified as Data import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.IO.Intra (notify) +import Brig.Options import Brig.Types.User.Event import Brig.User.API.Handle import Brig.User.Search.SearchIndex qualified as Q import Control.Error.Util +import Control.Lens ((^.)) import Control.Monad.Trans.Except import Data.Domain import Data.Handle (Handle (..), parseHandle) @@ -95,8 +97,12 @@ federationSitemap = -- with the subset of those we aren't connected to. getFederationStatus :: Domain -> DomainSet -> Handler r NonConnectedBackends getFederationStatus _ request = do - fedDomains <- fromList . fmap (.domain) . (.remotes) <$> getFederationRemotes - pure $ NonConnectedBackends (request.dsDomains \\ fedDomains) + cfg <- ask + case setFederationStrategy (cfg ^. settings) of + Just AllowAll -> pure $ NonConnectedBackends mempty + _ -> do + fedDomains <- fromList . fmap (.domain) . (.remotes) <$> getFederationRemotes + pure $ NonConnectedBackends (request.dsDomains \\ fedDomains) sendConnectionAction :: Domain -> NewConnectionRequest -> Handler r NewConnectionResponse sendConnectionAction originDomain NewConnectionRequest {..} = do From 0cf66be2a1fbe270ec9f4414c19dade780edd8f2 Mon Sep 17 00:00:00 2001 From: fisx Date: Mon, 21 Aug 2023 22:35:57 +0200 Subject: [PATCH 091/225] Distinguish between update and upsert cassandra commands (#3513) --- changelog.d/5-internal/wpb-3915 | 1 + services/brig/src/Brig/API/OAuth.hs | 2 +- services/brig/src/Brig/Data/Client.hs | 4 +- services/brig/src/Brig/Data/Connection.hs | 4 +- services/brig/src/Brig/Data/MLS/KeyPackage.hs | 6 +- services/brig/src/Brig/Data/User.hs | 40 ++++++------ services/brig/src/Brig/Provider/DB.hs | 33 +++++----- services/brig/src/Brig/Unique.hs | 8 +-- .../brig/test/integration/API/User/Auth.hs | 5 +- .../galley/src/Galley/Cassandra/Client.hs | 2 +- .../src/Galley/Cassandra/CustomBackend.hs | 2 +- .../galley/src/Galley/Cassandra/Queries.hs | 64 ++++++++++--------- services/galley/src/Galley/Cassandra/Team.hs | 1 + services/gundeck/src/Gundeck/Push/Data.hs | 2 +- .../src/Spar/Sem/IdPConfigStore/Cassandra.hs | 4 +- tools/db/migrate-sso-feature-flag/src/Work.hs | 3 +- tools/db/repair-handles/src/Work.hs | 2 +- 17 files changed, 98 insertions(+), 85 deletions(-) create mode 100644 changelog.d/5-internal/wpb-3915 diff --git a/changelog.d/5-internal/wpb-3915 b/changelog.d/5-internal/wpb-3915 new file mode 100644 index 00000000000..fcaeeec676c --- /dev/null +++ b/changelog.d/5-internal/wpb-3915 @@ -0,0 +1 @@ +Distinguish between update and upsert cassandra commands (follow-up to #3504) (#3513) \ No newline at end of file diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index c7ff94be6ab..b2196be7be7 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -313,7 +313,7 @@ updateOAuthClient' :: (MonadClient m) => OAuthClientId -> OAuthApplicationName - updateOAuthClient' cid name uri = retry x5 . write q $ params LocalQuorum (name, uri, cid) where q :: PrepQuery W (OAuthApplicationName, RedirectUrl, OAuthClientId) () - q = "UPDATE oauth_client SET name = ?, redirect_uri = ? WHERE id = ?" + q = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE oauth_client SET name = ?, redirect_uri = ? WHERE id = ?" insertOAuthClient :: (MonadClient m) => OAuthClientId -> OAuthApplicationName -> RedirectUrl -> Password -> m () insertOAuthClient cid name uri pw = retry x5 . write q $ params LocalQuorum (cid, name, uri, pw) diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 2a8eebeafcb..0d893fa656d 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -382,10 +382,10 @@ insertClient :: PrepQuery W (UserId, ClientId, UTCTimeMillis, ClientType, Maybe insertClient = "INSERT INTO clients (user, client, tstamp, type, label, class, cookie, lat, lon, model, capabilities) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" updateClientLabelQuery :: PrepQuery W (Maybe Text, UserId, ClientId) () -updateClientLabelQuery = "UPDATE clients SET label = ? WHERE user = ? AND client = ?" +updateClientLabelQuery = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE clients SET label = ? WHERE user = ? AND client = ?" updateClientCapabilitiesQuery :: PrepQuery W (Maybe (C.Set ClientCapability), UserId, ClientId) () -updateClientCapabilitiesQuery = "UPDATE clients SET capabilities = ? WHERE user = ? AND client = ?" +updateClientCapabilitiesQuery = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE clients SET capabilities = ? WHERE user = ? AND client = ?" updateClientLastActiveQuery :: PrepQuery W (UTCTime, UserId, ClientId) Row updateClientLastActiveQuery = "UPDATE clients SET last_active = ? WHERE user = ? AND client = ? IF EXISTS" diff --git a/services/brig/src/Brig/Data/Connection.hs b/services/brig/src/Brig/Data/Connection.hs index f4d8b56e3ed..16031d654eb 100644 --- a/services/brig/src/Brig/Data/Connection.hs +++ b/services/brig/src/Brig/Data/Connection.hs @@ -340,7 +340,7 @@ connectionInsert :: PrepQuery W (UserId, UserId, RelationWithHistory, UTCTimeMil connectionInsert = "INSERT INTO connection (left, right, status, last_update, conv) VALUES (?, ?, ?, ?, ?)" connectionUpdate :: PrepQuery W (RelationWithHistory, UTCTimeMillis, UserId, UserId) () -connectionUpdate = "UPDATE connection SET status = ?, last_update = ? WHERE left = ? AND right = ?" +connectionUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE connection SET status = ?, last_update = ? WHERE left = ? AND right = ?" connectionSelect :: PrepQuery R (UserId, UserId) (UserId, UserId, RelationWithHistory, UTCTimeMillis, Maybe ConvId) connectionSelect = "SELECT left, right, status, last_update, conv FROM connection WHERE left = ? AND right = ?" @@ -391,7 +391,7 @@ remoteConnectionSelectFrom :: PrepQuery R (UserId, Domain, UserId) (RelationWith remoteConnectionSelectFrom = "SELECT status, last_update, conv_domain, conv_id FROM connection_remote where left = ? AND right_domain = ? AND right_user = ?" remoteConnectionUpdate :: PrepQuery W (RelationWithHistory, UTCTimeMillis, UserId, Domain, UserId) () -remoteConnectionUpdate = "UPDATE connection_remote set status = ?, last_update = ? WHERE left = ? and right_domain = ? and right_user = ?" +remoteConnectionUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE connection_remote set status = ?, last_update = ? WHERE left = ? and right_domain = ? and right_user = ?" remoteConnectionDelete :: PrepQuery W (UserId, Domain, UserId) () remoteConnectionDelete = "DELETE FROM connection_remote where left = ? AND right_domain = ? AND right_user = ?" diff --git a/services/brig/src/Brig/Data/MLS/KeyPackage.hs b/services/brig/src/Brig/Data/MLS/KeyPackage.hs index dce1b7cfc58..d06e3645099 100644 --- a/services/brig/src/Brig/Data/MLS/KeyPackage.hs +++ b/services/brig/src/Brig/Data/MLS/KeyPackage.hs @@ -189,11 +189,11 @@ addKeyPackageRef :: MonadClient m => KeyPackageRef -> NewKeyPackageRef -> m () addKeyPackageRef ref nkpr = do retry x5 $ write - q + upsertQuery (params LocalQuorum (nkprClientId nkpr, qUnqualified (nkprConversation nkpr), qDomain (nkprConversation nkpr), qDomain (nkprUserId nkpr), qUnqualified (nkprUserId nkpr), ref)) where - q :: PrepQuery W (ClientId, ConvId, Domain, Domain, UserId, KeyPackageRef) x - q = "UPDATE mls_key_package_refs SET client = ?, conv = ?, conv_domain = ?, domain = ?, user = ? WHERE ref = ?" + upsertQuery :: PrepQuery W (ClientId, ConvId, Domain, Domain, UserId, KeyPackageRef) x + upsertQuery = "UPDATE mls_key_package_refs SET client = ?, conv = ?, conv_domain = ?, domain = ?, user = ? WHERE ref = ?" -- | Update key package ref, used in Galley when commit reveals key package ref update for the sender. -- Nothing is changed if the previous key package ref is not found in the table. diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 1891e135f43..d170ed4e427 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -622,64 +622,64 @@ userInsert = \VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" userDisplayNameUpdate :: PrepQuery W (Name, UserId) () -userDisplayNameUpdate = "UPDATE user SET name = ? WHERE id = ?" +userDisplayNameUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET name = ? WHERE id = ?" userPictUpdate :: PrepQuery W (Pict, UserId) () -userPictUpdate = "UPDATE user SET picture = ? WHERE id = ?" +userPictUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET picture = ? WHERE id = ?" userAssetsUpdate :: PrepQuery W ([Asset], UserId) () -userAssetsUpdate = "UPDATE user SET assets = ? WHERE id = ?" +userAssetsUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET assets = ? WHERE id = ?" userAccentIdUpdate :: PrepQuery W (ColourId, UserId) () -userAccentIdUpdate = "UPDATE user SET accent_id = ? WHERE id = ?" +userAccentIdUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET accent_id = ? WHERE id = ?" userEmailUpdate :: PrepQuery W (Email, UserId) () -userEmailUpdate = "UPDATE user SET email = ? WHERE id = ?" +userEmailUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email = ? WHERE id = ?" userEmailUnvalidatedUpdate :: PrepQuery W (Email, UserId) () -userEmailUnvalidatedUpdate = "UPDATE user SET email_unvalidated = ? WHERE id = ?" +userEmailUnvalidatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email_unvalidated = ? WHERE id = ?" userEmailUnvalidatedDelete :: PrepQuery W (Identity UserId) () -userEmailUnvalidatedDelete = "UPDATE user SET email_unvalidated = null WHERE id = ?" +userEmailUnvalidatedDelete = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email_unvalidated = null WHERE id = ?" userPhoneUpdate :: PrepQuery W (Phone, UserId) () -userPhoneUpdate = "UPDATE user SET phone = ? WHERE id = ?" +userPhoneUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET phone = ? WHERE id = ?" userSSOIdUpdate :: PrepQuery W (Maybe UserSSOId, UserId) () -userSSOIdUpdate = "UPDATE user SET sso_id = ? WHERE id = ?" +userSSOIdUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET sso_id = ? WHERE id = ?" userManagedByUpdate :: PrepQuery W (ManagedBy, UserId) () -userManagedByUpdate = "UPDATE user SET managed_by = ? WHERE id = ?" +userManagedByUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET managed_by = ? WHERE id = ?" userHandleUpdate :: PrepQuery W (Handle, UserId) () -userHandleUpdate = "UPDATE user SET handle = ? WHERE id = ?" +userHandleUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET handle = ? WHERE id = ?" userSupportedProtocolUpdate :: PrepQuery W (Set BaseProtocolTag, UserId) () -userSupportedProtocolUpdate = "UPDATE user SET supported_protocols = ? WHERE id = ?" +userSupportedProtocolUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET supported_protocols = ? WHERE id = ?" userPasswordUpdate :: PrepQuery W (Password, UserId) () -userPasswordUpdate = "UPDATE user SET password = ? WHERE id = ?" +userPasswordUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET password = ? WHERE id = ?" userStatusUpdate :: PrepQuery W (AccountStatus, UserId) () -userStatusUpdate = "UPDATE user SET status = ? WHERE id = ?" +userStatusUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET status = ? WHERE id = ?" userDeactivatedUpdate :: PrepQuery W (Identity UserId) () -userDeactivatedUpdate = "UPDATE user SET activated = false WHERE id = ?" +userDeactivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET activated = false WHERE id = ?" userActivatedUpdate :: PrepQuery W (Maybe Email, Maybe Phone, UserId) () -userActivatedUpdate = "UPDATE user SET activated = true, email = ?, phone = ? WHERE id = ?" +userActivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET activated = true, email = ?, phone = ? WHERE id = ?" userLocaleUpdate :: PrepQuery W (Language, Maybe Country, UserId) () -userLocaleUpdate = "UPDATE user SET language = ?, country = ? WHERE id = ?" +userLocaleUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET language = ?, country = ? WHERE id = ?" userEmailDelete :: PrepQuery W (Identity UserId) () -userEmailDelete = "UPDATE user SET email = null WHERE id = ?" +userEmailDelete = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email = null WHERE id = ?" userPhoneDelete :: PrepQuery W (Identity UserId) () -userPhoneDelete = "UPDATE user SET phone = null WHERE id = ?" +userPhoneDelete = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET phone = null WHERE id = ?" userRichInfoUpdate :: PrepQuery W (RichInfoAssocList, UserId) () -userRichInfoUpdate = "UPDATE rich_info SET json = ? WHERE user = ?" +userRichInfoUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE rich_info SET json = ? WHERE user = ?" ------------------------------------------------------------------------------- -- Conversions diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index ab7f85df1b9..e83919f5bb0 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -72,11 +72,11 @@ updateAccountProfile p name url descr = retry x5 . batch $ do for_ descr $ \x -> addPrepQuery cqlDescr (x, p) where cqlName :: PrepQuery W (Name, ProviderId) () - cqlName = "UPDATE provider SET name = ? WHERE id = ?" + cqlName = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE provider SET name = ? WHERE id = ?" cqlUrl :: PrepQuery W (HttpsUrl, ProviderId) () - cqlUrl = "UPDATE provider SET url = ? WHERE id = ?" + cqlUrl = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE provider SET url = ? WHERE id = ?" cqlDescr :: PrepQuery W (Text, ProviderId) () - cqlDescr = "UPDATE provider SET descr = ? WHERE id = ?" + cqlDescr = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE provider SET descr = ? WHERE id = ?" -- | Lookup the raw account data of a (possibly unverified) provider. lookupAccountData :: @@ -136,7 +136,7 @@ updateAccountPassword pid pwd = do retry x5 $ write cql $ params LocalQuorum (p, pid) where cql :: PrepQuery W (Password, ProviderId) () - cql = "UPDATE provider SET password = ? where id = ?" + cql = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE provider SET password = ? where id = ?" -------------------------------------------------------------------------------- -- Unique (Natural) Keys @@ -156,10 +156,12 @@ insertKey p old new = retry x5 . batch $ do where cqlKeyInsert :: PrepQuery W (Text, ProviderId) () cqlKeyInsert = "INSERT INTO provider_keys (key, provider) VALUES (?, ?)" + cqlKeyDelete :: PrepQuery W (Identity Text) () cqlKeyDelete = "DELETE FROM provider_keys WHERE key = ?" + cqlEmail :: PrepQuery W (Email, ProviderId) () - cqlEmail = "UPDATE provider SET email = ? WHERE id = ?" + cqlEmail = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE provider SET email = ? WHERE id = ?" lookupKey :: MonadClient m => @@ -306,15 +308,15 @@ updateService pid sid svcName svcTags nameChange summary descr assets tagsChange for_ assets $ \x -> addPrepQuery cqlAssets (x, pid, sid) where cqlName :: PrepQuery W (Name, ProviderId, ServiceId) () - cqlName = "UPDATE service SET name = ? WHERE provider = ? AND id = ?" + cqlName = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE service SET name = ? WHERE provider = ? AND id = ?" cqlSummary :: PrepQuery W (Text, ProviderId, ServiceId) () - cqlSummary = "UPDATE service SET summary = ? WHERE provider = ? AND id = ?" + cqlSummary = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE service SET summary = ? WHERE provider = ? AND id = ?" cqlDescr :: PrepQuery W (Text, ProviderId, ServiceId) () - cqlDescr = "UPDATE service SET descr = ? WHERE provider = ? AND id = ?" + cqlDescr = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE service SET descr = ? WHERE provider = ? AND id = ?" cqlAssets :: PrepQuery W ([Asset], ProviderId, ServiceId) () - cqlAssets = "UPDATE service SET assets = ? WHERE provider = ? AND id = ?" + cqlAssets = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE service SET assets = ? WHERE provider = ? AND id = ?" cqlTags :: PrepQuery W (C.Set ServiceTag, ProviderId, ServiceId) () - cqlTags = "UPDATE service SET tags = ? WHERE provider = ? AND id = ?" + cqlTags = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE service SET tags = ? WHERE provider = ? AND id = ?" -- NB: can take a significant amount of time if many teams were using the service deleteService :: @@ -436,16 +438,17 @@ updateServiceConn pid sid url tokens keys enabled = retry x5 . batch $ do for_ enabled $ \x -> addPrepQuery cqlEnabled (x, pid, sid) where (pks, fps) = (fmap fst &&& fmap snd) (unzip . toList <$> keys) + cqlBaseUrl :: PrepQuery W (HttpsUrl, ProviderId, ServiceId) () - cqlBaseUrl = "UPDATE service SET base_url = ? WHERE provider = ? AND id = ?" + cqlBaseUrl = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE service SET base_url = ? WHERE provider = ? AND id = ?" cqlTokens :: PrepQuery W (List1 ServiceToken, ProviderId, ServiceId) () - cqlTokens = "UPDATE service SET auth_tokens = ? WHERE provider = ? AND id = ?" + cqlTokens = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE service SET auth_tokens = ? WHERE provider = ? AND id = ?" cqlKeys :: PrepQuery W ([ServiceKey], ProviderId, ServiceId) () - cqlKeys = "UPDATE service SET pubkeys = ? WHERE provider = ? AND id = ?" + cqlKeys = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE service SET pubkeys = ? WHERE provider = ? AND id = ?" cqlFps :: PrepQuery W ([Fingerprint Rsa], ProviderId, ServiceId) () - cqlFps = "UPDATE service SET fingerprints = ? WHERE provider = ? AND id = ?" + cqlFps = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE service SET fingerprints = ? WHERE provider = ? AND id = ?" cqlEnabled :: PrepQuery W (Bool, ProviderId, ServiceId) () - cqlEnabled = "UPDATE service SET enabled = ? WHERE provider = ? AND id = ?" + cqlEnabled = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE service SET enabled = ? WHERE provider = ? AND id = ?" -------------------------------------------------------------------------------- -- Service "Indexes" (tag and prefix); contain only enabled services diff --git a/services/brig/src/Brig/Unique.hs b/services/brig/src/Brig/Unique.hs index 88e325e8c4d..6c5d5f0a2a8 100644 --- a/services/brig/src/Brig/Unique.hs +++ b/services/brig/src/Brig/Unique.hs @@ -66,13 +66,13 @@ withClaim u v t io = do -- [Note: Guarantees] claim = do let ttl = max minTtl (fromIntegral (t #> Second)) - retry x5 $ write cql $ params LocalQuorum (ttl * 2, C.Set [u], v) + retry x5 $ write upsertQuery $ params LocalQuorum (ttl * 2, C.Set [u], v) claimed <- (== [u]) <$> lookupClaims v if claimed then liftIO $ timeout (fromIntegral ttl # Second) io else pure Nothing - cql :: PrepQuery W (Int32, C.Set (Id a), Text) () - cql = "UPDATE unique_claims USING TTL ? SET claims = claims + ? WHERE value = ?" + upsertQuery :: PrepQuery W (Int32, C.Set (Id a), Text) () + upsertQuery = "UPDATE unique_claims USING TTL ? SET claims = claims + ? WHERE value = ?" deleteClaim :: MonadClient m => @@ -91,7 +91,7 @@ deleteClaim u v t = do retry x5 $ write cql $ params LocalQuorum (ttl * 2, C.Set [u], v) where cql :: PrepQuery W (Int32, C.Set (Id a), Text) () - cql = "UPDATE unique_claims USING TTL ? SET claims = claims - ? WHERE value = ?" + cql = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE unique_claims USING TTL ? SET claims = claims - ? WHERE value = ?" -- | Lookup the current claims on a value. lookupClaims :: MonadClient m => Text -> m [Id a] diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 8979a1b9415..eb97912341f 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -202,17 +202,20 @@ testLoginWith6CharPassword brig db = do (PasswordLogin (PasswordLoginData (LoginByEmail email) pw Nothing Nothing)) PersistentCookie !!! const expectedStatusCode === statusCode + -- Since 8 char passwords are required, when setting a password via the API, -- we need to write this directly to the db, to be able to test this writeDirectlyToDB :: UserId -> PlainTextPassword6 -> Http () writeDirectlyToDB uid pw = liftIO (runClient db (updatePassword uid pw >> revokeAllCookies uid)) + updatePassword :: MonadClient m => UserId -> PlainTextPassword6 -> m () updatePassword u t = do p <- liftIO $ mkSafePassword t retry x5 $ write userPasswordUpdate (params LocalQuorum (p, u)) + userPasswordUpdate :: PrepQuery W (Password, UserId) () - userPasswordUpdate = "UPDATE user SET password = ? WHERE id = ?" + userPasswordUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET password = ? WHERE id = ?" -------------------------------------------------------------------------------- -- ZAuth test environment for generating arbitrary tokens. diff --git a/services/galley/src/Galley/Cassandra/Client.hs b/services/galley/src/Galley/Cassandra/Client.hs index c9fdc5e01bf..cb30b340185 100644 --- a/services/galley/src/Galley/Cassandra/Client.hs +++ b/services/galley/src/Galley/Cassandra/Client.hs @@ -41,7 +41,7 @@ import UnliftIO qualified updateClient :: Bool -> UserId -> ClientId -> Client () updateClient add usr cls = do - let q = if add then Cql.addMemberClient else Cql.rmMemberClient + let q = if add then Cql.upsertMemberAddClient else Cql.upsertMemberRmClient retry x5 $ write (q cls) (params LocalQuorum (Identity usr)) -- Do, at most, 16 parallel lookups of up to 128 users each diff --git a/services/galley/src/Galley/Cassandra/CustomBackend.hs b/services/galley/src/Galley/Cassandra/CustomBackend.hs index 0878c0a0a79..cabe4a3a43e 100644 --- a/services/galley/src/Galley/Cassandra/CustomBackend.hs +++ b/services/galley/src/Galley/Cassandra/CustomBackend.hs @@ -51,7 +51,7 @@ getCustomBackend domain = setCustomBackend :: MonadClient m => Domain -> CustomBackend -> m () setCustomBackend domain CustomBackend {..} = do - retry x5 $ write Cql.updateCustomBackend (params LocalQuorum (backendConfigJsonUrl, backendWebappWelcomeUrl, domain)) + retry x5 $ write Cql.upsertCustomBackend (params LocalQuorum (backendConfigJsonUrl, backendWebappWelcomeUrl, domain)) deleteCustomBackend :: MonadClient m => Domain -> m () deleteCustomBackend domain = do diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 4a03725ae4c..8118d0cd542 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -176,6 +176,10 @@ deleteTeamAdmin = "delete from team_admin where team = ? and user = ?" listTeamAdmins :: PrepQuery R (Identity TeamId) (Identity UserId) listTeamAdmins = "select user from team_admin where team = ?" +-- | This is not an upsert, but we can't add `IF EXISTS` here, or cassandra will yell `Invalid +-- "Batch with conditions cannot span multiple tables"` at us. So we make sure in the +-- application logic to only call this if the user exists (in the handler, not entirely +-- race-condition-proof, unfortunately). updatePermissions :: PrepQuery W (Permissions, TeamId, UserId) () updatePermissions = "update team_member set perms = ? where team = ? and user = ?" @@ -186,25 +190,25 @@ deleteUserTeam :: PrepQuery W (UserId, TeamId) () deleteUserTeam = "delete from user_team where user = ? and team = ?" markTeamDeleted :: PrepQuery W (TeamStatus, TeamId) () -markTeamDeleted = "update team set status = ? where team = ?" +markTeamDeleted = {- `IF EXISTS`, but that requires benchmarking -} "update team set status = ? where team = ?" deleteTeam :: PrepQuery W (TeamStatus, TeamId) () -deleteTeam = "update team using timestamp 32503680000000000 set name = 'default', icon = 'default', status = ? where team = ? " +deleteTeam = {- `IF EXISTS`, but that requires benchmarking -} "update team using timestamp 32503680000000000 set name = 'default', icon = 'default', status = ? where team = ? " updateTeamName :: PrepQuery W (Text, TeamId) () -updateTeamName = "update team set name = ? where team = ?" +updateTeamName = {- `IF EXISTS`, but that requires benchmarking -} "update team set name = ? where team = ?" updateTeamIcon :: PrepQuery W (Text, TeamId) () -updateTeamIcon = "update team set icon = ? where team = ?" +updateTeamIcon = {- `IF EXISTS`, but that requires benchmarking -} "update team set icon = ? where team = ?" updateTeamIconKey :: PrepQuery W (Text, TeamId) () -updateTeamIconKey = "update team set icon_key = ? where team = ?" +updateTeamIconKey = {- `IF EXISTS`, but that requires benchmarking -} "update team set icon_key = ? where team = ?" updateTeamStatus :: PrepQuery W (TeamStatus, TeamId) () -updateTeamStatus = "update team set status = ? where team = ?" +updateTeamStatus = {- `IF EXISTS`, but that requires benchmarking -} "update team set status = ? where team = ?" updateTeamSplashScreen :: PrepQuery W (Text, TeamId) () -updateTeamSplashScreen = "update team set splash_screen = ? where team = ?" +updateTeamSplashScreen = {- `IF EXISTS`, but that requires benchmarking -} "update team set splash_screen = ? where team = ?" -- Conversations ------------------------------------------------------------ @@ -264,34 +268,34 @@ insertMLSSelfConv = <> ", ?, ?)" updateConvAccess :: PrepQuery W (C.Set Access, C.Set AccessRole, ConvId) () -updateConvAccess = "update conversation set access = ?, access_roles_v2 = ? where conv = ?" +updateConvAccess = {- `IF EXISTS`, but that requires benchmarking -} "update conversation set access = ?, access_roles_v2 = ? where conv = ?" updateConvReceiptMode :: PrepQuery W (ReceiptMode, ConvId) () -updateConvReceiptMode = "update conversation set receipt_mode = ? where conv = ?" +updateConvReceiptMode = {- `IF EXISTS`, but that requires benchmarking -} "update conversation set receipt_mode = ? where conv = ?" updateConvMessageTimer :: PrepQuery W (Maybe Milliseconds, ConvId) () -updateConvMessageTimer = "update conversation set message_timer = ? where conv = ?" +updateConvMessageTimer = {- `IF EXISTS`, but that requires benchmarking -} "update conversation set message_timer = ? where conv = ?" updateConvName :: PrepQuery W (Text, ConvId) () -updateConvName = "update conversation set name = ? where conv = ?" +updateConvName = {- `IF EXISTS`, but that requires benchmarking -} "update conversation set name = ? where conv = ?" updateConvType :: PrepQuery W (ConvType, ConvId) () -updateConvType = "update conversation set type = ? where conv = ?" +updateConvType = {- `IF EXISTS`, but that requires benchmarking -} "update conversation set type = ? where conv = ?" updateConvEpoch :: PrepQuery W (Epoch, ConvId) () -updateConvEpoch = "update conversation set epoch = ? where conv = ?" +updateConvEpoch = {- `IF EXISTS`, but that requires benchmarking -} "update conversation set epoch = ? where conv = ?" deleteConv :: PrepQuery W (Identity ConvId) () deleteConv = "delete from conversation using timestamp 32503680000000000 where conv = ?" markConvDeleted :: PrepQuery W (Identity ConvId) () -markConvDeleted = "update conversation set deleted = true where conv = ?" +markConvDeleted = {- `IF EXISTS`, but that requires benchmarking -} "update conversation set deleted = true where conv = ?" selectPublicGroupState :: PrepQuery R (Identity ConvId) (Identity (Maybe OpaquePublicGroupState)) selectPublicGroupState = "select public_group_state from conversation where conv = ?" updatePublicGroupState :: PrepQuery W (OpaquePublicGroupState, ConvId) () -updatePublicGroupState = "update conversation set public_group_state = ? where conv = ?" +updatePublicGroupState = {- `IF EXISTS`, but that requires benchmarking -} "update conversation set public_group_state = ? where conv = ?" -- Conversations accessible by code ----------------------------------------- @@ -349,16 +353,16 @@ removeMember :: PrepQuery W (ConvId, UserId) () removeMember = "delete from member where conv = ? and user = ?" updateOtrMemberMutedStatus :: PrepQuery W (MutedStatus, Maybe Text, ConvId, UserId) () -updateOtrMemberMutedStatus = "update member set otr_muted_status = ?, otr_muted_ref = ? where conv = ? and user = ?" +updateOtrMemberMutedStatus = {- `IF EXISTS`, but that requires benchmarking -} "update member set otr_muted_status = ?, otr_muted_ref = ? where conv = ? and user = ?" updateOtrMemberArchived :: PrepQuery W (Bool, Maybe Text, ConvId, UserId) () -updateOtrMemberArchived = "update member set otr_archived = ?, otr_archived_ref = ? where conv = ? and user = ?" +updateOtrMemberArchived = {- `IF EXISTS`, but that requires benchmarking -} "update member set otr_archived = ?, otr_archived_ref = ? where conv = ? and user = ?" updateMemberHidden :: PrepQuery W (Bool, Maybe Text, ConvId, UserId) () -updateMemberHidden = "update member set hidden = ?, hidden_ref = ? where conv = ? and user = ?" +updateMemberHidden = {- `IF EXISTS`, but that requires benchmarking -} "update member set hidden = ?, hidden_ref = ? where conv = ? and user = ?" updateMemberConvRoleName :: PrepQuery W (RoleName, ConvId, UserId) () -updateMemberConvRoleName = "update member set conversation_role = ? where conv = ? and user = ?" +updateMemberConvRoleName = {- `IF EXISTS`, but that requires benchmarking -} "update member set conversation_role = ? where conv = ? and user = ?" -- Federated conversations ----------------------------------------------------- -- @@ -379,7 +383,7 @@ selectRemoteMembers :: PrepQuery R (Identity ConvId) (Domain, UserId, RoleName) selectRemoteMembers = "select user_remote_domain, user_remote_id, conversation_role from member_remote_user where conv = ?" updateRemoteMemberConvRoleName :: PrepQuery W (RoleName, ConvId, Domain, UserId) () -updateRemoteMemberConvRoleName = "update member_remote_user set conversation_role = ? where conv = ? and user_remote_domain = ? and user_remote_id = ?" +updateRemoteMemberConvRoleName = {- `IF EXISTS`, but that requires benchmarking -} "update member_remote_user set conversation_role = ? where conv = ? and user_remote_domain = ? and user_remote_id = ?" removeRemoteDomain :: PrepQuery W (ConvId, Domain) () removeRemoteDomain = "delete from member_remote_user where conv = ? and user_remote_domain = ?" @@ -424,13 +428,13 @@ selectLocalMembersByDomain = "select conv_remote_id, user from user_remote_conv -- remote conversation status for local user updateRemoteOtrMemberMutedStatus :: PrepQuery W (MutedStatus, Maybe Text, Domain, ConvId, UserId) () -updateRemoteOtrMemberMutedStatus = "update user_remote_conv set otr_muted_status = ?, otr_muted_ref = ? where conv_remote_domain = ? and conv_remote_id = ? and user = ?" +updateRemoteOtrMemberMutedStatus = {- `IF EXISTS`, but that requires benchmarking -} "update user_remote_conv set otr_muted_status = ?, otr_muted_ref = ? where conv_remote_domain = ? and conv_remote_id = ? and user = ?" updateRemoteOtrMemberArchived :: PrepQuery W (Bool, Maybe Text, Domain, ConvId, UserId) () -updateRemoteOtrMemberArchived = "update user_remote_conv set otr_archived = ?, otr_archived_ref = ? where conv_remote_domain = ? and conv_remote_id = ? and user = ?" +updateRemoteOtrMemberArchived = {- `IF EXISTS`, but that requires benchmarking -} "update user_remote_conv set otr_archived = ?, otr_archived_ref = ? where conv_remote_domain = ? and conv_remote_id = ? and user = ?" updateRemoteMemberHidden :: PrepQuery W (Bool, Maybe Text, Domain, ConvId, UserId) () -updateRemoteMemberHidden = "update user_remote_conv set hidden = ?, hidden_ref = ? where conv_remote_domain = ? and conv_remote_id = ? and user = ?" +updateRemoteMemberHidden = {- `IF EXISTS`, but that requires benchmarking -} "update user_remote_conv set hidden = ?, hidden_ref = ? where conv_remote_domain = ? and conv_remote_id = ? and user = ?" selectRemoteMemberStatus :: PrepQuery R (Domain, ConvId, UserId) (Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text) selectRemoteMemberStatus = "select otr_muted_status, otr_muted_ref, otr_archived, otr_archived_ref, hidden, hidden_ref from user_remote_conv where conv_remote_domain = ? and conv_remote_id = ? and user = ?" @@ -443,13 +447,13 @@ selectClients = "select user, clients from clients where user in ?" rmClients :: PrepQuery W (Identity UserId) () rmClients = "delete from clients where user = ?" -addMemberClient :: ClientId -> QueryString W (Identity UserId) () -addMemberClient c = +upsertMemberAddClient :: ClientId -> QueryString W (Identity UserId) () +upsertMemberAddClient c = let t = LT.fromStrict (client c) in QueryString $ "update clients set clients = clients + {'" <> t <> "'} where user = ?" -rmMemberClient :: ClientId -> QueryString W (Identity UserId) () -rmMemberClient c = +upsertMemberRmClient :: ClientId -> QueryString W (Identity UserId) () +upsertMemberRmClient c = let t = LT.fromStrict (client c) in QueryString $ "update clients set clients = clients - {'" <> t <> "'} where user = ?" @@ -566,7 +570,7 @@ selectSearchVisibility = updateSearchVisibility :: PrepQuery W (TeamSearchVisibility, TeamId) () updateSearchVisibility = - "update team set search_visibility = ? where team = ?" + {- `IF EXISTS`, but that requires benchmarking -} "update team set search_visibility = ? where team = ?" -- Custom Backend ----------------------------------------------------------- @@ -574,8 +578,8 @@ selectCustomBackend :: PrepQuery R (Identity Domain) (HttpsUrl, HttpsUrl) selectCustomBackend = "select config_json_url, webapp_welcome_url from custom_backend where domain = ?" -updateCustomBackend :: PrepQuery W (HttpsUrl, HttpsUrl, Domain) () -updateCustomBackend = +upsertCustomBackend :: PrepQuery W (HttpsUrl, HttpsUrl, Domain) () +upsertCustomBackend = "update custom_backend set config_json_url = ?, webapp_welcome_url = ? where domain = ?" deleteCustomBackend :: PrepQuery W (Identity Domain) () diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index bd22909d671..bbea1852b86 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -232,6 +232,7 @@ addTeamMember t m = m ^? invitation . _Just . _2 ) addPrepQuery Cql.insertUserTeam (m ^. userId, t) + when (m `hasPermission` SetBilling) $ addPrepQuery Cql.insertBillingTeamMember (t, m ^. userId) diff --git a/services/gundeck/src/Gundeck/Push/Data.hs b/services/gundeck/src/Gundeck/Push/Data.hs index f00ddc19037..c688f64f4db 100644 --- a/services/gundeck/src/Gundeck/Push/Data.hs +++ b/services/gundeck/src/Gundeck/Push/Data.hs @@ -52,7 +52,7 @@ updateArn :: MonadClient m => UserId -> Transport -> AppName -> Token -> Endpoin updateArn uid transport app token arn = retry x5 $ write q (params LocalQuorum (arn, uid, transport, app, token)) where q :: PrepQuery W (EndpointArn, UserId, Transport, AppName, Token) () - q = "update user_push set arn = ? where usr = ? and transport = ? and app = ? and ptoken = ?" + q = {- `IF EXISTS`, but that requires benchmarking -} "update user_push set arn = ? where usr = ? and transport = ? and app = ? and ptoken = ?" delete :: MonadClient m => UserId -> Transport -> AppName -> Token -> m () delete u t a p = retry x5 $ write q (params LocalQuorum (u, t, a, p)) diff --git a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs index 2b3d347007a..30c985dd9df 100644 --- a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs @@ -356,7 +356,7 @@ setReplacedBy (Replaced old) (Replacing new) = do retry x5 . write ins $ params LocalQuorum (new, old) where ins :: PrepQuery W (SAML.IdPId, SAML.IdPId) () - ins = "UPDATE idp SET replaced_by = ? WHERE idp = ?" + ins = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE idp SET replaced_by = ? WHERE idp = ?" -- | See also: 'setReplacedBy'. clearReplacedBy :: @@ -367,7 +367,7 @@ clearReplacedBy (Replaced old) = do retry x5 . write ins $ params LocalQuorum (Identity old) where ins :: PrepQuery W (Identity SAML.IdPId) () - ins = "UPDATE idp SET replaced_by = null WHERE idp = ?" + ins = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE idp SET replaced_by = null WHERE idp = ?" -- | If the IdP is 'WireIdPAPIV1', it must be deleted globally, if it is 'WireIdPAPIV2', it -- must be deleted inside one team. 'V1' can be either in the old table without team index, diff --git a/tools/db/migrate-sso-feature-flag/src/Work.hs b/tools/db/migrate-sso-feature-flag/src/Work.hs index ff56a094478..d64570ce438 100644 --- a/tools/db/migrate-sso-feature-flag/src/Work.hs +++ b/tools/db/migrate-sso-feature-flag/src/Work.hs @@ -67,5 +67,6 @@ writeSsoFlags = mapM_ (`setSSOTeamConfig` FeatureStatusEnabled) setSSOTeamConfig :: MonadClient m => TeamId -> FeatureStatus -> m () setSSOTeamConfig tid ssoTeamConfigStatus = do retry x5 $ write updateSSOTeamConfig (params LocalQuorum (ssoTeamConfigStatus, tid)) + updateSSOTeamConfig :: PrepQuery W (FeatureStatus, TeamId) () - updateSSOTeamConfig = "update team_features set sso_status = ? where team_id = ?" + updateSSOTeamConfig = {- `IF EXISTS`, but that requires benchmarking -} "update team_features set sso_status = ? where team_id = ?" diff --git a/tools/db/repair-handles/src/Work.hs b/tools/db/repair-handles/src/Work.hs index 7b91c7c98cf..313c4021179 100644 --- a/tools/db/repair-handles/src/Work.hs +++ b/tools/db/repair-handles/src/Work.hs @@ -142,7 +142,7 @@ executeAction env = \case params LocalQuorum (handle, uid) where updateHandle :: PrepQuery W (Handle, UserId) () - updateHandle = "UPDATE user SET handle = ? WHERE id = ?" + updateHandle = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET handle = ? WHERE id = ?" removeHandle :: Env -> Handle -> IO () removeHandle Env {..} handle = From 0691a690984a8278c118d6a5b4e863cebfb463c2 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Tue, 22 Aug 2023 08:54:30 +0200 Subject: [PATCH 092/225] Remove billing-team-member-backfill tool (#3520) --- cabal.project | 3 - nix/local-haskell-packages.nix | 1 - nix/wire-server.nix | 2 - tools/db/billing-team-member-backfill/.ormolu | 1 - .../db/billing-team-member-backfill/README.md | 14 ---- .../billing-team-member-backfill.cabal | 81 ------------------- .../billing-team-member-backfill/default.nix | 42 ---------- .../billing-team-member-backfill/src/Main.hs | 57 ------------- .../src/Options.hs | 75 ----------------- .../billing-team-member-backfill/src/Work.hs | 81 ------------------- 10 files changed, 357 deletions(-) delete mode 120000 tools/db/billing-team-member-backfill/.ormolu delete mode 100644 tools/db/billing-team-member-backfill/README.md delete mode 100644 tools/db/billing-team-member-backfill/billing-team-member-backfill.cabal delete mode 100644 tools/db/billing-team-member-backfill/default.nix delete mode 100644 tools/db/billing-team-member-backfill/src/Main.hs delete mode 100644 tools/db/billing-team-member-backfill/src/Options.hs delete mode 100644 tools/db/billing-team-member-backfill/src/Work.hs diff --git a/cabal.project b/cabal.project index c2d8ae65d22..60b607ea01b 100644 --- a/cabal.project +++ b/cabal.project @@ -40,7 +40,6 @@ packages: , services/spar/ , tools/db/assets/ , tools/db/auto-whitelist/ - , tools/db/billing-team-member-backfill/ , tools/db/find-undead/ , tools/db/inconsistencies/ , tools/db/migrate-sso-feature-flag/ @@ -63,8 +62,6 @@ package background-worker ghc-options: -Werror package bilge ghc-options: -Werror -package billing-team-member-backfill - ghc-options: -Werror package brig ghc-options: -Werror package brig-types diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index dea24d5abe5..f06c352c734 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -44,7 +44,6 @@ spar = hself.callPackage ../services/spar/default.nix { inherit gitignoreSource; }; assets = hself.callPackage ../tools/db/assets/default.nix { inherit gitignoreSource; }; auto-whitelist = hself.callPackage ../tools/db/auto-whitelist/default.nix { inherit gitignoreSource; }; - billing-team-member-backfill = hself.callPackage ../tools/db/billing-team-member-backfill/default.nix { inherit gitignoreSource; }; find-undead = hself.callPackage ../tools/db/find-undead/default.nix { inherit gitignoreSource; }; inconsistencies = hself.callPackage ../tools/db/inconsistencies/default.nix { inherit gitignoreSource; }; migrate-sso-feature-flag = hself.callPackage ../tools/db/migrate-sso-feature-flag/default.nix { inherit gitignoreSource; }; diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 0f4241bb223..3b5675ba5eb 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -80,8 +80,6 @@ let proxy = [ "proxy" ]; spar = [ "spar" "spar-integration" "spar-schema" "spar-migrate-data" ]; stern = [ "stern" "stern-integration" ]; - - billing-team-member-backfill = [ "billing-team-member-backfill" ]; inconsistencies = [ "inconsistencies" ]; zauth = [ "zauth" ]; background-worker = [ "background-worker" ]; diff --git a/tools/db/billing-team-member-backfill/.ormolu b/tools/db/billing-team-member-backfill/.ormolu deleted file mode 120000 index ffc2ca9745e..00000000000 --- a/tools/db/billing-team-member-backfill/.ormolu +++ /dev/null @@ -1 +0,0 @@ -../../../.ormolu \ No newline at end of file diff --git a/tools/db/billing-team-member-backfill/README.md b/tools/db/billing-team-member-backfill/README.md deleted file mode 100644 index 5d72bd0e6a6..00000000000 --- a/tools/db/billing-team-member-backfill/README.md +++ /dev/null @@ -1,14 +0,0 @@ -## billing_team_member backfill - -A tool for filling table `billing_team_member` from existing data. - -### How to run this - -```sh -export GALLEY_HOST=... # ip address of galley cassandra DB node -export GALLEY_KEYSPACE=galley - -ssh -v -f ubuntu@${GALLEY_HOST} -L 2021:${GALLEY_HOST}:9042 -N - -./dist/billing-team-member-backfill --cassandra-host-galley=localhost --cassandra-port-galley=2021 --cassandra-keyspace-galley=${GALLEY_KEYSPACE} -``` diff --git a/tools/db/billing-team-member-backfill/billing-team-member-backfill.cabal b/tools/db/billing-team-member-backfill/billing-team-member-backfill.cabal deleted file mode 100644 index 8d5170c11ef..00000000000 --- a/tools/db/billing-team-member-backfill/billing-team-member-backfill.cabal +++ /dev/null @@ -1,81 +0,0 @@ -cabal-version: 1.12 -name: billing-team-member-backfill -version: 1.0.0 -synopsis: Backfill billing_team_member table -category: Network -author: Wire Swiss GmbH -maintainer: Wire Swiss GmbH -copyright: (c) 2020 Wire Swiss GmbH -license: AGPL-3 -build-type: Simple - -executable billing-team-member-backfill - main-is: Main.hs - other-modules: - Options - Paths_billing_team_member_backfill - Work - - hs-source-dirs: src - default-extensions: - NoImplicitPrelude - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts -Wredundant-constraints -Wunused-packages - - build-depends: - base - , cassandra-util - , conduit - , containers - , imports - , lens - , optparse-applicative - , text - , tinylog - , types-common - , wire-api - - default-language: GHC2021 diff --git a/tools/db/billing-team-member-backfill/default.nix b/tools/db/billing-team-member-backfill/default.nix deleted file mode 100644 index 18f60f04e76..00000000000 --- a/tools/db/billing-team-member-backfill/default.nix +++ /dev/null @@ -1,42 +0,0 @@ -# WARNING: GENERATED FILE, DO NOT EDIT. -# This file is generated by running hack/bin/generate-local-nix-packages.sh and -# must be regenerated whenever local packages are added or removed, or -# dependencies are added or removed. -{ mkDerivation -, base -, cassandra-util -, conduit -, containers -, gitignoreSource -, imports -, lens -, lib -, optparse-applicative -, text -, tinylog -, types-common -, wire-api -}: -mkDerivation { - pname = "billing-team-member-backfill"; - version = "1.0.0"; - src = gitignoreSource ./.; - isLibrary = false; - isExecutable = true; - executableHaskellDepends = [ - base - cassandra-util - conduit - containers - imports - lens - optparse-applicative - text - tinylog - types-common - wire-api - ]; - description = "Backfill billing_team_member table"; - license = lib.licenses.agpl3Only; - mainProgram = "billing-team-member-backfill"; -} diff --git a/tools/db/billing-team-member-backfill/src/Main.hs b/tools/db/billing-team-member-backfill/src/Main.hs deleted file mode 100644 index 720e33d3c3b..00000000000 --- a/tools/db/billing-team-member-backfill/src/Main.hs +++ /dev/null @@ -1,57 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 Main - ( main, - ) -where - -import Cassandra as C -import Cassandra.Settings as C -import Imports -import Options as O -import Options.Applicative -import System.Logger qualified as Log -import Work - -main :: IO () -main = do - s <- execParser (info (helper <*> settingsParser) desc) - lgr <- initLogger - gc <- initCas (setCasGalley s) lgr - runCommand lgr gc - where - desc = - header "billing-team-member-backfill" - <> progDesc "Backfill billing_team_member table" - <> fullDesc - initLogger = - Log.new - . Log.setOutput Log.StdOut - . Log.setFormat Nothing - . Log.setBufSize 0 - $ Log.defSettings - initCas cas l = - C.init - . C.setLogger (C.mkLogger l) - . C.setContacts (cHosts cas) [] - . C.setPortNumber (fromIntegral $ cPort cas) - . C.setKeyspace (cKeyspace cas) - . C.setProtocolVersion C.V4 - $ C.defSettings diff --git a/tools/db/billing-team-member-backfill/src/Options.hs b/tools/db/billing-team-member-backfill/src/Options.hs deleted file mode 100644 index b26a8379e55..00000000000 --- a/tools/db/billing-team-member-backfill/src/Options.hs +++ /dev/null @@ -1,75 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 Options - ( setCasGalley, - cHosts, - cPort, - cKeyspace, - settingsParser, - ) -where - -import Cassandra qualified as C -import Data.Text qualified as Text -import Imports -import Options.Applicative - -newtype MigratorSettings = MigratorSettings {setCasGalley :: CassandraSettings} - deriving (Show) - -data CassandraSettings = CassandraSettings - { cHosts :: !String, - cPort :: !Word16, - cKeyspace :: !C.Keyspace - } - deriving (Show) - -settingsParser :: Parser MigratorSettings -settingsParser = - MigratorSettings - <$> cassandraSettingsParser "galley" - -cassandraSettingsParser :: String -> Parser CassandraSettings -cassandraSettingsParser ks = - CassandraSettings - <$> strOption - ( long ("cassandra-host-" ++ ks) - <> metavar "HOST" - <> help ("Cassandra Host for: " ++ ks) - <> value "localhost" - <> showDefault - ) - <*> option - auto - ( long ("cassandra-port-" ++ ks) - <> metavar "PORT" - <> help ("Cassandra Port for: " ++ ks) - <> value 9042 - <> showDefault - ) - <*> ( C.Keyspace . Text.pack - <$> strOption - ( long ("cassandra-keyspace-" ++ ks) - <> metavar "STRING" - <> help ("Cassandra Keyspace for: " ++ ks) - <> value (ks ++ "_test") - <> showDefault - ) - ) diff --git a/tools/db/billing-team-member-backfill/src/Work.hs b/tools/db/billing-team-member-backfill/src/Work.hs deleted file mode 100644 index b7d1e9e4083..00000000000 --- a/tools/db/billing-team-member-backfill/src/Work.hs +++ /dev/null @@ -1,81 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 Work where - -import Cassandra -import Conduit -import Control.Lens (view) -import Data.Conduit.Internal (zipSources) -import Data.Conduit.List qualified as C -import Data.Id -import Data.Set qualified as Set -import Imports -import System.Logger (Logger) -import System.Logger qualified as Log -import Wire.API.Team.Permission - -runCommand :: Logger -> ClientState -> IO () -runCommand l galley = - runConduit $ - zipSources - (C.sourceList [(1 :: Int32) ..]) - (transPipe (runClient galley) getTeamMembers) - .| C.mapM - ( \(i, p) -> - Log.info l (Log.field "team members" (show (i * pageSize))) - >> pure p - ) - .| C.concatMap (filter isOwner) - .| C.map (\(t, u, _) -> (t, u)) - .| C.chunksOf 50 - .| C.mapM - ( \x -> - Log.info l (Log.field "writing billing team members" (show (length x))) - >> pure x - ) - .| C.mapM_ (runClient galley . createBillingTeamMembers) - -pageSize :: Int32 -pageSize = 1000 - ----------------------------------------------------------------------------- --- Queries - --- | Get team members from Galley -getTeamMembers :: ConduitM () [(TeamId, UserId, Maybe Permissions)] Client () -getTeamMembers = paginateC cql (paramsP LocalQuorum () pageSize) x5 - where - cql :: PrepQuery R () (TeamId, UserId, Maybe Permissions) - cql = "SELECT team, user, perms FROM team_member" - -createBillingTeamMembers :: [(TeamId, UserId)] -> Client () -createBillingTeamMembers pairs = - retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - mapM_ (addPrepQuery cql) pairs - where - cql :: PrepQuery W (TeamId, UserId) () - cql = "INSERT INTO billing_team_member (team, user) values (?, ?)" - -isOwner :: (TeamId, UserId, Maybe Permissions) -> Bool -isOwner (_, _, Just p) = SetBilling `Set.member` view self p -isOwner _ = False From ae46d4c6d872140239e924745972449ea1e37be5 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 22 Aug 2023 09:35:55 +0200 Subject: [PATCH 093/225] dockerephemeral: Increase nofile ulimits for ES and Fake DynamoDB (#3521) --- deploy/dockerephemeral/docker-compose.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index 04d804817f1..e02620b7677 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -14,6 +14,10 @@ services: container_name: demo_wire_dynamodb # image: cnadiminti/dynamodb-local:2018-04-11 image: julialongtin/dynamodb_local:0.0.9 + ulimits: + nofile: + soft: 65536 + hard: 65536 ports: - 127.0.0.1:4567:8000 networks: @@ -164,6 +168,10 @@ services: image: julialongtin/elasticsearch:0.0.9-amd64 # https://hub.docker.com/_/elastic is deprecated, but 6.2.4 did not work without further changes. # image: docker.elastic.co/elasticsearch/elasticsearch:6.2.4 + ulimits: + nofile: + soft: 65536 + hard: 65536 ports: - "127.0.0.1:9200:9200" - "127.0.0.1:9300:9300" From 9b29afe939e345a1934d18f4ff80c183481eac42 Mon Sep 17 00:00:00 2001 From: Owen Harvey Date: Tue, 22 Aug 2023 17:56:45 +1000 Subject: [PATCH 094/225] [WPB 3842] Federation completeness check (#3514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WPB-3842: Improving checks for adding users to a conversation. Added a check to `ensureAllowed` that checks for full federation connections for domains in a conversation, including the domains for new users. * WPB-3842: Adding the changelog * WPB-3842: Moving where the extra domain checks are being performed. Updating integration tests to reflect the updated semantics of conversation join semantics. Many of them weren't expecting errors relating to unreachable domains, and had to be updated to reflect this. * Fix asserted domains in an integration test * Integration test: assert on non-federating domains * WPB-3842: Changing parallel testing to sequential testing --------- Co-authored-by: Marko Dimjašević --- .../WPB-3842-federation-completeness-checks | 1 + integration/test/Test/Conversation.hs | 38 ++++++++++++++++--- integration/test/Testlib/Run.hs | 3 +- services/galley/src/Galley/API/Action.hs | 6 ++- 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-3842-federation-completeness-checks diff --git a/changelog.d/3-bug-fixes/WPB-3842-federation-completeness-checks b/changelog.d/3-bug-fixes/WPB-3842-federation-completeness-checks new file mode 100644 index 00000000000..47959209790 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-3842-federation-completeness-checks @@ -0,0 +1 @@ +Adding users to a conversation now enforces that all federation domains that will be in the conversation are federated with each other. \ No newline at end of file diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 7f974103b15..3324089b0c2 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -364,7 +364,7 @@ testAddReachableWithUnreachableRemoteUsers = do let overrides = def {dbBrig = setField "optSettings.setFederationStrategy" "allowAll"} <> fullSearchWithAll - ([alex, bob], conv) <- + ([alex, bob], conv, domains) <- startDynamicBackends [overrides, overrides] $ \domains -> do own <- make OwnDomain & asString other <- make OtherDomain & asString @@ -373,18 +373,24 @@ testAddReachableWithUnreachableRemoteUsers = do let newConv = defProteus {qualifiedUsers = [alex, charlie, dylan]} conv <- postConversation alice newConv >>= getJSON 201 - pure ([alex, bob], conv) + pure ([alex, bob], conv, domains) bobId <- bob %. "qualified_id" bindResponse (addMembers alex conv [bobId]) $ \resp -> do - resp.status `shouldMatchInt` 200 + -- This test is updated to reflect the changes in `performConversationJoin` + -- `performConversationJoin` now does a full check between all federation members + -- that will be in the conversation when adding users to a conversation. This is + -- to ensure that users from domains that aren't federating are not directly + -- connected to each other. + resp.status `shouldMatchInt` 533 + resp.jsonBody %. "unreachable_backends" `shouldMatchSet` domains testAddUnreachable :: HasCallStack => App () testAddUnreachable = do let overrides = def {dbBrig = setField "optSettings.setFederationStrategy" "allowAll"} <> fullSearchWithAll - ([alex, charlie], [charlieDomain, _dylanDomain], conv) <- + ([alex, charlie], [charlieDomain, dylanDomain], conv) <- startDynamicBackends [overrides, overrides] $ \domains -> do own <- make OwnDomain & asString [alice, alex, charlie, dylan] <- @@ -397,4 +403,26 @@ testAddUnreachable = do charlieId <- charlie %. "qualified_id" bindResponse (addMembers alex conv [charlieId]) $ \resp -> do resp.status `shouldMatchInt` 533 - resp.json %. "unreachable_backends" `shouldMatchSet` [charlieDomain] + -- All of the domains that are in the conversation, or will be in the conversation, + -- need to be reachable so we can check that the graph for those domains is fully connected. + resp.json %. "unreachable_backends" `shouldMatchSet` [charlieDomain, dylanDomain] + +testAddingUserNonFullyConnectedFederation :: HasCallStack => App () +testAddingUserNonFullyConnectedFederation = do + let overrides = + def {dbBrig = setField "optSettings.setFederationStrategy" "allowAll"} + <> fullSearchWithAll + startDynamicBackends [overrides] $ \domains -> do + own <- make OwnDomain & asString + other <- make OtherDomain & asString + [alice, alex, bob, charlie] <- + createAndConnectUsers $ [own, own, other] <> domains + + let newConv = defProteus {qualifiedUsers = [alex]} + conv <- postConversation alice newConv >>= getJSON 201 + + bobId <- bob %. "qualified_id" + charlieId <- charlie %. "qualified_id" + bindResponse (addMembers alex conv [bobId, charlieId]) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "non_federating_backends" `shouldMatchSet` (other : domains) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 70fa733c7cd..b53ec16cab6 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -11,6 +11,7 @@ import Data.Function import Data.Functor import Data.List import Data.Time.Clock +import Data.Traversable (for) import RunAllTests import System.Directory import System.Environment @@ -134,7 +135,7 @@ runTests tests cfg = do genv <- createGlobalEnv cfg withAsync displayOutput $ \displayThread -> do - report <- fmap mconcat $ pooledForConcurrently tests $ \(qname, _, _, action) -> do + report <- fmap mconcat $ for tests $ \(qname, _, _, action) -> do do (mErr, tm) <- withTime (runTest genv action) case mErr of diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 6e577eb0320..9e1ff71130f 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -442,7 +442,9 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do Local UserId -> Sem r () checkRemoteBackendsConnected lusr = do - let remoteDomains = tDomain <$> snd (partitionQualified lusr $ NE.toList invited) + let invitedDomains = tDomain <$> snd (partitionQualified lusr $ NE.toList invited) + existingDomains = tDomain . rmId <$> convRemoteMembers (tUnqualified lconv) + -- Note: -- -- In some cases, this federation status check might be redundant (for @@ -450,7 +452,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do -- it is important that we attempt to connect to the backends of the new -- users here, because that results in the correct error when those -- backends are not reachable. - checkFederationStatus (RemoteDomains $ Set.fromList remoteDomains) + checkFederationStatus (RemoteDomains . Set.fromList $ invitedDomains <> existingDomains) conv :: Data.Conversation conv = tUnqualified lconv From 834466960391bcccc407b884024faae98439a518 Mon Sep 17 00:00:00 2001 From: Owen Harvey Date: Tue, 22 Aug 2023 21:33:56 +1000 Subject: [PATCH 095/225] WPB-3798 incorrect json field names (#3518) * WPB-3798: Updating code and tests after renaming fields * WPB-3798: More updates to names after finding more JSON prefix mangling * WPB-3798: Fixing schema instances for SAML data * WPB-3798: Fixing instances that had errors, found by tests * WPB-3798: Adding changelogs * WPB-3798: PR feedback. * WPB-3798: Fixing an error with a field called `data'` The trailing ' would end up in the JSON representation. I've changed it to use a leading `_` like other structures, and wrote a newtype to handle the minimal prefix stripping. Also cleaning up the diff in regards to imports. * WPB-3798: Cleaning up imports to minimise the diff --- changelog.d/1-api-changes/WPB-3798 | 1 + changelog.d/5-internal/WPB-3798 | 3 + libs/types-common/src/Data/Code.hs | 4 +- libs/types-common/src/Data/Json/Util.hs | 12 +- libs/types-common/src/Util/Options.hs | 10 +- libs/types-common/src/Util/Options/Common.hs | 9 +- .../src/Wire/API/Federation/API/Brig.hs | 25 +- .../src/Wire/API/Federation/API/Cargohold.hs | 8 +- .../src/Wire/API/Federation/API/Galley.hs | 134 ++++---- .../Federation/Golden/ConversationCreated.hs | 44 +-- .../Federation/Golden/NewConnectionRequest.hs | 12 +- libs/wire-api/src/Wire/API/Provider.hs | 14 +- .../src/Wire/API/User/IdentityProvider.hs | 29 +- libs/wire-api/src/Wire/API/User/Orphans.hs | 13 +- libs/wire-api/src/Wire/API/User/Saml.hs | 2 +- libs/wire-api/src/Wire/API/Util/Aeson.hs | 33 +- .../CompletePasswordReset_provider.hs | 120 +++---- .../Golden/Generated/EmailUpdate_provider.hs | 40 +-- .../Generated/PasswordChange_provider.hs | 80 ++--- .../Generated/PasswordReset_provider.hs | 40 +-- .../src/Wire/BackgroundWorker.hs | 2 +- .../src/Wire/Defederation.hs | 5 +- .../background-worker/test/Test/Wire/Util.hs | 2 +- services/brig/src/Brig/API/Federation.hs | 20 +- services/brig/src/Brig/API/MLS/KeyPackages.hs | 4 +- services/brig/src/Brig/App.hs | 21 +- services/brig/src/Brig/Provider/API.hs | 6 +- services/brig/src/Brig/Run.hs | 4 +- services/brig/src/Brig/User/Search/Index.hs | 16 +- .../brig/test/integration/API/Federation.hs | 20 +- .../test/integration/API/Internal/Util.hs | 6 +- .../brig/test/integration/API/Provider.hs | 2 +- .../brig/test/integration/API/User/Auth.hs | 14 +- .../test/integration/API/User/Connection.hs | 78 ++--- services/brig/test/integration/Main.hs | 13 +- services/brig/test/integration/Util.hs | 8 +- .../cargohold/src/CargoHold/API/Federation.hs | 4 +- services/cargohold/src/CargoHold/API/V3.hs | 8 +- services/cargohold/src/CargoHold/AWS.hs | 10 +- services/cargohold/src/CargoHold/App.hs | 29 +- .../cargohold/src/CargoHold/Federation.hs | 10 +- services/cargohold/src/CargoHold/Options.hs | 42 +-- services/cargohold/src/CargoHold/Run.hs | 12 +- services/cargohold/src/CargoHold/S3.hs | 4 +- services/cargohold/test/integration/API.hs | 4 +- .../test/integration/API/Federation.hs | 48 +-- .../cargohold/test/integration/API/Util.hs | 12 +- services/cargohold/test/integration/App.hs | 14 +- .../cargohold/test/integration/TestSetup.hs | 10 +- services/federator/default.nix | 1 - services/federator/federator.cabal | 1 - services/federator/src/Federator/Run.hs | 16 +- .../integration/Test/Federator/IngressSpec.hs | 4 +- .../integration/Test/Federator/InwardSpec.hs | 10 +- .../test/integration/Test/Federator/JSON.hs | 2 +- .../test/integration/Test/Federator/Util.hs | 22 +- .../test/unit/Test/Federator/Client.hs | 2 +- services/galley/src/Galley/API/Action.hs | 24 +- services/galley/src/Galley/API/Federation.hs | 133 ++++---- services/galley/src/Galley/API/Internal.hs | 14 +- .../src/Galley/API/LegalHold/Conflicts.hs | 2 +- .../galley/src/Galley/API/MLS/GroupInfo.hs | 4 +- services/galley/src/Galley/API/MLS/Message.hs | 28 +- .../galley/src/Galley/API/MLS/Propagate.hs | 12 +- services/galley/src/Galley/API/Mapping.hs | 33 +- services/galley/src/Galley/API/Message.hs | 36 +-- services/galley/src/Galley/API/Query.hs | 6 +- services/galley/src/Galley/API/Teams.hs | 14 +- .../src/Galley/API/Teams/Features/Get.hs | 30 +- services/galley/src/Galley/API/Update.hs | 18 +- services/galley/src/Galley/API/Util.hs | 67 ++-- services/galley/src/Galley/App.hs | 43 +-- services/galley/src/Galley/Aws.hs | 6 +- .../galley/src/Galley/Cassandra/Client.hs | 2 +- services/galley/src/Galley/Cassandra/Code.hs | 2 +- services/galley/src/Galley/Cassandra/Team.hs | 2 +- services/galley/src/Galley/Env.hs | 7 +- .../Galley/Intra/BackendNotificationQueue.hs | 2 +- services/galley/src/Galley/Intra/Federator.hs | 7 +- .../galley/src/Galley/Intra/Push/Internal.hs | 2 +- services/galley/src/Galley/Intra/User.hs | 4 +- services/galley/src/Galley/Intra/Util.hs | 17 +- services/galley/src/Galley/Options.hs | 114 +++---- services/galley/src/Galley/Run.hs | 14 +- services/galley/src/Galley/Validation.hs | 2 +- services/galley/test/integration/API.hs | 297 +++++++++--------- .../galley/test/integration/API/Federation.hs | 57 ++-- .../galley/test/integration/API/MLS/Mocks.hs | 2 +- .../galley/test/integration/API/MLS/Util.hs | 44 +-- services/galley/test/integration/API/Teams.hs | 8 +- .../test/integration/API/Teams/Feature.hs | 26 +- .../test/integration/API/Teams/LegalHold.hs | 4 +- .../integration/API/Teams/LegalHold/Util.hs | 14 +- services/galley/test/integration/API/Util.hs | 80 +++-- .../test/integration/API/Util/TeamFeature.hs | 4 +- .../galley/test/integration/Federation.hs | 22 +- services/galley/test/integration/Main.hs | 32 +- .../galley/test/integration/TestHelpers.hs | 4 +- services/galley/test/integration/TestSetup.hs | 8 +- .../galley/test/unit/Test/Galley/Mapping.hs | 6 +- services/gundeck/src/Gundeck/Aws.hs | 17 +- services/gundeck/src/Gundeck/Env.hs | 37 +-- services/gundeck/src/Gundeck/Notification.hs | 6 +- .../gundeck/src/Gundeck/Notification/Data.hs | 16 +- services/gundeck/src/Gundeck/Options.hs | 62 ++-- services/gundeck/src/Gundeck/Push.hs | 16 +- services/gundeck/src/Gundeck/Push/Native.hs | 30 +- services/gundeck/src/Gundeck/React.hs | 4 +- services/gundeck/src/Gundeck/Run.hs | 8 +- .../src/Gundeck/ThreadBudget/Internal.hs | 18 +- services/gundeck/test/integration/API.hs | 13 +- services/gundeck/test/integration/Main.hs | 17 +- services/gundeck/test/integration/Util.hs | 8 +- services/gundeck/test/unit/ThreadBudget.hs | 2 +- services/spar/src/Spar/API.hs | 59 ++-- services/spar/src/Spar/App.hs | 20 +- services/spar/src/Spar/Run.hs | 28 +- .../src/Spar/Sem/IdPConfigStore/Cassandra.hs | 33 +- .../spar/src/Spar/Sem/IdPConfigStore/Mem.hs | 8 +- .../test-integration/Test/Spar/APISpec.hs | 54 ++-- .../test-integration/Test/Spar/AppSpec.hs | 2 +- .../test-integration/Test/Spar/DataSpec.hs | 8 +- .../Test/Spar/Scim/UserSpec.hs | 4 +- services/spar/test-integration/Util/Core.hs | 72 +++-- services/spar/test-integration/Util/Scim.hs | 2 +- services/spar/test-integration/Util/Types.hs | 11 +- services/spar/test/Arbitrary.hs | 5 +- tools/stern/src/Stern/API.hs | 2 +- tools/stern/src/Stern/App.hs | 2 +- tools/stern/src/Stern/Types.hs | 4 +- tools/stern/test/integration/Main.hs | 2 +- 131 files changed, 1505 insertions(+), 1437 deletions(-) create mode 100644 changelog.d/1-api-changes/WPB-3798 create mode 100644 changelog.d/5-internal/WPB-3798 diff --git a/changelog.d/1-api-changes/WPB-3798 b/changelog.d/1-api-changes/WPB-3798 new file mode 100644 index 00000000000..42435b4730f --- /dev/null +++ b/changelog.d/1-api-changes/WPB-3798 @@ -0,0 +1 @@ +The JSON schema of `NonConnectedBackends` has changed to have its single field now called `non_connected_backends`. \ No newline at end of file diff --git a/changelog.d/5-internal/WPB-3798 b/changelog.d/5-internal/WPB-3798 new file mode 100644 index 00000000000..625e4d9b15a --- /dev/null +++ b/changelog.d/5-internal/WPB-3798 @@ -0,0 +1,3 @@ +JSON derived schemas have been changed to no longer pre-process record fields to drop prefixes that were required to disambiguate fields. +Prefix processing still exists to drop leading underscores from field names, as we are using prefixed field names with `makeLenses`. +Code has been updated to use `OverloadedRecordDot` with the changed field names. \ No newline at end of file diff --git a/libs/types-common/src/Data/Code.hs b/libs/types-common/src/Data/Code.hs index 8d9d3c783d4..1820d85f403 100644 --- a/libs/types-common/src/Data/Code.hs +++ b/libs/types-common/src/Data/Code.hs @@ -119,8 +119,8 @@ deriving instance Cql Value -- (but without a type, using plain fields). This will make it easier to re-use a key/value -- pair in the API, keeping "code" in the JSON for backwards compatibility data KeyValuePair = KeyValuePair - { kcKey :: !Key, - kcCode :: !Value + { key :: !Key, + code :: !Value } deriving (Eq, Generic, Show) diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index 5235a039b48..62e7168d728 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -172,20 +172,16 @@ instance ToSchema A.Object where -- toJSONFieldName -- | Convenient helper to convert field names to use as JSON fields. --- it removes the prefix (assumed to be anything before an uppercase --- character) and converts the rest to underscore +-- it converts the field names to snake_case. -- -- Example: --- newtype TeamName = TeamName { tnTeamName :: Text } --- deriveJSON toJSONFieldName ''tnTeamName +-- newtype TeamName = TeamName { teamName :: Text } +-- deriveJSON toJSONFieldName ''teamName -- -- would generate {To/From}JSON instances where -- the field name is "team_name" toJSONFieldName :: A.Options -toJSONFieldName = A.defaultOptions {A.fieldLabelModifier = A.camelTo2 '_' . dropPrefix} - where - dropPrefix :: String -> String - dropPrefix = dropWhile (not . isUpper) +toJSONFieldName = A.defaultOptions {A.fieldLabelModifier = A.camelTo2 '_'} -------------------------------------------------------------------------------- diff --git a/libs/types-common/src/Util/Options.hs b/libs/types-common/src/Util/Options.hs index 925a8be7560..823f9bcc68a 100644 --- a/libs/types-common/src/Util/Options.hs +++ b/libs/types-common/src/Util/Options.hs @@ -75,8 +75,8 @@ urlPort u = do makeLenses ''AWSEndpoint data Endpoint = Endpoint - { _epHost :: !Text, - _epPort :: !Word16 + { _host :: !Text, + _port :: !Word16 } deriving (Show, Generic) @@ -85,14 +85,14 @@ deriveFromJSON toOptionFieldName ''Endpoint makeLenses ''Endpoint data CassandraOpts = CassandraOpts - { _casEndpoint :: !Endpoint, - _casKeyspace :: !Text, + { _endpoint :: !Endpoint, + _keyspace :: !Text, -- | If this option is unset, use all available nodes. -- If this option is set, use only cassandra nodes in the given datacentre -- -- This option is most likely only necessary during a cassandra DC migration -- FUTUREWORK: remove this option again, or support a datacentre migration feature - _casFilterNodesByDatacentre :: !(Maybe Text) + _filterNodesByDatacentre :: !(Maybe Text) } deriving (Show, Generic) diff --git a/libs/types-common/src/Util/Options/Common.hs b/libs/types-common/src/Util/Options/Common.hs index 97ac6a9cefd..c052a53c33b 100644 --- a/libs/types-common/src/Util/Options/Common.hs +++ b/libs/types-common/src/Util/Options/Common.hs @@ -28,12 +28,11 @@ import System.Posix.Env qualified as Posix -- NOTE: We typically use this for options in the configuration files! -- If you are looking into converting record field name to JSON to be used -- over the API, look for toJSONFieldName in the Data.Json.Util module. --- It removes the prefix (assumed to be anything before an uppercase --- character) and lowers the first character +-- It converts field names into snake_case -- -- Example: --- newtype TeamName = TeamName { tnTeamName :: Text } --- deriveJSON toJSONFieldName ''tnTeamName +-- newtype TeamName = TeamName { teamName :: Text } +-- deriveJSON toJSONFieldName ''teamName -- -- would generate {To/From}JSON instances where -- the field name is "teamName" @@ -44,7 +43,7 @@ toOptionFieldName = defaultOptions {fieldLabelModifier = lowerFirst . dropPrefix lowerFirst (x : xs) = toLower x : xs lowerFirst [] = "" dropPrefix :: String -> String - dropPrefix = dropWhile (not . isUpper) + dropPrefix = dropWhile ('_' ==) optOrEnv :: (a -> b) -> Maybe a -> (String -> b) -> String -> IO b optOrEnv getter conf reader var = case conf of diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs index 66963015786..eebc121ea0d 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs @@ -75,26 +75,29 @@ type BrigApi = :<|> FedEndpoint "get-not-fully-connected-backends" DomainSet NonConnectedBackends newtype DomainSet = DomainSet - { dsDomains :: Set Domain + { domains :: Set Domain } deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded DomainSet) newtype NonConnectedBackends = NonConnectedBackends + -- TODO: + -- The encoding rules that were in place would make this "connectedBackends" over the wire. + -- I do not think that this was intended, so I'm leaving this note as it will be an API break. { nonConnectedBackends :: Set Domain } deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded NonConnectedBackends) newtype GetUserClients = GetUserClients - { gucUsers :: [UserId] + { users :: [UserId] } deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded GetUserClients) data MLSClientsRequest = MLSClientsRequest - { mcrUserId :: UserId, -- implicitly qualified by the local domain - mcrSignatureScheme :: SignatureSchemeTag + { userId :: UserId, -- implicitly qualified by the local domain + signatureScheme :: SignatureSchemeTag } deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded MLSClientsRequest) @@ -117,10 +120,10 @@ data MLSClientsRequest = MLSClientsRequest data NewConnectionRequest = NewConnectionRequest { -- | The 'from' userId is understood to always have the domain of the backend making the connection request - ncrFrom :: UserId, + from :: UserId, -- | The 'to' userId is understood to always have the domain of the receiving backend. - ncrTo :: UserId, - ncrAction :: RemoteConnectionAction + to :: UserId, + action :: RemoteConnectionAction } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewConnectionRequest) @@ -144,9 +147,9 @@ type UserDeletedNotificationMaxConnections = 1000 data UserDeletedConnectionsNotification = UserDeletedConnectionsNotification { -- | This is qualified implicitly by the origin domain - udcnUser :: UserId, + user :: UserId, -- | These are qualified implicitly by the target domain - udcnConnections :: Range 1 UserDeletedNotificationMaxConnections [UserId] + connections :: Range 1 UserDeletedNotificationMaxConnections [UserId] } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserDeletedConnectionsNotification) @@ -154,10 +157,10 @@ data UserDeletedConnectionsNotification = UserDeletedConnectionsNotification data ClaimKeyPackageRequest = ClaimKeyPackageRequest { -- | The user making the request, implictly qualified by the origin domain. - ckprClaimant :: UserId, + claimant :: UserId, -- | The user whose key packages are being claimed, implictly qualified by -- the target domain. - ckprTarget :: UserId + target :: UserId } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ClaimKeyPackageRequest) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Cargohold.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Cargohold.hs index bcff826a17b..debe7a2a5d5 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Cargohold.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Cargohold.hs @@ -29,19 +29,19 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) data GetAsset = GetAsset { -- | User requesting the asset. Implictly qualified with the source domain. - gaUser :: UserId, + user :: UserId, -- | Asset key for the asset to download. Implictly qualified with the -- target domain. - gaKey :: AssetKey, + key :: AssetKey, -- | Optional asset token. - gaToken :: Maybe AssetToken + token :: Maybe AssetToken } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform GetAsset) deriving (ToJSON, FromJSON) via (CustomEncoded GetAsset) data GetAssetResponse = GetAssetResponse - {gaAvailable :: Bool} + {available :: Bool} deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform GetAssetResponse) deriving (ToJSON, FromJSON) via (CustomEncoded GetAssetResponse) 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 fdd17aa6f68..faf5a71fbcb 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 @@ -41,7 +41,7 @@ import Wire.API.MakesFederatedCall import Wire.API.Message import Wire.API.Routes.Public.Galley.Messaging import Wire.API.Unreachable -import Wire.API.Util.Aeson (CustomEncoded (..)) +import Wire.API.Util.Aeson (CustomEncoded (..), CustomEncodedLensable (..)) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- FUTUREWORK: data types, json instances, more endpoints. See @@ -134,9 +134,9 @@ type GalleyApi = :<|> FedEndpoint "on-connection-removed" Domain EmptyResponse data TypingDataUpdateRequest = TypingDataUpdateRequest - { tdurTypingStatus :: TypingStatus, - tdurUserId :: UserId, - tdurConvId :: ConvId + { typingStatus :: TypingStatus, + userId :: UserId, + convId :: ConvId } deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdateRequest) @@ -148,37 +148,37 @@ data TypingDataUpdateResponse deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdateResponse) data TypingDataUpdated = TypingDataUpdated - { tudTime :: UTCTime, - tudOrigUserId :: Qualified UserId, + { time :: UTCTime, + origUserId :: Qualified UserId, -- | Implicitely qualified by sender's domain - tudConvId :: ConvId, + convId :: ConvId, -- | Implicitely qualified by receiver's domain - tudUsersInConv :: [UserId], - tudTypingStatus :: TypingStatus + usersInConv :: [UserId], + typingStatus :: TypingStatus } deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdated) data ClientRemovedRequest = ClientRemovedRequest - { crrUser :: UserId, - crrClient :: ClientId, - crrConvs :: [ConvId] + { user :: UserId, + client :: ClientId, + convs :: [ConvId] } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ClientRemovedRequest) deriving (FromJSON, ToJSON) via (CustomEncoded ClientRemovedRequest) data GetConversationsRequest = GetConversationsRequest - { gcrUserId :: UserId, - gcrConvIds :: [ConvId] + { userId :: UserId, + convIds :: [ConvId] } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform GetConversationsRequest) deriving (ToJSON, FromJSON) via (CustomEncoded GetConversationsRequest) data RemoteConvMembers = RemoteConvMembers - { rcmSelfRole :: RoleName, - rcmOthers :: [OtherMember] + { selfRole :: RoleName, + others :: [OtherMember] } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform RemoteConvMembers) @@ -191,17 +191,17 @@ data RemoteConvMembers = RemoteConvMembers data RemoteConversation = RemoteConversation { -- | Id of the conversation, implicitly qualified with the domain of the -- backend that created this value. - rcnvId :: ConvId, - rcnvMetadata :: ConversationMetadata, - rcnvMembers :: RemoteConvMembers, - rcnvProtocol :: Protocol + id :: ConvId, + metadata :: ConversationMetadata, + members :: RemoteConvMembers, + protocol :: Protocol } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform RemoteConversation) deriving (FromJSON, ToJSON) via (CustomEncoded RemoteConversation) newtype GetConversationsResponse = GetConversationsResponse - { gcresConvs :: [RemoteConversation] + { convs :: [RemoteConversation] } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform GetConversationsResponse) @@ -213,30 +213,30 @@ newtype GetConversationsResponse = GetConversationsResponse -- separarate data type that can be reused in several data types in this module. data ConversationCreated conv = ConversationCreated { -- | The time when the conversation was created - ccTime :: UTCTime, + time :: UTCTime, -- | The user that created the conversation. This is implicitly qualified -- by the requesting domain, since it is impossible to create a regular/group -- conversation on a remote backend. - ccOrigUserId :: UserId, + origUserId :: UserId, -- | The conversation ID, local to the backend invoking the RPC - ccCnvId :: conv, + cnvId :: conv, -- | The conversation type - ccCnvType :: ConvType, - ccCnvAccess :: [Access], - ccCnvAccessRoles :: Set AccessRole, + cnvType :: ConvType, + cnvAccess :: [Access], + cnvAccessRoles :: Set AccessRole, -- | The conversation name, - ccCnvName :: Maybe Text, + cnvName :: Maybe Text, -- | Members of the conversation apart from the creator - ccNonCreatorMembers :: Set OtherMember, - ccMessageTimer :: Maybe Milliseconds, - ccReceiptMode :: Maybe ReceiptMode, - ccProtocol :: Protocol + nonCreatorMembers :: Set OtherMember, + messageTimer :: Maybe Milliseconds, + receiptMode :: Maybe ReceiptMode, + protocol :: Protocol } deriving stock (Eq, Show, Generic, Functor) deriving (ToJSON, FromJSON) via (CustomEncoded (ConversationCreated conv)) ccRemoteOrigUserId :: ConversationCreated (Remote ConvId) -> Remote UserId -ccRemoteOrigUserId cc = qualifyAs (ccCnvId cc) (ccOrigUserId cc) +ccRemoteOrigUserId cc = qualifyAs cc.cnvId cc.origUserId data ConversationUpdate = ConversationUpdate { cuTime :: UTCTime, @@ -263,10 +263,10 @@ instance FromJSON ConversationUpdate data LeaveConversationRequest = LeaveConversationRequest { -- | The conversation is assumed to be owned by the target domain, which -- allows us to protect against relay attacks - lcConvId :: ConvId, + convId :: ConvId, -- | The leaver is assumed to be owned by the origin domain, which allows us -- to protect against spoofing attacks - lcLeaver :: UserId + leaver :: UserId } deriving stock (Generic, Eq, Show) deriving (ToJSON, FromJSON) via (CustomEncoded LeaveConversationRequest) @@ -286,27 +286,27 @@ data RemoveFromConversationError -- federation RPC), and for conversations with an arbitrary Qualified or Remote id -- (e.g. as the argument of the corresponding handler). data RemoteMessage conv = RemoteMessage - { rmTime :: UTCTime, - rmData :: Maybe Text, - rmSender :: Qualified UserId, - rmSenderClient :: ClientId, - rmConversation :: conv, - rmPriority :: Maybe Priority, - rmPush :: Bool, - rmTransient :: Bool, - rmRecipients :: UserClientMap Text + { time :: UTCTime, + _data :: Maybe Text, + sender :: Qualified UserId, + senderClient :: ClientId, + conversation :: conv, + priority :: Maybe Priority, + push :: Bool, + transient :: Bool, + recipients :: UserClientMap Text } deriving stock (Eq, Show, Generic, Functor) deriving (Arbitrary) via (GenericUniform (RemoteMessage conv)) - deriving (ToJSON, FromJSON) via (CustomEncoded (RemoteMessage conv)) + deriving (ToJSON, FromJSON) via (CustomEncodedLensable (RemoteMessage conv)) data RemoteMLSMessage = RemoteMLSMessage - { rmmTime :: UTCTime, - rmmMetadata :: MessageMetadata, - rmmSender :: Qualified UserId, - rmmConversation :: ConvId, - rmmRecipients :: [(UserId, ClientId)], - rmmMessage :: Base64ByteString + { time :: UTCTime, + metadata :: MessageMetadata, + sender :: Qualified UserId, + conversation :: ConvId, + recipients :: [(UserId, ClientId)], + message :: Base64ByteString } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform RemoteMLSMessage) @@ -321,11 +321,11 @@ data RemoteMLSMessageResponse data ProteusMessageSendRequest = ProteusMessageSendRequest { -- | Conversation is assumed to be owned by the target domain, this allows -- us to protect against relay attacks - pmsrConvId :: ConvId, + convId :: ConvId, -- | Sender is assumed to be owned by the origin domain, this allows us to -- protect against spoofing attacks - pmsrSender :: UserId, - pmsrRawMessage :: Base64ByteString + sender :: UserId, + rawMessage :: Base64ByteString } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ProteusMessageSendRequest) @@ -334,18 +334,18 @@ data ProteusMessageSendRequest = ProteusMessageSendRequest data MLSMessageSendRequest = MLSMessageSendRequest { -- | Conversation (or sub conversation) is assumed to be owned by the target -- domain, this allows us to protect against relay attacks - mmsrConvOrSubId :: ConvOrSubConvId, + convOrSubId :: ConvOrSubConvId, -- | Sender is assumed to be owned by the origin domain, this allows us to -- protect against spoofing attacks - mmsrSender :: UserId, - mmsrRawMessage :: Base64ByteString + sender :: UserId, + rawMessage :: Base64ByteString } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform MLSMessageSendRequest) deriving (ToJSON, FromJSON) via (CustomEncoded MLSMessageSendRequest) newtype MessageSendResponse = MessageSendResponse - {msResponse :: PostOtrResponse MessageSendingStatus} + {response :: PostOtrResponse MessageSendingStatus} deriving stock (Eq, Show) deriving (ToJSON, FromJSON) @@ -355,7 +355,7 @@ newtype MessageSendResponse = MessageSendResponse ) newtype LeaveConversationResponse = LeaveConversationResponse - {leaveResponse :: Either RemoveFromConversationError ()} + {response :: Either RemoveFromConversationError ()} deriving stock (Eq, Show) deriving (ToJSON, FromJSON) @@ -365,9 +365,9 @@ type UserDeletedNotificationMaxConvs = 1000 data UserDeletedConversationsNotification = UserDeletedConversationsNotification { -- | This is qualified implicitly by the origin domain - udcvUser :: UserId, + user :: UserId, -- | These are qualified implicitly by the target domain - udcvConversations :: Range 1 UserDeletedNotificationMaxConvs [ConvId] + conversations :: Range 1 UserDeletedNotificationMaxConvs [ConvId] } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserDeletedConversationsNotification) @@ -376,11 +376,11 @@ data UserDeletedConversationsNotification = UserDeletedConversationsNotification data ConversationUpdateRequest = ConversationUpdateRequest { -- | The user that is attempting to perform the action. This is qualified -- implicitly by the origin domain - curUser :: UserId, + user :: UserId, -- | Id of conversation the action should be performed on. The is qualified -- implicity by the owning backend which receives this request. - curConvId :: ConvId, - curAction :: SomeConversationAction + convId :: ConvId, + action :: SomeConversationAction } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ConversationUpdateRequest) @@ -399,7 +399,7 @@ data ConversationUpdateResponse -- | A wrapper around a raw welcome message newtype MLSWelcomeRequest = MLSWelcomeRequest - { unMLSWelcomeRequest :: Base64ByteString + { mlsWelcomeRequest :: Base64ByteString } deriving stock (Eq, Generic, Show) deriving (Arbitrary) via (GenericUniform MLSWelcomeRequest) @@ -428,10 +428,10 @@ data MLSMessageResponse data GetGroupInfoRequest = GetGroupInfoRequest { -- | Conversation is assumed to be owned by the target domain, this allows -- us to protect against relay attacks - ggireqConv :: ConvId, + conv :: ConvId, -- | Sender is assumed to be owned by the origin domain, this allows us to -- protect against spoofing attacks - ggireqSender :: UserId + sender :: UserId } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform GetGroupInfoRequest) diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs index 6f0765b8138..c5f6c5687bd 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs @@ -34,14 +34,14 @@ import Wire.API.Provider.Service testObject_ConversationCreated1 :: ConversationCreated ConvId testObject_ConversationCreated1 = ConversationCreated - { ccTime = read "1864-04-12 12:22:43.673 UTC", - ccOrigUserId = Id (fromJust (UUID.fromString "eed9dea3-5468-45f8-b562-7ad5de2587d0")), - ccCnvId = Id (fromJust (UUID.fromString "d13dbe58-d4e3-450f-9c0c-1e632f548740")), - ccCnvType = RegularConv, - ccCnvAccess = [InviteAccess, CodeAccess], - ccCnvAccessRoles = Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole], - ccCnvName = Just "gossip", - ccNonCreatorMembers = + { time = read "1864-04-12 12:22:43.673 UTC", + origUserId = Id (fromJust (UUID.fromString "eed9dea3-5468-45f8-b562-7ad5de2587d0")), + cnvId = Id (fromJust (UUID.fromString "d13dbe58-d4e3-450f-9c0c-1e632f548740")), + cnvType = RegularConv, + cnvAccess = [InviteAccess, CodeAccess], + cnvAccessRoles = Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole], + cnvName = Just "gossip", + nonCreatorMembers = Set.fromList [ OtherMember { omQualifiedId = @@ -66,23 +66,23 @@ testObject_ConversationCreated1 = omConvRoleName = roleNameWireMember } ], - ccMessageTimer = Just (Ms 1000), - ccReceiptMode = Just (ReceiptMode 42), - ccProtocol = ProtocolProteus + messageTimer = Just (Ms 1000), + receiptMode = Just (ReceiptMode 42), + protocol = ProtocolProteus } testObject_ConversationCreated2 :: ConversationCreated ConvId testObject_ConversationCreated2 = ConversationCreated - { ccTime = read "1864-04-12 12:22:43.673 UTC", - ccOrigUserId = Id (fromJust (UUID.fromString "eed9dea3-5468-45f8-b562-7ad5de2587d0")), - ccCnvId = Id (fromJust (UUID.fromString "d13dbe58-d4e3-450f-9c0c-1e632f548740")), - ccCnvType = One2OneConv, - ccCnvAccess = [], - ccCnvAccessRoles = Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole], - ccCnvName = Nothing, - ccNonCreatorMembers = Set.fromList [], - ccMessageTimer = Nothing, - ccReceiptMode = Nothing, - ccProtocol = ProtocolMLS (ConversationMLSData (GroupId "group") (Epoch 3) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) + { time = read "1864-04-12 12:22:43.673 UTC", + origUserId = Id (fromJust (UUID.fromString "eed9dea3-5468-45f8-b562-7ad5de2587d0")), + cnvId = Id (fromJust (UUID.fromString "d13dbe58-d4e3-450f-9c0c-1e632f548740")), + cnvType = One2OneConv, + cnvAccess = [], + cnvAccessRoles = Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole], + cnvName = Nothing, + nonCreatorMembers = Set.fromList [], + messageTimer = Nothing, + receiptMode = Nothing, + protocol = ProtocolMLS (ConversationMLSData (GroupId "group") (Epoch 3) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) } diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/NewConnectionRequest.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/NewConnectionRequest.hs index 514cb18a53d..de8b20ca950 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/NewConnectionRequest.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/NewConnectionRequest.hs @@ -25,15 +25,15 @@ import Wire.API.Federation.API.Brig testObject_NewConnectionRequest1 :: NewConnectionRequest testObject_NewConnectionRequest1 = NewConnectionRequest - { ncrFrom = Id (fromJust (UUID.fromString "69f66843-6cf1-48fb-8c05-1cf58c23566a")), - ncrTo = Id (fromJust (UUID.fromString "1669240c-c510-43e0-bf1a-33378fa4ba55")), - ncrAction = RemoteConnect + { from = Id (fromJust (UUID.fromString "69f66843-6cf1-48fb-8c05-1cf58c23566a")), + to = Id (fromJust (UUID.fromString "1669240c-c510-43e0-bf1a-33378fa4ba55")), + action = RemoteConnect } testObject_NewConnectionRequest2 :: NewConnectionRequest testObject_NewConnectionRequest2 = NewConnectionRequest - { ncrFrom = Id (fromJust (UUID.fromString "69f66843-6cf1-48fb-8c05-1cf58c23566a")), - ncrTo = Id (fromJust (UUID.fromString "1669240c-c510-43e0-bf1a-33378fa4ba55")), - ncrAction = RemoteRescind + { from = Id (fromJust (UUID.fromString "69f66843-6cf1-48fb-8c05-1cf58c23566a")), + to = Id (fromJust (UUID.fromString "1669240c-c510-43e0-bf1a-33378fa4ba55")), + action = RemoteRescind } diff --git a/libs/wire-api/src/Wire/API/Provider.hs b/libs/wire-api/src/Wire/API/Provider.hs index 67bec9b77cb..99b58238b2d 100644 --- a/libs/wire-api/src/Wire/API/Provider.hs +++ b/libs/wire-api/src/Wire/API/Provider.hs @@ -255,7 +255,7 @@ instance FromJSON DeleteProvider where -- Password Change/Reset -- | The payload for initiating a password reset. -newtype PasswordReset = PasswordReset {nprEmail :: Email} +newtype PasswordReset = PasswordReset {email :: Email} deriving stock (Eq, Show) deriving newtype (Arbitrary) @@ -263,9 +263,9 @@ deriveJSON toJSONFieldName ''PasswordReset -- | The payload for completing a password reset. data CompletePasswordReset = CompletePasswordReset - { cpwrKey :: Code.Key, - cpwrCode :: Code.Value, - cpwrPassword :: PlainTextPassword6 + { key :: Code.Key, + code :: Code.Value, + password :: PlainTextPassword6 } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform CompletePasswordReset) @@ -274,8 +274,8 @@ deriveJSON toJSONFieldName ''CompletePasswordReset -- | The payload for changing a password. data PasswordChange = PasswordChange - { cpOldPassword :: PlainTextPassword6, - cpNewPassword :: PlainTextPassword6 + { oldPassword :: PlainTextPassword6, + newPassword :: PlainTextPassword6 } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform PasswordChange) @@ -283,7 +283,7 @@ data PasswordChange = PasswordChange deriveJSON toJSONFieldName ''PasswordChange -- | The payload for updating an email address -newtype EmailUpdate = EmailUpdate {euEmail :: Email} +newtype EmailUpdate = EmailUpdate {email :: Email} deriving stock (Eq, Show, Generic) deriving newtype (Arbitrary) diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs index 0295cab6d37..e17db47e465 100644 --- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs +++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs @@ -39,6 +39,7 @@ import SAML2.WebSSO qualified as SAML import SAML2.WebSSO.Types.TH (deriveJSONOptions) import Servant.API as Servant hiding (MkLink, URI (..)) import Wire.API.User.Orphans (samlSchemaOptions) +import Wire.API.Util.Aeson (defaultOptsDropChar) import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform)) -- | The identity provider type used in Spar. @@ -48,17 +49,17 @@ newtype IdPHandle = IdPHandle {unIdPHandle :: Text} deriving (Eq, Ord, Show, FromJSON, ToJSON, ToSchema, Arbitrary, Generic) data WireIdP = WireIdP - { _wiTeam :: TeamId, + { _team :: TeamId, -- | list of issuer names that this idp has replaced, most recent first. this is used -- for finding users that are still stored under the old issuer, see -- 'findUserWithOldIssuer', 'moveUserToNewIssuer'. - _wiApiVersion :: Maybe WireIdPAPIVersion, - _wiOldIssuers :: [SAML.Issuer], + _apiVersion :: Maybe WireIdPAPIVersion, + _oldIssuers :: [SAML.Issuer], -- | the issuer that has replaced this one. this is set iff a new issuer is created -- with the @"replaces"@ query parameter, and it is used to decide whether users not -- existing on this IdP can be auto-provisioned (if 'isJust', they can't). - _wiReplacedBy :: Maybe SAML.IdPId, - _wiHandle :: IdPHandle + _replacedBy :: Maybe SAML.IdPId, + _handle :: IdPHandle } deriving (Eq, Show, Generic) @@ -80,7 +81,9 @@ defWireIdPAPIVersion = WireIdPAPIV1 makeLenses ''WireIdP deriveJSON deriveJSONOptions ''WireIdPAPIVersion -deriveJSON deriveJSONOptions ''WireIdP + +-- Changing the encoder since we've dropped the field prefixes +deriveJSON (defaultOptsDropChar '_') ''WireIdP instance BSC.ToByteString WireIdPAPIVersion where builder = @@ -124,13 +127,14 @@ instance Cql.Cql WireIdPAPIVersion where -- | A list of 'IdP's, returned by some endpoints. Wrapped into an object to -- allow extensibility later on. data IdPList = IdPList - { _idplProviders :: [IdP] + { _providers :: [IdP] } deriving (Eq, Show, Generic) makeLenses ''IdPList -deriveJSON deriveJSONOptions ''IdPList +-- Same as WireIdP, we want the lenses, so we have to drop a prefix +deriveJSON (defaultOptsDropChar '_') ''IdPList -- | JSON-encoded information about metadata: @{"value": }@. (Here we could also -- implement @{"uri": , "cert": }@. check both the certificate we get @@ -167,14 +171,19 @@ instance ToJSON IdPMetadataInfo where -- Swagger instances +-- Same as WireIdP, check there for why this has different handling instance ToSchema IdPList where - declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions + declareNamedSchema = genericDeclareNamedSchema $ fromAesonOptions $ defaultOptsDropChar '_' instance ToSchema WireIdPAPIVersion where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions instance ToSchema WireIdP where - declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions + -- We don't want to use `samlSchemaOptions`, as it pulls from saml2-web-sso json options which + -- as a `dropWhile not . isUpper` modifier. All we need is to drop the underscore prefix and + -- keep the rest of the default processing. This isn't strictly in line with WPB-3798's requirements + -- but it is close, and maintains the lens template haskell. + declareNamedSchema = genericDeclareNamedSchema $ fromAesonOptions $ defaultOptsDropChar '_' -- TODO: would be nice to add an example here, but that only works for json? diff --git a/libs/wire-api/src/Wire/API/User/Orphans.hs b/libs/wire-api/src/Wire/API/User/Orphans.hs index f2b29ccdf8b..05f49534e4f 100644 --- a/libs/wire-api/src/Wire/API/User/Orphans.hs +++ b/libs/wire-api/src/Wire/API/User/Orphans.hs @@ -21,6 +21,8 @@ module Wire.API.User.Orphans where import Control.Lens +import Data.Aeson qualified as A +import Data.Char import Data.Currency qualified as Currency import Data.ISO3166_CountryCodes import Data.LanguageCodes @@ -51,9 +53,18 @@ instance ToSchema CountryCode -- | The options to use for schema generation. Must match the options used -- for 'ToJSON' instances elsewhere. +-- +-- FUTUREWORK: This should be removed once the saml2-web-sso types are updated to remove their prefixes. +-- FUTUREWORK: Ticket for these changes https://wearezeta.atlassian.net/browse/WPB-3972 +-- Preserve the old prefix semantics for types that are coming from outside of this repo. samlSchemaOptions :: SchemaOptions -samlSchemaOptions = fromAesonOptions deriveJSONOptions +samlSchemaOptions = fromAesonOptions $ deriveJSONOptions {A.fieldLabelModifier = fieldMod . dropPrefix} + where + fieldMod = A.fieldLabelModifier deriveJSONOptions + dropPrefix = dropWhile (not . isUpper) +-- This type comes from a seperate repo, so we're keeping the prefix dropping +-- for the moment. instance ToSchema SAML.XmlText where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions diff --git a/libs/wire-api/src/Wire/API/User/Saml.hs b/libs/wire-api/src/Wire/API/User/Saml.hs index e42fbbf8f63..8ff2e27c954 100644 --- a/libs/wire-api/src/Wire/API/User/Saml.hs +++ b/libs/wire-api/src/Wire/API/User/Saml.hs @@ -56,7 +56,7 @@ type AssId = ID Assertion -- so that the verdict handler can act on it. data VerdictFormat = VerdictFormatWeb - | VerdictFormatMobile {_verdictFormatGrantedURI :: URI, _verdictFormatDeniedURI :: URI} + | VerdictFormatMobile {_formatGrantedURI :: URI, _formatDeniedURI :: URI} deriving (Eq, Show, Generic) makeLenses ''VerdictFormat diff --git a/libs/wire-api/src/Wire/API/Util/Aeson.hs b/libs/wire-api/src/Wire/API/Util/Aeson.hs index b1a28f1fdf1..209d55efb7e 100644 --- a/libs/wire-api/src/Wire/API/Util/Aeson.hs +++ b/libs/wire-api/src/Wire/API/Util/Aeson.hs @@ -17,12 +17,15 @@ module Wire.API.Util.Aeson ( customEncodingOptions, + customEncodingOptionsDropChar, + defaultOptsDropChar, CustomEncoded (..), + CustomEncodedLensable (..), ) where import Data.Aeson -import Data.Char qualified as Char +import Data.Json.Util (toJSONFieldName) import GHC.Generics (Rep) import Imports hiding (All) @@ -31,9 +34,22 @@ import Imports hiding (All) -- -- For example, it converts @_recordFieldLabel@ into @field_label@. customEncodingOptions :: Options -customEncodingOptions = +customEncodingOptions = toJSONFieldName + +-- This is useful for structures that are also creating lenses. +-- If the field name doesn't have a leading underscore then the +-- default `makeLenses` call won't make any lenses. +customEncodingOptionsDropChar :: Char -> Options +customEncodingOptionsDropChar c = + toJSONFieldName + { fieldLabelModifier = fieldLabelModifier toJSONFieldName . dropWhile (c ==) + } + +-- Similar to customEncodingOptionsDropChar, but not doing snake_case +defaultOptsDropChar :: Char -> Options +defaultOptsDropChar c = defaultOptions - { fieldLabelModifier = camelTo2 '_' . dropWhile (not . Char.isUpper) + { fieldLabelModifier = fieldLabelModifier defaultOptions . dropWhile (c ==) } newtype CustomEncoded a = CustomEncoded {unCustomEncoded :: a} @@ -43,3 +59,14 @@ instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (CustomEncoded a) where instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (CustomEncoded a) where parseJSON = fmap CustomEncoded . genericParseJSON @a customEncodingOptions + +-- Similar to CustomEncoded except that it will first strip off leading '_' characters. +-- This is important for records with field names that would otherwise be keywords, like type or data +-- It is also useful if the record has lenses being generated. +newtype CustomEncodedLensable a = CustomEncodedLensable {unCustomEncodedLensable :: a} + +instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (CustomEncodedLensable a) where + toJSON = genericToJSON @a (customEncodingOptionsDropChar '_') . unCustomEncodedLensable + +instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (CustomEncodedLensable a) where + parseJSON = fmap CustomEncodedLensable . genericParseJSON @a (customEncodingOptionsDropChar '_') diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_provider.hs index 36c405ce542..e96883648e2 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_provider.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_provider.hs @@ -27,9 +27,9 @@ import Wire.API.Provider (CompletePasswordReset (..)) testObject_CompletePasswordReset_provider_1 :: CompletePasswordReset testObject_CompletePasswordReset_provider_1 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "Cd9b4n7KaooqOhOciMIf"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "W0CLFxLOL"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "Cd9b4n7KaooqOhOciMIf"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "W0CLFxLOL"))}, + password = plainTextPassword6Unsafe "\1012683\1112273\39028\&5\168679\169133rs\93986\&4wo~\1002561l=\1032023\13042\SI1nt\35892\1050889N\46503>?\"\aT\69782\USgg\\f\SYN\165120#tS\NAK8\DC1C\36700q\r!2d\DC4\189369m\SUB\a\\V'W\\\110825,\r\143398?\ACKx\agVQy9\SI3'h]\78709n0ue\b\1032695?@\ETB1zJ6\NULI\a;DL\ENQ\37006c\92669\US\ETBz\1097017?0\NUL\184657\"A&&\36577E\157691\US7fG\1081322Vpx\DELI'\1102879\DLE\1008567g,\NULH\DC2@+\1085033\1064315\DC4\1091186\STXJ\1103240dPQ\STX|\EOT9^9_\1033902\SO]\a\1022683Of'd\SYN\"^\EOTw\1073515_\1113440\DLE}\95632\DC1s5\161851N1\1078798RkTZ&\150149X\1065364~''v{4MDK\153974\US\SOH|oB\143604'q,HU\1025306\SUB\NUL\1060487+%~v\DEL\97853V|5\127943|\999498\1059223HTFhF\FSdelLB\CAN\SUBbiC\1027783\n\110976u}g!\38540M\141506\1037727Pt$2(W%\149078\&0i-H\SUB@ii\1037533\NAK2\2636hg\50874\28429#{\23697\SO\NUL\146715\f\f\1039241A\GS:\EOT]\99785qf\SOH'\DELx\139534\SYN\f\DLE\nT\149322sK5O\EOT\SYN^&3\SOf!\150976\GS\SYN\f\1112187wy\1052535\1091937\1045148\SYN\ACKijjq\58477&\RS\"\DC2\1063939e\129001\ETX-\\\DC2E\ETX\40256\39310Z\DC3\22084iD7Xv\137008m\SUB>~\CANW\139109\33037YYZE\1022090J|\5247\CAN.\137437p\1011705\ETXS:Y<.YBcP\31609\1107733v4U\f\987772\1070124W!9Z\1035690;\1106506\DLE\132101\SOH(kH\SUB\"\vdX\136713\10837x\154948\&6/b$A\"jH\133538\48869\&9\DC3,\144088\1091851{\DC2\12495&>\1040461" } @@ -37,9 +37,9 @@ testObject_CompletePasswordReset_provider_1 = testObject_CompletePasswordReset_provider_2 :: CompletePasswordReset testObject_CompletePasswordReset_provider_2 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "8XosCtq4Dzhyo=UoMRg_"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "EoNo4PH=cFSyQ-yuHhP"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "8XosCtq4Dzhyo=UoMRg_"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "EoNo4PH=cFSyQ-yuHhP"))}, + password = plainTextPassword6Unsafe "\DC3~d{ \988098\1008471\&7\DLE\NULd\1065586\SOH?NT.\186651\1106270JJ\64065^rd\146603N[\43292\SOHt#dn\142707}u\SO\1022368<\1094323\18349\51616\GS\CANn\n05\983885\&4Z\vIJXz1ia\20698&\SYN'<\162555\v\19677B\ENQ\SI\1049058\DLE1dt\1038032)$\135798\&1b\97041Fvi\36729J\a_T(-`S\NAK\fU\20849dBbTgi\167678\rfp\171973ED=\STX\1086228\SUBXa<*#\1037916<\1106037\191075^%Xx\ESCOM\DEL\994881\1059244X _3\DC4K\GS\a(&6\59167\&8[\1045759\1111435M\681>f]o\ENQ`m\DEL\1112157\1102641\11945\f\161652)Q1\1018093q\1005011\&9\1102348UD]$\41477\f6j\190919\&3jAG\1007534!ys\NAKs?\17249Z\160153cfpz\fGC_\SIf%xb\99796\&1\ESCj\94762\&4K\rQ7\150803:\55009%:\r\"-Zq\DELU|\DLENa>\131324K\131830G\ACK3#\"V\NAK-w\ACK\1081085(\23629\1091792\\H\21182\ENQ\1049732\1036941~M;FHW$X\988437Wy|x5N\CANTrX\US,\n!\51726U==I}\ACK\1067103\1041045\1085401\EOT\983701{ }1\144729yu8_\DC2p\1053610l)S\128946fZ7\ETB>hnRX\458M{U~Hw;\69816\1035492v=J\8990:\1000731\1096086\70367o\ESCs=\NAK\1017016\SOH\NULb\1111472\152433H%f\1040890\EOT" } @@ -47,9 +47,9 @@ testObject_CompletePasswordReset_provider_2 = testObject_CompletePasswordReset_provider_3 :: CompletePasswordReset testObject_CompletePasswordReset_provider_3 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "=aYXtgLJZX77qMIx0Oah"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "RMQ-RtgFDI-b"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "=aYXtgLJZX77qMIx0Oah"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "RMQ-RtgFDI-b"))}, + password = plainTextPassword6Unsafe "\1073786\1022541\1030619|@\DLE\1050256\58722\5028\SOH\25945J\EMkH\986937=\11472\"SP\\\FSw\95016lR[.29\137466\&1W_\64827\96388M\RSU\a!\GS\43687NKv\993525\1097611X\50069;?\157751\&47.\CAN\1103688\137799\186574}\v8\STX3fj\DC1\SI\181630=-3ZmNn10\DC1\997119\1059249\161874CT\NUL:N\"\SYN\\@|q\128174\FSv_u\95666\1080533J-*\1034203;\1068818hC (_u\161608g\43952\33809\NAK\US',m}\a\30792\DC2Dt\171459\152195Him\395|\125271q\161223r\110828\&27A\NAK\EOT\FSgP\1090390\US\993009\62450\1042020O9\EOTEB]\DLE<\156612\127142\133358\1015398rJu\t\1027420\1050082F\bfxm/f\a\rC\152680t~D\ESCO]_i\US\39307\SOH\35670>\SYN\1086602\NAK\STXDz\DC3\1048748ZC\DC1x0bLFjXI\148199\EMZ\GSR2!\ENQ\DC3\\Mffm\986388\1043076\94041F\1096421u\7179*\DLEM.q\33878\a\1106357GdxHmu\DELSTrb`cn\NAK+(@KZ\ENQ]\1034430QEf?fw\ETX\177531.W\STX~k\ENQ\993340\1112261\US\tB\SO-\STX4b\185882o,\CAN}P\SOKD\v\1100259O*\b\1061589\RS\1106367\ACK\NAK=\1048333eh\DLE\EMY\12994\986285\185764\GS\DC1#)v>a\1050729L\DEL\16992&gh1\SO\24688\&18\DC1\1091353(\167196\1031220lc\ACK#\1096547Poe\178761~\ETX[%e\133630{\1020978\&31\99380\45215\SOHI1z\1093633s#y\1048198\FS\8988g\USPE5P\SO/\n\1089996 *Z\DC3\2954\33162p}sh;[Sr\STX\1015744\ESC\tO\152390\STX/_Q^a\157142\1101351\985165y" } @@ -67,9 +67,9 @@ testObject_CompletePasswordReset_provider_4 = testObject_CompletePasswordReset_provider_5 :: CompletePasswordReset testObject_CompletePasswordReset_provider_5 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "OU56F44t-0ybJj7eKUaS"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "rr3lleg-Tu4eJ"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "OU56F44t-0ybJj7eKUaS"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "rr3lleg-Tu4eJ"))}, + password = plainTextPassword6Unsafe "k\1044075Pnu'6Z\NAK\1017783\149108\ENQ\129297l\18438[\1054432TMgddIb\186517mt.TCQW\1025717O\1111819M\ETX\27672\ETX\ETB\1083603\1091383F\RS^\182596C\SOH<\rs\f#\STX?A\n\170555\68821\t 88|;\SUB\1015442&\n\1042330'\1003626\151074 <\63465\v\EOT\1043258w\1012648\DC3l\62396\FS2)\SYN\1003311o4G\161486\&1;0IVKt6t$Y\":\13086\156982\1055032\"\GS\6275$y\ESC\15469)#\1011445H\SUB \SYNLk|\DLE$\GSh;\19798G(?ft*V%|\9608\bC\b,\131877\SYN\7628eI?:T1\ENQ2\1042416B+\STX\\\GS>4\1042921\1015196\DEL\1050654\ENQ\RSdH\NAK\SI\vK\NUL\1020294\a\b:9\163015\&3\53363%^[X\r:\1044970c\n\1035333kk'RA\78616\1054694\24158\1051573c\RS!\167908\28730\ENQ\SI\1068557\r/\SUB\1106472\&1ott&\SOK" } @@ -77,9 +77,9 @@ testObject_CompletePasswordReset_provider_5 = testObject_CompletePasswordReset_provider_6 :: CompletePasswordReset testObject_CompletePasswordReset_provider_6 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "54Yh4fa_ClTVqjEEubnW"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "54Yh4fa_ClTVqjEEubnW"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))}, + password = plainTextPassword6Unsafe "\1068965Mz\1112587\\b\988910\33388\1081682\FSSi8:\"\r3\GSc\989625I=8L>uA'\SI&I\94104!W\995368\&7z;r\ENQnj_+3u/8\31470{\32573\170260\EM$vy\rB)\125105l\58284\1022117'iN8\SO}vd\1025869\132023uw\996610\&17\ETBF#\154217:s\1019264\EOT\CAN\12331\127284p$\53580\&2\14658\DLE\13233\SUB\59635Hl\25906\SOHw\1054216\&4[\171724\DC1\RS\SO!lS\EM\1073106\66443\\(\47504\61628N\1029483M\NUL\"\SOHd\1088943 \58859U?\31664d\138217(o\RS'\47111\v\1097785{A\ETBb=\1039402\1096760?o\n\164402*\12095P\SO84,Qf\1065714D\EMZ\SOHux\1096460<\v)\1109779\185595\25160\69876\&8t\136448Ya\GS\ENQ\9575\NUL`\US7\1022950p\1032880\&42\32304h\68036\EOT+W\a\1022685aH+XE\1016645p\SUB\8531\n\DLE\136210\1080841\1069380\119885\t\31849k\1020979\159730\RS\99244\1100479\14782G\nh\168920\SUB\DC4{\1107942\&5,\US\DC2L\DC1(\137496<|\bZ\172359\SIK\EM7\t2V|K\ETX,\SYN)F\50452\20991\100678\1098846\1109927\tJ\SYN)\133930" } @@ -87,9 +87,9 @@ testObject_CompletePasswordReset_provider_6 = testObject_CompletePasswordReset_provider_7 :: CompletePasswordReset testObject_CompletePasswordReset_provider_7 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "muTkNflRkN4ZV2Tsx=ZS"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "X-ySKT"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "muTkNflRkN4ZV2Tsx=ZS"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "X-ySKT"))}, + password = plainTextPassword6Unsafe ")jtk/z\184222F!N~\ETX\990448\1055900{8\73979\153166!D\1043025%\135850\168364u7WynrV\ETB\148520p\1077327Lt\842e^}?\1093891l`.`Y\vZ\STX\1112581P}[~\30935=}L\1095875\a\v!\1028719\ETBH)>5\ETX{\NAKD\ETXUEh^ ~\EOTCC\ETX\SO\16392p\38296z3jt\NAK\984409\bB7 P\CANSu_\183789o\17912\DC2\178168I\v`,\1022887N8\\\DC1^\10311m\CAN\1030400\FSZ_\"$\ETBB/\NUL!\SI[\DC3\vy\f\ENQ\ESC\137923OC\SIt\12293:\EOTl\\\b\EOTrG@\US\45550J\95310\166637-\10023\&8tTT#MD\FS\DC4lJQ9s\64189\25142\DC1jlVF\96794P{\5228\25037\NAKKEC\1098620[kg2*C\991918\NUL[\35874&\74062\188051?\182094\&8\145055\rSYlf\95342q\30892\94613\NULM.\b\t\1102963\1018631;\DC3_\1029835|@\SYNd\1082087)\n$an\SI\RSp\n=\1013045D+\97624\f\1106118\988197\1113\GSb\181818\SI\1091492YQx]\1063062c\18044\993702\148181\1072483\1042478J:\ESC\RS\1052622\186566<>\EOT\DC1\FS,\1076029i4@\ENQu^\178972\1082722Dd\63135\1006290\EOT\66041>Tx\1091471#u\\`\STX\1093786,Kt-\1035926D\1024804\154425,I.\190722:\15722&3n\v!\40042Pm\41694$\n\SOH\183103\75035\1093394\3121>ihpLGl@L\DEL\ENQ\ETB\182031\SOH \21434\SI)D` wC\STX\v\ENQ`\54406}$\39750\DLE[\"\1087944'q\1043619tP\EOT%\ENQeG\r\1058468\1110447C\DC3g\1038268#\FSYrht\164459@\1085349tMo\ACKWM\SUB\v\40317o&}~45\160190\&4K\1104579\CANl!x\167229k\ESC\\h\ENQ/4,\177887Yp\995759d\98258N\1108317vw\ESCK\1098528\FS\ETBRSf0\DLE\148633\93011*Wukxd3>\ACK'gN\1044418\DC38;2FN\747 '\1005699Yt<\1105770\21737\1045228\DC3]\13220\ETX@\f\1101655\42506f9i.\1005751\&5\n\131677\&2%$\1047618N\169552Y~47\986154\SO\1007292\1001379\31676\&3\1056996le\1059155\&4\DLE1Q\FS\986744#5?\73770\1092436\1011458\171368\167096\&4l<\1069261H7]=\DC1a\62925od\1064417A\GS:l\SI4q^b\1057856D\173253\1059916$b _oH'\DC1Kv\\n<-\t\US\1083436\163231\ESC\1098850F\1329\STX-\ENQ,\CANG$\NUL\38340.\1107219;\125009\169728\167O\ENQH\1018301%\ACK\1025545\1011306j\RS\994143\1094533mEB\120644\1031761A\20411\180256YN\STXFRm\US\ETXQ\1072397V`+\95270m\SYN1\1013314\b\1024313\&1}O\1108229\1002097\49175\f\1007287j$t\47188\&4!8%#v\f=\t<\49120\61960\ACKM\1056844\SUB3\"\r\989243\SUBX%~n+:\NULM\134421X\DEL-v\72197\f\ETB\996041\EOT\DLE07\1009115\CANU},,}\141362\bHy\fLa\\\n\64444\983949;jo0\157407\1061450\1041761\EOTMlW\DLE7\45112\1113654\984581B\1087787Z \1067937/\1027501R5F]X\ENQF|`\162826E\128973\r\v\984688c\1100696\1074387T\1041206\SO*(\RS\ACKbNs\1056623ST\139333\170914K\1032627?\SOH\1095798\1006647\13962\"S[TY};\SOH*r55\aT\1006364\SYN\SOH\1111555\1082650\RSZ\a\1020940s\162901t\1055866}\1055756deI\153662\46739\rR\\]'\1084483\1056412\\y\135616\FS)@o\30437Ci\1081016\1042881|[Q}}\1025142\SOH^\1085438|S\EOTWa\nE\DEL,\1014498S\DLEq\DC3s\"h\36770)\1084960\RSB:" } @@ -97,9 +97,9 @@ testObject_CompletePasswordReset_provider_7 = testObject_CompletePasswordReset_provider_8 :: CompletePasswordReset testObject_CompletePasswordReset_provider_8 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "4h1kCFffI4sHePSIIfS1"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "jgfbzV60"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "4h1kCFffI4sHePSIIfS1"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "jgfbzV60"))}, + password = plainTextPassword6Unsafe "[_.VrDh\1015708\1032560\&3\DC2=M\163597rhfOlZuP\65504\DC1\SUB\f\rx\FSJ5\f\DEL\181294si\166877{P\CAN\GSG3%'O\a\f\RSa\1092468x:\1053642\61514\1073484+\39638fMP\1054011B\\nu\\\SI:a4\15010qI\n{\1029779\NAK\1041484(r\44941EI\13466G\141832>\FS\1022348\EM@*y\US4{,\ETB\151574p\ACK\1107549R\1055583H,\DC1\v\US\1009911C!\SYN\1027699i}2\1006393\1013086pu\t?4\ETB\35803\44095\NUL\t7&\f\94064\993295\1068521\1077762\t&\ab\160257'\NULM:\29880oI\DC3\ENQtG\DC3`/0\RS\166279v\b_c}m\UST7;he\155120#\99948\1018238\1062963S4K\EMR;\ETB\US\ENQ\1021792\STX\1003450:\24440\DEL\EOT|p^ZN\30349&WtKz(S&M\SO`\SO\181996#\1011887C:^\ETB\147530f\EM\a3jp/\1058108|p\SYN/9?Wn\13780\RSH\ENQ*\168131\1075215\119182gh\2225\1089941T1\133460\77864\1037953=\986510\1004229&1Z[\1043805\1002639\&4U\DC4\998270K%\DEL2\USp&q\1055724o3QhHE:}\ENQhil\1096277fc\f\SYN1U\ACKTK\DC2\173882!4Ch>f\DEL\SYNV\49106QcXO3\t\SYN1\185658\147541ii5;?\ACK\1023746\994599W\63325\DC2\45506yDu\132949\140075\1007168\"\EOTVsg\1088989`\1042945:\38432'\STXE\992832\SYNJ\ETX\64654\DEL\RS\rV)6K\1001241u\n\1061707\ESCWq4k'xZ\CAN\1004671Pp`\78706\DC3s\vb'\1026286\DLE\51253\49630.v\1078713W2u*\1026823\f\rc;=l2.\135778\1067475\66363'AT\1038064\20692mc\ESC\DC3?Y\EM\1043502erF?lU\177756\SYN2\137736ZW\SYNe}\110678i\r8\1045526%\DLE\1060820Wu\ESCwr\SYNZ\984526\DC1\DC4*F\1025876j\4244\NAK\69844\SI&\24155t?\SYN:\996677\EOT\1096939\\d\ESC\rV\1048902\DLEY\SOH\DELHDi#'#\SO3\DLE\1033528\1066728hP'\SI>,#;B-\DEL\ETX\FS\b\1080220\\O\173118\155899\33548\161628r\DC3v\1036063\NAKwY>@P{&\126581muC\30489\DLE\RSW\DC3bzp#\SINO\ng.f\SOH8\1044888\USM3\STX9M#\31452A,S\144295\DLEiK\ACKi5\DC2\1106504\163392\&9\DEL3~\SUB;z\37537H\SOn\74309\1097966\22046h\SOHH\SO\1014941rSW!\1076838\1019303\ENQ,Texo\1103981\\U\60688\1107601ef~\NAKA\CAN\1095090\b1\FSiW\EM:i\1063110\100555\1028434\f@\45876^20\EMn!\1110881\ETB'\t`\"^" } @@ -107,9 +107,9 @@ testObject_CompletePasswordReset_provider_8 = testObject_CompletePasswordReset_provider_9 :: CompletePasswordReset testObject_CompletePasswordReset_provider_9 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "8QW8mjnVnIisvrtQDzWV"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "qXaaBJ"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "8QW8mjnVnIisvrtQDzWV"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "qXaaBJ"))}, + password = plainTextPassword6Unsafe "7\CAN\995057\1082858>#\149981T\1113543e\thUr\189434/\186737o%\DC4\SI\8198o6n8\20176c\1043600[C\1057789\&72@t;6t\169068\11814\120655\DC1\EOT\1079958\v\aS\SOH\EMey\ACK:\aii>\1079059u-<\1112894\1083324\SYN[#b$<\r\1056477\1033082\1105819\ETB!eWg\991833d\DLE\CAN\ETX @\SI\185824\&7\b(\40642\&3\NUL\1110157!X\FSe,t\ETX\1095428\&3\128629\1025661p\1000552\184281\184297l\25688V]\1068327v\152194MF\v*\1050101\1065061 \ESCT\SUB-\21105\&0>`{|bal\1060553\ESC\US\GS|\ACK\1028192\DEL\DELV\143705dq'\DEL6mCCjv&\1015677\DC4n9\1022140My[ K0p\\`6r\182750\1080218\DC1$|#\137636H\DC2?0{%`\STX\1005371k!\RSIg\SOH\SYNJ\FS{9b\1059876/4@\1060707ldKAH\ETX'8\180338\178999\1013270O\1075685Dko\23121\&04%/N9B\1003052aW*\1070751\1043722w8\SYN\RSr=\EMnX\1071326]\NUL\GS\1082718\139251\1079728\DC2EfW\t\SYN&G\196\&2\1008326b\1023329\1102771\1047159\&5[f\NAK\100090J/7\26364\t4\SOHS\CAN;7\185137R<;`L\1112382\1022626\&1?yCIiS\153111\GS\FS\EM\ESC\156314LH\140232\\:K\1002577MP}q\139293J\ETX\151699\1052232\1108510\NUL+X\1029314\181545D}-!EF\SOHE|\131183\&6\39841\1062330\21504\SOH<*x\179748\1015132k\DC1\DC3\98575\ETB\EOT|\SI~gr\DEL\2694YyEY)Z\155604&\DC4\997375\1004619\36183\151489\143359\29364\DC3P0R(|\1044843(%Y4\1044821?3\ENQ\v\ACKU\988376\30638Y\f0L\b\986153\STX\997297,\ru['" } @@ -117,17 +117,17 @@ testObject_CompletePasswordReset_provider_9 = testObject_CompletePasswordReset_provider_10 :: CompletePasswordReset testObject_CompletePasswordReset_provider_10 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "Myzdj2g7NTl0ppCPXiN1"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "BBwqW3"))}, - cpwrPassword = plainTextPassword6Unsafe "\ESC.\63992\SYN\128619\1086386\&0EI\50894\1058818A\ny\65231\1092012~\CAN;p" + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "Myzdj2g7NTl0ppCPXiN1"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "BBwqW3"))}, + password = plainTextPassword6Unsafe "\ESC.\63992\SYN\128619\1086386\&0EI\50894\1058818A\ny\65231\1092012~\CAN;p" } testObject_CompletePasswordReset_provider_11 :: CompletePasswordReset testObject_CompletePasswordReset_provider_11 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "nQSYG43lVn8kYS-MPtOO"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "5BuwQHalK"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "nQSYG43lVn8kYS-MPtOO"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "5BuwQHalK"))}, + password = plainTextPassword6Unsafe "\1045282k\1026750)t\NUL\15552jgI\ETBP\SO\188738\147525\1066604G\39626jb\f`\nTq+Ut\92361\27743UBpVU \992919f9\21139\1020059\&1hYp9Ja\147132EBN\DC3t@\1079146i;P\1042445\180008\&8a\1091375T\158952V\138448\SI\18953fx\11087d:\SO\\\1054972?b\NAKKtz\GS\1104407\39067\n\1074206\&3\SOH\1025715\r87\ENQ9@\5471Y\ESC\62699\11493O\1045551\ETB\10550\1037708$Fph\US9\ETBe\fC\20273%>\USP@\STXo\34112h*\1042645\1104430\987562E\43000\11020\32229Ft{A=\38646#k\SYN\185887Fi\99911>&oy\98658\f_\1099272YIL\65827\&2\184583\1063350v3\RS\DC4\27853T\141265S\1048343\NAKK\150089\&1Y\1059308\NULk\DLEy\1067797\162645\92680B\78890 of ," } @@ -135,9 +135,9 @@ testObject_CompletePasswordReset_provider_11 = testObject_CompletePasswordReset_provider_12 :: CompletePasswordReset testObject_CompletePasswordReset_provider_12 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "NeuDtdyLCvq11nGkkEal"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "sj64oWB"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "NeuDtdyLCvq11nGkkEal"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "sj64oWB"))}, + password = plainTextPassword6Unsafe "an-<\r\ESCa$\n\SOH\150355@, daTW\1040876\1086641\1008932O\a\173984\1089573\187195E=\1033471\v\142301l\ESC\ACK\ACK\67616g>h(;\983436\ENQN:d\6803R\SYN$<;\18099:@\fWrO\119365\\\SOHE\NULX\SIL&Qc\143803\ACKKx+K\FSF\US\1018415\n:\\\SOH~a\147255\78868\RS\DC3Jr0c\DEL5\1004711\&88\1048447(m\1018537e-cWRQ`N\1091454\127355+i\DEL?\DC1\172339(\1079229\1021542\1023479\1095290\NULzNO\"h\61187q\NAK%\148825|\8495A\171333>\1023153\NUL*\144781\b\1096599\&6\ty\1084884\1106372\&9\991761\&4\54666\993909\&69\188610\78768/\EM\3120\SYNX\160680\1093419e\1101140!(|\EOT\180765C\15108\GSj\73803\t@\rsQ{ZZ-\22170\ETB_\EM&6#\bsD\v\n\td\74406D\37637\&9\72882\1015558\&3\NAKN\1028309Fnk\ENQ&\EOT3Q&\28043Ys\97711H\181981\999099\46018\&5VB\1044294I+\1104448\61690\&7f\12643\133501]\r '\163623\SYNmbE\1015369\n2?HK@\DLE\DC3\DC3\1023424Z(\DC4\SO\DLEk{r6%*\1034286\EOTM=/\STX\1035914&4\1098394\b)TI}\998716[+2\EMV\SOH0h\v\18412@\bQZG\GS_\DEL\999345Cim\DC4)m\1021546\RSP\23785zv\50314\1005770mi\DC1\100847\1042938\USC-zT\SIQY^;f\NUL\ESC\30038N\93068\SOH\DLET\1038908I\DC1\187625n\DC3\CAN\\\r&<\SOH5\71118\1027153X\1092148mF#h/{\DLE!(\16202\&3\ESC\32283\185971[h}I\1071533:\183293R/\1004445\140257#\"\1028937\v\177329\61790_m\138219(\SOH%^\1105873[\1035020RF\CAN\1054790\24076\DC1\FS\NUL\72138\STX\ACKd\ETX>#qn\SYNw=}\1001530\177147&^\NUL$BP%5\3450\179283\DLE\ETB\SYN{y\34999\1114051\NAK:\17208na\133899\1014430c\1106626NDB\160028\15282u\43902Xpr*#\172705\a\SUB\188880\1026535F$\ENQ2\ETBB?Q'asS\ESC\96583\DLE\DLE\1012383e?\f\STXT\1096814Q{\DC3R\CAN\1065288$\1074134j\SI\135241\&3\DEL\1035586\1073529\43493\&9ecd2\ETX\139431@Pvv\123147\157284q\r\1091419\1052105\23426\185829\1098874,[a%\1087411\"RLOU\31476V\1060394K\NAK\EOT\180111S\"Wes\ACKH415\78735-S\SUB\DC4h:d\1036393NZ\t\1043380m\167051i& \1107753`dP4/\DC4Q\ETX\54045B%\186624\&0;Nb1\DC1\EOT9\SO.\1014579\187014q\ESC\1078099f\ETX\64604H\1060225\vY\RS\1045658\DLEC\179470\a\NAKWw\ENQ\1035817[3^3B\154130\"_\nPK\1076894{\ACK\ETXO\DLEr\SIvc=+af\SO\ACK\1101910\167540\STX@\GSQ\1011496s\ETB^c\CANwJY$\1107843s\DC1Gs\1049240\DC4\NAK\171080k\US\ETB|\1065322\EM\1035477tJ(\1075051\1687xc\b\1056830q.\34099\&7\NAKF\1023165\DC3C\a\172318S][\DC1:\ACK\26422qL\1039209\&2\EM\44805\ETX?NG|x\1065136>/Iz\1061649ms\US\SI\1005398\131153\159667\&3\NAK\1048772\997425nd\tv4\DC2\1080172\1101786\v1Iw\1050069\v}?1m^\STX5#V\147028\1063172w\EOT#\1030144\145884\f\DC2\131840\6065\FS8O\NULS\ESC\1033971X8N\142482\1041006\59926.\ETX\163181\ACK\DC2\RS2\GSr\EMV\nK\NUL\DC1\1019014\30036_W\61065\9477\SOH\1094473/\392\20690\159848\181387\EM\vGDR\188046\SOH2G0{\FS\1084240JX)\188982SE\176663B\1089777\US\132402&\SYNg\"\DEL\1902UCP\1054969\1106547\1106033YU\EOTi+,?\147075\1044086\1028895\1110977\1016778\1106548\DC21\186874\1095378L\1030254\997653\998721\SYNc'\ESCp[\STX\EOTN\ETB8^\1021121Bk\b\t[" } @@ -155,9 +155,9 @@ testObject_CompletePasswordReset_provider_13 = testObject_CompletePasswordReset_provider_14 :: CompletePasswordReset testObject_CompletePasswordReset_provider_14 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "4hqO6D9=V3BKXLXcLie2"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "emsaYZVuPvQ1U"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "4hqO6D9=V3BKXLXcLie2"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "emsaYZVuPvQ1U"))}, + password = plainTextPassword6Unsafe "\1028432Q~\7949F+9Dc7\1026106jl\SIC?xdB\ENQx\33993\62067\ETX\DEL\GSj/^#NS}fiO\119558At>\GSh0U\62526`\r\aV\US\1112085_v\33980w\ENQ\184054\&8\11831\1032958}e\ETXi\NULRC-- \37583Xd\ENQ\an/,?\SUB\SUB\1066224\42328gQ`\70388\41959\1012806Q\US\ETB\184603&LR\149821>\1012033tR\DELg" } @@ -165,9 +165,9 @@ testObject_CompletePasswordReset_provider_14 = testObject_CompletePasswordReset_provider_15 :: CompletePasswordReset testObject_CompletePasswordReset_provider_15 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "bbFHebGp6h_3F4QpSrud"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "K_xVBcX5bLpjvL"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "bbFHebGp6h_3F4QpSrud"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "K_xVBcX5bLpjvL"))}, + password = plainTextPassword6Unsafe "\ETB\26894'\fd3E\120233\1029573\1064918Y\r\1104541H~4aF\16111&&\US\1085044\1086081Kt\2880XoS\r\DLE\NUL\1044509\v\983386q\182823\1075779\148618N=\1062701\1004214R.\\\t6!E\164333\GS_$F\STX\DC4l#!\ETB\DC3+\1078110P_\1037691\GS\1003847~HhsY\1008817\CAN'\996168wy\ESCA\1096877\&2\170187\1060412\SOHO\SUB\ETBc\t\1090646ud\1037884\CAN~\173115\1096337\rB7\1049690o\DLE\1095190fL\996695\ETXi_=\a\SYNLE}<\1106966\aEl\49881\v.6H\CANw\995916ZH\176178\49327\19051o1?\61005\1065006!W\DLEY@!\1058199S`mq\15087\161424\167582\a8\127764~rL\41008\171779\DC3[\989714K\SO\ETB\152791$jRxH\SYNm\1076533\DLE\169669'\EM/xd\52526\95412\&8\ESC\38505\&6bYZ%\139602\20809\35764~\72852SG\1075777`0pL.\185639\&4f`7\DC3\1113337*\v\60813/T\180136C\1111167Z\SUBZ\44799\DC1@\\\1060472\&2\EOT\11121K\1039363||\DC1h]3&\DLEY]\GSk \EOT\SOHU\161853.\DLEk_\133547O\1041592h\1083420{\64532\aw\ETX3K\41041l\1069560\a=\EOT\152591\"\fyH\78163\SOH/L09\149680\DLEvw:\ETBO\1066598%#(Js\169845'9s_&9\32941m\149591$\1021728N\984156WE\DC4=y$=3\1083024\21817<\t\th\1087258\NAKx/\999799V)\STX\183098\1073874BI)\\gk2I]#l\NAKPn$\172450\ETX&\DC1;\GSY\EMO\180851\ETB\1095722d\ESC6\151131}$\175277\NAKajf\1093922v\184717 \ACKa!\v\166519\EOTS\1012345'\153953j\1098235\EOT\FSE\1061729\1052832#5Zg=\172012\4883\66029ZU\36791\22747&C\STXZh,\1088719\7021\1087041\ENQxC\987916\39597=l,\fu\998370\ETX\60675 }&\183212\989435\165094\1040277\135097\"\v\SId\US\EMJ\59927'6WK\13266\ACK>\70807\995567y\r\"\98652,\DC4\SUBd\NULne+\64011,&!9Y\15584T\127281<\1077668d\31074e.#w|?\1034255G\1027753S1\24647\\\1090505\bz3\DC4,\988313<\\\1073727\DC3\1032879\997224%-\64532\EOTC\ESC!2\156292\145116DT \f\SIXja\\R\1014521\SYN`\CANJ\n\45882\1023562\SO\13921ab\NAK d0\SYNX{:\51467\CAN?\187194\&1txQ]\1005159?\176303[)\78300\&1O[Xl#\DLE\38014\50691~g\1043081{\132217_g\ESC!\SOH't\1101558I\1003044\1063761?\137915`Nd\182690`*1rD25c\169907owK\20714(\1055173\&6i&j(U6p\1104351zK\73918mE\11375\vjr:\43447`\1094897w\SOH\SYN^3\DC3<**`j\f " } @@ -195,9 +195,9 @@ testObject_CompletePasswordReset_provider_17 = testObject_CompletePasswordReset_provider_18 :: CompletePasswordReset testObject_CompletePasswordReset_provider_18 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "m3ruXwhym9ERHyTAJo1y"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "88xU9QOF1FPXdL6e4"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "m3ruXwhym9ERHyTAJo1y"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "88xU9QOF1FPXdL6e4"))}, + password = plainTextPassword6Unsafe "cW\ac,#\12631\n}\188543\EOT\NUL`v\STX\48639\SYNO^\1061062\1045730\1096180\tUs\EM\SOH[b[uz\DC1\1106362\DC3-\995083\1054859+]\GS90\DC3X\GS\v>8\154400\78507\t!\SYN\24475\&2 \RS\fW?\SO\EM\17276>\\S\ACK@Qt~8`T\1073440\97220a,#\1054560]X\1019169-\1112078]\1005234\SYN}\1112649T\999008\1062688\rH\171818d@9/pab%\52983**pK4q$\984140+e\NAK\USZ\DC3\29392\&3\176130\1073376(\ENQy\\(5$'|\2800!_\154876_\1073420\&2\SYN\996743\187317I\DC4Y\"B\1049376=\1103936\&6\66599v\153333\NUL\137084\119859\147584'\16885GJe\FS\SIPk\NAK\ENQ#\29575\23580\SIa>m.\161669to\SO\1049661_\v\165212\FS\ETB\ESC\trn\1029796\1078206\61317\rM\149956\&2S8\a35\\\ETBo\191128\DC4\58762~?\178576\STX/\nNZSjbWH\r\1098678X\993718\4120/\1046632y}#\63730\ACK.Voi9\50993\f*Y\STX\1001056\43180\DC1\rS\1021396\144641qSj\17576X\149262\1081745\1076445M\1063531\22347\CAN\45875\40887B\NAK)U\"~V\1036888\1007909/S\61542y\SOH<\b\ENQ^\SOY\1013585;\SYN-RKy\ESCO\1033537[;\b\1094937EDjW\997383\182740bKx\128165lZ\DC3J(\1032322\&1mK\DEL\69897z\131148\144121i/RxiQ\1085090}E\23345pA\1065790q\v.\v\ng\20319Gb\46475Kt#\USP@8s0\vg[c\169328\DC3\US}{/\1002448D8\170376\159999\987435\67200\1053165M\1079934\1073683\DC10i\159626\1111106\ESC\"\1019962\SYNg\1025072M\1022474\1059584IsD\b\1086244\70682Z\1015255g\DC2;\SOH\1009422cQ0f\STX]0p<\1065421.j\DC3\r[W^rsM\fU\65479=h\1059093L94\993336oNs\1016719Z;8\30468lw\t;S\GS`V\\f\993287\1001923\49875\1018016\1032042X\ESC\NAK\132703sb\SI\1110714C\ETXi_7\138308\NAK\35645}\12913\100683go\FSVj\vtr\SYN\181280\166083;\137762\161816\EM6\1068253\1058678/\n\71064\fp\1036795O\SUB\45835>S1=\DC4>m=Li]y\1014422\r\US\5961+D\230\54691UWo@\1104594n/\EOT\FSDR\1084131\&4\CAN`-}/\v=<\DC1\1011393\DLE\SYN\1000229oB8\1073774\fT\185994?\DLE5lJ\917988\1051232\993358\&1\\\SYNGx\160450\993275HF\988493\1096467N&\DC3A\1078985\ETB\1085595\71193@\a\SON\ETB7\RS8kT\13512\SOH\128792}!`].7C\ACK\EMa\991996\SOH\ESCR\\Iw/y\1052927\162141;*!\SUB;)\1034215\DC3\GSjQ)\98905\1083130 \aQJf\143466\3112\1088669q\183516D\47434Z\r\1051585\1066298\1011799\DEL\31175\1077158\19157\f}\1074960\CAN{\1026108\ACK\165269\989993\1021383\4839\993646_9\FSYzI=JL0]\45720/W\NAKD\ENQ\143508WJ\CAN\DLE8\EOT2JsPn\1025590\20415\bhB\DC1\1537Xj\ESC\GS%:p\64920@gL\ETB\1087542\145056\1111605Q" } @@ -205,9 +205,9 @@ testObject_CompletePasswordReset_provider_18 = testObject_CompletePasswordReset_provider_19 :: CompletePasswordReset testObject_CompletePasswordReset_provider_19 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "nBmwchpz6q_fDPCPZYQe"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "AHGkmRBXJr="))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "nBmwchpz6q_fDPCPZYQe"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "AHGkmRBXJr="))}, + password = plainTextPassword6Unsafe "o\58515ZHN\83385\a%S\1039235I\1072272\&0\DC4!\1105051\&0\DC4\FSc|\SYNo\r\141433m\DC4\1017077&\ETBX\GS\101008sXzlOS\1086195nZ\"\DC29\30572S\n\1028791\1110650\SUB1>\155164*K\63527\917991\34537\"\152219\174017GW\165251\917540PPO\1099839\983424\&9c\1001124U{fLy\SOH\FS\STX|\35808\&52\SOHP\n \126255/\42693\188778s^_\1012451MW7M\EOTs\DC2M\RS9\ENQg\1084863\61924@%o\DC2\DC16m&B\95458\190903wL\1066100o\r\1082662y\ETX\tk;>\1108088\1053265A\US9\4469\STX%\44556\SUBghb\1046982\DEL\b%0\1011473\16374'kz\61155\126123\NAK\1040333\&3:<\1028617\12709\1085958X[g77\59354_\ETX\1018780\1031200\&7\STX\GS\163106\60867\n\f\129490\993680iG\181984\58073\168758\44094`i\49314r\65104\GSu\1030407b\1002850\1053366O)\1042687YQ\190781\DEL=fZa)T0j\1070016K/\1104693%v\100085qsY\1017025R\33451\997088\&3a\45926\r\DEL\ESC\1031881\NAK\35199\183615'a\11657B\111310\STX=\RS>n\1055557)d\US\FS.F\1111038M\133759\1021129\&3x\ETX\51747\1102182\11790Z\35206\&3\173723P\RSV3GeGN\v3\28136a^k`\29343\22637\rK\SYN\NAKe(\GSQ%b\1080735u\DLE\ACK\NUL\99272\1095099y{\61538\&9IbP\SI% \CAN^\1064103+f\144228\59518r\18266G\DC2?y\DC3\1073829\RS8\34346}r\45176|y,\b\1026988\145851/\DC1R\1017813\&0W\988979N\NUL\35979q\bc\1091121\NUL\1087940\GS,d\CAN{J8[\1031817\CAN\r\138592\EM|\nz\28160G-{\SI(1\47823\GSl1\18854iL\77903j^\DLE4\ETX\159954\1105693\83316\FSoj\ESC2z\STX\1021083t\17703;K\STX\DEL4Yhr\STX\987287fO6h\158330t\1076871\&3Tpef\SI\ETB\1109588jC\150352eh\10328nf" } @@ -215,9 +215,9 @@ testObject_CompletePasswordReset_provider_19 = testObject_CompletePasswordReset_provider_20 :: CompletePasswordReset testObject_CompletePasswordReset_provider_20 = CompletePasswordReset - { cpwrKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "n7oUiCMAvjokyCwCwIZx"))}, - cpwrCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "padtz-lbyFICM8PEzCj"))}, - cpwrPassword = + { key = Key {asciiKey = unsafeRange (fromRight undefined (validate "n7oUiCMAvjokyCwCwIZx"))}, + code = Value {asciiValue = unsafeRange (fromRight undefined (validate "padtz-lbyFICM8PEzCj"))}, + password = plainTextPassword6Unsafe "n.\30576`Ab}1x?dyrWEI\SO\b|\r n\\\174375\163710yzk\CAN\1032873\13076q\1104973h\1078766\1065080\1073163\1024180\SYNzt\1041454oY\SIv\1109814\999708!\DC4b\r\ETB\139978\1037986\&97|\68831Is\78227\48210\984397\1108736R\1076978y*\ESC \1080452\6350;\1002645*F\ETB|l0\\-\49792/d,76W\SOH\1091954a\52507\ESCkY6\EOTe*6\1068076\1010489\STX\1109890%B\"\DC1X\145857Z\1107907\988601\SOH\r\983601\ETB\1027493's\\g\SI_=\187494\52638\139634oOcZ\EM ZWd\EM\STXe\25610Zz\1055806\1023881Me\25012\DC2N\1061919o\154179BmCD\ESC\146744\165530Wsw<\ETXs\SO\992482\11825,\NAK\97960\ACK\1112588je1m\1080113\&5\CAN@\SI)(\39581y%~d\1022649\&0z1||L{1\EOT\1083342Ja\74536\EOThHi\NAK\1033200\SOH\CAN\ACK\175120\183861\DC34&q\GS>\SOH\184139\SOH\ESCQs\41951a\59763\1069217bv[u\bX\1078841\1048633^\1015710;[\STX\DC3\1001312jYw\1003565\1077047\te3\148232+\7427\\\SUBq\1108026r$(zD)\\7p\"\168984\STX\59311?\8657<\NUL\1035836cP\194909[>\USm2Y\1010432\1106430\21518P\DC1.\1074512\35480\ACK\EM\SOH1npVW\SUB2+\ESC\1059649\33997\GSk\v \993759A)*uk\1030453L0\1078688\1044139\DC2\1029875\DLEn\EM$\1054292?\NUL\vji4Y\DC1\EM\1027716=/S\1024040`P.\ENQ\SI\GS\1090161\50097mww\61962\59664e\994460\1030466\f\83226f\"\CAN{X)\v\4796\SYN\STX\119946\DEL\992301+\39597jv^\169149\SUB%C\"]v5?\185720/M\991044\1010224\1027231\984290+\SUB+\186874VG\SOH2\1003544VM\SI\f2'\1009297\1059762?Lx\986666<,Q\1009359t?\1067784\18910\GS\CAN-\1090445C\2603\1004458\10478XZ\STXo\1019324\by\985769,\46054'\a\21265\DLE\SIH\1003281$J\US\1051584\ETB\r6\ACK\FS_1\45810\1013879\998189\167043\DC2>\1082944<0\47209G,T\1055523\12871\1057078:>4\1005909\1060368\GS\FSED\DC2:\rzSMQw)P\50826s\1051230^-~\95981,v\DEL,\SOH1=/GNsO\129350\&6\GS\16013_4\62900\1097318!\SOH'M\139907+$\29092\154621<~E\96994, U8\ETX\986557#\1092210[\1042274.H\DLE\1098681\ACK\1062248\"\133455?I\1005507e\167230\t\28751\1016604\159825\GS'\160639\&9k\EOTZPj\1084498\1039215O\131535L@\171949\r%2>M<\n\120995\1031232\&9/\985482C\SOHav\142062\ENQ-|\ESC)\b$\"\DC26\16379\CANCT|Ut\131524\149842\96725X\64829\v\28384\DEL'yR\1022028\1056329r\25908o\165079\1077144.\185928\&8\NUL;\NAK5\ENQ\ETB8Y\DC4g\1101865Q\1085552\150701\&7!HO;\br\1026135C\24186\37827\SO\ACK\165967E\r\DC2\DC4l3\1090105\127078\DC4\SYN\bUF\15427\DC3.wO\EOThm\164680L]y&\1024985\&0\308;Nwyw\61385#\r8Om@x\1007233\b\ACKo823U\146708C\SIMN6t\DC28\1047608\SOF\ETXRna\a~\r6\fE\US#\tn\1006471wr'B\rnlolj\1017148\144338\1087477tT\119355\1044444\SYN|4G}\SYNEn\1000211\&0D\DLE\SOjn}`0\994578+\1019070\184767" } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs index 84ce0c6a946..8673f2ba821 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs @@ -21,12 +21,12 @@ import Wire.API.Provider (EmailUpdate (..)) import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) testObject_EmailUpdate_provider_1 :: EmailUpdate -testObject_EmailUpdate_provider_1 = EmailUpdate {euEmail = Email {emailLocal = "sL\98765", emailDomain = "%"}} +testObject_EmailUpdate_provider_1 = EmailUpdate {email = Email {emailLocal = "sL\98765", emailDomain = "%"}} testObject_EmailUpdate_provider_2 :: EmailUpdate testObject_EmailUpdate_provider_2 = EmailUpdate - { euEmail = + { email = Email { emailLocal = "7\160957>t\21165\ACK\69619n9\b\USskT.\"\1106936\r\DC4`", emailDomain = "^/>1Rp<\EM\1110261\1087553\STX#\a[E\ETX#\30865\162265\3392eJ " @@ -36,7 +36,7 @@ testObject_EmailUpdate_provider_2 = testObject_EmailUpdate_provider_3 :: EmailUpdate testObject_EmailUpdate_provider_3 = EmailUpdate - { euEmail = + { email = Email { emailLocal = "1[Z\68778\r\35821\&3\1087344|u\996796\167850\GS \1071086" @@ -157,7 +157,7 @@ testObject_EmailUpdate_provider_19 = testObject_EmailUpdate_provider_20 :: EmailUpdate testObject_EmailUpdate_provider_20 = EmailUpdate - { euEmail = + { email = Email { emailLocal = "o\SOH\1002138\aLL$\SO\65490\1099895l*p\984607\SUB", emailDomain = "q\30683\DC3\12589\1001477\1015970q\1002402\145416\1056480&^\176848Z" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_provider.hs index 605e1e76eaf..092515c8c0d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_provider.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_provider.hs @@ -23,10 +23,10 @@ import Wire.API.Provider (PasswordChange (..)) testObject_PasswordChange_provider_1 :: PasswordChange testObject_PasswordChange_provider_1 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "\RS\148930<7jc~MQ\SOH\US\7333[\1084132Lz\US\1022735?q'p\1099657\ETBy\DLE\EOT\FS\1107730x\ETB$\1060369\DLE\61681\13692\993364\b9\FSiU\NULz\fb\22561bP\60643f\SO^\\\1008115\NUL\ESC\STXd\DLE>\1040220\1103806\SYNOIc\189228l^l]\1031063JY8J\1036381t\70171AcI\SIi]Bh\989297\ESC\140714R\r\NULz\ACK\1088597' A\DEL\\feuBiG\993059SDoWa\DLE\ACKW'\ESC!\"erY7Y`\\I\4948\"`y)\1045668iN)Z\1012930T5Q\1076971\147595\993658g|)\US\1003237\ETXJ\39701\1106744\NAKY_\a\vJL\1027083\CAN P\ETB\1078145+%.BF\153802Ay9M\98346\1008748\1104393s\25042pI\1005219_\1002925\rM\CAN\39386EZ\58119ZXS\1105928\RS5\DC4b\97371\SOH\1106103\184422b\100457\\X\STX\144554;l\49694xQo\NAK\138070\SOHJ\989399XE0^&\167968\n_1\USS\DEL^\SO?W9~\1099319\DC3=\DC2\1068564@*\155081yLc\ACK9\15796\1059875\RS\98699bV\1105827W\1005933o\1072486\FS\1000032Bmr>\1053735\1030072\157751\1056352m\1072516\142673G\STX'\1013038\&3K\"%Hk]\61616f\178900\RS[\ACK\1112290\DELvq7n\146589}F*m\DC2\ETX\1038640w$R?%\1048405\991130T%\1086216o\DC1\139752G\1070667\SYN|\1085639\111068\1068350B\a\ESC\1063604\1088194T\1019860@\SOH,1\1094852?\DC3\ENQ,)ipt\146004NXe\DELd\1015278\DC2 6\984739\1037131\&14\31204p\f\23571\134629\60442q|\EOTh!\DC4z\GS\NAK\1046271\RS0\120694\ACK\b\DC1\1057519\ENQN\22139P\139372?\",\955a\ETB\f\ESC+\"JpO\39005\1043690\54002\ETB\ENQ\1093734\CAN\1005666\SOH\DLE\DEL\2414I\55278'=I\EOT\62705N \SO{Vtdy\DEL\"\163827\37015hC,\1053062i\NAK!o\SUB\1075233(u&\fz\1049825\ETBA\1045850\175742\RS\US~A\40004:D-\ESC~A:p^\1079564\135140:\151246w\1025133m\61429>\6832:.M\1073891\10252'\CAN\1071280\24496R\1003679P\1024693*\SYNq\GSUX\1072603|\r\US.\1062109\NAK\1109389W~5O\1000925\STX\184929s\1006565\150194\DC1r\SUB\b\1057499ZN\SUB\rf\20741\SOH5\GSJe\66771\1067879b:z\SI!\DLE\ESC\t\NAKd\v@'C/6\DC4h\983790@", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "l\992017jfd]\CANOG\1037448\9679E-FG\DELb\DC3c\168198\179584\r\EM\98126~\tW+\ACK\33446,\1082952M\128295s\1000699M'\DEL\STX\SIz\ESC}\EM\154156\CAN~;k\53216t\ETXx\149224\132305y\96580\ESC\DEL\142174#O\r\bJacD\GS}}M\n\aY\1033126\&01C\EM$E/\NAK-B\175854I\1023972\1070217\6129\1013299\184147\&83$E\1009863\22083\&0\SOn[T\GSB\DLE-\1007136cZ\46079\174945\38508\985022\173232\ESC7\1110907'/2\9477B$SL\NUL^\EM2\NAKG\1093469\&2H\ACK\DLE\DEL\43539\58839XR\1080271h,\78748\174767\1054239\1041868Y?\SOH8DF\EOTCh#;/x\DLE-\EM\bA\DLE\159735\bkX\SO\188157g\94522\172555\NUL:2<\166889\998353\1083925zp\SI#\1022316\48223\NUL\ESC\141660\1089351T\27451\&0bA\7868\v\v$&qMQD\994988\12182}\SOZM\ETX\179973.\ENQ\21913\58375\47428\191199f=j{\47820\1111986\1073477\100913\61587\1073940@\ESC!u\1077743R\7637EJ\1016579\1016763\DLE\1058455\FS_@\1076367V\1005325j1Z\NUL\1004454>t6\1079007\ENQ\1084309$\SO\DC3\98064\137557\47918M\ACK\ETB\172727r\b\1100397)\SOw\ETX3\1010263\DC4\1066921w5>\1035509\96260Ga\USz\1047694n\SUB>\t'\92445\r\148219 \1025075;(4!\1073109,\",\"x\EOTyf \f\aRW'z\133849\1048674\24036\ETXYx%\1080586\42394\1045626n\13335[/'\"2-2jR{=\1012515E;\1026542\SO0+\142703H\987291\24296@\1106752%\SUBNnZVt-k1\DC2\155707bTV\DC1k\1003798a\1071366\DEL(%*h#\1030597\SUBL\STXa" } @@ -34,10 +34,10 @@ testObject_PasswordChange_provider_1 = testObject_PasswordChange_provider_2 :: PasswordChange testObject_PasswordChange_provider_2 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "J6\1063462\\\ETB\1078893\993390\ru.# ?_\DC4=\1064091E\a\1053958-\150000L\1061533\&3\1048772`'\DC2I\SI\31152,0K\NUL\\G}];\187087\RS\176174d\136667\1076870\t\1047442\48335.\152068\15337{\70067a\EM{9W\1063171\SO}'U\SYNB0a\SOHI\EM:^~\DC1\146030]\ETX\1068275E\ENQ\EOT\SUB\GS\38515-,d\1087008\146851:\1009309\DLE3\nn*4U8\f\f/\164529_Jq<\23032j\163500*e0\152734\DC3\58971v{'\57450^3;\EOT&yY\NUL~\194865\94514ct\33770o}\1015771\1008056?Y/<\SOH\1076816iJ\62386V$\\\CANd\CAN?\DC3n}\1001195\t\1017145\139912\1110640\r\EM&C\FS(A!Ke\DLEV]iB\136999\137089]\173378~\995379\&9\NAK/sY\FS5\165215\142850\&3\1074963\ETBX\DC2\92297\nzeKE\\\v3\128136\r\ETX\n\1110227Mr:M:\n-#'L\142407\121103\&7eo\57512\1043763!L$#", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "\STX5{IYj.N\1097942\1034521]}l~\ETB+\DC2XU\51470pB\121102\25755\ACKH)V\ENQ{\DEL}\ACK\SOH\991933\SO~YZ\1000116`vCT\CANU\3236<\NUL\ETB\1097339\SO\SOHgfBz\NUL\DEL\27119V+q\n\53764 \1025256\FSG\145697\184862~+]A\97342\70080N4r3ly$\EOT\1088639Mv\1002336\1050997$\CAN\DC4\23032\ETX\a\SOi+L\1016399Q4\99029c\1019418\EM>z?'\CANO\SOH\77963l2\SI-nZ\\\NAK\187610\&7'VHQ\ACKf\182917\US\1083627H.1\US*\SI\35903\SYNw\1090044\EOTR(Z\165917\1084964t\1085428\ETXV\184675\b\RSbU>\986822\NUL\DLEZ)X\137883\22578*n\1035455a\RS\SIvZ\CAN\ACKe?eB\998053\991381~\CAN(\ESC\a\1007645;\121445q\1042993\1070820{\SYNk\GSA\DLE@8#sG\1095900O\78803oKfrR*\SI)'\22262L\35087I\1030025;LR\988091\STX\917885\93968b\1033563\986558\&3SJ|\61654E\54642(}\985223J+\\ED\187753\182558\987551\&7*6\ETX9V5mm79s\b<5\146014\ETXe\1070748\\=1|>J \DLE\DC1#\1036249\174789\a\119052\DC4\ENQ\7214A\1057521\991526\ETB\1096433\159048=B\DLE\b\EOT\99076mR7\GS\\_{||o\191444)\1077352SJA5" } @@ -45,10 +45,10 @@ testObject_PasswordChange_provider_2 = testObject_PasswordChange_provider_3 :: PasswordChange testObject_PasswordChange_provider_3 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "\1090639\STX\132492\SI/\183791\DEL|9\\\b_\1111257E\18560\1026473\37179O-9\SYN1L\11112\153612SYM6`XP\185928x\1091969BP\SIV\1094629\1099550\&3Dt\1051594 \185390\127986\STX\993509\1072672\\P\182591\ACK5\v6\SIW;\178969\&1\nE:[\182420\96819a>\1066339\17724rv\12096OvI'-\1062972\166884\ETX\SO\SOu{\999818\164786\"_\984751~_u\1023831L\RS?\1032751L\97379m\1012949\b\189539H\DC3\3277Zd8\CAN\173661j\STX\59973\STXU\b\1058747\EOT\39158a(\ESC\NAK\"\ETBc_?95\1048328\r\SYNpa\NAK\b\SI!\DLE;\GSI\b@\20755D\1058888 \DLE}f\1025872\989393*\\\DLE\DC4l4sG#{\175474O~3!n\1070630\1046460`X>n\r%\vR%\12500\GS\SO\b \NAKJ\FSe6y:\DEL)T!\163863\19105(p\STX\ETXS\1035778g.v7\EOTU\\\ESC{9\1029424\&7\1096509\182402 \11429`3\v'\1011010\6241H\SUB\67713`\1102842\SI\ENQ\36671XSL\5184\74631i6*H[`bk\187634DB|\1082864tF\STX\1095470\40232\24100W$\DEL\SYN|AjF\SYN\1060698\ETX\1002438\151035s)$.U++'\n\DC3C\1092666.k\ESCgAo?,\1098414\SUB(sBt\994422X\51349C!\1079241<\1003009)5\147358[\25065\RS\993654\988949~(8:\1099034\94463\67647M\NAK\STXFs\\\162758u|~\DC22@D\39863?L\SOH\EOT\CAN\78252_\95345+\b\1047730)f\DC3\147188r\SYN+=8\17990+\131480k\n\1004620kv\ENQ%zD\1087067Q\EOT\DEL<\DC4U\EOT\98368$\CANw<+\125229*\171804/\az[q[\DLE\ACK\132467d\bv\DC1\DC1{Q]\155471\n\rokp\CAN\DLE\180903\EOTNn\147253.!\63250\65540z&|\983968W\164923\1015875\47406\r%B\NUL0\62411\58998\989796jn\EOTg1\1030731U|\1069001.Z\147615-\DLEUdT\SOHP\ENQr:\993057@J\172264r+\22908f\189795\1008819\12565\1059459?!rR\184591\1059540d\1010396\153681\1087402\ETBms\157686SQ\SO\1013566\159622\"S8aV\t\ETX\69642\NUL\1018708\GSj>0!f\1048850\172491d<\1090475c-!+\v\157251\USB\CAN0u\f\1109289@n\US=\EOT&;\FS\1094127\147561Sc1\tkL\f\ETB.V.m\1102645NZ\1025114\171053=\9900w\SI=p)\983196\110627\n*\EOT\4264S\142455c`h\1064905\CAN\DC1}U\16412\RS(.\ETXBQ\RS\SI|2\DC2\t\58697\26979J\1059222\t\n:1\1076824\DC4\20071\DEL\\?AkiJ&\ax^0\t\RS\1008082\145465'\vEB*\n\133263C;EZh*\1096287\v\172007\&27nR\1030319'7\1067756\ETBV\1096588\SIC\25035\NUL\ACKcl\1015672a\DC2\1054753_X3\1087036\&3\174910mvT&\v`r>\1008758_.\28801\DELo*\USR@!g\5064\DELo6'\61374~\11251\1055297Rv\96087", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "\ACKb\65690.z\DELS\RS\1043455|@ou6\SI)^#Nv\173721\nK (\49735\"+\180317r\168250hgU_\NAK>.~\42075\EM\DC3rNA\1003405\1064814O\1080126f\DEL\170710@Mj\DC1\rd6\157492)<\NULZN\STX\EOT]\164074%Ki\ETX;\1074300\&1lkk:Mk\1032789{\1085268Hq\1095137\991423[\1046704\1069727Y;(\DEL<<\NAKGs *\1064528\SYNGz+\r_f\162493\EME\179877\SOHo\GSA%'+v\145346:lQ\183770\120729\1008624\54333~\46645a\18566h\RS\nEX=*~\b\38738\DELk\47265\26012Y,r=\SOH2\12321\&9\FS\f\61703\146118\1101857\41261_\177617~\150584\1101236rA\1057119JA\\~\t\67201\GSrIk%3\179007%\EOT=%\13237\1079931b\FS\1080016`}\SI\167993`@\NAK\1037573\f\179386{\984491b\DC4\191263\SUB`\tV\ETX$\34881\DC2FH\ETX4E\128495\1074641\155862\f SL\176565{A&\FS@\DC3q~p78<\1029510t\1072174&&\1064641W\62128T\148445gn^K\1032060\DEL\1073914TK-\1032280t\69863*B\NAK\ACK}G3W\119556aj\135982\&0\35872\48740O1\EOT[o>\SYN5w\GS\tEd\ESC.}\27602\&1{'\1023415\1007968a|\am\181403\bu\DELo5$07\tK\1101735*\t\bv@F\ACK\ENQrQx\RS\52315\r\f\18064\60859\1043018\42814\1082068\16599p,*\RS\DLE6\SUBH_\994350\SIi\145754\17091\1001085\NULUq\176242\\\1081511[j\1020996PN1\DC15+!\1067420.\34561+7mLuRk\1036698)N)dlu.^\1109734\EOTzU\1019326#3\1055275V/\SUB6^\DLEq\60060\153909j1\n\ENQ\143934\DLEC\1094908\174654w)\SI\1095019\17787\155204\\y0/@_\1036276\\;\1044245U+\":|\1008444L\\" } @@ -56,10 +56,10 @@ testObject_PasswordChange_provider_3 = testObject_PasswordChange_provider_4 :: PasswordChange testObject_PasswordChange_provider_4 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "\1005314~=N\SI\SOH\a alq\ENQ\ESCj\163982U\133814*f\"\72875\SI\1015050\1072188\52409\177675M@0\\\1060741s\b\131914\&4\NAK{\EOT\125006\ESC7\1042522\n\DC3\ACK\41709\CAN\DEL&\RS\984654\DC2\1039379\GS\NAKbHZNbM,\53068\EOT\SOH\1086193Qw>56\NAKF\"Q6\USp0\ACK_\1071681\22914\GS+\1113265\1033881\&1XL\1057692\r\b,l\1090712UrQ3) \DLE\1020313I{\ENQ\168366\v&\STX\182716k\1077895zs\1099425 \120690G(gf\1060305O\38802\n\1054049\1065585j>|Kg{.V\163868\ENQL\131757[\1096643@;=zg\134436\183794\158027biF\132561\&6ZPA\n\135254\t\1032483\NAK0\CAN\SO\SOHaE\1060005\EOT}Ys )\STX5\172564\SOH\1025776\1058126\1057981Tx#\r\b\DC1Lc5#\1035228Y\1093589/B-Sv\159168\161208\n\1031751S\39534\f\r-\167002Gq:;\168477\&4T\96990\917580,ST\FS\1079935&c\ETB@+\180969\DC1\NULd\167999\143044\CANP\1021550\988126\1020951\1108300z\ACKU\SUB?UV\148371p5\161618D\f\USo\165498#x#_\1054438\991493H\1053912\1101113$m\1108341,$\1028517(\1361\EM\FSr{(\DC2\36604:Hr02Z\CANj\EOT_C\RS\SIt\143715{(-\f\184102\&2q\r\r\155913Z\1042726Ko\t\ENQY\1105826}\a7h\40363\&39\DC1\40241}1P L\nj\GS\123621'\94253Q\1094248%n\144018\"$R'S=W\EOT\nr\v%O_\187746Pz(&v\t8V!\150217O\1087987\1109209G\120191'~q\6433b;\b\33127\&6\6978XNr\ENQ~K\DC1\184383O\STX\43136\186449q,~A\STX\995391f=JwT\f-Z0\DC1\STXJ\135448\SYN)\t\28369\989463oI`'s\aG.ggB\SUB\1089552s\7042Iv\995920f\DC4.NGl\167789\SUB6\ESC\SOfJpG2\ESC\151792J\165772\1060235\RS9\164444@pMICAP\a\STX*_\41597^e(M\EOT\a\GSuYl\1027529\\*\ETB\1000487GSnZO\184505\a\151353Do\185571S,\n\ETX[\1024125\12994%\1048335\158640\b$fm\DELJYdZSTOY\1011389\1000717\22006K-~n\101099Us<\63668\42529r\SOH{\1001552}\1035280\143766-\SI\191007\&0\30696\1067137\1100998y\34996\&3\1012120,d\t{\1060327\1023821H\EOTV.\CANu\1087782A\78628r", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "=:kk\1018981\&8-\\HYms@'?v\ESC\133198\147317\t\1008759.\v\1033547R\DEL\1060763\DC1Z1\95927\CANSmD\143932\1024564\ENQ\FS\"Z\1030825@n?zS\119833\DLE\988733bz\189538\1082413\SYNSv1(\132603\rQ\EM\f\149020'.9\SI^|v`\a\1039850+\DLElBv \RS\23734\35305\1109565\DC2\bJm\1085701\1095232?_rXd\1019687T\137292\STX\147778VK0\134410H?.H3H|\1089668\&5\1020896\RStKw1k\1046876e$A\1046587\66888\1106484w\22514\aRYT\1048339T{|!\1076458\50629;*\1111661\169350\1009115\1100512\&6B^\36743Hbz\DLEK.w\STX]m\1105522\3984`3@\1035182/\113800Y#\1048181{X\74883?^aYz)\STX:$\148676\nd:\"Q\FS1\1083955\DC4}s*}%^CWY\SOH5w<\NULla\65202\1015084\STXa1\1073755L]\GS\ETBV8\SUB\133836\1011042rS\152914\1109488}\SYNl}\1018153p?Yi\1111523\&7v}reh\993180w\DC3\DC1k\US`)\f\181331bSd\184792A6hx\1018142#n<~V-\987055h\143466LT\53862q\f7\SYN\\92\137339\ENQ\r\CANU\1071144%lYedK\vGHs?rN\177663\&9\99852\52785FV\22264\127076\154269`.\DELXv,\1085673\&4\r*\191239\135780\152535\&6/\EOT;V\1045100.Z\DC3\1073857\SYN%\ETBTVv\ACK\5241\&40c\STX\\:_T\49351\&0D=\138839\ESCg\SO2\16967\RS0\1009235b\n\SIe\NUL]=Ir\\[,;\ENQ\1109755E]\b3\1057051\100809c\NULya\1062732\NAK/t\US\r\DELS \1052333QQYH\53161\15320\EOT6\1045701pY-\EOT4\7658L\1039028\155730kB\172820\EM$,8\120808[\1087257DGgn@\ACK\16782+@\169718[\teJf$G/B\8949\\&k)bT<\1074663iE2h`\189858&p2\DC3r4g*\1040011M\SUB\1034202.\14977\25151\994175\1080867\r\156624\&6V\163857?Z\7344(\SOHF(z\1085772\EM@r.\1041715\EMBi_\1023342!I\DLEj\1069951\NULHy?\SYN\1024843R\1061663J4kQ\DC4\SYN`1\SOHNnp\167659\ETB\36188\ACK6p[.q\DLEZBF\\\b\NUL|\f,\989077\161119\150164z1\187508M\DC4!\1041102\DC1\1034211c~\1085907\DC3\1085359V\1024822\SUBJ/#'U4t\119160\1101637\ETXW]\EM\1003131V\FS},\1018388z\28107nOj<%7\SIk^\ETXiZ^*Fz\STX\\g.\np\DLE4o\NAK\DEL\DC1,\1048642\&4L\EOT\DC3]\83046[\DC3y\1111455\RS\ETB0L\US;\\\161083\1038455\165809VOG\97134 R/\1068148\136637J\1008468\SOi\31819Qm9\DC3P\178289\1062875m;\33449\151544\1112165g%\119045\ETX\SOH\9817U\ETB9\63207M\68250\EOT\12530\&3:1\8937s\ETB\t\1073799\160211\1013614\99221Ycje\US\1021337\bC:S\EM\DC3\EM\1080393\&0;Y\1006472unM#\ENQs3(\1012482\ETB\SUB$\1108830\SUB\SO\ESC\1011341\&9xk#\"\1107050o\152443`leDo?\EOT\166427'\178290\1083767KWa\SI\983521\&5N\1062943\ETX*\1069873E\95635\49809B0~\rG\146763Pkj\134528C\994565\1112409\FS\77994\DLE\DLE\1083005G\1079213q\1078280\SO)\SO`\1044446\r,\GS;\141525\13757\USO5\184789\1024674\1052970a\SUB5G\DEL\vN)\1061733\SIi\EOT\1053143`\181217\RS]VL\b\1013194\&0\a\EOT\1039988W\51437c\SO\1065070*av\98257'\170757 ;c\996012.\131792~\DEL\4838\182631\1055951a\135094I\ACK\1052557\f{\1072125\&1\27594Jd\1063195|\8005\SOH\1024659\EM\ESC8\120279\986941,HPp\3786\ETX=\135249\SOHD.\DC4\GSwi\1040647\CAN\SOH;\1021350!|^\DC1\ETX\ACK\1063454~y<8\1011154=|\134143q\f\FSE\149852\bnT\25647I\NAK\STXYB\1111567\1092081Gp)\1057864\STXj\EOTBi\1015288\72231\1105732\vzO\EOT)\1021734IRz\1036141Ty[\SOtj\994518q\ESC\DC1\ENQA\78643`\1033140\SUB\1086534\119199pUM\74032U5\SOE>i\DC4\1026198H\r\f\a\ETX\SYN\EM#\STX0m[\DC4Y\ETX\nY\1041053\1032715\&2xROJP_\1069998\r[\139994o\1038593\1022439-\CAN\FS\1053876\162419\GSI\1114021\1099881\DC2\ENQ=\DC1]\14160\178652ee\NUL3\165586CA$\1096608>,Sc\"\DC24$\98460\US\1104391|\148368vh\EM\b\1110174\SOH\51262EC[7Kh\n\26878\1037105qWk\142931\NULQ7i~gM\RSp\r^\nJy-[\41948\v\1088158!\164120\&4\DC4\41370\1083111\1100437\1027623gln%\r7q2\8168\DC2!\EM\NAK\175295Tl6\1071902\STX\38040+mo`VuL!\n7<*\\\157558>\68039\64688\RS\CAN\37133\ENQ#\DC1zi(3OU\ESC\NAK\ESC%\137946\1049584[8.G\1111459-E\1110194\1084255\1058892\v\1064396\1062440\&9M\99681v\SYNl=\US\15311\1047155&\1053601\ESC\SOH\1047114\1071949\172567$H\n1Y\51322\CANLg\47625\ETX(;\GS\61177lZP|E\ACK-8\GS~\ENQ\v\189891\1107362zv\bQ\USbkv\94908*S\DLEs\1075777B`]\1046292\SO\52998I:\65296D\167913\r\37306\182476P-wN\173628\RS\bD4WD?\63663\SO\1113823\1023204\149429\fI6,6h\b\1004711nP9!G\27578-\DLE.\SYNF[\160877%Q\1097530^\STXH\157909}\v\US:hx{\1038469+\1090842|\1014387M\DEL\\;G\28870\1101783\48530\EM\SO\162503zq\r%\SYNCUS!+%{\127862'w\996607q\1104160^!XSCAa[N'Vm\DC1\DC4~\189916P+w\164548\5708#LI-k\118975V!\121316\1113106md:\SUB\t\FS4\1004433z\1078080Zg:^\NUL\995376Qs\184644o\1095386\SOH\158723\EOT\1021483lPb\nBT@}\2545'&e\NUL\1065941,X0\135225c\tu\CAN|`4\1020041t*wK\DC3\f\25439RD\b\SYNGZ\1006639g0F^n\f\1105456R>f\1100409\1100823\\;`\SUBoh9\ACK\DC3\1071927K\v\11722\"\1060736\DEL\99248\GS\1040422\8236h\194957\23896+T{\52879\1008639\ETB\57964\987068\&6\DC4\998395\&99\SO\1098197\1097876\STXR\1090815\EMQb\165117a|c\150904~\FS\NUL\132829\DC3\15008V\DLE7K\167075\DC16=E\ETX\fTv\1034496<;\rLEGuZY\118839iNm\SYNJ\rI\190474\&2\1095050lI\ENQ\GS\1034351\63865\STXaPo\FS=8DtkQe\SO\147814\&0vQm\153309\1071911x\128401\164053\1008099$o-(t\DC2z\"AQ\1020511e;\SI\70124C\ETBH\29202#\1074721nCh'#\1094035'\1064442\35450Nx5=\37407\177998\94806\49674\\Y\35646Bec\1095406\1051005\DC1r|*\EM\38243}.A~\1079182\1042143\ETB\SI{Pt\1011810\ESCS8\160032\ESC\22627\SI\153862h\998542dZLu A\7299\149281+jc\1513<^\157390\DC4:\1083899\f\1031499]\bl\1036256\128520\38650d\1056973\DC2v\1044284\987395r\ESC#R\1022711Xr\27081\20760R|f\1092090?\1013931+\ACK\1107788M\CAN\1020010;\USJ\DC4\1012811\1028415>\1053853\STXj_)cWt B\18936f\1012599p-\vJ8\51800m7\167922R\NUL\171175\1057562\STXk\1020080(9\DC3%x\34431\DC2}(dMC1\ESC[", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "\131784\138514\157899#Jk\r\1106537K\DC4RP~\ESC\150747\1093719\NUL_\1070253F\EOT*a\RS3we+u\163806\SYN[\183120BlK\n\FSF\DC3\SI\1031201\51899\1091000\1006948y\b;\83374$=\EOT$\100771\tJvs\155623\&2H\1097133<3\49632\18894\&7&L'W\169743\&9\1100463\1016241\DC3\EM1\998556V\DC3Spzv\rZ\98169M\rg\60865d\r}\1017655\6434" } @@ -89,10 +89,10 @@ testObject_PasswordChange_provider_6 = testObject_PasswordChange_provider_7 :: PasswordChange testObject_PasswordChange_provider_7 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "o\194642\1097637\NAK\1096957q;\40241\78060@G7os\ACK&\NUL\rb\1064652\r!\v\EMh\SIf\1009912\FS\1111085\994910/m\1095852zr\21631\&70p\ESCxcNP;>ah\1038533\1088598?s\DC2\161084+l=\SYNtKk\1112851\NAK<%Wy\DC2\rdO\"wl\r\1073877&P[ x\EOT4\1070910\94261k\110789\EOTO\2408CJ\FSh\CAN,j\FS\1051419\&8\145785\EMDM=?\1044107rN'uaGQD\DC3c\DC1\SYN\50022\SYN\SI\191191\23297_\f\f}(lT\1001335-\1102617\1055091an\ETX\",S#\182773\ETXhx\ETX\"S\29957\133313\44208$Gpk\135573\74317\28585]Dx\SOVr]\1106090\15330\EMt][\190740e#B ;#\RSwWZ`'-K\1031198\1079899\149986'G:I<\ESCgN79G5\1076698\134552\1001620\&4k\178721Z\61166\ENQ\DC2A(@ScQ\1036811\RSS.\1083926?&5\59387\&4N/S\162222\1084995\&74w\171315\176694\31631\132086\f0l\vn\1041562\&22.\995582\162439\178300\DLE\CAN\GS?!\SI\19976usZ8sJD\1016223A]ExUW\18012tdEm\160005{7\b{\ETB<\DEL.\187515\26357\1022998!<\\\ESCGPS%b\1101700\13570\EOT_X6\\a~u.E,\145264\151278B,\1110132\178446!\146205}j\DELw\1094539wAOIN\74851suE\1011582LH\50805\1075175.8g,4\b\994127\158463A\STXO\CANYgM_<\134150`2t\5592/\184130\ESC\1025744\a\EOT@\t\n6\SYN\1051619\153044\US\24861\NAKqOwDI;\158935\STX\142163T\153718\EOT#EZVVq\nj*w\1099335*\SI@[5\1010626\n\DC4\\1\DC4\DEL[\CAN}\NULBUTcW\DLE;-D-\GS[\EOT8o/\26515\b\FSU\DC4}\\\140030~\SOH.%r\24914\33259\ESC=aoY\121055z\135293\180565t:\18518NUy\986819o\v%\1031392\EOT_\1056629\990992Vv\96494\1073204>%E@H\t\171158*\1055587W\131453U#p\ACK:\1001700{?.Dv'\US4VZR\140754\138375\14865\ACK=\1010074\a?\DC3\DC1\1104713F\1094183%\NUL\11085\NUL\119900\50952\1091734\1096788Z\131351\1071405K\STX$H\b\25145\SYN\137614}Uu>\1059256QJ}\1092477c\1005961f?\1098417dP\ESC\1112602eA\f3q\EOT\DC3\1085835\SUBP\DEL$\\\1043723\148046x\EM$J^ I[\ACKgl@k9\14259oq8\60943f%\"J\1057317\73689\1041929t\SO\fi\NUL'tD38k\ESC*z\v\DC4'\38081i2}}K\1000483\NAK ]=\GSzA\SI\STX\GS\65784@5\DEL\32084\RSJ'I\1061395\ENQC\132576k\36936Bde\132392S_]e\22951K\STXh\1080765\&9w\1031828\1079907\"\1075875x]\v\"\1004384\1034557\a\13954X+!S\188785k9\1336\STXL*Z\992108E\988071E\157741\1059002\49383l&M\78548\68781\1059405\&1\1024148\98156\&4JY\vb\1009588\CAN\1012372Xqx\DC4\STX\rW\3001RRPX\GS\RS^m\58278\a\1082402\US\990568QD\1066036\EOTAzaVl\NUL0[[\997199\992592SJ\100209Io K\US\SI?'.\100329Zv\26227\183271\&9?C\\\SUB\1055968\&5x{", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "_90\1017274=\175182\\eIq\EM\29319@\US\ESC\USr\GS{\bq\SYNX{k\998765\1113634#>\v\127239\\L4z^\47811+N\1113429\STX?9C!^\1072965\155787\1051243f\n\189595\DLE\a\ENQ\1019894\50928&\rJw\CANZE\178133jc\ETBb%\50684va\11406<\US*\77953\&9?P\f" } @@ -100,10 +100,10 @@ testObject_PasswordChange_provider_7 = testObject_PasswordChange_provider_8 :: PasswordChange testObject_PasswordChange_provider_8 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "\180818[@\EOTO\EOTr\t0h\154811\1097619Ls,\RS\177254\1001237\US\53799!\174182i\33534gi\12980Ul\DC2c\"Q!h\1078688\ETB\32168j<\143648\SYN8\120997\&3z\1109784\140076\8229\EML\DC4)L\1086339hE\FSYD v\60832\&3:\b\127281~\t\DC2|\NUL\STX>\1037988\&9\52889\ETB%2TZ\1057438\1019124\34595Ba\36978\&5\n\f\66575M\DEL'.HktRZK\US\b\1045482\SUBQfa:NA.(\az\DEL\131940Jviu\SYNt\141599$5b];W\SUBPM\1063367\1063525\135883,\17207W2L~_\DC2\6631@DJ;|\DELa\ESC.h\1052121\1098974\1033911p\1087765\EM^\t1X<15#N\1026411\1084279\&8G!R\147770\&2t;\ETB\ESC\1064735d~\v\NUL\1102025\DC4B1T\"\1109782}x;!no\r\1106009\RSt\18334uj\EM#r.W'}@b)\STX\162952i\SYN\167204Y\NAK g.xl\63576L\1084858\&5\186390\182838\990328\th\DC14\26177Np\ENQRLe\1082057\&5k\SOH\997985\1062349D;KY\189276A|\ETX&t:7$\ESC\NAKX\176629&\ETB,S\SI\18829\29542K;\CAN\184587,RWVP\RS6C\24675\a\187635\992522\154218\41884\b\STX\ENQX\9568$\SYNK", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "x\1083148\ETX+:\1080028wSH\SOH\62978\1102729N6\182762a.$\992539\120747GK\158987%\136043\DC2S\1037479Ym\1008949\NUL_Fe\GS\n4\990156\39344\1010528\&9\t\ESCEZ\"\aE\SIO]\1045645\1079319mZv\14455\&4Ie\166474iF$\n5\EM\ACK\1075904\1113583F\SOHG|\STX4g\1002980XHN\989865?\1099099\173477\&3\50673\83417\11655\1099379B\ETB1\r\1013634Tn[\EOT$&}'}\141322\&3_m\1077163W\3968Pt\ESC\1071158\&6f\1068159=\183495:\1036808\DLE/p\1004364:\r\1085290\ETB9`\1080065\984968h\1027137kOs7\NUL(t\10489\1068176,G@C\63384\1024509\186856\190455B\1113082\SUB\CANQjW\1008166~U(\1009483\&5\1109177\20348!\1077035E\1005&Mj.(\DC3y\152919\172701\ETX8p)'1ep\n~&{\62654\167895\DC2*7\990132\DEL\185308<\DEL\SI\1098650)j-abs\CAN\GSL\25312\DC3Pl\51906\&2`$bh\SYN_\1086252+>h\59671#\1056101RE\"{n.wh\n\64038\154124c\1069890\GS\170451\1076325N#I\1062645\nX\n3S0+\tr\CAN\39868\t9\1031811`/\1019167\1036273(N\57792\ACKD5\1096310a5\DC2Tf=}\ETB\1108347_yHue0\n\1092905\1099428H\b'\1091583T+:\183409${\1057811\GSE0\RS\1043155ly\SIobfRk\ENQ\RS\145619h`\\\95626\NUL>rV1\ETBfXk=cyaI\EOT\ETX!_\CAN\26741xR\DC4\1038343<%\1073241,`TXaz@^\\^s\180706Y\NUL\1081447\156985\DC2c\EOT&\n\DC3S\44890\NULE@\13815\&0`B\t\1039059N\CAN\32617\167086\999897\34753B\149257\USp[\SYN\186181\ETX\1040852=;\ENQ+$\a#\1020966\ACKc<\1106724\NAK\ACK\CAN#\41741\66650\DC2^\f\1089620!\ENQ\ACKC\SUB+\NAK\1070506f}\SOH>\bz\184367-{\37662z\128698\191437G\ENQ\n\1036769O\1112827\ENQPs}T'q\1049540\1059171+\ESCW9U\ETB1m\ETX\1044364\1110248\1011325\1077049\\\1070234}z^f\v8p\58049u\DC1Dn@7\SO\178338y'\t\CAN\DELX\138703\44901\111212Mz\1060998\&5\\\"\128701>\EOTNZdWO*\177619\DC3NV\1105635\44906vyM\45692\145400z\CAN\63310J\DC4\RS\ESC-FY]`k\DLE\DLE\GS\US4l\DC3(Ot\SOH1\156591\DC1Daok\131703\1053478u\1047598\RS=ES?\1105503v\119021\1077338\1108555\1105842!\EOTICQ\64082\167240\1027279\ETBu?%\1093608v/\47051e\DEL!M\bLA\SOHN\STXWi\1013467\176220*\aU+\SOAiO-(\n\7942m_\1015104khe^:\rQ\bZS?\1043829k\n8eh\984956\ETBU\146314k#]\ESC\32013\58442,\ETX\DC3.3\SI}\30711=8\NAK\1023884o`TI(\992144-\ENQUR?\152908\DLE\1110035\1106113?VYfS\EM\SUB\1095315\33553\1096655\ETXC\RS8j0\tKC\190493[_&\46172\1060818\SOl}:\r\SI8(x\135429?8\98588\&7)R\985918Q\ESC\ACK(z\SOH.\1107353\&2Y\US\SOH\1035764|\DEL|3&\DC3\94271Q'D{\NUL??\ETX7HDW\184522]`f\bPO\ENQx?\33111A\DC2|hP@;We\35075;\1057215R\ACK8\"$!A3.[\ag\997090\1017693.\STXI\1107916\1089611\19348U\983048Y\1008717)G\RSY\1107954\DC3+u/\DC2\DLE$\STXN\DLE\185464(>[\SIH\12867'KOC\DC4P\38328.\65734M\ns\ETXl\NAKj\DC4M\1046104=v\US\FS\1033829=p\157189s\SYN\DC4T\113713e\SIXhO\v0\SYN\159832\SOH>!\161626\n\RS\999549\135814\49229\1051757.\EOT'(,f8NT+J\1006984O\1062064\RS\187616\309D\152878\b\EMg\v^\22870\SYN\1026918\62565\ACKf", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "\47540\188511\&0\STX\SOHF!v\ENQ\11373 l[\SYN\137894\&4R\15581\&7\1083947rK\15414+\n\135750\1065844\SUB\afb'|\RS\"\995385\1090151\DC4\132765/\SOH\153829P\33605B=\72999\a1\1017925F\1051495k\ETXmF\1017174#\177930\148698b\168141ZG$\1112470dbB\SOH\983969\72724\EOT!\996099\&7\SI\1066289{\ETB\1024612\DC1\NAKSr$o\63124\&4<\163973q\1060394\NULXX\DC1;#%fM[dR\EM\1044817N\62150\139272\US\SI\1073067\985245d\RS\GS\DC2EbQ\191179l\1028785r`Tf\DEL\191144\&0\ACKHJ\1016730T6X\DC1cypE\ACKy \DC3\DC2\25565\SI7Wv\SI\1046192Z\vY\"\v\156204\DEL\1106419\995387/\DLE+D?\f;B\163188(\FSyI\1060531Zi\1051115~\993288XN\13032M\DLEPzB\US6\38727gj&L_\1061368\EMj9\1018863V8*Hf`?25\154173Y\SO!dS\1033424O'\1099719`\GS_T\1012344\48568\NAK\1052118\b 'ss\179793Ug\62366\ENQ\rQ:NVc\46684\ETX\147041$\33117K\97385]rNq\1088791\14261\&5g\1108158>i\1060212t@=Io/nm\ENQE{\1051318\v\181086Yk:\SYN9o\NUL\v\16507 [K%J\97955\fM-\1066437rvqm$^b9mkM\1039402\&2;\ETX2\1043146\SUBU\18461]\SOHD\ESCF\DC3<\CANu:\EM\174389\n\DLE\24984\142121yXK\1034045\52191\FSA\62973\&4(K,\168483\SO%gE/B1D\1107948\DC2\161658C\SYN\EOT\DLE\CAN9O5De\29644seu*9\DELk~B\ESC\DLE-\rT?t@\1000006\151230;6\ETB$\1003656\FS\1041307\637\1085577\1005683u\194601{>6\DC1E\48817kO\1071212\DEL60\183739\&9wk\177129\DC3\156315VX\1016207C\141727\"\155769N\153799\CANT\SI\STX\1049621\985530Rl1Qr\21745eC\GS\SYN2z\RSi\161367D6s1y\1095652F&\1040517lTt\ENQ$p\GSRi\1048949-\58685\SIu\21111v\19578P\1077429l:IZ\181939\17566V9e.\DELp\v_6)|q\1034902{\ENQ\29955MXK\1056306ax\1003137_\1006056#0\US\r\CAN\"\DC4`O\1049859\CAN~w|f\EM\126607Oi\1023015\SO\FS?h/('\DC1^\39065i\185517J`a3\ENQ\v\1060183\11345g\DC3\48063X\29116Ya\"6\r \132135Bb\1062624\DC1&\13220\EM\ENQy8anV7y\134882T\1047562\SUBcn\SUBk\168542\US_a\EM?\1106016\1088608\DC3I.Z\1069178\ETX\SIrX )!p\1067306Y\183358\&07\GS\1086052?\169845#m\EOTuZegqUxW{\100361\34246\33073\36773L\EOTg\154155\998821]\ETB\a\1059432\ENQz-\97879\187856j4\ACK\STXM\STX%4\EOT{\59661s" } @@ -122,10 +122,10 @@ testObject_PasswordChange_provider_9 = testObject_PasswordChange_provider_10 :: PasswordChange testObject_PasswordChange_provider_10 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "1\RS\10044\NULv\987768z\1055172|%\1068184n\150620-g\146786\100842a\132317\EOT%?\97207\1068876AA\"Hmj-\EM\29734\&8\39432,\EOTP\58685>`L\STX:\127298\ACKhOh*\54301md\DC3\STX\144021\1098966LJ+\ACK\"\186180+\1079127\1032187\1090115\SYN9\147876`\63187X9(i\48707\163570\&6\1042913dn~\f>\DC2\1059432\1061679\1009814gq>!\1009228\1047046\63767R&|/\996634fC(D\180586\163947p\\0D$G\23465(J\btdn\1057718u\SYN\NAK:[gvX\50684\NULA\DC3T |@A)\f;\10521R-q?fi\142601\34542#\131181\68251\NULF\1056804\RS\1089058?*\1094737iy \1005023\126576EJ3\153530F\SUB\3963\&2\16235\1015286\SUB\1066520X($}aH\118969!M\1077359\SOH3])\\\ESC\1049797h4sn\FS\10735ztNR\STXxt~:Rb\12611\996694\SI\n\1112987\b\154951C\83302+\\K\1035224\STXs\RSa\47166+d\1073064\&1Z9g\RS)\93030&)\1043446,EIg?K\EOTm\1090815\&9\n\175897\US.u\51778\\\DEL\195063^\DC1|\DC2\SO\SO*LJVVT\1033808WO\USWmOS\1066607\"Z.\SO\1113376C-8\f\DC2miZ\ACK\1084935~C\153854sbIc\"\\-x\10336L\162894\NAK\EOT\1011330\DC4\1065068 \DC1I;\50247;\FS\STX9gU]\151272\154324;\131933v\ESC\n9?QY\SUB\176268\137386Y\78635\1037339Y\DC4\1005665@ll\26187\FS-\v\1059041l\1096164\1084819\SIWrw\ACKU:\135072{\GS\SO\1015883*\f@n.\70686f~\1087845\1045524u0y\SUB\1057096fX\SUB\1018748#e~V\"/[\ESC\CAN\152318\&4\27910_6 q\1092940P<8.MdP\CANV\RS\1046864\991518\NUL\ETXy<\CAN!\US,\RSt`\t\CAN~2E\vs6E\28962\1105957J\vo\1034354O?h7\NULQ>\1091553\&6\"V\138663s.<\"\1088335Myg\"\1103252}>O8\\N\FS\EOTu\DC3\SOF/\RS\GSO\27243?t\177484p$<\1089949VrW+\148070\"Ss\CAN``\v\DC3j@\NAKkeV\bE\164215\11921\FS\NAK1\1061345~aQ\f\RS\SO\1083547~\RS\1064714\GS/t:\DC2\tH\1019658\176743'2\77831\ETX\SO\CAND\SUB\19747ux\DC2\1085285\b@b#\US\17662\NULS>\ENQp\DC1\EM2\NUL\145191\"\"Z\133654\&1, Isn\f\160554\EMR-\58719RsaR\FSu\b\1055113>O\1004908\n\59796\136706\DC2F\1085126y\SOZ\93820\ESC\158354\DC2 \46585\n\1106215J!\STXg\ETX\988927\1065461rba\NUL_\42383\STX\STX\ETB#\aAqacXF1b.$\fFvU\173641\ESCAa:\154419\67985", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "\1903\59455\FS2\DELpfHg\1002321@hB\1104441\136229ae\"@\178717\DC1[;F?\1059559{\r.~Q8q^^\1032632p~\54629A\SO8epx\v\1091069\36029\&9Q#`B>\\\STX\ESC]\62056a\aH\GS\1017283=\133882\DLE\US|K\1089241SU\"`TOG)E\1107458\EM\ENQ\1100349V\DC3\SUB\131700\35766\&11eL\136082.\1077925\f\30081\139237\ETX\1018509\&33\3857\1020021@\\56/RC\9618VomQo\189775\1090226\54508|b5\1008980\t\38541\148813W\1052824u\172429\19279\1097359\&2\SIHs\1009437'\\\bt)A\DC4|\vh\141292\US9K\1006653\1113093wY\SO\1070366\40254\17618\&4u\997723'\1071258Wtl,\1028539\180872c\SYN\169621p5<\989014\SOd\1098426(P\SYN41.!\1051130(\DC4L{m\SUB\1109045dYlO\ENQ\180749l\27773r\1015113tS\DC1d\1103375\RS\150389\\U\137134)\1068287\EOTRc\DC4N\ETB\SYNrzEm\763#q39\1045616lY\CANHr\156951\26672:\877w\135480\DC2\r:CA\29971\110984\1082925\n\DC3VE,od{J@]\21278P\29049\FS\139994\1100241\1102005!\136294\1017333H\23052\&5\\\DC1\148824%\181207\165938-p\bN{Ky(N\52156B\a\990465u\119232\")\14788Q\1031053.\183810J\160516a\18817\EM`c\DLEh3\150349\SI>\59607\58218\987987L7II@C%\170472b>\NAKgl*\EOTzI4Vc\78060\22721+)\ETX,\DC3:\42649\&5\1054588\SO[UL\SYNCQ\41627(\12611\ACK-;O|\120383\SOH\25185;VBv[\fj\n$jq\"\NULuYx\EOT\1042364\&9:F\94542\1103197omPG6\fX$\DC1\ETX\SO\EOT-\137576Yk\147970M`\DC4K0\v?\1041183t\ESCT\1068218\30904Z\ta\1045178\SOH\EM0tf\4343U\NULz \98491~jq\1078216\ETXV\174194=\47181vn\143157oly\DC2i\n~_R$8;fbOK\NULz-?CM*\STX5!\1105218\181223\98689i~\189811!\DC3\134655\DEL+\1100972\1088541>\US\1083023\988420\59101'75K\FSf\CANY\SOzC^b2w-uD\1106649p" } @@ -133,10 +133,10 @@ testObject_PasswordChange_provider_10 = testObject_PasswordChange_provider_11 :: PasswordChange testObject_PasswordChange_provider_11 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "\1020927\1032547/~G!t3\ETBpT(lfd\987373Uv\US\1047331n/\1037165\v\1000590\&4P\SI\1060450gP\1107120\&3\132634\147692\&0o\57446~\1081825\&4.p\155579xg\169376\1084103yGDY\1068206\EMx^\151320\141047u*R\t21=|\\2\7811\ESC\11201W\147769\STX&\USGu\1097263+#&\ACK]m\159187>\94801\186556\&6W\ESC;w\1017208b\\\ENQqz\\|\95815o`\\\184139\&1\ENQ&@\SUBF\DELC]G[G\164490$z\1112723\1032192\ESC\1044057\EM\1048741\NUL>d\1033451\1015039\1073811\n\166720\180329P&s/!qt\162927@\vP\DC1p\n.n_N\1112206\DLEV~\1101479$h\138762Is\1004025`!\22537$\996552\r\RS\1098882@\16250Rd87\16682\69640\1041864Zo_Ps?\58225\ETB\DEL\25967wU\r [t\174359/\GS.\ETB\178764}UyN\DEL{w\NUL\1036907:u\\\US\SYN\179040K\1072719\6945J>%.\147824'\19885\184555Z\DEL?\4231\STXjg\145989G\DC4\SO\37110\ACK\43793\SIv\134991\NUL\174407ei\\]\n\998741f/a y\1002649F\SIlm\995784\1000687\141212\ESC\RS\US$|\1035150\&1\CAN;6\150884\&6rxN\135999RVa\STX(\GS\ENQ*pSX2-\GS\53557\DC1x\b\".\38402\ETBM\"\1082676\178592\DC4g\r\1100889)\DLEk\190056t\1043965^0\1008489\DC36\US\t\52881m\SUB\DEL;OV\1030445p]-p\DLE\152006D\37776\19566>d\DC34@/\994339\141918c\SI+dol\r{\RS\DC1\1017180\1064450\SUB1>\FSy&#\169442>\DEL?x[~^dAc\f\DLEW\SI1\995822\&0S\ETX_\1047048#9\175054wfVhc\1042539\1020713L#J7GBX(\4705\1048737\989333/W5\SOH\1112863@gv\STX\162785Z\ACKw^\CANk\986491HyU@I#}Xc?\1028091\28123\DC3\DC2)5+\1059942h\DC1\NAK[>\58609\ENQ#\RS\997513\nG\135550\&3\CAN\FS2:M\49436\ENQ\1074694\1023446\1073068\1089664>pne\178174s\SOd\1091829\138029\&1\24380_\1036947PM)\EMGb\986632$z\46384\ETXv'\re\CAN2\12453NA\n\1033330H:\148549y\ETB4\ETBm\132498\185138\DC3\1010645c\at\184247^\b\ETB\1097131*\SUB\1075368Q}\1107305r%\984574gdAS\EMX\ETB\ENQ\NUL\ETB3\NUL\DC1\99185k\fJ.\1031366\48850A\DC2\185849i5V\1044560\996851:)*\tO\DLE", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "P7\8027gJ\1025598\1050728\tb\1046936s\179130\FS*\r\163897~yj\67377qm)Y\EM\1082698k]SS\990645~;\rp\ENQ\SUBm\1109081QAT\SYNe:\1037799\3798\a=nU\DC2n2\97679c\1105464|\SOH\NAK\EOTL\STX\1014848>\n\SON\US\990037\SI\GS`JOJ\991087v\USY\43061\NULp(S\1006785<\1009666\DC2j\ETB\1074651\1111117s\49393\167041J.,OGU\1015263\1026361.\1082540\&71U\1093020\&8/\1053449\1043583T\EOT\GS c=\53746t}RiOL\RSUsY*h\120237_| \1025261\991499\63211\27137s2redy\1033031Vg\6578\EOT\155956\62493bdQ\EM\1060795-\1079855\1078796\EM\ETB\18365\170958\5129\1084739(Qw!\SOHh\1045601N\52593\STXb\43616d1\1081703{\1034023\FSBE+\r\a\1042611I\988095Rbat=\DEL\57734\v\1087456\139092\1002353oA$\ETX\EOT{o|\DC1Y6\SIleK\984319c\DC2\NAK\9960H\SI\97430\&5\988979\&8'\142119\SOH/\12561\bKo6+Iw\179708\18608\v\SYNN\1061814\SILuF\164362J\1037455\&1\1050949\&0\168187\12201/i\SO\990270\996257\&6g\DC2eR;\ACK\159365\1095404\83044\1057125\SOH\42192_=\1021400" } @@ -144,10 +144,10 @@ testObject_PasswordChange_provider_11 = testObject_PasswordChange_provider_12 :: PasswordChange testObject_PasswordChange_provider_12 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "t\176015z\63099\RS\1019165\72134\1094758\1087142Z\NAKB\1067933+\ETXXj`3.Q\1032039\SUBK\1109183r\1017216\141033\EOT\1016663\52892f=\t\b\CAN\SOH\40431\ETB\a1\187032J4n2eUq\165606\STX\1002769\95144\n\DC1\v'P K\ENQC\49957\&6\995828\abB\\\SYN$E\rCp(\ENQ\997577Z\r\5958\1064586\6052\169035(2\DC3\nwz\EM\1057822`[,\a(\168052\ETBn\1024741\170909\&3\166910-\NAKTp26!\EM%\1067691\983629\1105810X\1084137Ww\160494.S\SUB\RSs5\tS/\188321\STX\179825\14815E\143720\5499::Y%\SI\151589iV\139252BU}]*\GSp'\51146\77903\40692\1032384\EM}\14702k\EOT\1048493a\1024981\&8E\26598\f\1016469\"1uT\US\97604\1086313hC$ND\99182\ENQJ\"S\vCyu\SO\DLE9\137054b\44966\DC3Pe\21040l\44235u\1093696\2097VSM\a\n\1107484\&9\DC4(\a\177489\150593>-\NAK\1030177Mr\1050563\&8\DC3\194810\141213PiK9\EMm\ACKW\SOHvIFX\984936\ac4<\SUBU'\ENQ\SYN\155860x\9048Y>@\1041311\STXp5C\r\163993!z$\1015059\GS\ESC?# UW\1052214\&0&w\ETX\1102267H\190128>-[9z\29338\1008713\SUB@\EOTiA\1113779n1S\136784\tG\DC3\DC4RT*", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "\1029941\182208*\50406\144479mqK=\SYN\NULg\3986a^U !\164592\1000979F\1091010J\137728\43445\ENQTB\152909=N[/5\187278\SYNT\SOH\NULp\1045227\&8\155213q\ETB\v\ESCA!w=x\DC1\RS\29768Z\EM\vpx\US\b(\143914mvW6~'\DC2n&\RS\ETB^]\1092638\b|\1066156d\SOHv-\1092522T\b\ETB\158149x\46579 Mo3)\FS_5\182825Mf\156710VFG\STXOEC\142253(\ESC<1bYVMM7h.:n+\ESCXB~juz{<\GS1\23218\1073013v\1085890Ge\SYN9X\STX\1027702<\ENQx\61722>\ACK^\1108099\1049394\140867P\ETXuh\ESC0\15060?R\SO\CANcT\1025381\1026223C\DC2\"8BY*8\121167XO1\v?z\US9RR\139465\NAKdG\160065\DC4/k\1101568\1097562\46923\ENQk\70387HNx\139984#\997549 \apU\24875\161412E~p\DELe\1024027\32616%(\EOT/\165473Oa\1068906:9'b\b\48968\63083bSw\1089284\DC2pQ\DC4\SO\997853G\143790(A+!\SI\SOH9O\1021380\rWZ\ETBHVe\1038354\SO\SYN>\1084362\SOH}Bcj\1040105\SODC8\180947F\t\1078880yrR\126464|\1002350\CAN\9877e\160489\SYN\fo\n#M\140761\1050789u:j\DC4\"\39095\SOH\1091919I\64643`tpdU\SOH@\ETX5\1015281\169940\&8\1084283C\EMI\70725\EM\DC2M\172398\1098890\&20\17785\ENQzq\v=KLK\1026521\160303\191191\FS\1022904&\1044176\EOT;e\DEL\1092828,59~\DC4\1095506s04[C\74207YbmD\f\40061\\\ETX\EM\153974\169857Z\f~:.\r^?#e\1054794du_I\ETBHy\NAK\DC3C@]Q\167956\65170a\v\1065540<\1097822zRr\vA\1005063\1042686%]\US!\99727.dfV\STXR\54637\1083007CJ3\t\1100221d=\DC4b0\96187\ETB\STXc\NUL\32051B(\RSsx\DLE\FSlYr#@\1048685\145684\1032535\995750\NUL7Py\176787aL?7\SOH'\1032303\1026443\166147;\ENQ\1101976\178862\1064385CP\GSy\NAK2@h4D\74062\98439\98790;\991778+%\142285\1032969V\DLE+Af\ETXr*}v\74561\EOT\b\SYN\DLE\SOH\CAN\NUL\DLElI\1103471Kof\59742\DC4\ESC\185307O\1025693fr/\STXq\62224{\SYN@\1075147p\1092437\DELmvx:fk\1096766\r4\1079176\9458\ETX\t\fP\1068111%c:\\4\403\1072385A\SI\1107936\188641\154662?1\US-\EM5j[([\40806\aQWI1\1012550\1013491H3Xf+A#YF\SUB_lIo\169072\997652\998922X\13580X\1093433\SOH\1032578\989439)P\172195\&0\1014194d\a\RS|\174744$Eu \SOH\ETXF8 a\b\1022530\1066904/]\US{3E9Sf\138114\a_:V\ACK\1111019QWU3\1098773\3051OS XdA[\183160\28570.\31939(b\nL#\1107788[\STX7l*C\1033328\984136\GS1.Z\998716\EOT\46839H\n\1071926\1079240\SOH(l\RS\143037\v\38887>\1090554:-\NULj?%F\1084391I[6v\170273\22502\100077\49274~7G\166097\4519\EM!'\ETX\38398Q?,\GSUu@%(H(I\ETX~>\DC4K\DC3>\72246\EM5\1022086 \1090756O\f\1111314\&2\DC3\25931\994780G9\999039\990590*%\1000152\NAK-\983159\SYN\\\US;\152905JP0$\1106049\37822]\DLE`\1036169:0K\997106hkB\144462\RSH\1102093\33454\RS\989572\1107706:A#]R\SIXFW\"8\STXHW0\986050\45557j\37948\995320\1100585A\1035623}\61155l\49913\1091660\ETXg&?\SO\&H\1054389\1089082\NAKL\98924RG\1101738\&76\5639\1081113@\EOT\SOv:\29442*\DC2xph\136453\"F\n3lu\61648\STXuf\DC1A2M\154097\1091702\ACK\DELtXj\DLE\1096523fT\168155\160774\62409Q\ETX\1073552ah:\1045093ctrC\1008972&:\ENQ1\984019\DEL\72254_,}\1059043Y9`:\DLE\DC3d\1103970\1096003\1102898*Z!\187234\148461\v\52269\&7\63614\SOH'\\TOs!%?\SOH\1093845Z>\DC4\1204t^P\v\a\34585`\147989_\SOE*\1011406\SI\162221<\DC4\USW{\83494[\1054677\&1\1049205duWR\25182\1059779\&9l\FS\a\US\ETB\r\1036646J$Ea\1052569\173473\SUBLpR2\27762A\167459\b\SOUJao\1025597@\17412h`\SO\163155\DC4\1066350E\157076o\1110972dZPjbt\54921\985661k{\1102674\&0|\ETB\50568Q\SOH\152060\&5\rfAj\1062496T\983117U N\31082u\1075887\DC1\157116\DC1IP>\995210'\rz\1046533A\1066921\"\181434\&8\164987|\63500wXC\NUL\1064912?,X\1019667m\US\EOT\96637N\185883_:d\USU\167304A\1106870*'`w|6\1045529\&9+\166106mjC:v\1053515\39282\10936388!Acu\a\ETB6k\SYN>\EM-\22513g\149536S\DEL\RS\1023314\1096302jyZ\1066742\1070063U1G\SYN(\180738\US\1006809\SOH\1114037M\172262\a$CT\CAN;iezO\150819!\1105298t<\1055348\149076oogs\SI\ESCO\a~\DC1\a\DC3\1018432\159829t\15910\37325|\DEL\CAN%\1010165\&9i\156087\&5\144925k\1050355\49336\11211\174004\1051581\DC1K:\RSE\ETX\5991:f\1098856\19403\rN\49047:t\SI\1053824\1038465z\149922V^?D\v\vki\SI\ETB\1001377?u\"4L\1021111&\987357\21606B2H\173390\35107\&5F\34214\GS\DC3\\\1059063\&4" } @@ -166,10 +166,10 @@ testObject_PasswordChange_provider_13 = testObject_PasswordChange_provider_14 :: PasswordChange testObject_PasswordChange_provider_14 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe ")\1036280s\137216'`\59330\STX_\CAN\180010D\t\ENQzJ\1063390\1045233`D^\111264>\132440\NAK\DC3\176932\49323l\SOH\99704\1013404\&8\1098260\DC3&>\43426\30743x\173643.o\158967=6\1633\1098022P1M\162604\"RG\SUB\1038025\FS\STX\CAN:\DC4\ETBu\CANx\1089236\1061187@(E\1002850\&4~\ETX\NUL\51238\&4X\NULH\DC2pll\EOT\DC2\177807\1104201\DLE\17185[K|W\DLE;\144266z,C\USD\983816O\NUL\riV\">=^oX#&L\1049388Kq\25975YW\1033425t\1055427\22674\nPF~\1082938;42%b.;t \1040882\1039127\165132\DC1\1064926PV\26969z_xZ\SOH)Bz\t1f\DLE9/\FS7\1093628,J\33998\72145&Q\NULe=\FSK+\SI\25383\1028788\1022136n\97202\SO\173088\&8p\DLE\ETX\111158|,\STX\1033460\1104436d\1050868o`C\SO\132042\f?Hy&\36586A\46227\141006e\NAK\DC1wJ[\fc\b\1070422l\141230\DC4\64151W\DC4R\1045214t$\136334v\61852~r\1060898f\1071586\&3\SI\1019583\\D\ETB>\164308b\EM\133344w)\1053343_(\1058134[ra5U\SYN\178080\72861fC\141152\&6\1011495\STXy\100396Ii\1109445\f\184085z0\164727~\78749D\rhqu'\DLE\SOH\DC4\145824V\\\SOH+Mu\1041477S;w\141810Z\1041792\EOT\NAK5mo\USZ\1079915\172082\1069321\1090200/\ETB]\aD'\NULx+\STX\ENQI\NUL\DEL8\RS\1055834gcU7\1066759\NUL\7502\RS\995972T-", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "\DC2\t!' czxpt\186414}\1036760Z|\1105851\&3\1098742\&9cE9;qlI\128862\&0d\1041894\1071062\ETB\SUB\13291C(q\CAN\DC4\92178r\992045\n\37621\SOH*\26079\&3xLD\97230\SI/AS\CANX\DC40\NULC\134548\DLE^\1113807P\SYN6U\"2N\STXv%\1078605} \17042\4719\ACKB\v\t\989972s\138208\&5\DLE#\53263\1088280\STX13H\173756#@t)\188467BEV\ACKLCX\SYN]\DC2P\181437\FSN\149514\186718?\22604(\1090926\15413\1065637\EMO\27585\\r\FS\\\SI\1035346\18565\1013435vU\at#\1062175\"\EOT\SOH{pE\12478xJ\DC2g]\ETX\DEL\DC3c\SOH\fPX|\1066931wEAe!\993681R\ACKu\1113614..$\1068793\27316\&9(\SYN\58010c\1002603\RSw!\SOi\1030926>qPl\ACK\STXXmG-\v\120608`\1088763B\FS%gW\ENQ\DC4\n\STX(\1074703\b\DC2N\1109769\&7H\"k\EMty\ETB\187962A\1020329.:\ESCj\re\GS\161991\1034321-*q\ETX\1093441Y%A\46791hf\n\141128\DC3J\157117\SUB}4\EOT\161237\v 8$d%\b\28128n\989856K\DELf\t\NUL|;X\ACK\142667*\CAN\SYN\24303\v\4878\1760Yj\vyyk\1026116'i\1080447?U]\ACKb\ETX\48209&\"\29031\1039525}\63031P\13226%\ESC\t\1047966\1098216\ENQ\ACK\NUL@\ENQ\1106062%D\41968\94208C\NAK~p\SI;\GSz&\SOj\DC1\"7\66276\1081689\EOT\DEL\990793\bdiX@[%}\1072552\1098336\&6K]S\ENQ\1012057T\990893\154290\&24g\USe\STXpS\82979E\FS\ACK\71075\1087888\DELo\29366'\t\SIv\995645\&7,\ETX\SOH!i\1031533\1081283\&43XW\t4\FS7\1101016\1108161\31300?\1083887@\1048301g\DLE\DC3\1067632Mn\GS;\1044539*k\147748\&1\DLEW\1112391\1103992*\vB6\158062\f\68308=P!\1112874\45394|9Qv\DC4q\1063868\1105018\1004357Z\DC4\1097524;9J\160053+\fLa\DC4\1010823\&2\1049908<\1055415\EM\1053244M\ACKDO7NUa\34227\1041181[ \181881\a1\US\vc.\1012405\ACKhi)\162677\35949nG\27679;\132913\1026232f\182327#t\1027272B=\152450`\180605\&4\NUL\1006343*\1075473\b\\*\ESC\177216\CAN;\ao%pAH|\n\EM\tC\143291ZrZ\151170IF\ETBO6=bh\STXzV\1013862P\1064457\\\159157:,U)\58595\DC4\1013245\1075630\GSg\SUByv\110788l\\\v\b^\31887Ii\ENQ[\65195y\1088707\&8a\CAN\t\vDeb\1007440i{t;oR]", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "\152852s\1068988Df{\1109851>\DLEN\1096871:,\1104054 \134810\3245;L\133604uN\989402-\"A9t\1089099f\CAN;q\1046261\36961R<&3\ESC\DEL\72254\NULq\n9e\SUB[d\1062509^\v\CAN\61899U\18913BM)\DC1r\SOHevfz\EOT\118837k\152950\&8zn![nE\40274\a\148239\146181\1034404\DLE\NAK#\1021834\\\7383\f)We\135919\ENQ\1003933\1042129Y\1083464W\184760\1043368\&3\DLE\\\b/\1058055<JXk3\78374\987565\151899\147262\27256\ENQLe\138865V3>\142078\32373\&4\1065316\35039F\\$k1\f\DC2f \v\121169\FS_\1066161b-\6727&\995927'X?Jb,\12990\158842\9204-\ACK/\983518\1100707`\ETX*\9732!\DELf2&/\SO\78519\ENQ\1006374\ESCU\1108463\993917\USJq\155123R\133864\&2Q@\ETXq\30171sM\"\DC2{b\v}i=\118851\DC4p98\DELsa\1110153 NiV\1016779f@\162996\1062077F+Z\154196\DC1\1093124Zd\66026gh^\SOK%\142282I" } @@ -188,10 +188,10 @@ testObject_PasswordChange_provider_15 = testObject_PasswordChange_provider_16 :: PasswordChange testObject_PasswordChange_provider_16 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "gB[\SOa\136597\&4I_\1070522$\US-E\f\bD\184839l\53618j\145196\1063189e\96537*\1053243g(hJ+E\\\STX\100410/\ENQb.\7738\1113147\\\27214\9423\EMu)-\DEL\FSRh\DC1G>\DLE\ENQ#\162187\94536\v&\DLEd[5\DEL\DEL7U+wZ~e\1065056RT\1015376~\47206\1049409\SUBL~\ESC]\175086KpHWu_\1093704uN=\b\1029782\EM\US\1041680\DC3\DLEd47\1016259\1094650\SYNZOiX\61511M\DLE\DC14\1023510\55250d3h>Jw<\SUB\1105863$*)\173139\&5\t~\36451U\v\1007351\&8nb\DLEXH\137571\DC1Q7kP\186382L<\1078705$+\1081663#\ESC\38858*K\180009%\154955\65738\f2\v~A\1011551\f}\1100334}(%\SUB\997989PA\NUL\1081198o\1064382\SYNV\NULv\159271\ETX\54311\1064618os-\1091683\NULE\STXcl0\137068f\1050864\186833\174746\EM\25158sQ\1071799\60428\5196k^=_l\1066392f;O|\1063397YO\"P\NUL\69230\\\35862\"\ACK\"~\141584X\1038172\ETB$\95964\CAN\27381%UQ\NAKy\1029066\1073585-W\1020228z~\GS\22450\1113567J~\DC4-?:\1110536\rf\1065914\a\187988\1098168^.\26197T!*\1037028~\1073514\SOHF\am\27257\158078\175061\\\SI\147288Vk\99196w\1092949\186929_D\DLEq\1086094r\131393\NAKy2\ETX\ACK{Pq)\1037265\99424\993708t\169393e\NUL\US\988887`\159377\EM\1002749\STXY!\50906\fY\ENQ\1078545ERU\990479>\NUL:ZT\1035772.3\CAN\1096695J\SUBN\ETBZ\153481\a\16088\DC2m\ENQ\ACKDzhd(<\SIF9-N^|\983096;3\993521%\164480|\SOH\1080654\32149L\DELs\72704/G\161452\&6\1001045T<\US\SO\176234\b\132812}\1056421\7504k\19220\NAK[t\DC2U\SOHIX(\1069119E?9*|-/)3U\ETX\ESCo\SO\1099860\SO\RS\1009682E\te#\DC4m;\DLEfx\SIm\1046538\a\97848\187828\&1\137971Kd\CAN\1042040%\ENQ\173735;\1027791:4kcX\37202!>j\153067hT\DC4\163467~\1060082\995785\1005593\190617\1113881\199\DEL(6r\DC3\145739\EM\ENQx\GS9E\SOH,6\1064646\984090\&5Zv\US\70154)dssy(\NULco.#\"\NAKXW\119053gS_\31565O*\142194\"\1067622&Xt\DC2\r{Fi\48778 ]ZZq\994122:\177062:\1052139U\ESC$zN\SYN9\"\10761\&10b}X'`\190793C\1043334}\r\1111369\1021511*\r\SIk\1062958`E?=`\46022\78512l\1068151*j\36518M\1020065\37308;\159311j&\DC4?#\191316\ACKs[iF:\n\16090\991139\1059764v!b\1112922\1068230\SO\985232\141755\1112084j\DC4A\SYNT\NULNY1\1078882\a\136436\1088153blf_n\DC3\GSB\ETB\11380\50340q\991037-B/B\16269j\178019\ACK\1079155rr7x.x~\151350\DLEcIIK l~\vt8z!)/\24279~?yP1BY\ESC\1044'#7\\(p\159717Xx}\150971\145409x\15522X}8\SIw$\1067337Dy\68912SR\7036\54589\1086756R'\EOT\1016478 \1086591>\1072777\&65\DLE+\EOTm\1097089*vMO,4\24600R\1049889W\991833\FS\EM\154481\":H\SYN_K\v1(\23407\&5g\189510=\ESCY\v||]A3\1090464\&8fI\f\1089249d\27681dw:\53053:e\FSX\140812&\26383\58555\1020960\153568\&6\ETX7?Z8\DC3i\10727\32848U\987253'D>\ACK\1099263\167302n\1084348\SUBe\5445\DC4=F\SI\GS,!jSM\1037019_\n", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "s/\DC3\a\1019919;\186152\DC1\ACKL\NAK\SUB\123639\&0\DLE5\988934\1085942\ENQ]y\n\1088660\EOT%#o\181283k'N\5859\164247\1012481.U+D\ACK\US7^\1106302\DC4&\73089\US46zmN\1078548EX\1100831&hV\147473\&2#B,EY\1062234`\DLE\986448\25566\EOT\EOTv\1060131\1011335\6586\8606[H89oS\141447\v(Isb\DLE\1009984\999533Dv>S\ENQ8\NAK\1013739\31809CBi9C\f\ENQ<\1060837\EM\1002394\RS,/g\1021216\&6S$\CAN\CANTc\145792+~\CAN\38440;P\EOTh\SOH\990223\GS3}j {p\1006877\169025\158467D`\1014376mN-\\V\STX\1106691{\SI\ETB \NAK1#6p\994323~\992677\1043440\&6\1059978Q\\8\ENQ\STX\1011902]ynrf~>n&\6380\94374 m_`\1080547D@\DC2k1,>\1098067v\132299\&6?\ACK\1012654\DC1\SOHjL\16576\&6\USld\1008037\189738\&5\7878w\62207$l\f57\NUL\64524\1073108sg\"Mf\ETBuKPsa\190219zLTtY\136016\&2\"R\18939\66650\FSw\b\167281\US\1065135\50725W/\126507\94452}S\164388;X\EMe`\94080\NULZ-\995401\43858\n\\\EOTy\187923f?\1090882!\SI3\n\ETX0\1046274S\39868T\189058\ENQ\SOHB_{&\FS:\FS\CAN\988362\9506k\1054019N\GSWR_>J?,(\1101196r\1035425\1088638\1003569\43364)\997661\1029696q4U{\21915uqQA/\1036564\1028269 ]4L\100553\29035\25204m\ETX\179784b\64823\1106448\GS\151115\DC4F\1004969G0@6\1077837\DLE\137744BV\1081634l\1081851C\1065998Q*<\SUB4\ENQ\99901\DC1dh\1085582\1100640V\128022\FS\1012025oZ6>\24971I\1046586\&2@I^\DC4B\ETB\ACKmD.\137741`sB\19558\183636L2\DC32;\GS1L;\DEL\141722p/\bCj\47458G\27338SXe\97073Z\ENQf\43154\&2_\6385\SYNr\DC4/C1R\DC2" } @@ -199,10 +199,10 @@ testObject_PasswordChange_provider_16 = testObject_PasswordChange_provider_17 :: PasswordChange testObject_PasswordChange_provider_17 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "I{A\1081115\1040244WH\t^\1024680\STX\DEL]~D\n9:\NAK\ESCy\1040393$%7?\1048769>bm\DC26V\1065346\16705\ESCQMle,\1015761\21766\1105355\NUL\1020694\GS\v@K\1043881aC\1094072GI\37368\1075187\1079031@W\1038319\1001363\997424\157615\EMa}\SOH\12756\1081169RP\1087906\SImNi\98040.6\14300\35116\STXe2S:X\\y\1027289R1\163603}\1012190\1110662\119901d\146377Xi_\1058064z \185806\47296\28402f#\ACK\b\1018085\fDK>\1092488AV=6\b\DLE\GSi\DC1\SYN\1108215=W$M\1102069\ESC \DC4u\990358Q\FS\78744u)\1056471Qf\US\SOH-\STX,\DELnCr\\[\25698\CAN\ENQ-G\SO7\9176m\RSs5\CAN\NAK\NAK\1081901`-fY\1028198\ETBL\63765\ETBX2[X$B\FS\ESC|\n\SOc\1093611X\NAKW\1010429q\1046880^`CD\DC3SMJ\STX/wR\NAKmB(\EM-\1034880xI\DC4A\1078737\1103535\1083873yw\fn\1057985E\1101283cP1\189927\141738\1011422j\1037710_\NULb>&^\EOT\58470<\r\b\rr#hu.LY,\tS}\ETX=Jz\1113866\78095B[\f(yR\997282\SUB3\140630\DELl\1016964\SYNIa^\50526*\988502\&3z\11825%<\ESCx\48956=H&u=\FSs5_X/\1021976Q\1059092dJ\172609ghu\44563\ESCE>3\1072325\189630Wls=)'NG\1094331r\SYN\DC1%}8%&\154657\&8:\ETBKQ@?>k\1027270\NULb\9895+Xmu\1011506\n\33550-\f\49393\51542\DEL6\1055009\CANw70*;Q\1072772}y#\ACK}\ENQu+2\163564\171133\vTF\v.Pq\DC2`M\174005\96741\&1\SO\njL^sg7\1081135C*\1102887o\1088885{y\52998\ETX|\1023709\EMF\135291vxdQ\137699:\SYN]e\DC3^\SOrv\SUBf[5\NUL6x1E\1005855\1048944\1080103\v\993780\v\DC1\SO\988187\"p\35477k]^/=+\1067771\n\1074075\SUB}A\DEL_8\RS62\ESC\DC2Bk\29783C~S\EM-`\n\186446\EM\NAK]J!p\SUB!Y\SI8m(cS,\176277<\168390\1057601\1082774'\1092313z\989733\ETBwe\1013727\aa\n\59433Fb>A\1088804|\SYN-\STXXz&/'~(cy)hIF\181057\DLE-/XyR\147207\1035531\99846\ENQr[\r\1052642Q\1054366u/r\995938\NUL\ETX\r\tTj\190394\155541d\1046953\DELm\vb'<\1059546\45701@v\53022T5dlF\f\157714gRJ\1037469\995950\1090231\US\62219j\bF\\\v\f]}4[\1091004\&1J5\1101864fM\1002697P}\1109954]xd\44742rn\USi\127844zuK2\64008qwmc\57449\SYN\SUB\r55w5q|\DEL\1004339\DC4\1000580\NAK\\;\97824(d\STX\1069352\126106#\SIeP\1014578pc\b\1006213\78367C\119203\b\DC4#C\183272M_\DC4 EY \140657\STXg\US\rq.\99689._\44617E\ENQ\STX\21293\994346.\1010642\165378s 7p?I\\\1016448NG\1016271\vH.\1011097\&4Hz\DC3a\1112965\1099391LS\956\NULnx\ESCK]\v\1014996\ESC\1001785\DC1<\1059607M)\1103444M\52771o\DLE\FS)6\52537\78102YfQ@F\140229W\NAK+\158031\f_b\999514_p\1026159N\vF\RS\"f0\FS\DEL?#v{\1062480\GS\ETX_p\STX[\170629Qb\37443\&5\1079682b\b\1113122O\987041\r\30653z\GS\1034134I`\ENQ\1074991\ENQ4\NUL|\GS\rt-\FSY\ETXea\164217'\ETX|\25860qY\137837\EOT9Gyz$ME\1012376HF8\1101936\DC4\1040797\&4\DC3d\38807w\EM\1087666PY6$aKV\RSkHNh\ENQ7\b\154234;I-J\1010947rSDa\51431C\157687\SUB\US\SOHV%\ETXB\DC2N\DELGq#Q\173949>\1049166\ETB&\SUB\1027480\&6c{:\NUL\1026276{\tq3\96886\1014266n\DC2C\993425\17816nLR<2bXS\ESCe[l\1097388\fjjZ\1004264w,a\143819\r\SYNL\1049703K\EOT\FS\v(x\141566\1002452\49875l{\986046cp\\\GS>DA*\186399\189082v1`[I\1087573f\160956\&8j\SO\144181n\60434\EOT7jx \v\DC4\SOH-\"\1051346+`\STXF{y56\186936T\17962D\111297^C:F%5B\1113608\183649eU" } @@ -210,10 +210,10 @@ testObject_PasswordChange_provider_17 = testObject_PasswordChange_provider_18 :: PasswordChange testObject_PasswordChange_provider_18 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe "\ENQ>$;GNS\NAKq\178874\58968$-\68366|\151635\70868GG\1069152+N\994629igAv\68290\n\1040872\DC2Tl\ETX\CAN\173661\ETX\161735N\42419v\149562\98179\&9$v$R/\69765U,\a\21766\CAN\ETB\1040043-O\DC1HY\fK\1067449aS6V\NAK\1040221\996914\850D\54816Ke\ACKj!|!\ETBo]Xg)X8P=\ETX\ENQ\RS.b\"xp\1105843\&3i?kTDu\1098220T'.\1071583x%0\DC1+GA$A\45313\a\ETB\ESCa\1028493\1096329+\1019183\1111460\SO\n\SOH\ESC\65042\155997\STX(]\1046910GL\DC2a\151097F\a\168528\fdB\ETB'\165547+\vTf\992507I\1045958\&4r\175158%\RS\999749Xq_*g\994465q\RS\SYN.\1013841 \SYNzQ\NAK\986554:\181318(d0i\1025168Z\1057887\nn\988266sa\61001\SYNx\DEL\170759A\1027650?LZ\CANO\1039286\57543\&8\RS\EOT\992522M:unvs\ACKco/\1103889\61376:$\1017208O0l:\4771j\1071859\NULB\DC4\165947e|\1104789\119138`BCH\92363\&42\59532\999574%Y6\1036574\b|\119002n\1008926\NAK\FSb\EOTso`\DELy7H\1144\SI0\12299B`!\n\EOT#L\51662\1103006|\ESCa\1038787\DLE\r\1067701\&0Y\135747#NKj\1106283\42365\SO,9\ETXh\DLEw\128979R\"(\EOTV\r\14540\&1\989621\DC1yq-e\f\1035089r1YU2\1087695\171952\DC2\SO\1105513\1062941[\1019605_Za\52679\DC2\ENQ\v\1046141\\#\1076425\a\FS\16658b5C&>dt~y)\66452\ETXg\141548@\DC3\182735\ETB*$\DLE\1313UnKs:vVS\1095798m\997335\15618X5f)}@{ha\DC2\f\1041518\ETB-\1054867\t@\STX,\1099907\990571\STXD\1087623\SOH Cm\1004594sDY#YL\1090422;F\156423\v\GS n\a\v\157412c6+,\1004205\EOTH\1063061\33351\vf\1065194-ZS\ACKB\SIFd\buy)\128544\1074733i\1092468/\SOvZw`JB>\RSu}\1107475\1036872\35763%\185556n5\CAN\60703js\1039874\1078173\135796uvW\"\28047h\1043723P\DLE\1097958S-<\187968\34464\186710/wb(\26583\&3\n*kt+\180850\SUB\1021642\1068226x\983171@\DC3\SI\US\\0\NULRA?\SO\RS\189447LuW:Xh1\SOHT\DLE\94916 VOG\DEL\RSO\189514k\3015\1025016;l`g?q_ \1011679\92993\984171~\ny\US\1045482\52577N\1029913\EOT\SIA/j\STX\1084693\DEL\176033\136608m\GS#z(\127161jW[\1038238\1073630\a\1060787\&6\nnn\145137\188550\DC1\1068174\989085t(l;\1017830Sm`tO\r\57362\NUL\1101579|`x\1071012t\153686\EM\163617/\DC2yuB\1072146s_\183212UrcO\99677\1081043\n\41654b\1797\GS^\132600\142485}Q\1109755\DC3G!\1035773\&9L\195083y\2453*\SIU\vk?T\1064435;\f\\Q\RSk\1085726\172950\188191|\6976\1114033Z\155291\tl\NULb%WA\49679r}u2\1059498I\DC3V'i0\158983/Icz\74116\1054934\DLE5L2K.\ESCKLr\DC4\1046820\1052056\STX\\\SYN+\DC4G>\132569>N%R\DEL\vR\\L\1082431*D\SI4u7\DC3AW\STXG\144748e2%y\bw\US\SUBZ4x,\ESC\67889\f\47421\DC2r\1004074r]%ws\DC4]y0\1014309I\993296n\1010689.l\NAKLAv\EOTJ3z@d\27165\42869\t" } @@ -221,10 +221,10 @@ testObject_PasswordChange_provider_18 = testObject_PasswordChange_provider_19 :: PasswordChange testObject_PasswordChange_provider_19 = PasswordChange - { cpOldPassword = + { oldPassword = plainTextPassword6Unsafe ";\93029\64850k{\EM\ETB\183122'\ACK`{2\16834\ETB\DC17\b\aQ}P%O!_^d Tf\STX\177895sQDd\DC1?}\b\NAK\64801!7T\180836L\ENQ\30743\STX\DC3\GS\a\US\1011186\DC2\DEL1EEV(x'\DC1\SUB\1061407\t\GS\ri\1100649\&4*\1060200Fs<3\FS>}Y:\DC2)8+4+\STX\985320\1012240\134405\vl\NUL$\v]rZ~Dy\1045002MF\CANlw\995758x\1076847-I*bE\1065762A\189306\&5\50057c:R^f\\G)\44960\&9\DC4$|\STX\"y\1020665\39604AW =\20322\1091813\&1\1108618'\1051166x\39102\&6U_\997336\ENQ\50873<\1066165*\ENQ\1029616z\186193U\DC1:4a\148141\32437@\135977\177323\&5p\1110836\NAK$[\US\74077.\987765\&7\NUL_\988982\&8\ETX\r\bg+'R\1027482\1029411~E\rP\1034583\r\181175\46544S\\&.^\60125L\1104199L\a\144365G:Wiws\NAKQc\r\164901\b\141361;\999696|Q\23549\&4\1036417\72875y\29622\f_&fGgH[\1029620u\1052069\23938$\ACK\STX}M2y\99985Z&\189182\f\1110805nzK&\1066016w\n\NUL!-U\SOH\ESC7\ETX6\1026958so$\991139\t\138455\DC1A\27842M1\DC3\SO3.Q[Uy\1006799e\1005623\fl\146202\171029W\1104958C\SIk\71104p@$+\ENQr5\1029753\nB(X)Y\62054>\149953%'\180534H\187026\135153s\11937A2MqW/\18450$#I]\137728\&8sv\49908\ESC\1061880t\1103799sB\988567\&5\"\a\128637{t\23482oJA($\RS;\1067956:\SO\98842\128224\v\141160c\992280\"\1037303\95310oQ>\STXyD\186030\1035343\186166g@&\EOT\95865{x0R6\989091tn\12077x\1106050d}\1016609m\DC4bHo{\f\rM\184517\t\137817\147706<\NAK\179286;dz\EOTC9.\CAN\15836V\SUB;\992386\RS\44724Ho\DC2\93805/\1100285\141534\RS\t\1016167c\STX\1023078\1034365\1046848DavSJ\SOH\STXk\NAK.\FS{Z4\1035470\&0j\1013919{\161435\59533{\148113\EMIW\143598\147178_[", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "4)N`WV1t\171789cC\1064787\SUBi\r\181549K.\1026553\EOTpO\USm\172246\&4JS\119270\NUL)bL>\ENQDBd!\SYNc@\SI,Y\30028`M\47712M\SO\8923{\1025087\ETXk.\30014\EOT\1066825`\120441Zu\169464;>\1079442\CAN\1106555\1111972\ETB\51402%\1109382,Zp+\rM~Q\r)Hv#l\168927\FSDHS\174628\999635j\987945\163692eWG\156827\\7\RS\1052535\&9\\\t\DC1t\1112332\1073530`\143677N\1010456h&\188126\1030547b\6757\1027169\1067336\183902[U\136396\991560>\CAN%\nO\78186\24509\b\1052137\&3Za.u\160812T\US\1033241\CAN6VR>P\188486\1062205\1040066!n(\1023327sWSekN@\24878t\985533V\145005\1095963\r+\1017241B*K\ACK\1074664]Ani&\tsuX/a|,,\74249^\129600\SOHty\ESCf\146951\NAK\150050\DC3E\fS\1032730\SOH\3856\SUB:7=\bgux(Q}s\20372]\DLE\r\44488(\1014999\148549\&2E`yqv;=\DC1\985264#{\EOT=l4PE5\181488D\1035492\138684:_a.{\"F[\n#~\1063106\&50K>\bTp\US\DC4K\1084112/zT\186543\991672\&7\150007\1111071X)\DC1\\,\22596%\SUB1\SIA\STX\RS\10664l\135150\1044470:e\SYN\179418a\17938v\34834\ENQj\NAK[\1095674f\177286W\159061\19064\180331\\]\162954&\EOT,6\GS\DLE\v&g\1039892&36\120707\1051886[IO\1066559\ESC\DC4sS\1010996_a><\f\1020047(.Y}%7z\DC2Q\NUL\CAN\1050803v\1028497\US\DC3J\EM\DC1dEh'Em\133130\999811\SIH\FS\120427E\v\1050653,{\190522iQ\1097210\165473k\FS\1112852\&3}6#\1003388 \94215\1112185\n{\vY\ETB\70801\1019457\v|~W{\1035159\8185\1045959'\DLEO!\SO[\DLEUk\23328)\1101657*\1069854B7#\988898`\992584$C\r\SI\1055353\1075772i\121086\1096003e{N\a\US\SYN\7098\1049584\DC1\SYN\164973\1005568^\US\1074252\1056920\FS\180867^6\SYNWPe\nx\1022949:^\SUB[\EOT~u\26460\&7\RS \44431\DC2\23328O\ENQ}zi\1081683d d\166200t\996444\fk\172000r0\1044826\50464*\994866\&1)$j*<\51631\1008420v|F~\1020301\NAKm#^l\EOT\148762\139353\US\GSW\15496^j\DC3\1080990\aA\\.=3J\995313>\95982}.u\STX\989998:sk\ESC\1107636E\ACKBR2o)\STX\7667S(V\"\n\50026dvp\997353f\DC1\DC3\2034\ACK~KH4K\92412'\58542h\1050852k\1045053i&\a~\20933W\1033711{\1058407mp%\1091729H\1011114i\DLEdZ\1104024W\NAKYb%\bXwgAa\1084701/\1060643\ACK\DC4W+h\1089169S~\EOTQzZ~\SO\1094174E\1061848\t*\100572v\STX#\\K\RS\1045046\111171\EM\149568O'\DC2!4]\DC2\163000\ACK\ESC:\59217\NAK>Y|\31158\t\149865\NULuv\1087835\aV\tb\STX7\1095030~#\184376\165077\41742\1035571\US\DLE\13715\v\1079101\NUL\EOT\1064658\136028\ESC`\1005448V?\1069622{x\SO)\DC24n*\997306nsI L\DC2\CANvo\EM\ap_P\tJ-Pk#ui\1049155\159822b[\1067444Yd\180222\35890\42013V|\49216q\1097565\r\CAN(yW?#+E\SO?Y\29463\63850\DC3\DLE3Q$\a'1\1110820%\r\153831@W\26659-\DC4\52370v\DC1\1013997a\r\SUB\NUL\1009250Jz\983893n\986832\133570\5867\163028O\t\ENQ\999543\&8\DLE\38142A\\$\5442\n)\1058130t\178355\166333CE\163128rpj \DC4O|}\72274\150000&1\1012087)\98960\138897\133873\53513", - cpNewPassword = + newPassword = plainTextPassword6Unsafe "\GSATD~\GS\EMK$\1042815h\993958PO\rPp6}vL}Q\SIO?\SO\SYN2!%\140960\98456\14965IQ\SYN\DC3T\DEL\EMb\\n.\EM\v6G)j\NAKo*G\vn\DC1q\DLE4\1057182\190279\FSG\a\1078359\CAN\STX5\US\r\SO&JH\v\v\GS\SOy\FSZ\1087012*\1054369C36Z\100291I\1006927d\167128\136845X\NAK}\vX\13655\ETBE@\1047002}S\DC1\44983:nS\1033701y\1032307\&6\a1\RSF\151742;\78820|\NAK0\1046714\ACK-\DC3'\1052372\t\DC1J\1031338\t\60374\ACK\1100306&\EOT\USfx\1078008\998032ev\t\17415^p\ESC\41380=1de]\988835zK\ETXt\168935\EM_\133793\SI\128297A\GS" } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordReset_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordReset_provider.hs index afccd106599..a652aed54af 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordReset_provider.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordReset_provider.hs @@ -23,7 +23,7 @@ import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) testObject_PasswordReset_provider_1 :: PasswordReset testObject_PasswordReset_provider_1 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "\1086444\r\1014286bW\1044115\989541\1013077\r\ETX\SOH\ESCj\150487", emailDomain = "sC.\DC2PW" @@ -33,7 +33,7 @@ testObject_PasswordReset_provider_1 = testObject_PasswordReset_provider_2 :: PasswordReset testObject_PasswordReset_provider_2 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "mh\DC1\1112540\a\1111919#\63011e\994580m\122892\189689\161506D]", emailDomain = "\GSJ-0\123200DU~\7828\1089171\NAKF$" @@ -43,7 +43,7 @@ testObject_PasswordReset_provider_2 = testObject_PasswordReset_provider_3 :: PasswordReset testObject_PasswordReset_provider_3 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "-BP\CAN\1007058F\19503\1100657]W\1039512d\138837\1077790\ACK\GS\138454Dy\ESCx\CAN\158675uOU\987404\CAN\1075830\ACK", @@ -53,12 +53,12 @@ testObject_PasswordReset_provider_3 = testObject_PasswordReset_provider_4 :: PasswordReset testObject_PasswordReset_provider_4 = - PasswordReset {nprEmail = Email {emailLocal = "\ETX!\DC4]$Zp", emailDomain = "R\STX\DLEQ"}} + PasswordReset {email = Email {emailLocal = "\ETX!\DC4]$Zp", emailDomain = "R\STX\DLEQ"}} testObject_PasswordReset_provider_5 :: PasswordReset testObject_PasswordReset_provider_5 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "\1008001Mm\145584\FS9` \146161\994039\DLE\150684\ETX\44961]\1047951\27506\&2\SI\ETB\45081", emailDomain = "B\989792{\n\1049497O\EOT,P" @@ -68,7 +68,7 @@ testObject_PasswordReset_provider_5 = testObject_PasswordReset_provider_6 :: PasswordReset testObject_PasswordReset_provider_6 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "\5297\1059611\152727\ETXLv \1037185\&9", emailDomain = @@ -79,7 +79,7 @@ testObject_PasswordReset_provider_6 = testObject_PasswordReset_provider_7 :: PasswordReset testObject_PasswordReset_provider_7 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "\SOH\ETXrra\SOH4|]c&4%#Al\DC2*U\STX\82983m9\SOH\985551UQ\41944\1046828", emailDomain = "1G*\155832f\CANV\996525\15378\98283lR\51561" @@ -88,12 +88,12 @@ testObject_PasswordReset_provider_7 = testObject_PasswordReset_provider_8 :: PasswordReset testObject_PasswordReset_provider_8 = - PasswordReset {nprEmail = Email {emailLocal = "6\1063459C\37237(|\NUL\RS\133203", emailDomain = "\35140\EM\39282"}} + PasswordReset {email = Email {emailLocal = "6\1063459C\37237(|\NUL\RS\133203", emailDomain = "\35140\EM\39282"}} testObject_PasswordReset_provider_9 :: PasswordReset testObject_PasswordReset_provider_9 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "ui0^p\1017396\ETX\994732\DELu<8\"YgWb\bx[\RS},W\v\1043359\32800\SYN", emailDomain = "\b*\1030521\&0>*N`\134311\DC3 t" @@ -102,12 +102,12 @@ testObject_PasswordReset_provider_9 = testObject_PasswordReset_provider_10 :: PasswordReset testObject_PasswordReset_provider_10 = - PasswordReset {nprEmail = Email {emailLocal = "", emailDomain = "$y0=|\GS\1042508E\1079919!tN:"}} + PasswordReset {email = Email {emailLocal = "", emailDomain = "$y0=|\GS\1042508E\1079919!tN:"}} testObject_PasswordReset_provider_11 :: PasswordReset testObject_PasswordReset_provider_11 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "\57466\DLE\SOH\97075\40644K!z|\135037\&0\9622,1,\1083909\&4\\\38025Q", emailDomain = ">Xi\1078572\SOH\DC1:\1037092\180278\166228\SUB[\CAN.+uOgWp" @@ -117,7 +117,7 @@ testObject_PasswordReset_provider_11 = testObject_PasswordReset_provider_12 :: PasswordReset testObject_PasswordReset_provider_12 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "\1068401\168354\128598>", emailDomain = @@ -128,7 +128,7 @@ testObject_PasswordReset_provider_12 = testObject_PasswordReset_provider_13 :: PasswordReset testObject_PasswordReset_provider_13 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "\994700\&5\ACK\132331!\1085699\nVb\1027357nU&\1037025u\169968", emailDomain = "+I\176471q\1064856\SYN\1069753#A\163779\DLE}.\SOHu\1015059" @@ -136,12 +136,12 @@ testObject_PasswordReset_provider_13 = } testObject_PasswordReset_provider_14 :: PasswordReset -testObject_PasswordReset_provider_14 = PasswordReset {nprEmail = Email {emailLocal = "v", emailDomain = "\1090313"}} +testObject_PasswordReset_provider_14 = PasswordReset {email = Email {emailLocal = "v", emailDomain = "\1090313"}} testObject_PasswordReset_provider_15 :: PasswordReset testObject_PasswordReset_provider_15 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "+\150753~\1073496VFc\RS\1102900R\a\ESC4J_\1087106I\f\1043823Dj\DC1\EOT\62142q", emailDomain = "\1020153\138280n\1062475Gh?\vPXOO\v\1092723\DC2" @@ -151,7 +151,7 @@ testObject_PasswordReset_provider_15 = testObject_PasswordReset_provider_16 :: PasswordReset testObject_PasswordReset_provider_16 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "]\1111436Dn\b\NAK\n\17695\167052\ENQ\1024236\&2\r\1069249\1002489\1038720", emailDomain = "%L(\EM\1109782\STXk\EOTo\170961B\18655O*/+", emailDomain = "\48353"} } testObject_PasswordReset_provider_18 :: PasswordReset testObject_PasswordReset_provider_18 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "\FS\1022850\1012117^3\68431*(\1037814\99655", emailDomain = @@ -179,7 +179,7 @@ testObject_PasswordReset_provider_18 = testObject_PasswordReset_provider_19 :: PasswordReset testObject_PasswordReset_provider_19 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "x|\58643\1101318J8\1007195|%\142798'9\1089195\172026\1085440F\1098543xyP\1054659 4,", emailDomain = "!]w6:\SOHd4t(\1103884\1052833$\SOHrl9\9929\120677t8" @@ -189,7 +189,7 @@ testObject_PasswordReset_provider_19 = testObject_PasswordReset_provider_20 :: PasswordReset testObject_PasswordReset_provider_20 = PasswordReset - { nprEmail = + { email = Email { emailLocal = "\39795\&2\SYN)=Xd\155177}o", emailDomain = "4\SUB\188588\1054317g\NUL\1092307\984568Q`\\\SOU\1017696" diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index f6aec92223b..117b135aa6b 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -64,7 +64,7 @@ run opts = do -- Close the channel. `extended` will then close the connection, flushing messages to the server. Log.info l $ Log.msg $ Log.val "Closing RabbitMQ channel" Q.closeChannel chan - let server = defaultServer (cs $ opts.backgroundWorker._epHost) opts.backgroundWorker._epPort env.logger env.metrics + let server = defaultServer (cs $ opts.backgroundWorker._host) opts.backgroundWorker._port env.logger env.metrics settings <- newSettings server -- Additional cleanup when shutting down via signals. runSettingsWithCleanup cleanup settings (servantApp env) Nothing diff --git a/services/background-worker/src/Wire/Defederation.hs b/services/background-worker/src/Wire/Defederation.hs index 6f1efbb09bc..e8da0e9366b 100644 --- a/services/background-worker/src/Wire/Defederation.hs +++ b/services/background-worker/src/Wire/Defederation.hs @@ -19,6 +19,7 @@ import Network.HTTP.Types import Servant.Client (BaseUrl (..), ClientEnv, Scheme (Http), mkClientEnv) import System.Logger.Class qualified as Log import Util.Options +import Util.Options qualified as O import Wire.API.Federation.BackendNotifications import Wire.API.Routes.FederationDomainConfig qualified as Fed import Wire.BackgroundWorker.Env @@ -99,8 +100,8 @@ req env dom = defaultRequest { method = methodDelete, secure = False, - host = galley env ^. epHost . to encodeUtf8, - port = galley env ^. epPort . to fromIntegral, + host = galley env ^. O.host . to encodeUtf8, + port = galley env ^. O.port . to fromIntegral, path = "/i/federation/" <> toByteString' dom, requestHeaders = ("Accept", "application/json") : requestHeaders defaultRequest, responseTimeout = defederationTimeout env diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index 51e8ce51b38..d80121fdc69 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -6,7 +6,7 @@ import Control.Concurrent.Chan import Imports import Network.HTTP.Client import System.Logger.Class qualified as Logger -import Util.Options +import Util.Options (Endpoint (..)) import Wire.API.Routes.FederationDomainConfig import Wire.BackgroundWorker.Env hiding (federatorInternal, galley) import Wire.BackgroundWorker.Env qualified as E diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index ece2d3c737e..c8da1a5e702 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -102,17 +102,17 @@ getFederationStatus _ request = do Just AllowAll -> pure $ NonConnectedBackends mempty _ -> do fedDomains <- fromList . fmap (.domain) . (.remotes) <$> getFederationRemotes - pure $ NonConnectedBackends (request.dsDomains \\ fedDomains) + pure $ NonConnectedBackends (request.domains \\ fedDomains) sendConnectionAction :: Domain -> NewConnectionRequest -> Handler r NewConnectionResponse sendConnectionAction originDomain NewConnectionRequest {..} = do - active <- lift $ wrapClient $ Data.isActivated ncrTo + active <- lift $ wrapClient $ Data.isActivated to if active then do - self <- qualifyLocal ncrTo - let other = toRemoteUnsafe originDomain ncrFrom + self <- qualifyLocal to + let other = toRemoteUnsafe originDomain from mconnection <- lift . wrapClient $ Data.lookupConnection self (tUntagged other) - maction <- lift $ performRemoteAction self other mconnection ncrAction + maction <- lift $ performRemoteAction self other mconnection action pure $ NewConnectionResponseOk maction else pure NewConnectionResponseUserNotActivated @@ -166,8 +166,8 @@ fedClaimKeyPackages :: Domain -> ClaimKeyPackageRequest -> Handler r (Maybe KeyP fedClaimKeyPackages domain ckpr = isMLSEnabled >>= \case True -> do - ltarget <- qualifyLocal (ckprTarget ckpr) - let rusr = toRemoteUnsafe domain (ckprClaimant ckpr) + ltarget <- qualifyLocal ckpr.target + let rusr = toRemoteUnsafe domain ckpr.claimant lift . fmap hush . runExceptT $ claimLocalKeyPackages (tUntagged rusr) Nothing ltarget False -> pure Nothing @@ -220,12 +220,12 @@ getUserClients _ (GetUserClients uids) = API.lookupLocalPubClientsBulk uids !>> getMLSClients :: Domain -> MLSClientsRequest -> Handler r (Set ClientInfo) getMLSClients _domain mcr = do - Internal.getMLSClients (mcrUserId mcr) (mcrSignatureScheme mcr) + Internal.getMLSClients mcr.userId mcr.signatureScheme onUserDeleted :: Domain -> UserDeletedConnectionsNotification -> (Handler r) EmptyResponse onUserDeleted origDomain udcn = lift $ do - let deletedUser = toRemoteUnsafe origDomain (udcnUser udcn) - connections = udcnConnections udcn + let deletedUser = toRemoteUnsafe origDomain udcn.user + connections = udcn.connections event = pure . UserEvent $ UserDeleted (tUntagged deletedUser) acceptedLocals <- map csv2From diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index 1b99d97975b..bc297e2c196 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -107,8 +107,8 @@ claimRemoteKeyPackages lusr target = do $ runBrigFederatorClient (tDomain target) $ fedClient @'Brig @"claim-key-packages" $ ClaimKeyPackageRequest - { ckprClaimant = tUnqualified lusr, - ckprTarget = tUnqualified target + { claimant = tUnqualified lusr, + target = tUnqualified target } -- validate and set up mappings for all claimed key packages diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index b5bc4eeed1f..1131a971830 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -300,16 +300,15 @@ newEnv o = do where emailConn _ (Opt.EmailAWS aws) = pure (Just aws, Nothing) emailConn lgr (Opt.EmailSMTP s) = do - let host = Opt.smtpEndpoint s ^. epHost - port = Just $ fromInteger $ toInteger $ Opt.smtpEndpoint s ^. epPort + let h = Opt.smtpEndpoint s ^. host + p = Just $ fromInteger $ toInteger $ Opt.smtpEndpoint s ^. port smtpCredentials <- case Opt.smtpCredentials s of - Just (Opt.EmailSMTPCredentials u p) -> do - pass <- initCredentials p - pure $ Just (SMTP.Username u, SMTP.Password pass) + Just (Opt.EmailSMTPCredentials u p') -> do + Just . (SMTP.Username u,) . SMTP.Password <$> initCredentials p' _ -> pure Nothing - smtp <- SMTP.initSMTP lgr host port smtpCredentials (Opt.smtpConnType s) + smtp <- SMTP.initSMTP lgr h p smtpCredentials (Opt.smtpConnType s) pure (Nothing, Just smtp) - mkEndpoint service = RPC.host (encodeUtf8 (service ^. epHost)) . RPC.port (service ^. epPort) $ RPC.empty + mkEndpoint service = RPC.host (encodeUtf8 (service ^. host)) . RPC.port (service ^. port) $ RPC.empty mkIndexEnv :: Opts -> Logger -> Manager -> Metrics -> Endpoint -> IndexEnv mkIndexEnv o lgr mgr mtr galleyEndpoint = @@ -425,21 +424,21 @@ initCassandra :: Opts -> Logger -> IO Cas.ClientState initCassandra o g = do c <- maybe - (Cas.initialContactsPlain (Opt.cassandra o ^. casEndpoint . epHost)) + (Cas.initialContactsPlain (Opt.cassandra o ^. endpoint . host)) (Cas.initialContactsDisco "cassandra_brig" . unpack) (Opt.discoUrl o) p <- Cas.init $ Cas.setLogger (Cas.mkLogger (Log.clone (Just "cassandra.brig") g)) . Cas.setContacts (NE.head c) (NE.tail c) - . Cas.setPortNumber (fromIntegral (Opt.cassandra o ^. casEndpoint . epPort)) - . Cas.setKeyspace (Keyspace (Opt.cassandra o ^. casKeyspace)) + . Cas.setPortNumber (fromIntegral (Opt.cassandra o ^. endpoint . port)) + . Cas.setKeyspace (Keyspace (Opt.cassandra o ^. keyspace)) . Cas.setMaxConnections 4 . Cas.setPoolStripes 4 . Cas.setSendTimeout 3 . Cas.setResponseTimeout 10 . Cas.setProtocolVersion Cas.V4 - . Cas.setPolicy (Cas.dcFilterPolicyIfConfigured g (Opt.cassandra o ^. casFilterNodesByDatacentre)) + . Cas.setPolicy (Cas.dcFilterPolicyIfConfigured g (Opt.cassandra o ^. filterNodesByDatacentre)) $ Cas.defSettings runClient p $ versionCheck schemaVersion pure p diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 9c5cccfce1d..d30876aa078 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -549,11 +549,11 @@ updateAccountPasswordH (pid ::: req) = do updateAccountPassword :: ProviderId -> Public.PasswordChange -> (Handler r) () updateAccountPassword pid upd = do pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - unless (verifyPassword (cpOldPassword upd) pass) $ + unless (verifyPassword (oldPassword upd) pass) $ throwStd (errorToWai @'E.BadCredentials) - when (verifyPassword (cpNewPassword upd) pass) $ + when (verifyPassword (newPassword upd) pass) $ throwStd newPasswordMustDiffer - wrapClientE $ DB.updateAccountPassword pid (cpNewPassword upd) + wrapClientE $ DB.updateAccountPassword pid (newPassword upd) addServiceH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.NewService -> (Handler r) Response addServiceH (pid ::: req) = do diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 305a43911cc..f5ecdff2ae2 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -110,8 +110,8 @@ run o = do Async.cancel authMetrics closeEnv e where - endpoint = brig o - server e = defaultServer (unpack $ endpoint ^. epHost) (endpoint ^. epPort) (e ^. applog) (e ^. metrics) + endpoint' = brig o + server e = defaultServer (unpack $ endpoint' ^. host) (endpoint' ^. port) (e ^. applog) (e ^. metrics) mkApp :: Opts -> IO (Wai.Application, Env) mkApp o = do diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index ea06246298a..812842aef82 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -49,17 +49,19 @@ module Brig.User.Search.Index ) where -import Bilge (MonadHttp, expect2xx, header, lbytes) -import Bilge qualified as RPC +import Bilge (expect2xx, header, lbytes, paths) +import Bilge.IO (MonadHttp) +import Bilge.IO qualified as RPC import Bilge.RPC (RPCException (RPCException)) -import Bilge.Request (paths) +import Bilge.Request qualified as RPC (empty, host, method, port) import Bilge.Response (responseJsonThrow) import Bilge.Retry (rpcHandlers) import Brig.Data.Instances () import Brig.Index.Types (CreateIndexSettings (..)) import Brig.Types.Search (SearchVisibilityInbound, defaultSearchVisibilityInbound, searchVisibilityInboundFromFeatureStatus) import Brig.User.Search.Index.Types as Types -import Cassandra qualified as C +import Cassandra.CQL qualified as C +import Cassandra.Exec qualified as C import Cassandra.Util import Control.Lens hiding ((#), (.=)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, throwM, try) @@ -85,13 +87,13 @@ import Data.Text.Lens hiding (text) import Data.UUID qualified as UUID import Database.Bloodhound qualified as ES import Imports hiding (log, searchable) -import Network.HTTP.Client hiding (path) +import Network.HTTP.Client hiding (host, path, port) import Network.HTTP.Types (StdMethod (POST), hContentType, statusCode) import SAML2.WebSSO.Types qualified as SAML import System.Logger qualified as Log import System.Logger.Class (Logger, MonadLogger (..), field, info, msg, val, (+++), (~~)) import URI.ByteString (URI, serializeURIRef) -import Util.Options (Endpoint, epHost, epPort) +import Util.Options (Endpoint, host, port) import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi import Wire.API.Team.Feature (SearchVisibilityInboundConfig, featureNameBS) import Wire.API.User @@ -933,7 +935,7 @@ getTeamSearchVisibilityInboundMulti tids = do Left x -> throwM $ RPCException nm rq x Right x -> pure x where - mkEndpoint service = RPC.host (encodeUtf8 (service ^. epHost)) . RPC.port (service ^. epPort) $ RPC.empty + mkEndpoint service = RPC.host (encodeUtf8 (service ^. host)) . RPC.port (service ^. port) $ RPC.empty x3 :: RetryPolicy x3 = limitRetries 3 <> exponentialBackoff 100000 diff --git a/services/brig/test/integration/API/Federation.hs b/services/brig/test/integration/API/Federation.hs index 0a539e44555..d69cc968046 100644 --- a/services/brig/test/integration/API/Federation.hs +++ b/services/brig/test/integration/API/Federation.hs @@ -134,7 +134,7 @@ testFulltextSearchMultipleUsers opts brig = do update'' :: UserUpdate <- liftIO $ generate arbitrary let update' = update'' {uupName = Just (Name (fromHandle handle))} update = RequestBodyLBS . encode $ update' - put (brig . path "/self" . contentJson . zUser (userId identityThief) . zConn "c" . body update) !!! const 200 === statusCode + put (brig . path "/self" . contentJson . zUser identityThief.userId . zConn "c" . body update) !!! const 200 === statusCode refreshIndex brig @@ -272,9 +272,9 @@ testGetUsersByIdsSuccess :: Brig -> FedClient 'Brig -> Http () testGetUsersByIdsSuccess brig fedBrigClient = do user1 <- randomUser brig user2 <- randomUser brig - let uid1 = userId user1 + let uid1 = user1.userId quid1 = userQualifiedId user1 - uid2 = userId user2 + uid2 = user2.userId quid2 = userQualifiedId user2 profiles <- runFedClient @"get-users-by-ids" fedBrigClient (Domain "example.com") [uid1, uid2] liftIO $ do @@ -287,7 +287,7 @@ testGetUsersByIdsPartial brig fedBrigClient = do absentUserId :: UserId <- Id <$> lift UUIDv4.nextRandom profiles <- runFedClient @"get-users-by-ids" fedBrigClient (Domain "example.com") $ - [userId presentUser, absentUserId] + [presentUser.userId, absentUserId] liftIO $ assertEqual "should return the present user and skip the absent ones" [userQualifiedId presentUser] (profileQualifiedId <$> profiles) @@ -302,7 +302,7 @@ testGetUsersByIdsNoneFound fedBrigClient = do testClaimPrekeySuccess :: Brig -> FedClient 'Brig -> Http () testClaimPrekeySuccess brig fedBrigClient = do user <- randomUser brig - let uid = userId user + let uid = user.userId let new = defNewClient PermanentClientType [head somePrekeys] (head someLastPrekeys) c <- responseJsonError =<< addClient brig uid new mkey <- runFedClient @"claim-prekey" fedBrigClient (Domain "example.com") (uid, clientId c) @@ -351,7 +351,7 @@ addTestClients brig uid idxs = testGetUserClients :: Brig -> FedClient 'Brig -> Http () testGetUserClients brig fedBrigClient = do - uid1 <- userId <$> randomUser brig + uid1 <- (.userId) <$> randomUser brig clients :: [Client] <- addTestClients brig uid1 [0, 1, 2] UserMap userClients <- runFedClient @"get-user-clients" fedBrigClient (Domain "example.com") (GetUserClients [uid1]) liftIO $ @@ -372,10 +372,10 @@ testGetUserClientsNotFound fedBrigClient = do testRemoteUserGetsDeleted :: Opt.Opts -> Brig -> Cannon -> FedClient 'Brig -> Http () testRemoteUserGetsDeleted opts brig cannon fedBrigClient = do - connectedUser <- userId <$> randomUser brig - pendingUser <- userId <$> randomUser brig - blockedUser <- userId <$> randomUser brig - unconnectedUser <- userId <$> randomUser brig + connectedUser <- (.userId) <$> randomUser brig + pendingUser <- (.userId) <$> randomUser brig + blockedUser <- (.userId) <$> randomUser brig + unconnectedUser <- (.userId) <$> randomUser brig remoteUser <- fakeRemoteUser sendConnectionAction brig opts connectedUser remoteUser (Just FedBrig.RemoteConnect) Accepted diff --git a/services/brig/test/integration/API/Internal/Util.hs b/services/brig/test/integration/API/Internal/Util.hs index 4837e7a1019..ab6824f4d76 100644 --- a/services/brig/test/integration/API/Internal/Util.hs +++ b/services/brig/test/integration/API/Internal/Util.hs @@ -30,7 +30,7 @@ module API.Internal.Util where import API.Team.Util (createPopulatedBindingTeamWithNamesAndHandles) -import Bilge +import Bilge hiding (host, port) import Control.Lens (view, (^.)) import Control.Monad.Catch (MonadCatch, MonadThrow, throwM) import Data.ByteString.Base16 qualified as B16 @@ -46,7 +46,7 @@ import Servant.API.ContentTypes (NoContent) import Servant.Client qualified as Client import System.Random (randomIO) import Util -import Util.Options (Endpoint, epHost, epPort) +import Util.Options (Endpoint, host, port) import Wire.API.Connection import Wire.API.Push.V2.Token qualified as PushToken import Wire.API.Routes.Internal.Brig as IAPI @@ -146,5 +146,5 @@ deleteAccountConferenceCallingConfigClient brigep mgr uid = runHereClientM brige runHereClientM :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> Client.ClientM a -> m (Either Client.ClientError a) runHereClientM brigep mgr action = do let env = Client.mkClientEnv mgr baseurl - baseurl = Client.BaseUrl Client.Http (cs $ brigep ^. epHost) (fromIntegral $ brigep ^. epPort) "" + baseurl = Client.BaseUrl Client.Http (cs $ brigep ^. host) (fromIntegral $ brigep ^. port) "" liftIO $ Client.runClientM action env diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 815bf4025aa..2025bd45e86 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -1545,7 +1545,7 @@ testRegisterProvider db' brig = do getProviderActivationCodeInternal brig email Http () testHandleLogin brig = do - usr <- userId <$> randomUser brig + usr <- (.userId) <$> randomUser brig hdl <- randomHandle let update = RequestBodyLBS . encode $ HandleUpdate hdl put (brig . path "/self/handle" . contentJson . zUser usr . zConn "c" . Http.body update) @@ -703,7 +703,7 @@ testLimitRetries conf brig = do testRegularUserLegalHoldLogin :: Brig -> Http () testRegularUserLegalHoldLogin brig = do -- Create a regular user - uid <- userId <$> randomUser brig + uid <- (.userId) <$> randomUser brig -- fail if user is not a team user legalHoldLogin brig (LegalHoldLogin uid (Just defPassword) Nothing) PersistentCookie !!! do const 403 === statusCode @@ -788,7 +788,7 @@ testLegalHoldLogout brig galley = do testEmailSsoLogin :: Brig -> Http () testEmailSsoLogin brig = do -- Create a user - uid <- userId <$> randomUser brig + uid <- (.userId) <$> randomUser brig now <- liftIO getCurrentTime -- Login and do some checks _rs <- @@ -803,7 +803,7 @@ testEmailSsoLogin brig = do testSuspendedSsoLogin :: Brig -> Http () testSuspendedSsoLogin brig = do -- Create a user and immediately suspend them - uid <- userId <$> randomUser brig + uid <- (.userId) <$> randomUser brig setStatus brig uid Suspended -- Try to login and see if we fail ssoLogin brig (SsoLogin uid Nothing) PersistentCookie !!! do @@ -833,7 +833,7 @@ testInvalidCookie z b = do const 403 === statusCode const (Just "Invalid user token") =~= responseBody -- Expired - user <- userId <$> randomUser b + user <- (.userId) <$> randomUser b let f = set (ZAuth.userTTL (Proxy @u)) 0 t <- toByteString' <$> runZAuth z (ZAuth.localSettings f (ZAuth.newUserToken @u user Nothing)) liftIO $ threadDelay 1000000 @@ -845,7 +845,7 @@ testInvalidCookie z b = do testInvalidToken :: ZAuth.Env -> Brig -> Http () testInvalidToken z b = do - user <- userId <$> randomUser b + user <- (.userId) <$> randomUser b t <- toByteString' <$> runZAuth z (ZAuth.newUserToken @ZAuth.User user Nothing) -- Syntactically invalid @@ -1421,7 +1421,7 @@ testLogout b = do testReauthentication :: Brig -> Http () testReauthentication b = do - u <- userId <$> randomUser b + u <- (.userId) <$> randomUser b let js = Http.body . RequestBodyLBS . encode $ object ["foo" .= ("bar" :: Text)] get (b . paths ["/i/users", toByteString' u, "reauthenticate"] . contentJson . js) !!! do const 403 === statusCode diff --git a/services/brig/test/integration/API/User/Connection.hs b/services/brig/test/integration/API/User/Connection.hs index 85260c9d002..972d4ddeda0 100644 --- a/services/brig/test/integration/API/User/Connection.hs +++ b/services/brig/test/integration/API/User/Connection.hs @@ -44,7 +44,7 @@ import Util import Wire.API.Connection import Wire.API.Conversation import Wire.API.Federation.API.Brig -import Wire.API.Federation.API.Galley (GetConversationsRequest (..), GetConversationsResponse (gcresConvs), RemoteConvMembers (rcmOthers), RemoteConversation (rcnvMembers)) +import Wire.API.Federation.API.Galley (GetConversationsRequest (..), GetConversationsResponse (convs), RemoteConvMembers (others), RemoteConversation (members)) import Wire.API.Federation.Component import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.MultiTablePaging @@ -113,7 +113,7 @@ tests cl _at opts p b _c g fedBrigClient fedGalleyClient db = testCreateConnectionInvalidUser :: Brig -> Http () testCreateConnectionInvalidUser brig = do - uid1 <- userId <$> randomUser brig + uid1 <- (.userId) <$> randomUser brig -- user does not exist uid2 <- Id <$> liftIO UUID.nextRandom postConnection brig uid1 uid2 !!! do @@ -142,14 +142,14 @@ testCreateConnectionInvalidUserQualified brig = do testCreateManualConnections :: Brig -> Http () testCreateManualConnections brig = do - uid1 <- userId <$> randomUser brig - uid2 <- userId <$> randomUser brig + uid1 <- (.userId) <$> randomUser brig + uid2 <- (.userId) <$> randomUser brig postConnection brig uid1 uid2 !!! const 201 === statusCode assertConnections brig uid1 [ConnectionStatus uid1 uid2 Sent] assertConnections brig uid2 [ConnectionStatus uid2 uid1 Pending] -- Test that no connections to anonymous users can be created, -- as well as that anonymous users cannot create connections. - uid3 <- userId <$> createAnonUser "foo3" brig + uid3 <- (.userId) <$> createAnonUser "foo3" brig postConnection brig uid1 uid3 !!! const 400 === statusCode postConnection brig uid3 uid1 !!! const 403 === statusCode @@ -168,8 +168,8 @@ testCreateManualConnectionsQualified brig = do testCreateMutualConnections :: Brig -> Galley -> Http () testCreateMutualConnections brig galley = do - uid1 <- userId <$> randomUser brig - uid2 <- userId <$> randomUser brig + uid1 <- (.userId) <$> randomUser brig + uid2 <- (.userId) <$> randomUser brig postConnection brig uid1 uid2 !!! const 201 === statusCode assertConnections brig uid1 [ConnectionStatus uid1 uid2 Sent] assertConnections brig uid2 [ConnectionStatus uid2 uid1 Pending] @@ -210,8 +210,8 @@ testCreateMutualConnectionsQualified brig galley = do testAcceptConnection :: Brig -> Http () testAcceptConnection brig = do - uid1 <- userId <$> randomUser brig - uid2 <- userId <$> randomUser brig + uid1 <- (.userId) <$> randomUser brig + uid2 <- (.userId) <$> randomUser brig -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- B accepts @@ -219,7 +219,7 @@ testAcceptConnection brig = do assertConnections brig uid1 [ConnectionStatus uid1 uid2 Accepted] assertConnections brig uid2 [ConnectionStatus uid2 uid1 Accepted] -- Mutual connection request with a user C - uid3 <- userId <$> randomUser brig + uid3 <- (.userId) <$> randomUser brig postConnection brig uid1 uid3 !!! const 201 === statusCode postConnection brig uid3 uid1 !!! const 200 === statusCode assertConnections brig uid1 [ConnectionStatus uid1 uid3 Accepted] @@ -238,8 +238,8 @@ testAcceptConnectionQualified brig = do testIgnoreConnection :: Brig -> Http () testIgnoreConnection brig = do - uid1 <- userId <$> randomUser brig - uid2 <- userId <$> randomUser brig + uid1 <- (.userId) <$> randomUser brig + uid2 <- (.userId) <$> randomUser brig -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- B ignores A @@ -267,8 +267,8 @@ testIgnoreConnectionQualified brig = do testCancelConnection :: Brig -> Http () testCancelConnection brig = do - uid1 <- userId <$> randomUser brig - uid2 <- userId <$> randomUser brig + uid1 <- (.userId) <$> randomUser brig + uid2 <- (.userId) <$> randomUser brig -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- A cancels the request @@ -296,8 +296,8 @@ testCancelConnectionQualified brig = do testCancelConnection2 :: Brig -> Galley -> Http () testCancelConnection2 brig galley = do - uid1 <- userId <$> randomUser brig - uid2 <- userId <$> randomUser brig + uid1 <- (.userId) <$> randomUser brig + uid2 <- (.userId) <$> randomUser brig -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- A cancels the request @@ -373,8 +373,8 @@ testBlockConnection :: Brig -> Http () testBlockConnection brig = do u1 <- randomUser brig u2 <- randomUser brig - let uid1 = userId u1 - let uid2 = userId u2 + let uid1 = u1.userId + let uid2 = u2.userId -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- Even connected users cannot see each other's email @@ -418,8 +418,8 @@ testBlockConnectionQualified :: Brig -> Http () testBlockConnectionQualified brig = do u1 <- randomUser brig u2 <- randomUser brig - let uid1 = userId u1 - uid2 = userId u2 + let uid1 = u1.userId + uid2 = u2.userId quid1 = userQualifiedId u1 quid2 = userQualifiedId u2 -- Initiate a new connection (A -> B) @@ -465,8 +465,8 @@ testBlockAndResendConnection :: Brig -> Galley -> Http () testBlockAndResendConnection brig galley = do u1 <- randomUser brig u2 <- randomUser brig - let uid1 = userId u1 - let uid2 = userId u2 + let uid1 = u1.userId + let uid2 = u2.userId -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- B blocks A @@ -516,8 +516,8 @@ testBlockAndResendConnectionQualified brig galley = do testUnblockPendingConnection :: Brig -> Http () testUnblockPendingConnection brig = do - u1 <- userId <$> randomUser brig - u2 <- userId <$> randomUser brig + u1 <- (.userId) <$> randomUser brig + u2 <- (.userId) <$> randomUser brig postConnection brig u1 u2 !!! const 201 === statusCode putConnection brig u1 u2 Blocked !!! const 200 === statusCode assertConnections brig u1 [ConnectionStatus u1 u2 Blocked] @@ -539,8 +539,8 @@ testUnblockPendingConnectionQualified brig = do testAcceptWhileBlocked :: Brig -> Http () testAcceptWhileBlocked brig = do - u1 <- userId <$> randomUser brig - u2 <- userId <$> randomUser brig + u1 <- (.userId) <$> randomUser brig + u2 <- (.userId) <$> randomUser brig postConnection brig u1 u2 !!! const 201 === statusCode putConnection brig u1 u2 Blocked !!! const 200 === statusCode assertConnections brig u1 [ConnectionStatus u1 u2 Blocked] @@ -576,8 +576,8 @@ testUpdateConnectionNoopQualified brig = do testBadUpdateConnection :: Brig -> Http () testBadUpdateConnection brig = do - uid1 <- userId <$> randomUser brig - uid2 <- userId <$> randomUser brig + uid1 <- (.userId) <$> randomUser brig + uid2 <- (.userId) <$> randomUser brig postConnection brig uid1 uid2 !!! const 201 === statusCode assertBadUpdate uid1 uid2 Pending assertBadUpdate uid1 uid2 Ignored @@ -606,9 +606,9 @@ testBadUpdateConnectionQualified brig = do testLocalConnectionsPaging :: Brig -> Http () testLocalConnectionsPaging b = do - u <- userId <$> randomUser b + u <- (.userId) <$> randomUser b replicateM_ total $ do - u2 <- userId <$> randomUser b + u2 <- (.userId) <$> randomUser b postConnection b u u2 !!! const 201 === statusCode foldM_ (next u 2) (0, Nothing) [2, 2, 1, 0] foldM_ (next u total) (0, Nothing) [total, 0] @@ -672,21 +672,21 @@ testAllConnectionsPaging b db = do testConnectionLimit :: Brig -> ConnectionLimit -> Http () testConnectionLimit brig (ConnectionLimit l) = do - uid1 <- userId <$> randomUser brig + uid1 <- (.userId) <$> randomUser brig (uid2 : _) <- replicateM (fromIntegral l) (newConn uid1) - uidX <- userId <$> randomUser brig + uidX <- (.userId) <$> randomUser brig postConnection brig uid1 uidX !!! assertLimited -- blocked connections do not count towards the limit putConnection brig uid1 uid2 Blocked !!! const 200 === statusCode postConnection brig uid1 uidX !!! const 201 === statusCode -- the next send/accept hits the limit again - uidY <- userId <$> randomUser brig + uidY <- (.userId) <$> randomUser brig postConnection brig uid1 uidY !!! assertLimited -- (re-)sending an already accepted connection does not affect the limit postConnection brig uid1 uidX !!! const 200 === statusCode where newConn from = do - to <- userId <$> randomUser brig + to <- (.userId) <$> randomUser brig postConnection brig from to !!! const 201 === statusCode pure to assertLimited = do @@ -737,7 +737,7 @@ testConnectOK brig galley fedBrigClient = do testConnectWithAnon :: Brig -> FedClient 'Brig -> Http () testConnectWithAnon brig fedBrigClient = do fromUser <- randomId - toUser <- userId <$> createAnonUser "anon1234" brig + toUser <- (.userId) <$> createAnonUser "anon1234" brig res <- runFedClient @"send-connection-action" fedBrigClient (Domain "far-away.example.com") $ NewConnectionRequest fromUser toUser RemoteConnect @@ -746,7 +746,7 @@ testConnectWithAnon brig fedBrigClient = do testConnectFromAnon :: Brig -> Http () testConnectFromAnon brig = do - anonUser <- userId <$> createAnonUser "anon1234" brig + anonUser <- (.userId) <$> createAnonUser "anon1234" brig remoteUser <- fakeRemoteUser postConnectionQualified brig anonUser remoteUser !!! const 403 === statusCode @@ -790,13 +790,13 @@ testConnectMutualRemoteActionThenLocalAction opts brig fedBrigClient fedGalleyCl let request = GetConversationsRequest - { gcrUserId = qUnqualified quid2, - gcrConvIds = [qUnqualified convId] + { userId = qUnqualified quid2, + convIds = [qUnqualified convId] } res <- runFedClient @"get-conversations" fedGalleyClient (qDomain quid2) request liftIO $ - fmap (fmap omQualifiedId . rcmOthers . rcnvMembers) (gcresConvs res) @?= [[]] + fmap (fmap omQualifiedId . others . members) res.convs @?= [[]] -- The mock response has 'RemoteConnect' as action, because the remote backend -- cannot be sure if the local backend was previously in Ignored state or not diff --git a/services/brig/test/integration/Main.hs b/services/brig/test/integration/Main.hs index aafb086f845..dcdef16cd5a 100644 --- a/services/brig/test/integration/Main.hs +++ b/services/brig/test/integration/Main.hs @@ -36,7 +36,8 @@ import API.TeamUserSearch qualified as TeamUserSearch import API.User qualified as User import API.UserPendingActivation qualified as UserPendingActivation import API.Version qualified -import Bilge hiding (header) +import Bilge hiding (header, host, port) +import Bilge qualified import Brig.API (sitemap) import Brig.AWS qualified as AWS import Brig.CanonicalInterpreter @@ -131,9 +132,9 @@ runTests iConf brigOpts otherArgs = do Opts.TurnSourceFiles files -> files Opts.TurnSourceDNS _ -> error "The integration tests can only be run when TurnServers are sourced from files" localDomain = brigOpts ^. Opts.optionSettings . Opts.federationDomain - casHost = (\v -> Opts.cassandra v ^. casEndpoint . epHost) brigOpts - casPort = (\v -> Opts.cassandra v ^. casEndpoint . epPort) brigOpts - casKey = (\v -> Opts.cassandra v ^. casKeyspace) brigOpts + casHost = (\v -> Opts.cassandra v ^. endpoint . host) brigOpts + casPort = (\v -> Opts.cassandra v ^. endpoint . port) brigOpts + casKey = (\v -> Opts.cassandra v ^. keyspace) brigOpts awsOpts = Opts.aws brigOpts lg <- Logger.new Logger.defSettings -- TODO: use mkLogger'? db <- defInitCassandra casKey casHost casPort lg @@ -193,9 +194,9 @@ runTests iConf brigOpts otherArgs = do federationEnd2End ] where - mkRequest (Endpoint h p) = host (encodeUtf8 h) . port p + mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p - mkVersionedRequest endpoint = maybeAddPrefix . mkRequest endpoint + mkVersionedRequest ep = maybeAddPrefix . mkRequest ep maybeAddPrefix :: Request -> Request maybeAddPrefix r = case pathSegments $ getUri r of diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index d9a5bf5e976..3c7ee83324a 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -22,7 +22,7 @@ module Util where -import Bilge +import Bilge hiding (host, port) import Bilge.Assert import Brig.AWS.Types import Brig.App (applog, fsWatcher, sftEnv, turnEnv) @@ -176,14 +176,14 @@ runFedClient :: FedClient comp -> Domain -> Servant.Client Http api -runFedClient (FedClient mgr endpoint) domain = +runFedClient (FedClient mgr ep) domain = Servant.hoistClient (Proxy @api) (servantClientMToHttp domain) $ Servant.clientIn (Proxy @api) (Proxy @Servant.ClientM) where servantClientMToHttp :: Domain -> Servant.ClientM a -> Http a servantClientMToHttp originDomain action = liftIO $ do - let brigHost = Text.unpack $ endpoint ^. epHost - brigPort = fromInteger . toInteger $ endpoint ^. epPort + let brigHost = Text.unpack $ ep ^. host + brigPort = fromInteger . toInteger $ ep ^. port baseUrl = Servant.BaseUrl Servant.Http brigHost brigPort "/federation" clientEnv = Servant.ClientEnv mgr baseUrl Nothing (makeClientRequest originDomain) eitherRes <- Servant.runClientM action clientEnv diff --git a/services/cargohold/src/CargoHold/API/Federation.hs b/services/cargohold/src/CargoHold/API/Federation.hs index 934aedca3f4..f5cbf4b3f62 100644 --- a/services/cargohold/src/CargoHold/API/Federation.hs +++ b/services/cargohold/src/CargoHold/API/Federation.hs @@ -45,13 +45,13 @@ federationSitemap = checkAsset :: F.GetAsset -> Handler Bool checkAsset ga = fmap isJust . runMaybeT $ - checkMetadata Nothing (F.gaKey ga) (F.gaToken ga) + checkMetadata Nothing (F.key ga) (F.token ga) streamAsset :: Domain -> F.GetAsset -> Handler AssetSource streamAsset _ ga = do available <- checkAsset ga unless available (throwE assetNotFound) - AssetSource <$> S3.downloadV3 (F.gaKey ga) + AssetSource <$> S3.downloadV3 (F.key ga) getAsset :: Domain -> F.GetAsset -> Handler F.GetAssetResponse getAsset _ = fmap F.GetAssetResponse . checkAsset diff --git a/services/cargohold/src/CargoHold/API/V3.hs b/services/cargohold/src/CargoHold/API/V3.hs index 8491f39db24..d96d772d5ce 100644 --- a/services/cargohold/src/CargoHold/API/V3.hs +++ b/services/cargohold/src/CargoHold/API/V3.hs @@ -69,8 +69,8 @@ upload own bdy = do let cl = fromIntegral $ hdrLength hdrs when (cl <= 0) $ throwE invalidLength - maxTotalBytes <- view (settings . setMaxTotalBytes) - when (cl > maxTotalBytes) $ + maxBytes <- view (CargoHold.App.settings . maxTotalBytes) + when (cl > maxBytes) $ throwE assetTooLarge ast <- liftIO $ Id <$> nextRandom tok <- if sets ^. V3.setAssetPublic then pure Nothing else Just <$> randToken @@ -172,7 +172,7 @@ metadataHeaders = cl <- contentLength hdrs pure (ct, cl) -assetHeaders :: Parser AssetHeaders +assetHeaders :: Parser CargoHold.Types.V3.AssetHeaders assetHeaders = eol *> boundary @@ -180,7 +180,7 @@ assetHeaders = <* eol where go hdrs = - AssetHeaders + CargoHold.Types.V3.AssetHeaders <$> contentType hdrs <*> contentLength hdrs diff --git a/services/cargohold/src/CargoHold/AWS.hs b/services/cargohold/src/CargoHold/AWS.hs index 39301e7fee1..4593fa6d7d3 100644 --- a/services/cargohold/src/CargoHold/AWS.hs +++ b/services/cargohold/src/CargoHold/AWS.hs @@ -44,7 +44,7 @@ import Amazonka (AWSRequest, AWSResponse) import qualified Amazonka as AWS import qualified Amazonka.S3 as S3 import CargoHold.CloudFront -import CargoHold.Options +import CargoHold.Options hiding (cloudFront, s3Bucket) import Conduit import Control.Lens hiding ((.=)) import Control.Monad.Catch @@ -116,7 +116,7 @@ mkEnv lgr s3End s3AddrStyle s3Download bucket cfOpts mgr = do cf <- mkCfEnv cfOpts pure (Env g bucket e s3Download cf) where - mkCfEnv (Just o) = Just <$> initCloudFront (o ^. cfPrivateKey) (o ^. cfKeyPairId) 300 (o ^. cfDomain) + mkCfEnv (Just o) = Just <$> initCloudFront (o ^. privateKey) (o ^. keyPairId) 300 (o ^. domain) mkCfEnv Nothing = pure Nothing mkAwsEnv g s3 = do baseEnv <- @@ -172,7 +172,7 @@ exec :: (Text -> r) -> m (AWSResponse r) exec env request = do - let req = request (_s3Bucket env) + let req = request env._s3Bucket resp <- execute env (sendCatch (env ^. amazonkaEnv) req) case resp of Left err -> do @@ -191,7 +191,7 @@ execStream :: (Text -> r) -> ResourceT IO (AWSResponse r) execStream env request = do - let req = request (_s3Bucket env) + let req = request env._s3Bucket resp <- sendCatch (env ^. amazonkaEnv) req case resp of Left err -> do @@ -210,7 +210,7 @@ execCatch :: (Text -> r) -> m (Maybe (AWSResponse r)) execCatch env request = do - let req = request (_s3Bucket env) + let req = request env._s3Bucket resp <- execute env (retrying retry5x (const canRetry) (const (sendCatch (env ^. amazonkaEnv) req))) case resp of Left err -> do diff --git a/services/cargohold/src/CargoHold/App.hs b/services/cargohold/src/CargoHold/App.hs index de2a6777a08..b8d4b9c6e8d 100644 --- a/services/cargohold/src/CargoHold/App.hs +++ b/services/cargohold/src/CargoHold/App.hs @@ -53,7 +53,8 @@ import Bilge (Manager, MonadHttp, RequestId (..), newManager, withResponse) import qualified Bilge import Bilge.RPC (HasRequestId (..)) import qualified CargoHold.AWS as AWS -import CargoHold.Options as Opt +import CargoHold.Options (AWSOpts, Opts, S3Compatibility (..)) +import qualified CargoHold.Options as Opt import Control.Error (ExceptT, exceptT) import Control.Exception (throw) import Control.Lens (Lens', makeLenses, non, view, (?~), (^.)) @@ -93,18 +94,18 @@ data Env = Env makeLenses ''Env settings :: Lens' Env Opt.Settings -settings = options . optSettings +settings = options . Opt.settings newEnv :: Opts -> IO Env newEnv o = do met <- Metrics.metrics - lgr <- Log.mkLogger (o ^. optLogLevel) (o ^. optLogNetStrings) (o ^. optLogFormat) + lgr <- Log.mkLogger (o ^. Opt.logLevel) (o ^. Opt.logNetStrings) (o ^. Opt.logFormat) checkOpts o lgr - mgr <- initHttpManager (o ^. optAws . awsS3Compatibility) + mgr <- initHttpManager (o ^. Opt.aws . Opt.s3Compatibility) h2mgr <- initHttp2Manager - ama <- initAws (o ^. optAws) lgr mgr + ama <- initAws (o ^. Opt.aws) lgr mgr multiIngressAWS <- initMultiIngressAWS lgr mgr - let loc = toLocalUnsafe (o ^. optSettings . Opt.setFederationDomain) () + let loc = toLocalUnsafe (o ^. Opt.settings . Opt.federationDomain) () pure $ Env ama met lgr mgr h2mgr def o loc multiIngressAWS where initMultiIngressAWS :: Logger -> Manager -> IO (Map String AWS.Env) @@ -114,10 +115,10 @@ newEnv o = do ( \(k, v) -> initAws (patchS3DownloadEndpoint v) lgr mgr >>= \v' -> pure (k, v') ) - (Map.assocs (o ^. optAws . Opt.optMultiIngress . non Map.empty)) + (Map.assocs (o ^. Opt.aws . Opt.multiIngress . non Map.empty)) patchS3DownloadEndpoint :: AWSEndpoint -> AWSOpts - patchS3DownloadEndpoint endpoint = (o ^. optAws) & awsS3DownloadEndpoint ?~ endpoint + patchS3DownloadEndpoint e = (o ^. Opt.aws) & Opt.s3DownloadEndpoint ?~ e -- | Validate (some) options (`Opts`) -- @@ -134,19 +135,19 @@ checkOpts opts lgr = do error errorMsg where multiIngressConfigured :: Bool - multiIngressConfigured = (not . null) (opts ^. (optAws . Opt.optMultiIngress . non Map.empty)) + multiIngressConfigured = (not . null) (opts ^. (Opt.aws . Opt.multiIngress . non Map.empty)) cloudFrontConfigured :: Bool - cloudFrontConfigured = isJust (opts ^. (optAws . Opt.awsCloudFront)) + cloudFrontConfigured = isJust (opts ^. (Opt.aws . Opt.cloudFront)) singleAwsDownloadEndpointConfigured :: Bool - singleAwsDownloadEndpointConfigured = isJust (opts ^. (optAws . Opt.awsS3DownloadEndpoint)) + singleAwsDownloadEndpointConfigured = isJust (opts ^. (Opt.aws . Opt.s3DownloadEndpoint)) initAws :: AWSOpts -> Logger -> Manager -> IO AWS.Env -initAws o l = AWS.mkEnv l (o ^. awsS3Endpoint) addrStyle downloadEndpoint (o ^. awsS3Bucket) (o ^. awsCloudFront) +initAws o l = AWS.mkEnv l (o ^. Opt.s3Endpoint) addrStyle downloadEndpoint (o ^. Opt.s3Bucket) (o ^. Opt.cloudFront) where - downloadEndpoint = fromMaybe (o ^. awsS3Endpoint) (o ^. awsS3DownloadEndpoint) - addrStyle = maybe S3AddressingStylePath unwrapS3AddressingStyle (o ^. awsS3AddressingStyle) + downloadEndpoint = fromMaybe (o ^. Opt.s3Endpoint) (o ^. Opt.s3DownloadEndpoint) + addrStyle = maybe S3AddressingStylePath Opt.unwrapS3AddressingStyle (o ^. Opt.s3AddressingStyle) initHttpManager :: Maybe S3Compatibility -> IO Manager initHttpManager s3Compat = diff --git a/services/cargohold/src/CargoHold/Federation.hs b/services/cargohold/src/CargoHold/Federation.hs index 5517e0dce0f..d1d57d0a5bf 100644 --- a/services/cargohold/src/CargoHold/Federation.hs +++ b/services/cargohold/src/CargoHold/Federation.hs @@ -56,12 +56,12 @@ downloadRemoteAsset :: downloadRemoteAsset usr rkey tok = do let ga = GetAsset - { gaKey = tUnqualified rkey, - gaUser = tUnqualified usr, - gaToken = tok + { key = tUnqualified rkey, + user = tUnqualified usr, + token = tok } exists <- - fmap gaAvailable . executeFederated rkey $ + fmap available . executeFederated rkey $ fedClient @'Cargohold @"get-asset" ga if exists then @@ -77,7 +77,7 @@ mkFederatorClientEnv :: Remote x -> Handler FederatorClientEnv mkFederatorClientEnv remote = do loc <- view localUnit endpoint <- - view (options . optFederator) + view (options . federator) >>= maybe (throwE federationNotConfigured) pure mgr <- view http2Manager pure diff --git a/services/cargohold/src/CargoHold/Options.hs b/services/cargohold/src/CargoHold/Options.hs index 9741c3b9e85..4f99c8300ad 100644 --- a/services/cargohold/src/CargoHold/Options.hs +++ b/services/cargohold/src/CargoHold/Options.hs @@ -35,11 +35,11 @@ import Wire.API.Routes.Version -- | AWS CloudFront settings. data CloudFrontOpts = CloudFrontOpts { -- | Domain - _cfDomain :: CF.Domain, + _domain :: CF.Domain, -- | Keypair ID - _cfKeyPairId :: CF.KeyPairId, + _keyPairId :: CF.KeyPairId, -- | Path to private key - _cfPrivateKey :: FilePath + _privateKey :: FilePath } deriving (Show, Generic) @@ -62,7 +62,7 @@ instance FromJSON OptS3AddressingStyle where other -> fail $ "invalid S3AddressingStyle: " <> show other data AWSOpts = AWSOpts - { _awsS3Endpoint :: !AWSEndpoint, + { _s3Endpoint :: !AWSEndpoint, -- | S3 can either by addressed in path style, i.e. -- https:////, or vhost style, i.e. -- https://./. AWS's S3 offering has @@ -88,18 +88,18 @@ data AWSOpts = AWSOpts -- -- When this option is unspecified, we default to path style addressing to -- ensure smooth transition for older deployments. - _awsS3AddressingStyle :: !(Maybe OptS3AddressingStyle), + _s3AddressingStyle :: !(Maybe OptS3AddressingStyle), -- | S3 endpoint for generating download links. Useful if Cargohold is configured to use -- an S3 replacement running inside the internal network (in which case internally we -- would use one hostname for S3, and when generating an asset link for a client app, we -- would use another hostname). - _awsS3DownloadEndpoint :: !(Maybe AWSEndpoint), + _s3DownloadEndpoint :: !(Maybe AWSEndpoint), -- | S3 bucket name - _awsS3Bucket :: !Text, + _s3Bucket :: !Text, -- | Enable this option for compatibility with specific S3 backends. - _awsS3Compatibility :: !(Maybe S3Compatibility), + _s3Compatibility :: !(Maybe S3Compatibility), -- | AWS CloudFront options - _awsCloudFront :: !(Maybe CloudFrontOpts), + _cloudFront :: !(Maybe CloudFrontOpts), -- | @Z-Host@ header to s3 download endpoint `Map` -- -- This logic is: If the @Z-Host@ header is provided and found in this map, @@ -107,7 +107,7 @@ data AWSOpts = AWSOpts -- otherwise, `_awsS3DownloadEndpoint` is used. This option is only useful -- in the context of multi-ingress setups where one backend / deployment is -- reachable under several domains. - _optMultiIngress :: !(Maybe (Map String AWSEndpoint)) + _multiIngress :: !(Maybe (Map String AWSEndpoint)) } deriving (Show, Generic) @@ -128,9 +128,9 @@ makeLenses ''AWSOpts data Settings = Settings { -- | Maximum allowed size for uploads, in bytes - _setMaxTotalBytes :: !Int, + _maxTotalBytes :: !Int, -- | TTL for download links, in seconds - _setDownloadLinkTTL :: !Word, + _downloadLinkTTL :: !Word, -- | FederationDomain is required, even when not wanting to federate with other backends -- (in that case the 'allowedDomains' can be set to empty in Federator) -- Federation domain is used to qualify local IDs and handles, @@ -141,8 +141,8 @@ data Settings = Settings -- Remember to keep it the same in all services. -- This is referred to as the 'backend domain' in the public documentation; See -- https://docs.wire.com/how-to/install/configure-federation.html#choose-a-backend-domain-name - _setFederationDomain :: !Domain, - _setDisabledAPIVersions :: !(Maybe (Set Version)) + _federationDomain :: !Domain, + _disabledAPIVersions :: !(Maybe (Set Version)) } deriving (Show, Generic) @@ -154,19 +154,19 @@ makeLenses ''Settings -- modify the behavior. data Opts = Opts { -- | Hostname and port to bind to - _optCargohold :: !Endpoint, - _optAws :: !AWSOpts, - _optSettings :: !Settings, + _cargohold :: !Endpoint, + _aws :: !AWSOpts, + _settings :: !Settings, -- | Federator endpoint - _optFederator :: Maybe Endpoint, + _federator :: Maybe Endpoint, -- Logging -- | Log level (Debug, Info, etc) - _optLogLevel :: !Level, + _logLevel :: !Level, -- | Use netstrings encoding: -- - _optLogNetStrings :: !(Maybe (Last Bool)), - _optLogFormat :: !(Maybe (Last LogFormat)) --- ^ Log format + _logNetStrings :: !(Maybe (Last Bool)), + _logFormat :: !(Maybe (Last LogFormat)) --- ^ Log format } deriving (Show, Generic) diff --git a/services/cargohold/src/CargoHold/Run.hs b/services/cargohold/src/CargoHold/Run.hs index ae393ced1ca..556cabf3679 100644 --- a/services/cargohold/src/CargoHold/Run.hs +++ b/services/cargohold/src/CargoHold/Run.hs @@ -27,8 +27,8 @@ import qualified Amazonka as AWS import CargoHold.API.Federation import CargoHold.API.Public import CargoHold.AWS (amazonkaEnv) -import CargoHold.App -import CargoHold.Options +import CargoHold.App hiding (settings) +import CargoHold.Options hiding (aws) import Control.Exception (bracket) import Control.Lens (set, (^.)) import Control.Monad.Codensity @@ -65,8 +65,8 @@ run o = lowerCodensity $ do s <- Server.newSettings $ defaultServer - (unpack $ o ^. optCargohold . epHost) - (o ^. optCargohold . epPort) + (unpack $ o ^. cargohold . host) + (o ^. cargohold . port) (e ^. appLogger) (e ^. metrics) runSettingsWithShutdown s app Nothing @@ -78,7 +78,7 @@ mkApp o = Codensity $ \k -> where middleware :: Env -> Wai.Middleware middleware e = - versionMiddleware (fold (o ^. optSettings . setDisabledAPIVersions)) + versionMiddleware (fold (o ^. settings . disabledAPIVersions)) . servantPrometheusMiddleware (Proxy @CombinedAPI) . GZip.gzip GZip.def . catchErrors (e ^. appLogger) [Right $ e ^. metrics] @@ -87,7 +87,7 @@ mkApp o = Codensity $ \k -> let e = set requestId (maybe def RequestId (lookupRequestId r)) e0 in Servant.serveWithContext (Proxy @CombinedAPI) - ((o ^. optSettings . setFederationDomain) :. Servant.EmptyContext) + ((o ^. settings . federationDomain) :. Servant.EmptyContext) ( hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap :<|> hoistServerWithDomain @ServantAPI (toServantHandler e) servantSitemap :<|> hoistServerWithDomain @InternalAPI (toServantHandler e) internalSitemap diff --git a/services/cargohold/src/CargoHold/S3.hs b/services/cargohold/src/CargoHold/S3.hs index 44ccb280ed0..72695f0f477 100644 --- a/services/cargohold/src/CargoHold/S3.hs +++ b/services/cargohold/src/CargoHold/S3.hs @@ -43,7 +43,7 @@ import CargoHold.API.Error import CargoHold.AWS (amazonkaEnvWithDownloadEndpoint) import qualified CargoHold.AWS as AWS import CargoHold.App hiding (Env, Handler) -import CargoHold.Options +import CargoHold.Options (downloadLinkTTL) import qualified CargoHold.Types.V3 as V3 import qualified Codec.MIME.Parse as MIME import qualified Codec.MIME.Type as MIME @@ -218,7 +218,7 @@ signedURL path mbHost = do e <- awsEnvForHost let b = view AWS.s3Bucket e now <- liftIO getCurrentTime - ttl <- view (settings . setDownloadLinkTTL) + ttl <- view (settings . downloadLinkTTL) let req = newGetObject (BucketName b) (ObjectKey . Text.decodeLatin1 $ toByteString' path) signed <- presignURL (amazonkaEnvWithDownloadEndpoint e) now (Seconds (fromIntegral ttl)) req diff --git a/services/cargohold/test/integration/API.hs b/services/cargohold/test/integration/API.hs index 88f733e763e..781cbe125e1 100644 --- a/services/cargohold/test/integration/API.hs +++ b/services/cargohold/test/integration/API.hs @@ -23,7 +23,7 @@ import API.Util import Bilge hiding (body) import Bilge.Assert import CargoHold.API.Error -import CargoHold.Options (awsS3DownloadEndpoint, optAws) +import CargoHold.Options (aws, s3DownloadEndpoint) import CargoHold.Types import qualified CargoHold.Types.V3 as V3 import qualified Codec.MIME.Type as MIME @@ -228,7 +228,7 @@ testDownloadURLOverride = do -- This is a .example domain, it shouldn't resolve. But it is also not -- supposed to be used by cargohold to make connections. let downloadEndpoint = "external-s3-url.example" - withSettingsOverrides (optAws . awsS3DownloadEndpoint ?~ AWSEndpoint downloadEndpoint True 443) $ do + withSettingsOverrides (aws . s3DownloadEndpoint ?~ AWSEndpoint downloadEndpoint True 443) $ do uid <- liftIO $ Id <$> nextRandom -- Upload, should work, shouldn't try to use the S3DownloadEndpoint diff --git a/services/cargohold/test/integration/API/Federation.hs b/services/cargohold/test/integration/API/Federation.hs index 6e25283ea02..29c2d63992b 100644 --- a/services/cargohold/test/integration/API/Federation.hs +++ b/services/cargohold/test/integration/API/Federation.hs @@ -77,13 +77,13 @@ testGetAssetAvailable isPublicAsset = do let key = view assetKey ast let ga = GetAsset - { gaUser = uid, - gaToken = tok, - gaKey = qUnqualified key + { user = uid, + token = tok, + key = qUnqualified key } ok <- withFederationClient $ - gaAvailable <$> runFederationClient (unsafeFedClientIn @'Cargohold @"get-asset" ga) + available <$> runFederationClient (unsafeFedClientIn @'Cargohold @"get-asset" ga) -- check that asset is available liftIO $ ok @?= True @@ -97,13 +97,13 @@ testGetAssetNotAvailable = do let key = AssetKeyV3 assetId AssetPersistent let ga = GetAsset - { gaUser = uid, - gaToken = Just token, - gaKey = key + { user = uid, + token = Just token, + key = key } ok <- withFederationClient $ - gaAvailable <$> runFederationClient (unsafeFedClientIn @'Cargohold @"get-asset" ga) + available <$> runFederationClient (unsafeFedClientIn @'Cargohold @"get-asset" ga) -- check that asset is not available liftIO $ ok @?= False @@ -124,13 +124,13 @@ testGetAssetWrongToken = do let key = view assetKey ast let ga = GetAsset - { gaUser = uid, - gaToken = Just tok, - gaKey = qUnqualified key + { user = uid, + token = Just tok, + key = qUnqualified key } ok <- withFederationClient $ - gaAvailable <$> runFederationClient (unsafeFedClientIn @'Cargohold @"get-asset" ga) + available <$> runFederationClient (unsafeFedClientIn @'Cargohold @"get-asset" ga) -- check that asset is not available liftIO $ ok @?= False @@ -156,9 +156,9 @@ testLargeAsset = do let key = view assetKey ast let ga = GetAsset - { gaUser = uid, - gaToken = tok, - gaKey = qUnqualified key + { user = uid, + token = tok, + key = qUnqualified key } chunks <- withFederationClient $ do source <- getAssetSource <$> runFederationClient (unsafeFedClientIn @'Cargohold @"stream-asset" ga) @@ -188,9 +188,9 @@ testStreamAsset = do let key = view assetKey ast let ga = GetAsset - { gaUser = uid, - gaToken = tok, - gaKey = qUnqualified key + { user = uid, + token = tok, + key = qUnqualified key } respBody <- withFederationClient $ do source <- getAssetSource <$> runFederationClient (unsafeFedClientIn @'Cargohold @"stream-asset" ga) @@ -206,9 +206,9 @@ testStreamAssetNotAvailable = do let key = AssetKeyV3 assetId AssetPersistent let ga = GetAsset - { gaUser = uid, - gaToken = Just token, - gaKey = key + { user = uid, + token = Just token, + key = key } err <- withFederationError $ do runFederationClient (unsafeFedClientIn @'Cargohold @"stream-asset" ga) @@ -232,9 +232,9 @@ testStreamAssetWrongToken = do let key = view assetKey ast let ga = GetAsset - { gaUser = uid, - gaToken = Just tok, - gaKey = qUnqualified key + { user = uid, + token = Just tok, + key = qUnqualified key } err <- withFederationError $ do runFederationClient (unsafeFedClientIn @'Cargohold @"stream-asset" ga) diff --git a/services/cargohold/test/integration/API/Util.hs b/services/cargohold/test/integration/API/Util.hs index 3c1994a8af8..4f0b2c1c746 100644 --- a/services/cargohold/test/integration/API/Util.hs +++ b/services/cargohold/test/integration/API/Util.hs @@ -17,7 +17,7 @@ module API.Util where -import Bilge hiding (body) +import Bilge hiding (body, host, port) import CargoHold.Options import CargoHold.Run import qualified Codec.MIME.Parse as MIME @@ -64,11 +64,11 @@ uploadRaw :: Lazy.ByteString -> TestM (Response (Maybe Lazy.ByteString)) uploadRaw c usr bs = do - cargohold <- viewUnversionedCargohold + cargohold' <- viewUnversionedCargohold post $ apiVersion "v1" . c - . cargohold + . cargohold' . method POST . zUser usr . zConn "conn" @@ -177,7 +177,7 @@ deleteToken uid key = do . paths ["assets", toByteString' key, "token"] viewFederationDomain :: TestM Domain -viewFederationDomain = view (tsOpts . optSettings . setFederationDomain) +viewFederationDomain = view (tsOpts . settings . federationDomain) -------------------------------------------------------------------------------- -- Mocking utilities @@ -192,7 +192,7 @@ withSettingsOverrides f action = do liftIO $ runTestM (ts & tsEndpoint %~ setLocalEndpoint p) action setLocalEndpoint :: Word16 -> Endpoint -> Endpoint -setLocalEndpoint p = (epPort .~ p) . (epHost .~ "127.0.0.1") +setLocalEndpoint p = (port .~ p) . (host .~ "127.0.0.1") withMockFederator :: (FederatedRequest -> IO (HTTP.MediaType, LByteString)) -> @@ -201,5 +201,5 @@ withMockFederator :: withMockFederator respond action = do withTempMockFederator [] respond $ \p -> withSettingsOverrides - (optFederator . _Just %~ setLocalEndpoint (fromIntegral p)) + (federator . _Just %~ setLocalEndpoint (fromIntegral p)) action diff --git a/services/cargohold/test/integration/App.hs b/services/cargohold/test/integration/App.hs index f277a71d3c9..016de02496b 100644 --- a/services/cargohold/test/integration/App.hs +++ b/services/cargohold/test/integration/App.hs @@ -29,8 +29,8 @@ testMultiIngressCloudFrontFails = do ts <- ask let opts = view tsOpts ts - & (Opts.optAws . Opts.awsCloudFront) ?~ cloudFrontOptions - & (Opts.optAws . Opts.optMultiIngress) ?~ multiIngressMap + & (Opts.aws . Opts.cloudFront) ?~ cloudFrontOptions + & (Opts.aws . Opts.multiIngress) ?~ multiIngressMap msg <- liftIO $ catch @@ -44,9 +44,9 @@ testMultiIngressCloudFrontFails = do cloudFrontOptions :: CloudFrontOpts cloudFrontOptions = CloudFrontOpts - { _cfDomain = Domain (T.pack "example.com"), - _cfKeyPairId = KeyPairId (T.pack "anyId"), - _cfPrivateKey = "any/path" + { _domain = Domain (T.pack "example.com"), + _keyPairId = KeyPairId (T.pack "anyId"), + _privateKey = "any/path" } multiIngressMap :: Map String AWSEndpoint @@ -63,8 +63,8 @@ testMultiIngressS3DownloadEndpointFails = do ts <- ask let opts = view tsOpts ts - & (Opts.optAws . Opts.awsS3DownloadEndpoint) ?~ toAWSEndpoint "http://fake-s3:4570" - & (Opts.optAws . Opts.optMultiIngress) ?~ multiIngressMap + & (Opts.aws . Opts.s3DownloadEndpoint) ?~ toAWSEndpoint "http://fake-s3:4570" + & (Opts.aws . Opts.multiIngress) ?~ multiIngressMap msg <- liftIO $ catch diff --git a/services/cargohold/test/integration/TestSetup.hs b/services/cargohold/test/integration/TestSetup.hs index 759c760a698..03cc3ccd330 100644 --- a/services/cargohold/test/integration/TestSetup.hs +++ b/services/cargohold/test/integration/TestSetup.hs @@ -38,7 +38,7 @@ module TestSetup where import Bilge hiding (body, responseBody) -import CargoHold.Options +import CargoHold.Options hiding (domain) import Control.Exception (catch) import Control.Lens import Control.Monad.Codensity @@ -58,7 +58,7 @@ import qualified Network.Wai.Utilities.Error as Wai import Servant.Client.Streaming import Test.Tasty import Test.Tasty.HUnit -import Util.Options +import Util.Options (Endpoint (..)) import Util.Options.Common import Util.Test import Web.HttpApiData @@ -151,10 +151,10 @@ createTestSetup optsPath configPath = do tlsManagerSettings { managerResponseTimeout = responseTimeoutMicro 300000000 } - let localEndpoint p = Endpoint {_epHost = "127.0.0.1", _epPort = p} + let localEndpoint p = Endpoint {_host = "127.0.0.1", _port = p} iConf <- handleParseError =<< decodeFileEither configPath opts <- decodeFileThrow optsPath - endpoint <- optOrEnv cargohold iConf (localEndpoint . read) "CARGOHOLD_WEB_PORT" + endpoint <- optOrEnv @IntegrationConfig (.cargohold) iConf (localEndpoint . read) "CARGOHOLD_WEB_PORT" pure $ TestSetup { _tsManager = m, @@ -166,7 +166,7 @@ runFederationClient :: ClientM a -> ReaderT TestSetup (ExceptT ClientError (Code runFederationClient action = do man <- view tsManager Endpoint cHost cPort <- view tsEndpoint - domain <- view (tsOpts . optSettings . setFederationDomain) + domain <- view (tsOpts . settings . federationDomain) let base = BaseUrl Http (T.unpack cHost) (fromIntegral cPort) "/federation" let env = (mkClientEnv man base) diff --git a/services/federator/default.nix b/services/federator/default.nix index 5ed00b42be0..a6a88ed1d37 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -140,7 +140,6 @@ mkDerivation { imports kan-extensions lens - mtl optparse-applicative polysemy QuickCheck diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index 9b6dc292c8a..e3e28a30089 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -291,7 +291,6 @@ executable federator-integration , imports , kan-extensions , lens - , mtl , optparse-applicative , polysemy , QuickCheck diff --git a/services/federator/src/Federator/Run.hs b/services/federator/src/Federator/Run.hs index cd2f82f9ba2..e3072294ec6 100644 --- a/services/federator/src/Federator/Run.hs +++ b/services/federator/src/Federator/Run.hs @@ -74,18 +74,18 @@ run opts = do void $ waitAnyCancel [updateFedDomainsThread, internalServerThread, externalServerThread] where endpointInternal = federatorInternal opts - portInternal = fromIntegral $ endpointInternal ^. epPort + portInternal = fromIntegral $ endpointInternal ^. port endpointExternal = federatorExternal opts - portExternal = fromIntegral $ endpointExternal ^. epPort + portExternal = fromIntegral $ endpointExternal ^. port mkResolvConf :: RunSettings -> DNS.ResolvConf -> DNS.ResolvConf mkResolvConf settings conf = case (dnsHost settings, dnsPort settings) of - (Just host, Nothing) -> - conf {DNS.resolvInfo = DNS.RCHostName host} - (Just host, Just port) -> - conf {DNS.resolvInfo = DNS.RCHostPort host (fromIntegral port)} + (Just h, Nothing) -> + conf {DNS.resolvInfo = DNS.RCHostName h} + (Just h, Just p) -> + conf {DNS.resolvInfo = DNS.RCHostPort h (fromIntegral p)} (_, _) -> conf ------------------------------------------------------------------------------- @@ -99,8 +99,8 @@ newEnv o _dnsResolver _applog _domainConfigs = do _service Brig = Opt.brig o _service Galley = Opt.galley o _service Cargohold = Opt.cargohold o - _externalPort = o.federatorExternal._epPort - _internalPort = o.federatorInternal._epPort + _externalPort = o.federatorExternal._port + _internalPort = o.federatorInternal._port _httpManager <- initHttpManager sslContext <- mkTLSSettingsOrThrow _runSettings _http2Manager <- newIORef =<< mkHttp2Manager sslContext diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs index fb24eac5796..c5c13ea41af 100644 --- a/services/federator/test/integration/Test/Federator/IngressSpec.hs +++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs @@ -144,8 +144,8 @@ inwardBrigCallViaIngressWithSettings :: Sem r StreamingResponse inwardBrigCallViaIngressWithSettings sslCtx requestPath payload = do - Endpoint ingressHost ingressPort <- cfgNginxIngress . view teTstOpts <$> input - originDomain <- cfgOriginDomain . view teTstOpts <$> input + Endpoint ingressHost ingressPort <- nginxIngress . view teTstOpts <$> input + originDomain <- originDomain . view teTstOpts <$> input let target = SrvTarget (cs ingressHost) ingressPort headers = [(originDomainHeaderName, Text.encodeUtf8 originDomain)] mgr <- liftToCodensity . liftIO $ http2ManagerWithSSLCtx sslCtx diff --git a/services/federator/test/integration/Test/Federator/InwardSpec.hs b/services/federator/test/integration/Test/Federator/InwardSpec.hs index 066e9a50583..ae267dd67e8 100644 --- a/services/federator/test/integration/Test/Federator/InwardSpec.hs +++ b/services/federator/test/integration/Test/Federator/InwardSpec.hs @@ -31,7 +31,7 @@ import Data.ByteString.Lazy qualified as LBS import Data.Handle import Data.LegalHold (UserLegalHoldStatus (UserLegalHoldNoConsent)) import Data.Text.Encoding -import Federator.Options +import Federator.Options hiding (federatorExternal) import Imports import Network.HTTP.Types qualified as HTTP import Network.Wai.Utilities.Error qualified as E @@ -126,7 +126,7 @@ spec env = -- and "IngressSpec". it "rejectRequestsWithoutClientCertInward" $ runTestFederator env $ do - originDomain <- cfgOriginDomain <$> view teTstOpts + originDomain <- originDomain <$> view teTstOpts hdl <- randomHandle inwardCallWithHeaders "federation/brig/get-user-by-handle" @@ -145,7 +145,7 @@ inwardCallWithHeaders :: LBS.ByteString -> m (Response (Maybe LByteString)) inwardCallWithHeaders requestPath hh payload = do - Endpoint fedHost fedPort <- cfgFederatorExternal <$> view teTstOpts + Endpoint fedHost fedPort <- federatorExternal <$> view teTstOpts post ( host (encodeUtf8 fedHost) . port fedPort @@ -160,7 +160,7 @@ inwardCall :: LBS.ByteString -> m (Response (Maybe LByteString)) inwardCall requestPath payload = do - originDomain :: Text <- cfgOriginDomain <$> view teTstOpts + originDomain :: Text <- originDomain <$> view teTstOpts inwardCallWithOriginDomain (toByteString' originDomain) requestPath payload inwardCallWithOriginDomain :: @@ -170,7 +170,7 @@ inwardCallWithOriginDomain :: LBS.ByteString -> m (Response (Maybe LByteString)) inwardCallWithOriginDomain originDomain requestPath payload = do - Endpoint fedHost fedPort <- cfgFederatorExternal <$> view teTstOpts + Endpoint fedHost fedPort <- federatorExternal <$> view teTstOpts clientCertFilename <- clientCertificate . optSettings . view teOpts <$> ask clientCert <- liftIO $ BS.readFile clientCertFilename post diff --git a/services/federator/test/integration/Test/Federator/JSON.hs b/services/federator/test/integration/Test/Federator/JSON.hs index d585fd3f9d9..e69be554afb 100644 --- a/services/federator/test/integration/Test/Federator/JSON.hs +++ b/services/federator/test/integration/Test/Federator/JSON.hs @@ -26,4 +26,4 @@ deriveJSONOptions :: Options deriveJSONOptions = defaultOptions {fieldLabelModifier = labelmod} labelmod :: String -> String -labelmod = (ix 0 %~ toLower) . dropWhile (not . isUpper) +labelmod = (ix 0 %~ toLower) diff --git a/services/federator/test/integration/Test/Federator/Util.hs b/services/federator/test/integration/Test/Federator/Util.hs index 883e93bc7b0..6d8a61f0093 100644 --- a/services/federator/test/integration/Test/Federator/Util.hs +++ b/services/federator/test/integration/Test/Federator/Util.hs @@ -30,8 +30,7 @@ import Bilge.Assert import Control.Exception import Control.Lens hiding ((.=)) import Control.Monad.Catch -import Control.Monad.Except -import Crypto.Random.Types (MonadRandom, getRandomBytes) +import Crypto.Random.Types import Data.Aeson import Data.Aeson.TH import Data.Aeson.Types qualified as Aeson @@ -54,7 +53,8 @@ import Polysemy.Error import System.Random import Test.Federator.JSON import Test.Tasty.HUnit -import Util.Options +import Util.Options (Endpoint) +import Util.Options qualified as O import Wire.API.User import Wire.API.User.Auth @@ -104,11 +104,11 @@ data TestEnv = TestEnv type Select = TestEnv -> (Request -> Request) data IntegrationConfig = IntegrationConfig - { cfgBrig :: Endpoint, - cfgCargohold :: Endpoint, - cfgFederatorExternal :: Endpoint, - cfgNginxIngress :: Endpoint, - cfgOriginDomain :: Text + { brig :: Endpoint, + cargohold :: Endpoint, + federatorExternal :: Endpoint, + nginxIngress :: Endpoint, + originDomain :: Text } deriving (Show, Generic) @@ -152,8 +152,8 @@ mkEnv :: HasCallStack => IntegrationConfig -> Opts -> IO TestEnv mkEnv _teTstOpts _teOpts = do let managerSettings = mkManagerSettings (Network.Connection.TLSSettingsSimple True False False) Nothing _teMgr :: Manager <- newManager managerSettings - let _teBrig = endpointToReq (cfgBrig _teTstOpts) - _teCargohold = endpointToReq (cfgCargohold _teTstOpts) + let _teBrig = endpointToReq _teTstOpts.brig + _teCargohold = endpointToReq _teTstOpts.cargohold -- _teTLSSettings <- mkTLSSettingsOrThrow (optSettings _teOpts) _teSSLContext <- mkTLSSettingsOrThrow (optSettings _teOpts) let _teSettings = optSettings _teOpts @@ -163,7 +163,7 @@ destroyEnv :: HasCallStack => TestEnv -> IO () destroyEnv _ = pure () endpointToReq :: Endpoint -> (Bilge.Request -> Bilge.Request) -endpointToReq ep = Bilge.host (ep ^. epHost . to cs) . Bilge.port (ep ^. epPort) +endpointToReq ep = Bilge.host (ep ^. O.host . to cs) . Bilge.port (ep ^. O.port) -- All the code below is copied from brig-integration tests -- FUTUREWORK: This should live in another package and shared by all the integration tests diff --git a/services/federator/test/unit/Test/Federator/Client.hs b/services/federator/test/unit/Test/Federator/Client.hs index afe72d009e5..d5db3ae77f6 100644 --- a/services/federator/test/unit/Test/Federator/Client.hs +++ b/services/federator/test/unit/Test/Federator/Client.hs @@ -45,7 +45,7 @@ import Servant.Types.SourceT import Test.QuickCheck (arbitrary, generate) import Test.Tasty import Test.Tasty.HUnit -import Util.Options +import Util.Options (Endpoint (Endpoint)) import Wire.API.Federation.API import Wire.API.Federation.Client import Wire.API.Federation.Error diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 9e1ff71130f..811e78decdf 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -356,7 +356,7 @@ performAction :: ConversationAction tag -> Sem r (BotsAndMembers, ConversationAction tag) performAction tag origUser lconv action = do - let lcnv = fmap convId lconv + let lcnv = fmap (.convId) lconv conv = tUnqualified lconv case tag of SConversationJoinTag -> do @@ -436,7 +436,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do checkLHPolicyConflictsRemote (FutureWork (ulRemotes newMembers)) checkRemoteBackendsConnected lusr - addMembersToLocalConversation (fmap convId lconv) newMembers role + addMembersToLocalConversation (fmap (.convId) lconv) newMembers role where checkRemoteBackendsConnected :: Local UserId -> @@ -464,9 +464,9 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do Sem r () checkLocals lusr (Just tid) newUsers = do tms <- - Map.fromList . map (view userId &&& id) + Map.fromList . map (view Wire.API.Team.Member.userId &&& Imports.id) <$> E.selectTeamMembers tid newUsers - let userMembershipMap = map (id &&& flip Map.lookup tms) newUsers + let userMembershipMap = map (Imports.id &&& flip Map.lookup tms) newUsers ensureAccessRole (convAccessRoles conv) userMembershipMap ensureConnectedOrSameTeam lusr newUsers checkLocals lusr Nothing newUsers = do @@ -568,7 +568,7 @@ performConversationAccessData qusr lconv action = do pure (mempty, action) where - lcnv = fmap convId lconv + lcnv = fmap (.convId) lconv conv = tUnqualified lconv maybeRemoveBots :: BotsAndMembers -> Sem r BotsAndMembers @@ -673,7 +673,7 @@ updateLocalConversationUnchecked :: Sem r LocalConversationUpdate updateLocalConversationUnchecked lconv qusr con action = do let tag = sing @tag - lcnv = fmap convId lconv + lcnv = fmap (.convId) lconv conv = tUnqualified lconv -- retrieve member @@ -777,7 +777,7 @@ notifyConversationAction :: Sem r LocalConversationUpdate notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do now <- input - let lcnv = fmap convId lconv + let lcnv = fmap (.convId) lconv e = conversationActionToEvent tag now quid (tUntagged lcnv) Nothing action let mkUpdate uids = @@ -990,11 +990,11 @@ notifyTypingIndicator conv qusr mcon ts = do let (remoteMemsOrig, remoteMemsOther) = List.partition ((origDomain ==) . tDomain . rmId) (Data.convRemoteMembers conv) tdu users = TypingDataUpdated - { tudTime = now, - tudOrigUserId = qusr, - tudConvId = Data.convId conv, - tudUsersInConv = users, - tudTypingStatus = ts + { time = now, + origUserId = qusr, + convId = Data.convId conv, + usersInConv = users, + typingStatus = ts } void $ E.runFederatedConcurrentlyEither (fmap rmId remoteMemsOther) $ \rmems -> do diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index f939de7357e..99b5f72a771 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -92,7 +92,6 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Common (EmptyResponse (..)) -import Wire.API.Federation.API.Galley import Wire.API.Federation.API.Galley qualified as F import Wire.API.Federation.Error import Wire.API.FederationUpdate (fetch) @@ -145,16 +144,16 @@ onClientRemoved :: Member TinyLog r ) => Domain -> - ClientRemovedRequest -> + F.ClientRemovedRequest -> Sem r EmptyResponse onClientRemoved domain req = do - let qusr = Qualified (F.crrUser req) domain + let qusr = Qualified req.user domain whenM isMLSEnabled $ do - for_ (F.crrConvs req) $ \convId -> do + for_ req.convs $ \convId -> do mConv <- E.getConversation convId for mConv $ \conv -> do lconv <- qualifyLocal conv - removeClient lconv qusr (F.crrClient req) + removeClient lconv qusr (F.client req) pure EmptyResponse onConversationCreated :: @@ -171,11 +170,11 @@ onConversationCreated :: onConversationCreated domain rc = do let qrc = fmap (toRemoteUnsafe domain) rc loc <- qualifyLocal () - let (localUserIds, _) = partitionQualified loc (map omQualifiedId (toList (F.ccNonCreatorMembers rc))) + let (localUserIds, _) = partitionQualified loc (map omQualifiedId (toList (F.nonCreatorMembers rc))) addedUserIds <- addLocalUsersToRemoteConv - (F.ccCnvId qrc) + (F.cnvId qrc) (tUntagged (F.ccRemoteOrigUserId qrc)) localUserIds @@ -187,17 +186,17 @@ onConversationCreated domain rc = do (const True) . omQualifiedId ) - (F.ccNonCreatorMembers rc) + (F.nonCreatorMembers rc) -- Make sure to notify only about local users connected to the adder - let qrcConnected = qrc {F.ccNonCreatorMembers = connectedMembers} + let qrcConnected = qrc {F.nonCreatorMembers = connectedMembers} for_ (fromConversationCreated loc qrcConnected) $ \(mem, c) -> do let event = Event - (tUntagged (F.ccCnvId qrcConnected)) + (tUntagged (F.cnvId qrcConnected)) Nothing (tUntagged (F.ccRemoteOrigUserId qrcConnected)) - (F.ccTime qrcConnected) + qrcConnected.time (EdConversation c) pushConversationEvent Nothing event (qualifyAs loc [qUnqualified . Public.memId $ mem]) [] pure EmptyResponse @@ -252,8 +251,8 @@ leaveConversation :: F.LeaveConversationRequest -> Sem r F.LeaveConversationResponse leaveConversation requestingDomain lc = do - let leaver = Qualified (F.lcLeaver lc) requestingDomain - lcnv <- qualifyLocal (F.lcConvId lc) + let leaver = Qualified lc.leaver requestingDomain + lcnv <- qualifyLocal lc.convId res <- runError @@ -319,19 +318,19 @@ onMessageSent :: Sem r EmptyResponse onMessageSent domain rmUnqualified = do let rm = fmap (toRemoteUnsafe domain) rmUnqualified - convId = tUntagged $ F.rmConversation rm + convId = tUntagged rm.conversation msgMetadata = MessageMetadata - { mmNativePush = F.rmPush rm, - mmTransient = F.rmTransient rm, - mmNativePriority = F.rmPriority rm, - mmData = F.rmData rm + { mmNativePush = F.push rm, + mmTransient = F.transient rm, + mmNativePriority = F.priority rm, + mmData = F._data rm } - recipientMap = userClientMap $ F.rmRecipients rm + recipientMap = userClientMap rm.recipients msgs = toMapOf (itraversed <.> itraversed) recipientMap (members, allMembers) <- first Set.fromList - <$> E.selectRemoteMembers (Map.keys recipientMap) (F.rmConversation rm) + <$> E.selectRemoteMembers (Map.keys recipientMap) rm.conversation unless allMembers $ P.warn $ Log.field "conversation" (toByteString' (qUnqualified convId)) @@ -345,9 +344,9 @@ onMessageSent domain rmUnqualified = do void $ sendLocalMessages loc - (F.rmTime rm) - (F.rmSender rm) - (F.rmSenderClient rm) + rm.time + rm.sender + rm.senderClient Nothing (Just convId) mempty @@ -374,9 +373,9 @@ sendMessage :: F.ProteusMessageSendRequest -> Sem r F.MessageSendResponse sendMessage originDomain msr = do - let sender = Qualified (F.pmsrSender msr) originDomain - msg <- either throwErr pure (fromProto (fromBase64ByteString (F.pmsrRawMessage msr))) - lcnv <- qualifyLocal (F.pmsrConvId msr) + let sender = Qualified msr.sender originDomain + msg <- either throwErr pure (fromProto (fromBase64ByteString msr.rawMessage)) + lcnv <- qualifyLocal msr.convId F.MessageSendResponse <$> postQualifiedOtrMessage User sender Nothing lcnv msg where throwErr = throw . InvalidPayload . LT.pack @@ -398,9 +397,9 @@ onUserDeleted :: F.UserDeletedConversationsNotification -> Sem r EmptyResponse onUserDeleted origDomain udcn = do - let deletedUser = toRemoteUnsafe origDomain (F.udcvUser udcn) + let deletedUser = toRemoteUnsafe origDomain udcn.user untaggedDeletedUser = tUntagged deletedUser - convIds = F.udcvConversations udcn + convIds = F.conversations udcn E.spawnMany $ fromRange convIds <&> \c -> do @@ -461,13 +460,13 @@ updateConversation :: ) => Domain -> F.ConversationUpdateRequest -> - Sem r ConversationUpdateResponse + Sem r F.ConversationUpdateResponse updateConversation origDomain updateRequest = do loc <- qualifyLocal () - let rusr = toRemoteUnsafe origDomain (F.curUser updateRequest) - lcnv = qualifyAs loc (F.curConvId updateRequest) + let rusr = toRemoteUnsafe origDomain updateRequest.user + lcnv = qualifyAs loc updateRequest.convId - mkResponse $ case F.curAction updateRequest of + mkResponse $ case F.action updateRequest of SomeConversationAction tag action -> case tag of SConversationJoinTag -> mapToGalleyError @(HasConversationActionGalleyErrors 'ConversationJoinTag) @@ -514,11 +513,11 @@ updateConversation origDomain updateRequest = do $ updateLocalConversation @'ConversationAccessDataTag lcnv (tUntagged rusr) Nothing action where mkResponse = - fmap (either F.ConversationUpdateResponseError id) + fmap (either F.ConversationUpdateResponseError Imports.id) . runError @GalleyError . fmap (fromRight F.ConversationUpdateResponseNoChanges) . runError @NoChanges - . fmap (either F.ConversationUpdateResponseNonFederatingBackends id) + . fmap (either F.ConversationUpdateResponseNonFederatingBackends Imports.id) . runError @NonFederatingBackends . fmap (either F.ConversationUpdateResponseUnreachableBackends id) . runError @UnreachableBackends @@ -536,16 +535,16 @@ handleMLSMessageErrors :: ': r ) ) => - Sem r1 MLSMessageResponse -> - Sem r MLSMessageResponse + Sem r1 F.MLSMessageResponse -> + Sem r F.MLSMessageResponse handleMLSMessageErrors = - fmap (either (F.MLSMessageResponseProtocolError . unTagged) id) + fmap (either (F.MLSMessageResponseProtocolError . unTagged) Imports.id) . runError @MLSProtocolError - . fmap (either F.MLSMessageResponseError id) + . fmap (either F.MLSMessageResponseError Imports.id) . runError - . fmap (either (F.MLSMessageResponseProposalFailure . pfInner) id) + . fmap (either (F.MLSMessageResponseProposalFailure . pfInner) Imports.id) . runError - . fmap (either F.MLSMessageResponseNonFederatingBackends id) + . fmap (either F.MLSMessageResponseNonFederatingBackends Imports.id) . runError . fmap (either (F.MLSMessageResponseUnreachableBackends . Set.fromList . (.backends)) id) . runError @UnreachableBackends @@ -576,11 +575,11 @@ sendMLSCommitBundle :: sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do assertMLSEnabled loc <- qualifyLocal () - let sender = toRemoteUnsafe remoteDomain (F.mmsrSender msr) - bundle <- either (throw . mlsProtocolError) pure $ deserializeCommitBundle (fromBase64ByteString (F.mmsrRawMessage msr)) + let sender = toRemoteUnsafe remoteDomain msr.sender + bundle <- either (throw . mlsProtocolError) pure $ deserializeCommitBundle (fromBase64ByteString msr.rawMessage) let msg = rmValue (cbCommitMsg bundle) qcnv <- E.getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound - when (Conv (qUnqualified qcnv) /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch + when (Conv (qUnqualified qcnv) /= F.convOrSubId msr) $ throwS @'MLSGroupConversationMismatch uncurry F.MLSMessageResponseUpdates . (,mempty) . map lcuUpdate <$> postMLSCommitBundle loc (tUntagged sender) Nothing qcnv Nothing bundle @@ -609,12 +608,12 @@ sendMLSMessage :: sendMLSMessage remoteDomain msr = handleMLSMessageErrors $ do assertMLSEnabled loc <- qualifyLocal () - let sender = toRemoteUnsafe remoteDomain (F.mmsrSender msr) - raw <- either (throw . mlsProtocolError) pure $ decodeMLS' (fromBase64ByteString (F.mmsrRawMessage msr)) + let sender = toRemoteUnsafe remoteDomain msr.sender + raw <- either (throw . mlsProtocolError) pure $ decodeMLS' (fromBase64ByteString msr.rawMessage) case rmValue raw of SomeMessage _ msg -> do qcnv <- E.getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound - when (Conv (qUnqualified qcnv) /= F.mmsrConvOrSubId msr) $ throwS @'MLSGroupConversationMismatch + when (Conv (qUnqualified qcnv) /= F.convOrSubId msr) $ throwS @'MLSGroupConversationMismatch uncurry F.MLSMessageResponseUpdates . first (map lcuUpdate) <$> postMLSMessage loc (tUntagged sender) Nothing qcnv Nothing raw @@ -626,7 +625,7 @@ class ToGalleyRuntimeError (effs :: EffectRow) r where Sem r a instance ToGalleyRuntimeError '[] r where - mapToGalleyError = id + mapToGalleyError = Imports.id instance forall (err :: GalleyError) effs r. @@ -652,8 +651,8 @@ mlsSendWelcome :: Domain -> F.MLSWelcomeRequest -> Sem r F.MLSWelcomeResponse -mlsSendWelcome _origDomain (fromBase64ByteString . F.unMLSWelcomeRequest -> rawWelcome) = - fmap (either (const MLSWelcomeMLSNotEnabled) (const MLSWelcomeSent)) +mlsSendWelcome _origDomain (fromBase64ByteString . F.mlsWelcomeRequest -> rawWelcome) = + fmap (either (const F.MLSWelcomeMLSNotEnabled) (const F.MLSWelcomeSent)) . runError @(Tagged 'MLSNotEnabled ()) $ do assertMLSEnabled @@ -685,13 +684,13 @@ onMLSMessageSent :: F.RemoteMLSMessage -> Sem r F.RemoteMLSMessageResponse onMLSMessageSent domain rmm = - fmap (either (const RemoteMLSMessageMLSNotEnabled) (const RemoteMLSMessageOk)) + fmap (either (const F.RemoteMLSMessageMLSNotEnabled) (const F.RemoteMLSMessageOk)) . runError @(Tagged 'MLSNotEnabled ()) $ do assertMLSEnabled loc <- qualifyLocal () - let rcnv = toRemoteUnsafe domain (F.rmmConversation rmm) - let users = Set.fromList (map fst (F.rmmRecipients rmm)) + let rcnv = toRemoteUnsafe domain rmm.conversation + let users = Set.fromList (map fst rmm.recipients) (members, allMembers) <- first Set.fromList <$> E.selectRemoteMembers (toList users) rcnv @@ -704,14 +703,14 @@ onMLSMessageSent domain rmm = \ users not in the conversation" :: ByteString ) - let recipients = filter (\(u, _) -> Set.member u members) (F.rmmRecipients rmm) + let recipients = filter (\(u, _) -> Set.member u members) rmm.recipients -- FUTUREWORK: support local bots let e = - Event (tUntagged rcnv) Nothing (F.rmmSender rmm) (F.rmmTime rmm) $ - EdMLSMessage (fromBase64ByteString (F.rmmMessage rmm)) + Event (tUntagged rcnv) Nothing rmm.sender rmm.time $ + EdMLSMessage (fromBase64ByteString rmm.message) runMessagePush loc (Just (tUntagged rcnv)) $ - newMessagePush mempty Nothing (F.rmmMetadata rmm) recipients e + newMessagePush mempty Nothing rmm.metadata recipients e queryGroupInfo :: ( Member ConversationStore r, @@ -728,8 +727,8 @@ queryGroupInfo origDomain req = . mapToGalleyError @MLSGroupInfoStaticErrors $ do assertMLSEnabled - lconvId <- qualifyLocal . ggireqConv $ req - let sender = toRemoteUnsafe origDomain . ggireqSender $ req + lconvId <- qualifyLocal req.conv + let sender = toRemoteUnsafe origDomain req.sender state <- getGroupInfoFromLocalConv (tUntagged sender) lconvId pure . Base64ByteString @@ -746,27 +745,27 @@ updateTypingIndicator :: Domain -> F.TypingDataUpdateRequest -> Sem r F.TypingDataUpdateResponse -updateTypingIndicator origDomain TypingDataUpdateRequest {..} = do - let qusr = Qualified tdurUserId origDomain - lcnv <- qualifyLocal tdurConvId +updateTypingIndicator origDomain F.TypingDataUpdateRequest {..} = do + let qusr = Qualified userId origDomain + lcnv <- qualifyLocal convId ret <- runError . mapToRuntimeError @'ConvNotFound ConvNotFound $ do (conv, _) <- getConversationAndMemberWithError @'ConvNotFound qusr lcnv - notifyTypingIndicator conv qusr Nothing tdurTypingStatus + notifyTypingIndicator conv qusr Nothing typingStatus - pure (either TypingDataUpdateError TypingDataUpdateSuccess ret) + pure (either F.TypingDataUpdateError F.TypingDataUpdateSuccess ret) onTypingIndicatorUpdated :: ( Member GundeckAccess r ) => Domain -> - TypingDataUpdated -> + F.TypingDataUpdated -> Sem r EmptyResponse -onTypingIndicatorUpdated origDomain TypingDataUpdated {..} = do - let qcnv = Qualified tudConvId origDomain - pushTypingIndicatorEvents tudOrigUserId tudTime tudUsersInConv Nothing qcnv tudTypingStatus +onTypingIndicatorUpdated origDomain F.TypingDataUpdated {..} = do + let qcnv = Qualified convId origDomain + pushTypingIndicatorEvents origUserId time usersInConv Nothing qcnv typingStatus pure EmptyResponse -- Since we already have the origin domain where the defederation event started, diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index a21cf229c40..7f1565036fe 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -78,7 +78,7 @@ import Galley.Effects.ProposalStore import Galley.Effects.TeamStore import Galley.Intra.Push qualified as Intra import Galley.Monad -import Galley.Options +import Galley.Options hiding (brig) import Galley.Queue qualified as Q import Galley.Types.Bot (AddBot, RemoveBot) import Galley.Types.Bot.Service @@ -124,7 +124,7 @@ import Wire.Sem.Paging.Cassandra internalAPI :: API InternalAPI GalleyEffects internalAPI = - hoistAPI @InternalAPIBase id $ + hoistAPI @InternalAPIBase Imports.id $ mkNamedAPI @"status" (pure ()) <@> mkNamedAPI @"delete-user" (callsFed (exposeAnnotations rmUser)) <@> mkNamedAPI @"connect" (callsFed (exposeAnnotations Create.createConnectConversation)) @@ -140,7 +140,7 @@ federationAPI = mkNamedAPI @"get-federation-status" (const getFederationStatus) legalholdWhitelistedTeamsAPI :: API ILegalholdWhitelistedTeamsAPI GalleyEffects -legalholdWhitelistedTeamsAPI = mkAPI $ \tid -> hoistAPIHandler id (base tid) +legalholdWhitelistedTeamsAPI = mkAPI $ \tid -> hoistAPIHandler Imports.id (base tid) where base :: TeamId -> API ILegalholdWhitelistedTeamsAPIBase GalleyEffects base tid = @@ -149,13 +149,13 @@ legalholdWhitelistedTeamsAPI = mkAPI $ \tid -> hoistAPIHandler id (base tid) <@> mkNamedAPI @"get-team-legalhold-whitelisted" (LegalHoldStore.isTeamLegalholdWhitelisted tid) iTeamsAPI :: API ITeamsAPI GalleyEffects -iTeamsAPI = mkAPI $ \tid -> hoistAPIHandler id (base tid) +iTeamsAPI = mkAPI $ \tid -> hoistAPIHandler Imports.id (base tid) where hoistAPISegment :: (ServerT (seg :> inner) (Sem r) ~ ServerT inner (Sem r)) => API inner r -> API (seg :> inner) r - hoistAPISegment = hoistAPI id + hoistAPISegment = hoistAPI Imports.id base :: TeamId -> API ITeamsAPIBase GalleyEffects base tid = @@ -413,7 +413,7 @@ rmUser lusr conn = do for_ (maybeList1 (catMaybes pp)) - push + Galley.Effects.GundeckAccess.push -- FUTUREWORK: This could be optimized to reduce the number of RPCs -- made. When a team is deleted the burst of RPCs created here could @@ -571,7 +571,7 @@ deleteFederationDomainRemoteUserFromLocalConversations (fromRange -> maxPage) do getPaginatedData page env <- input let lCnvMap = foldr insertIntoMap mempty remoteUsers - localDomain = env ^. Galley.App.options . optSettings . setFederationDomain + localDomain = env ^. Galley.App.options . Galley.Options.settings . federationDomain for_ (Map.toList lCnvMap) $ \(cnvId, rUsers) -> do let mapAllErrors :: Text -> diff --git a/services/galley/src/Galley/API/LegalHold/Conflicts.hs b/services/galley/src/Galley/API/LegalHold/Conflicts.hs index 68b7920f856..a6d098b459b 100644 --- a/services/galley/src/Galley/API/LegalHold/Conflicts.hs +++ b/services/galley/src/Galley/API/LegalHold/Conflicts.hs @@ -84,7 +84,7 @@ guardLegalholdPolicyConflicts LegalholdPlusFederationNotImplemented _otherClient guardLegalholdPolicyConflicts UnprotectedBot _otherClients = pure () guardLegalholdPolicyConflicts (ProtectedUser self) otherClients = do opts <- input - case view (optSettings . setFeatureFlags . flagLegalHold) opts of + case view (settings . featureFlags . flagLegalHold) opts of FeatureLegalHoldDisabledPermanently -> case FutureWork @'LegalholdPlusFederationNotImplemented () of FutureWork () -> pure () -- FUTUREWORK: if federation is enabled, we still need to run the guard! FeatureLegalHoldDisabledByDefault -> guardLegalholdPolicyConflictsUid self otherClients diff --git a/services/galley/src/Galley/API/MLS/GroupInfo.hs b/services/galley/src/Galley/API/MLS/GroupInfo.hs index 58772657b25..73d74ffe589 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfo.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfo.hs @@ -87,8 +87,8 @@ getGroupInfoFromRemoteConv :: getGroupInfoFromRemoteConv lusr rcnv = do let getRequest = GetGroupInfoRequest - { ggireqSender = tUnqualified lusr, - ggireqConv = tUnqualified rcnv + { sender = tUnqualified lusr, + conv = tUnqualified rcnv } response <- E.runFederated rcnv (fedClient @'Galley @"query-group-info" getRequest) case response of diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index c8509d199e6..643048311c0 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -338,9 +338,9 @@ postMLSCommitBundleToRemoteConv loc qusr con bundle rcnv = do runFederated rcnv $ fedClient @'Galley @"send-mls-commit-bundle" $ MLSMessageSendRequest - { mmsrConvOrSubId = Conv $ tUnqualified rcnv, - mmsrSender = tUnqualified lusr, - mmsrRawMessage = Base64ByteString (serializeCommitBundle bundle) + { convOrSubId = Conv $ tUnqualified rcnv, + sender = tUnqualified lusr, + rawMessage = Base64ByteString (serializeCommitBundle bundle) } case resp of MLSMessageResponseError e -> rethrowErrors @MLSBundleStaticErrors e @@ -523,9 +523,9 @@ postMLSMessageToRemoteConv loc qusr _senderClient con smsg rcnv = do runFederated rcnv $ fedClient @'Galley @"send-mls-message" $ MLSMessageSendRequest - { mmsrConvOrSubId = Conv $ tUnqualified rcnv, - mmsrSender = tUnqualified lusr, - mmsrRawMessage = Base64ByteString (rmRaw smsg) + { convOrSubId = Conv $ tUnqualified rcnv, + sender = tUnqualified lusr, + rawMessage = Base64ByteString (rmRaw smsg) } case resp of MLSMessageResponseError e -> rethrowErrors @MLSMessageStaticErrors e @@ -728,7 +728,7 @@ processExternalCommit qusr mSenderClient lconv mlsMeta cm epoch action updatePat -- increment epoch number setConversationEpoch (Data.convId (tUnqualified lconv)) (succ epoch) -- fetch local conversation with new epoch - lc <- qualifyAs lconv <$> getLocalConvForUser qusr (convId <$> lconv) + lc <- qualifyAs lconv <$> getLocalConvForUser qusr ((.convId) <$> lconv) -- fetch backend remove proposals of the previous epoch kpRefs <- getPendingBackendRemoveProposals (cnvmlsGroupId mlsMeta) epoch -- requeue backend remove proposals for the current epoch @@ -929,10 +929,10 @@ applyProposalRef conv mlsMeta groupId epoch _suite (Ref ref) = do p <- getProposal groupId epoch ref >>= noteS @'MLSProposalNotFound checkEpoch epoch mlsMeta checkGroup groupId mlsMeta - applyProposal (convId conv) groupId (rmValue p) + applyProposal conv.convId groupId (rmValue p) applyProposalRef conv _mlsMeta groupId _epoch suite (Inline p) = do checkProposalCipherSuite suite p - applyProposal (convId conv) groupId p + applyProposal conv.convId groupId p applyProposal :: forall r. @@ -1021,11 +1021,11 @@ processProposal qusr conv mlsMeta msg prop = do foldQualified loc ( fmap isJust - . getLocalMember (convId conv) + . getLocalMember conv.convId . tUnqualified ) ( fmap isJust - . getRemoteMember (convId conv) + . getRemoteMember conv.convId ) qusr unless isMember' $ throwS @'ConvNotFound @@ -1273,8 +1273,8 @@ getRemoteMLSClients rusr ss = do runFederated rusr $ fedClient @'Brig @"get-mls-clients" $ MLSClientsRequest - { mcrUserId = tUnqualified rusr, - mcrSignatureScheme = ss + { userId = tUnqualified rusr, + signatureScheme = ss } -- | Check if the epoch number matches that of a conversation @@ -1321,7 +1321,7 @@ class HandleMLSProposalFailure eff r where handleMLSProposalFailure :: Sem (eff ': r) a -> Sem r a instance HandleMLSProposalFailures '[] r where - handleMLSProposalFailures = id + handleMLSProposalFailures = Imports.id instance ( HandleMLSProposalFailures effs r, diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index c95e4c7dca6..d40758fa735 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -87,12 +87,12 @@ propagateMessage qusr lconv cm con raw = do $ \(tUnqualified -> rs) -> fedClient @'Galley @"on-mls-message-sent" $ RemoteMLSMessage - { rmmTime = now, - rmmSender = qusr, - rmmMetadata = mm, - rmmConversation = tUnqualified lcnv, - rmmRecipients = rs >>= remoteMemberMLSClients, - rmmMessage = Base64ByteString raw + { time = now, + sender = qusr, + metadata = mm, + conversation = tUnqualified lcnv, + recipients = rs >>= remoteMemberMLSClients, + message = Base64ByteString raw } where localMemberMLSClients :: Local x -> LocalMember -> [(UserId, ClientId)] diff --git a/services/galley/src/Galley/API/Mapping.hs b/services/galley/src/Galley/API/Mapping.hs index 074cab6f2f9..ec6f0993e12 100644 --- a/services/galley/src/Galley/API/Mapping.hs +++ b/services/galley/src/Galley/API/Mapping.hs @@ -29,7 +29,6 @@ import Data.Id (UserId, idToText) import Data.Qualified import Galley.API.Error import Galley.Data.Conversation qualified as Data -import Galley.Data.Types (convId) import Galley.Types.Conversations.Members import Imports import Polysemy @@ -76,7 +75,7 @@ conversationViewWithCachedOthers remoteOthers localOthers conv luid = do val "User " +++ idToText (tUnqualified luid) +++ val " is not a member of conv " - +++ idToText (convId conv) + +++ idToText (Data.convId conv) throw BadMemberState -- | View for a given user of a stored conversation. @@ -89,7 +88,7 @@ conversationViewMaybe luid remoteOthers localOthers conv = do let others = filter (\oth -> tUntagged luid /= omQualifiedId oth) localOthers <> remoteOthers pure $ Conversation - (tUntagged . qualifyAs luid . convId $ conv) + (tUntagged . qualifyAs luid . Data.convId $ conv) (Data.convMetadata conv) (ConvMembers self others) (Data.convProtocol conv) @@ -101,8 +100,8 @@ remoteConversationView :: Remote RemoteConversation -> Conversation remoteConversationView uid status (tUntagged -> Qualified rconv rDomain) = - let mems = rcnvMembers rconv - others = rcmOthers mems + let mems = rconv.members + others = mems.others self = localMemberToSelf uid @@ -110,13 +109,13 @@ remoteConversationView uid status (tUntagged -> Qualified rconv rDomain) = { lmId = tUnqualified uid, lmService = Nothing, lmStatus = status, - lmConvRoleName = rcmSelfRole mems + lmConvRoleName = mems.selfRole } in Conversation - (Qualified (rcnvId rconv) rDomain) - (rcnvMetadata rconv) + (Qualified rconv.id rDomain) + rconv.metadata (ConvMembers self others) - (rcnvProtocol rconv) + rconv.protocol -- | Convert a local conversation to a structure to be returned to a remote -- backend. @@ -130,20 +129,20 @@ conversationToRemote :: conversationToRemote localDomain ruid conv = do let (selfs, rothers) = partition ((== ruid) . rmId) (Data.convRemoteMembers conv) lothers = Data.convLocalMembers conv - selfRole <- rmConvRoleName <$> listToMaybe selfs - let others = + selfRole' <- rmConvRoleName <$> listToMaybe selfs + let others' = map (localMemberToOther localDomain) lothers <> map remoteMemberToOther rothers pure $ RemoteConversation - { rcnvId = Data.convId conv, - rcnvMetadata = Data.convMetadata conv, - rcnvMembers = + { id = Data.convId conv, + metadata = Data.convMetadata conv, + members = RemoteConvMembers - { rcmSelfRole = selfRole, - rcmOthers = others + { selfRole = selfRole', + others = others' }, - rcnvProtocol = Data.convProtocol conv + protocol = Data.convProtocol conv } -- | Convert a local conversation member (as stored in the DB) to a publicly diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index 5b210ee8347..9302001017f 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -148,7 +148,7 @@ mkMessageSendingStatus time mismatch = } clientMismatchStrategyApply :: ClientMismatchStrategy -> QualifiedRecipientSet -> QualifiedRecipientSet -clientMismatchStrategyApply MismatchReportAll = id +clientMismatchStrategyApply MismatchReportAll = Imports.id clientMismatchStrategyApply MismatchIgnoreAll = const mempty clientMismatchStrategyApply (MismatchReportOnly users) = Set.filter (\(d, u, _) -> Set.member (Qualified u d) users) @@ -190,7 +190,7 @@ checkMessageClients :: ClientMismatchStrategy -> (Bool, Map (Domain, UserId, ClientId) ByteString, QualifiedMismatch) checkMessageClients sender participantMap recipientMap mismatchStrat = - let participants = setOf ((itraversed <. folded) . withIndex . to (\((d, u), c) -> (d, u, c))) participantMap + let participants = setOf ((itraversed <. folded) . withIndex . Control.Lens.to (\((d, u), c) -> (d, u, c))) participantMap expected = Set.delete sender participants expectedUsers :: Set (Domain, UserId) = Map.keysSet participantMap @@ -242,12 +242,12 @@ postRemoteOtrMessage :: postRemoteOtrMessage sender conv rawMsg = do let msr = ProteusMessageSendRequest - { pmsrConvId = tUnqualified conv, - pmsrSender = qUnqualified sender, - pmsrRawMessage = Base64ByteString rawMsg + { convId = tUnqualified conv, + sender = qUnqualified sender, + rawMessage = Base64ByteString rawMsg } rpc = fedClient @'Galley @"send-message" msr - msResponse <$> runFederated conv rpc + (.response) <$> runFederated conv rpc postBroadcast :: ( Member BrigAccess r, @@ -287,7 +287,7 @@ postBroadcast lusr con msg = runError $ do -- is used and length `report_missing` < limit since we cannot fetch larger teams than -- that. tMembers <- - fmap (view userId) <$> case qualifiedNewOtrClientMismatchStrategy msg of + fmap (view Wire.API.Team.Member.userId) <$> case qualifiedNewOtrClientMismatchStrategy msg of -- Note: remote ids are not in a local team MismatchReportOnly qus -> maybeFetchLimitedTeamMemberList @@ -411,7 +411,7 @@ postQualifiedOtrMessage senderType sender mconn lcnv msg = Set.fromList $ map (tUntagged . qualifyAs lcnv) localMemberIds <> map (tUntagged . rmId) (convRemoteMembers conv) - isInternal <- view (optSettings . setIntraListing) <$> input + isInternal <- view (settings . intraListing) <$> input -- check if the sender is part of the conversation unless (Set.member sender members) $ @@ -653,15 +653,15 @@ sendRemoteMessages domain now sender senderClient lcnv metadata messages = (hand (Map.assocs messages) rm = RemoteMessage - { rmTime = now, - rmData = mmData metadata, - rmSender = sender, - rmSenderClient = senderClient, - rmConversation = tUnqualified lcnv, - rmPriority = mmNativePriority metadata, - rmPush = mmNativePush metadata, - rmTransient = mmTransient metadata, - rmRecipients = UserClientMap rcpts + { time = now, + _data = mmData metadata, + sender = sender, + senderClient = senderClient, + conversation = tUnqualified lcnv, + priority = mmNativePriority metadata, + push = mmNativePush metadata, + transient = mmTransient metadata, + recipients = UserClientMap rcpts } let rpc = void $ fedQueueClient @'Galley @"on-message-sent" rm enqueueNotification domain Q.Persistent rpc @@ -716,7 +716,7 @@ class Unqualify a b where unqualify :: Domain -> a -> b instance Unqualify a a where - unqualify _ = id + unqualify _ = Imports.id instance Unqualify MessageSendingStatus ClientMismatch where unqualify domain status = diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 16635d55b37..02b9e3ad885 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -249,7 +249,7 @@ getRemoteConversationsWithFailures lusr convs = do lusr ( Map.findWithDefault defMemberStatus - (fmap rcnvId rconv) + ((.id) <$> rconv) statusMap ) rconv @@ -277,7 +277,7 @@ getRemoteConversationsWithFailures lusr convs = do Logger.msg ("Error occurred while fetching remote conversations" :: ByteString) . Logger.field "error" (show e) pure . Left $ failedGetConversationRemotely (sequenceA rcids) e - handleFailure (Right c) = pure . Right . traverse gcresConvs $ c + handleFailure (Right c) = pure . Right . traverse (.convs) $ c getConversationRoles :: ( Member ConversationStore r, @@ -690,7 +690,7 @@ getConversationGuestLinksFeatureStatus :: Maybe TeamId -> Sem r (WithStatus GuestLinksConfig) getConversationGuestLinksFeatureStatus mbTid = do - defaultStatus :: WithStatus GuestLinksConfig <- input <&> view (optSettings . setFeatureFlags . flagConversationGuestLinks . unDefaults) + defaultStatus :: WithStatus GuestLinksConfig <- input <&> view (settings . featureFlags . flagConversationGuestLinks . unDefaults) case mbTid of Nothing -> pure defaultStatus Just tid -> do diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 53f1a9ad815..921990a56fd 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -290,11 +290,11 @@ updateTeamStatus tid (TeamStatusUpdate newStatus cur) = do oldStatus <- fmap tdStatus $ E.getTeam tid >>= noteS @'TeamNotFound valid <- validateTransition (oldStatus, newStatus) when valid $ do - journal newStatus cur + runJournal newStatus cur E.setTeamStatus tid newStatus where - journal Suspended _ = Journal.teamSuspend tid - journal Active c = do + runJournal Suspended _ = Journal.teamSuspend tid + runJournal Active c = do teamCreationTime <- E.getTeamCreationTime tid -- When teams are created, they are activated immediately. In this situation, Brig will -- most likely report team size as 0 due to ES taking some time to index the team creator. @@ -305,7 +305,7 @@ updateTeamStatus tid (TeamStatusUpdate newStatus cur) = do then 1 else possiblyStaleSize Journal.teamActivate tid size c teamCreationTime - journal _ _ = throwS @'InvalidTeamStatusUpdate + runJournal _ _ = throwS @'InvalidTeamStatusUpdate validateTransition :: Member (ErrorS 'InvalidTeamStatusUpdate) r => (TeamStatus, TeamStatus) -> Sem r Bool validateTransition = \case (PendingActive, Active) -> pure True @@ -437,10 +437,10 @@ uncheckedDeleteTeam lusr zcon tid = do where pushDeleteEvents :: [TeamMember] -> Event -> [Push] -> Sem r () pushDeleteEvents membs e ue = do - o <- inputs (view optSettings) + o <- inputs (view settings) let r = list1 (userRecipient (tUnqualified lusr)) (membersToRecipients (Just (tUnqualified lusr)) membs) -- To avoid DoS on gundeck, send team deletion events in chunks - let chunkSize = fromMaybe defConcurrentDeletionEvents (o ^. setConcurrentDeletionEvents) + let chunkSize = fromMaybe defConcurrentDeletionEvents (o ^. concurrentDeletionEvents) let chunks = List.chunksOf chunkSize (toList r) forM_ chunks $ \case [] -> pure () @@ -1216,7 +1216,7 @@ ensureNotTooLarge :: ensureNotTooLarge tid = do o <- input (TeamSize size) <- E.getSize tid - unless (size < fromIntegral (o ^. optSettings . setMaxTeamSize)) $ + unless (size < fromIntegral (o ^. settings . maxTeamSize)) $ throwS @'TooManyTeamMembers pure $ TeamSize size diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index 9ba9d1e2159..0fae9d94deb 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -356,7 +356,7 @@ genericGetConfigForUser uid = do instance GetFeatureConfig SSOConfig where getConfigForServer = do status <- - inputs (view (optSettings . setFeatureFlags . flagSSO)) <&> \case + inputs (view (settings . featureFlags . flagSSO)) <&> \case FeatureSSOEnabledByDefault -> FeatureStatusEnabled FeatureSSODisabledByDefault -> FeatureStatusDisabled pure $ setStatus status defFeatureStatus @@ -366,14 +366,14 @@ instance GetFeatureConfig SSOConfig where instance GetFeatureConfig SearchVisibilityAvailableConfig where getConfigForServer = do status <- - inputs (view (optSettings . setFeatureFlags . flagTeamSearchVisibility)) <&> \case + inputs (view (settings . featureFlags . flagTeamSearchVisibility)) <&> \case FeatureTeamSearchVisibilityAvailableByDefault -> FeatureStatusEnabled FeatureTeamSearchVisibilityUnavailableByDefault -> FeatureStatusDisabled pure $ setStatus status defFeatureStatus instance GetFeatureConfig ValidateSAMLEmailsConfig where getConfigForServer = - inputs (view (optSettings . setFeatureFlags . flagsTeamFeatureValidateSAMLEmailsStatus . unDefaults . unImplicitLockStatus)) + inputs (view (settings . featureFlags . flagsTeamFeatureValidateSAMLEmailsStatus . unDefaults . unImplicitLockStatus)) instance GetFeatureConfig DigitalSignaturesConfig @@ -405,15 +405,15 @@ instance GetFeatureConfig LegalholdConfig where instance GetFeatureConfig FileSharingConfig where getConfigForServer = - input <&> view (optSettings . setFeatureFlags . flagFileSharing . unDefaults) + input <&> view (settings . featureFlags . flagFileSharing . unDefaults) instance GetFeatureConfig AppLockConfig where getConfigForServer = - input <&> view (optSettings . setFeatureFlags . flagAppLockDefaults . unDefaults . unImplicitLockStatus) + input <&> view (settings . featureFlags . flagAppLockDefaults . unDefaults . unImplicitLockStatus) instance GetFeatureConfig ClassifiedDomainsConfig where getConfigForServer = - input <&> view (optSettings . setFeatureFlags . flagClassifiedDomains . unImplicitLockStatus) + input <&> view (settings . featureFlags . flagClassifiedDomains . unImplicitLockStatus) instance GetFeatureConfig ConferenceCallingConfig where type @@ -428,7 +428,7 @@ instance GetFeatureConfig ConferenceCallingConfig where ) getConfigForServer = - input <&> view (optSettings . setFeatureFlags . flagConferenceCalling . unDefaults . unImplicitLockStatus) + input <&> view (settings . featureFlags . flagConferenceCalling . unDefaults . unImplicitLockStatus) getConfigForUser uid = do wsnl <- getAccountConferenceCallingConfigClient uid @@ -436,27 +436,27 @@ instance GetFeatureConfig ConferenceCallingConfig where instance GetFeatureConfig SelfDeletingMessagesConfig where getConfigForServer = - input <&> view (optSettings . setFeatureFlags . flagSelfDeletingMessages . unDefaults) + input <&> view (settings . featureFlags . flagSelfDeletingMessages . unDefaults) instance GetFeatureConfig GuestLinksConfig where getConfigForServer = - input <&> view (optSettings . setFeatureFlags . flagConversationGuestLinks . unDefaults) + input <&> view (settings . featureFlags . flagConversationGuestLinks . unDefaults) instance GetFeatureConfig SndFactorPasswordChallengeConfig where getConfigForServer = - input <&> view (optSettings . setFeatureFlags . flagTeamFeatureSndFactorPasswordChallengeStatus . unDefaults) + input <&> view (settings . featureFlags . flagTeamFeatureSndFactorPasswordChallengeStatus . unDefaults) instance GetFeatureConfig SearchVisibilityInboundConfig where getConfigForServer = - input <&> view (optSettings . setFeatureFlags . flagTeamFeatureSearchVisibilityInbound . unDefaults . unImplicitLockStatus) + input <&> view (settings . featureFlags . flagTeamFeatureSearchVisibilityInbound . unDefaults . unImplicitLockStatus) instance GetFeatureConfig MLSConfig where getConfigForServer = - input <&> view (optSettings . setFeatureFlags . flagMLS . unDefaults . unImplicitLockStatus) + input <&> view (settings . featureFlags . flagMLS . unDefaults . unImplicitLockStatus) instance GetFeatureConfig ExposeInvitationURLsToTeamAdminConfig where getConfigForTeam tid = do - allowList <- input <&> view (optSettings . setExposeInvitationURLsTeamAllowlist . to (fromMaybe [])) + allowList <- input <&> view (settings . exposeInvitationURLsTeamAllowlist . to (fromMaybe [])) mbOldStatus <- TeamFeatures.getFeatureConfig FeatureSingletonExposeInvitationURLsToTeamAdminConfig tid <&> fmap wssStatus let teamAllowed = tid `elem` allowList pure $ computeConfigForTeam teamAllowed (fromMaybe FeatureStatusDisabled mbOldStatus) @@ -477,11 +477,11 @@ instance GetFeatureConfig ExposeInvitationURLsToTeamAdminConfig where instance GetFeatureConfig OutlookCalIntegrationConfig where getConfigForServer = - input <&> view (optSettings . setFeatureFlags . flagOutlookCalIntegration . unDefaults) + input <&> view (settings . featureFlags . flagOutlookCalIntegration . unDefaults) instance GetFeatureConfig MlsE2EIdConfig where getConfigForServer = - input <&> view (optSettings . setFeatureFlags . flagMlsE2EId . unDefaults) + input <&> view (settings . featureFlags . flagMlsE2EId . unDefaults) -- -- | If second factor auth is enabled, make sure that end-points that don't support it, but should, are blocked completely. (This is a workaround until we have 2FA for those end-points as well.) -- -- diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index c16c805172c..eb3253d10a3 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -366,9 +366,9 @@ updateRemoteConversation :: updateRemoteConversation rcnv lusr conn action = getUpdateResult $ do let updateRequest = ConversationUpdateRequest - { curUser = tUnqualified lusr, - curConvId = tUnqualified rcnv, - curAction = SomeConversationAction (sing @tag) action + { user = tUnqualified lusr, + convId = tUnqualified rcnv, + action = SomeConversationAction (sing @tag) action } response <- E.runFederated rcnv (fedClient @'Galley @"update-conversation" updateRequest) convUpdate <- case response of @@ -751,7 +751,7 @@ joinConversation :: Access -> Sem r (UpdateResult Event) joinConversation lusr zcon conv access = do - let lcnv = qualifyAs lusr (convId conv) + let lcnv = qualifyAs lusr conv.convId ensureConversationAccess (tUnqualified lusr) conv access ensureGroupConversation conv -- FUTUREWORK: remote users? @@ -1110,7 +1110,7 @@ removeMemberFromRemoteConv cnv lusr victim | tUntagged lusr == victim = do let lc = LeaveConversationRequest (tUnqualified cnv) (qUnqualified victim) let rpc = fedClient @'Galley @"leave-conversation" lc - (either handleError handleSuccess . void . leaveResponse =<<) $ + (either handleError handleSuccess . void . (.response) =<<) $ E.runFederated cnv rpc | otherwise = throwS @('ActionDenied 'RemoveConversationMember) where @@ -1421,14 +1421,14 @@ memberTyping lusr zcon qcnv ts = do unless isMemberRemoteConv $ throwS @'ConvNotFound let rpc = TypingDataUpdateRequest - { tdurTypingStatus = ts, - tdurUserId = tUnqualified lusr, - tdurConvId = tUnqualified rcnv + { typingStatus = ts, + userId = tUnqualified lusr, + convId = tUnqualified rcnv } res <- E.runFederated rcnv (fedClient @'Galley @"update-typing-indicator" rpc) case res of TypingDataUpdateSuccess (TypingDataUpdated {..}) -> do - pushTypingIndicatorEvents tudOrigUserId tudTime tudUsersInConv (Just zcon) qcnv tudTypingStatus + pushTypingIndicatorEvents origUserId time usersInConv (Just zcon) qcnv typingStatus TypingDataUpdateError _ -> pure () ) qcnv diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 7dfa122432f..9fd9d441d86 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -68,7 +68,7 @@ import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P import Wire.API.Connection -import Wire.API.Conversation hiding (Member) +import Wire.API.Conversation hiding (Member, cnvAccess, cnvAccessRoles, cnvName, cnvType) import Wire.API.Conversation qualified as Public import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol @@ -83,6 +83,7 @@ import Wire.API.Password (verifyPassword) import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util import Wire.API.Team.Member +import Wire.API.Team.Member qualified as Mem import Wire.API.Team.Role import Wire.API.User (VerificationAction) import Wire.API.User qualified as User @@ -128,7 +129,7 @@ ensureConnectedOrSameTeam (tUnqualified -> u) uids = do uTeams <- getUserTeams u -- We collect all the relevant uids from same teams as the origin user sameTeamUids <- forM uTeams $ \team -> - fmap (view userId) <$> selectTeamMembers team uids + fmap (view Mem.userId) <$> selectTeamMembers team uids -- Do not check connections for users that are on the same team ensureConnectedToLocals u (uids \\ join sameTeamUids) @@ -488,8 +489,8 @@ nonTeamMembers cm tm = filter (not . isMemberOfTeam . lmId) cm uid -> isTeamMember uid tm membersToRecipients :: Maybe UserId -> [TeamMember] -> [Recipient] -membersToRecipients Nothing = map (userRecipient . view userId) -membersToRecipients (Just u) = map userRecipient . filter (/= u) . map (view userId) +membersToRecipients Nothing = map (userRecipient . view Mem.userId) +membersToRecipients (Just u) = map userRecipient . filter (/= u) . map (view Mem.userId) getSelfMemberFromLocals :: (Foldable t, Member (ErrorS 'ConvNotFound) r) => @@ -572,7 +573,7 @@ canDeleteMember :: TeamMember -> TeamMember -> Bool canDeleteMember deleter deletee | getRole deletee == RoleOwner = getRole deleter == RoleOwner -- owners can only be deleted by another owner - && (deleter ^. userId /= deletee ^. userId) -- owner cannot delete itself + && (deleter ^. Mem.userId /= deletee ^. Mem.userId) -- owner cannot delete itself | otherwise = True where @@ -674,19 +675,19 @@ toConversationCreated :: ConversationCreated ConvId toConversationCreated now Data.Conversation {convMetadata = ConversationMetadata {..}, ..} = do ConversationCreated - { ccTime = now, - ccOrigUserId = cnvmCreator, - ccCnvId = convId, - ccCnvType = cnvmType, - ccCnvAccess = cnvmAccess, - ccCnvAccessRoles = cnvmAccessRoles, - ccCnvName = cnvmName, + { time = now, + origUserId = cnvmCreator, + cnvId = convId, + cnvType = cnvmType, + cnvAccess = cnvmAccess, + cnvAccessRoles = cnvmAccessRoles, + cnvName = cnvmName, -- non-creator members are a function of the remote backend and will be -- overridden when fanning out the notification to remote backends. - ccNonCreatorMembers = Set.empty, - ccMessageTimer = cnvmMessageTimer, - ccReceiptMode = cnvmReceiptMode, - ccProtocol = convProtocol + nonCreatorMembers = Set.empty, + messageTimer = cnvmMessageTimer, + receiptMode = cnvmReceiptMode, + protocol = convProtocol } -- | The function converts a 'ConversationCreated' value to a @@ -699,7 +700,7 @@ fromConversationCreated :: ConversationCreated (Remote ConvId) -> [(Public.Member, Public.Conversation)] fromConversationCreated loc rc@ConversationCreated {..} = - let membersView = fmap (second Set.toList) . setHoles $ ccNonCreatorMembers + let membersView = fmap (second Set.toList) . setHoles $ nonCreatorMembers creatorOther = OtherMember (tUntagged (ccRemoteOrigUserId rc)) @@ -712,7 +713,7 @@ fromConversationCreated loc rc@ConversationCreated {..} = membersView where inDomain :: OtherMember -> Bool - inDomain = (== tDomain loc) . qDomain . omQualifiedId + inDomain = (== tDomain loc) . qDomain . Public.omQualifiedId setHoles :: Ord a => Set a -> [(a, Set a)] setHoles s = foldMap (\x -> [(x, Set.delete x s)]) s -- Currently this function creates a Member with default conversation attributes @@ -720,33 +721,33 @@ fromConversationCreated loc rc@ConversationCreated {..} = toMember :: OtherMember -> Public.Member toMember m = Public.Member - { memId = omQualifiedId m, - memService = omService m, + { memId = Public.omQualifiedId m, + memService = Public.omService m, memOtrMutedStatus = Nothing, memOtrMutedRef = Nothing, memOtrArchived = False, memOtrArchivedRef = Nothing, memHidden = False, memHiddenRef = Nothing, - memConvRoleName = omConvRoleName m + memConvRoleName = Public.omConvRoleName m } conv :: Public.Member -> [OtherMember] -> Public.Conversation conv this others = Public.Conversation - (tUntagged ccCnvId) + (tUntagged cnvId) ConversationMetadata - { cnvmType = ccCnvType, + { cnvmType = cnvType, -- FUTUREWORK: Document this is the same domain as the conversation -- domain - cnvmCreator = ccOrigUserId, - cnvmAccess = ccCnvAccess, - cnvmAccessRoles = ccCnvAccessRoles, - cnvmName = ccCnvName, + cnvmCreator = origUserId, + cnvmAccess = cnvAccess, + cnvmAccessRoles = cnvAccessRoles, + cnvmName = cnvName, -- FUTUREWORK: Document this is the same domain as the conversation -- domain. cnvmTeam = Nothing, - cnvmMessageTimer = ccMessageTimer, - cnvmReceiptMode = ccReceiptMode + cnvmMessageTimer = messageTimer, + cnvmReceiptMode = receiptMode } (ConvMembers this others) ProtocolProteus @@ -791,7 +792,7 @@ registerRemoteConversationMemberships now lc = deleteOnUnreachable $ do \rrms -> fedClient @'Galley @"on-conversation-created" ( rc - { ccNonCreatorMembers = + { nonCreatorMembers = toMembers (tUnqualified rrms) } ) @@ -905,7 +906,7 @@ anyLegalholdActivated :: Sem r Bool anyLegalholdActivated uids = do opts <- input - case view (optSettings . setFeatureFlags . flagLegalHold) opts of + case view (settings . featureFlags . flagLegalHold) opts of FeatureLegalHoldDisabledPermanently -> pure False FeatureLegalHoldDisabledByDefault -> check FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> check @@ -924,7 +925,7 @@ allLegalholdConsentGiven :: Sem r Bool allLegalholdConsentGiven uids = do opts <- input - case view (optSettings . setFeatureFlags . flagLegalHold) opts of + case view (settings . featureFlags . flagLegalHold) opts of FeatureLegalHoldDisabledPermanently -> pure False FeatureLegalHoldDisabledByDefault -> do flip allM (chunksOf 32 uids) $ \uidsPage -> do @@ -969,7 +970,7 @@ ensureMemberLimit :: Sem r () ensureMemberLimit old new = do o <- input - let maxSize = fromIntegral (o ^. optSettings . setMaxConvSize) + let maxSize = fromIntegral (o ^. settings . maxConvSize) when (length old + length new > maxSize) $ throwS @'TooManyMembers diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index f90daa29f29..55c1dfab8df 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -44,7 +44,7 @@ module Galley.App ) where -import Bilge hiding (Request, header, options, statusCode, statusMessage) +import Bilge hiding (Request, header, host, options, port, statusCode, statusMessage) import Cassandra hiding (Set) import Cassandra qualified as C import Cassandra.Settings qualified as C @@ -81,13 +81,14 @@ import Galley.Intra.BackendNotificationQueue import Galley.Intra.Effects import Galley.Intra.Federator import Galley.Keys -import Galley.Options +import Galley.Options hiding (brig, endpoint, federator) +import Galley.Options qualified as O import Galley.Queue import Galley.Queue qualified as Q import Galley.Types.Teams qualified as Teams import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) import Imports hiding (forkIO) -import Network.AMQP.Extended +import Network.AMQP.Extended (mkRabbitMqChannelMVar) import Network.HTTP.Client (responseTimeoutMicro) import Network.HTTP.Client.OpenSSL import Network.Wai.Utilities.JSONResponse @@ -101,7 +102,7 @@ import Polysemy.TinyLog qualified as P import Servant qualified import Ssl.Util import System.Logger qualified as Log -import System.Logger.Class +import System.Logger.Class (Logger) import System.Logger.Extended qualified as Logger import UnliftIO.Exception qualified as UnliftIO import Util.Options @@ -129,13 +130,13 @@ type GalleyEffects = Append GalleyEffects1 GalleyEffects0 -- Define some invariants for the options used validateOptions :: Opts -> IO () validateOptions o = do - let settings = view optSettings o + let settings' = view settings o optFanoutLimit = fromIntegral . fromRange $ currentFanoutLimit o - when (settings ^. setMaxConvSize > fromIntegral optFanoutLimit) $ + when (settings' ^. maxConvSize > fromIntegral optFanoutLimit) $ error "setMaxConvSize cannot be > setTruncationLimit" - when (settings ^. setMaxTeamSize < optFanoutLimit) $ + when (settings' ^. maxTeamSize < optFanoutLimit) $ error "setMaxTeamSize cannot be < setTruncationLimit" - case (o ^. optFederator, o ^. optRabbitmq) of + case (o ^. O.federator, o ^. rabbitmq) of (Nothing, Just _) -> error "RabbitMQ config is specified and federator is not, please specify both or none" (Just _, Nothing) -> error "Federator is specified and RabbitMQ config is not, please specify both or none" _ -> pure () @@ -146,32 +147,32 @@ createEnv m o l = do mgr <- initHttpManager o h2mgr <- initHttp2Manager validateOptions o - Env def m o l mgr h2mgr (o ^. optFederator) (o ^. optBrig) cass + Env def m o l mgr h2mgr (o ^. O.federator) (o ^. O.brig) cass <$> Q.new 16000 <*> initExtEnv - <*> maybe (pure Nothing) (fmap Just . Aws.mkEnv l mgr) (o ^. optJournal) - <*> loadAllMLSKeys (fold (o ^. optSettings . setMlsPrivateKeyPaths)) - <*> traverse (mkRabbitMqChannelMVar l) (o ^. optRabbitmq) + <*> maybe (pure Nothing) (fmap Just . Aws.mkEnv l mgr) (o ^. journal) + <*> loadAllMLSKeys (fold (o ^. settings . mlsPrivateKeyPaths)) + <*> traverse (mkRabbitMqChannelMVar l) (o ^. rabbitmq) initCassandra :: Opts -> Logger -> IO ClientState initCassandra o l = do c <- maybe - (C.initialContactsPlain (o ^. optCassandra . casEndpoint . epHost)) + (C.initialContactsPlain (o ^. cassandra . endpoint . host)) (C.initialContactsDisco "cassandra_galley" . unpack) - (o ^. optDiscoUrl) + (o ^. discoUrl) C.init . C.setLogger (C.mkLogger (Logger.clone (Just "cassandra.galley") l)) . C.setContacts (NE.head c) (NE.tail c) - . C.setPortNumber (fromIntegral $ o ^. optCassandra . casEndpoint . epPort) - . C.setKeyspace (Keyspace $ o ^. optCassandra . casKeyspace) + . C.setPortNumber (fromIntegral $ o ^. cassandra . endpoint . port) + . C.setKeyspace (Keyspace $ o ^. cassandra . keyspace) . C.setMaxConnections 4 . C.setMaxStreams 128 . C.setPoolStripes 4 . C.setSendTimeout 3 . C.setResponseTimeout 10 . C.setProtocolVersion C.V4 - . C.setPolicy (C.dcFilterPolicyIfConfigured l (o ^. optCassandra . casFilterNodesByDatacentre)) + . C.setPolicy (C.dcFilterPolicyIfConfigured l (o ^. cassandra . filterNodesByDatacentre)) $ C.defSettings initHttpManager :: Opts -> IO Manager @@ -186,8 +187,8 @@ initHttpManager o = do newManager (opensslManagerSettings (pure ctx)) { managerResponseTimeout = responseTimeoutMicro 10000000, - managerConnCount = o ^. optSettings . setHttpPoolSize, - managerIdleConnectionCount = 3 * (o ^. optSettings . setHttpPoolSize) + managerConnCount = o ^. settings . httpPoolSize, + managerIdleConnectionCount = 3 * (o ^. settings . httpPoolSize) } initHttp2Manager :: IO Http2Manager @@ -246,7 +247,7 @@ evalGalley e = . runInputSem (embed getCurrentTime) -- FUTUREWORK: could we take the time only once instead? . interpretWaiRoutes . runInputConst (e ^. options) - . runInputConst (toLocalUnsafe (e ^. options . optSettings . setFederationDomain) ()) + . runInputConst (toLocalUnsafe (e ^. options . settings . federationDomain) ()) . interpretInternalTeamListToCassandra . interpretTeamListToCassandra . interpretLegacyConversationListToCassandra @@ -276,4 +277,4 @@ evalGalley e = . interpretSparAccess . interpretBrigAccess where - lh = view (options . optSettings . setFeatureFlags . Teams.flagLegalHold) e + lh = view (options . settings . featureFlags . Teams.flagLegalHold) e diff --git a/services/galley/src/Galley/Aws.hs b/services/galley/src/Galley/Aws.hs index 176f8dc38db..07a84b42fca 100644 --- a/services/galley/src/Galley/Aws.hs +++ b/services/galley/src/Galley/Aws.hs @@ -57,7 +57,7 @@ import Network.TLS qualified as TLS import Proto.TeamEvents qualified as E import System.Logger qualified as Logger import System.Logger.Class -import Util.Options +import Util.Options hiding (endpoint) newtype QueueUrl = QueueUrl Text deriving (Show) @@ -102,14 +102,14 @@ mkEnv :: Logger -> Manager -> JournalOpts -> IO Env mkEnv lgr mgr opts = do let g = Logger.clone (Just "aws.galley") lgr e <- mkAwsEnv g - q <- getQueueUrl e (opts ^. awsQueueName) + q <- getQueueUrl e (opts ^. queueName) pure (Env e g q) where sqs e = AWS.setEndpoint (e ^. awsSecure) (e ^. awsHost) (e ^. awsPort) SQS.defaultService mkAwsEnv g = do baseEnv <- AWS.newEnv AWS.discover - <&> AWS.configureService (sqs (opts ^. awsEndpoint)) + <&> AWS.configureService (sqs (opts ^. endpoint)) pure $ baseEnv { AWS.logger = awsLogger g, diff --git a/services/galley/src/Galley/Cassandra/Client.hs b/services/galley/src/Galley/Cassandra/Client.hs index cb30b340185..419feef79e6 100644 --- a/services/galley/src/Galley/Cassandra/Client.hs +++ b/services/galley/src/Galley/Cassandra/Client.hs @@ -69,4 +69,4 @@ interpretClientStoreToCassandra = interpret $ \case CreateClient uid cid -> embedClient $ updateClient True uid cid DeleteClient uid cid -> embedClient $ updateClient False uid cid DeleteClients uid -> embedClient $ eraseClients uid - UseIntraClientListing -> embedApp . view $ options . optSettings . setIntraListing + UseIntraClientListing -> embedApp . view $ options . settings . intraListing diff --git a/services/galley/src/Galley/Cassandra/Code.hs b/services/galley/src/Galley/Cassandra/Code.hs index 784e8d1089a..9206425afe3 100644 --- a/services/galley/src/Galley/Cassandra/Code.hs +++ b/services/galley/src/Galley/Cassandra/Code.hs @@ -49,7 +49,7 @@ interpretCodeStoreToCassandra = interpret $ \case MakeKey cid -> Code.mkKey cid GenerateCode cid s t -> Code.generate cid s t GetConversationCodeURI -> - view (options . optSettings . setConversationCodeURI) <$> input + view (options . settings . conversationCodeURI) <$> input -- | Insert a conversation code insertCode :: Code -> Maybe Password -> Client () diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index bbea1852b86..06560c01baf 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -99,7 +99,7 @@ interpretTeamStoreToCassandra lh = interpret $ \case SetTeamStatus tid st -> embedClient $ updateTeamStatus tid st FanoutLimit -> embedApp $ currentFanoutLimit <$> view options GetLegalHoldFlag -> - view (options . optSettings . setFeatureFlags . flagLegalHold) <$> input + view (options . settings . featureFlags . flagLegalHold) <$> input EnqueueTeamEvent e -> do menv <- inputs (view aEnv) for_ menv $ \env -> diff --git a/services/galley/src/Galley/Env.hs b/services/galley/src/Galley/Env.hs index 2dbf0b40d33..32e45f2aa73 100644 --- a/services/galley/src/Galley/Env.hs +++ b/services/galley/src/Galley/Env.hs @@ -29,6 +29,7 @@ import Data.Misc (Fingerprint, Rsa) import Data.Range import Galley.Aws qualified as Aws import Galley.Options +import Galley.Options qualified as O import Galley.Queue qualified as Q import HTTP2.Client.Manager (Http2Manager) import Imports @@ -104,6 +105,6 @@ reqIdMsg = ("request" .=) . unRequestId currentFanoutLimit :: Opts -> Range 1 HardTruncationLimit Int32 currentFanoutLimit o = do - let optFanoutLimit = fromIntegral . fromRange $ fromMaybe defFanoutLimit (o ^. (optSettings . setMaxFanoutSize)) - let maxTeamSize = fromIntegral (o ^. (optSettings . setMaxTeamSize)) - unsafeRange (min maxTeamSize optFanoutLimit) + let optFanoutLimit = fromIntegral . fromRange $ fromMaybe defFanoutLimit (o ^. (O.settings . maxFanoutSize)) + let maxSize = fromIntegral (o ^. (O.settings . maxTeamSize)) + unsafeRange (min maxSize optFanoutLimit) diff --git a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs index 57e4628484c..fb2e02605fc 100644 --- a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs +++ b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs @@ -33,7 +33,7 @@ interpretBackendNotificationQueueAccess = interpret $ \case enqueueNotification :: Domain -> Q.DeliveryMode -> FedQueueClient c () -> App (Either FederationError ()) enqueueNotification remoteDomain deliveryMode action = do mChanVar <- view rabbitmqChannel - ownDomain <- view (options . optSettings . setFederationDomain) + ownDomain <- view (options . settings . federationDomain) case mChanVar of Nothing -> pure (Left FederationNotConfigured) Just chanVar -> do diff --git a/services/galley/src/Galley/Intra/Federator.hs b/services/galley/src/Galley/Intra/Federator.hs index b8e12474e42..e0ac966dcba 100644 --- a/services/galley/src/Galley/Intra/Federator.hs +++ b/services/galley/src/Galley/Intra/Federator.hs @@ -23,6 +23,7 @@ import Data.Bifunctor import Data.Qualified import Galley.Effects.FederatorAccess (FederatorAccess (..)) import Galley.Env +import Galley.Env qualified as E import Galley.Monad import Galley.Options import Imports @@ -48,15 +49,15 @@ interpretFederatorAccess = interpret $ \case RunFederatedConcurrentlyBucketsEither rs f -> embedApp $ runFederatedConcurrentlyBucketsEither rs f - IsFederationConfigured -> embedApp $ isJust <$> view federator + IsFederationConfigured -> embedApp $ isJust <$> view E.federator runFederatedEither :: Remote x -> FederatorClient c a -> App (Either FederationError a) runFederatedEither (tDomain -> remoteDomain) rpc = do - ownDomain <- view (options . optSettings . setFederationDomain) - mfedEndpoint <- view federator + ownDomain <- view (options . settings . federationDomain) + mfedEndpoint <- view E.federator mgr <- view http2Manager case mfedEndpoint of Nothing -> pure (Left FederationNotConfigured) diff --git a/services/galley/src/Galley/Intra/Push/Internal.hs b/services/galley/src/Galley/Intra/Push/Internal.hs index 0f6deec5619..272a4593493 100644 --- a/services/galley/src/Galley/Intra/Push/Internal.hs +++ b/services/galley/src/Galley/Intra/Push/Internal.hs @@ -191,7 +191,7 @@ newConversationEventPush e users = pushSlowly :: Foldable f => f Push -> App () pushSlowly ps = do - mmillis <- view (options . optSettings . setDeleteConvThrottleMillis) + mmillis <- view (options . settings . deleteConvThrottleMillis) let delay = 1000 * fromMaybe defDeleteConvThrottleMillis mmillis forM_ ps $ \p -> do push [p] diff --git a/services/galley/src/Galley/Intra/User.hs b/services/galley/src/Galley/Intra/User.hs index 1cf0ea992ef..a1b0b8cd6b4 100644 --- a/services/galley/src/Galley/Intra/User.hs +++ b/services/galley/src/Galley/Intra/User.hs @@ -35,7 +35,7 @@ module Galley.Intra.User ) where -import Bilge hiding (getHeader, options, statusCode) +import Bilge hiding (getHeader, host, options, port, statusCode) import Bilge.RPC import Brig.Types.Intra qualified as Brig import Control.Error hiding (bool, isRight) @@ -253,7 +253,7 @@ runHereClientM action = do mgr <- view manager brigep <- view brig let env = Client.mkClientEnv mgr baseurl - baseurl = Client.BaseUrl Client.Http (cs $ brigep ^. epHost) (fromIntegral $ brigep ^. epPort) "" + baseurl = Client.BaseUrl Client.Http (cs $ brigep ^. host) (fromIntegral $ brigep ^. port) "" liftIO $ Client.runClientM action env handleServantResp :: diff --git a/services/galley/src/Galley/Intra/Util.hs b/services/galley/src/Galley/Intra/Util.hs index d59fad9afd8..0ebff1f349f 100644 --- a/services/galley/src/Galley/Intra/Util.hs +++ b/services/galley/src/Galley/Intra/Util.hs @@ -22,7 +22,8 @@ module Galley.Intra.Util ) where -import Bilge hiding (getHeader, options, statusCode) +import Bilge hiding (getHeader, host, options, port, statusCode) +import Bilge qualified as B import Bilge.RPC import Bilge.Retry import Control.Lens (view, (^.)) @@ -32,7 +33,7 @@ import Data.ByteString.Lazy qualified as LB import Data.Misc (portNumber) import Data.Text.Encoding (encodeUtf8) import Data.Text.Lazy qualified as LT -import Galley.Env +import Galley.Env hiding (brig) import Galley.Monad import Galley.Options import Imports hiding (log) @@ -51,14 +52,14 @@ componentName Gundeck = "gundeck" componentRequest :: IntraComponent -> Opts -> Request -> Request componentRequest Brig o = - host (encodeUtf8 (o ^. optBrig . epHost)) - . port (portNumber (fromIntegral (o ^. optBrig . epPort))) + B.host (encodeUtf8 (o ^. brig . host)) + . B.port (portNumber (fromIntegral (o ^. brig . port))) componentRequest Spar o = - host (encodeUtf8 (o ^. optSpar . epHost)) - . port (portNumber (fromIntegral (o ^. optSpar . epPort))) + B.host (encodeUtf8 (o ^. spar . host)) + . B.port (portNumber (fromIntegral (o ^. spar . port))) componentRequest Gundeck o = - host (encodeUtf8 $ o ^. optGundeck . epHost) - . port (portNumber $ fromIntegral (o ^. optGundeck . epPort)) + B.host (encodeUtf8 $ o ^. gundeck . host) + . B.port (portNumber $ fromIntegral (o ^. gundeck . port)) . method POST . path "/i/push/v2" . expect2xx diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 584282051b7..ab34df7f996 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -19,39 +19,39 @@ module Galley.Options ( Settings, - setHttpPoolSize, - setMaxTeamSize, - setMaxFanoutSize, - setExposeInvitationURLsTeamAllowlist, - setMaxConvSize, - setIntraListing, - setDisabledAPIVersions, - setConversationCodeURI, - setConcurrentDeletionEvents, - setDeleteConvThrottleMillis, - setFederationDomain, - setMlsPrivateKeyPaths, - setFeatureFlags, + httpPoolSize, + maxTeamSize, + maxFanoutSize, + exposeInvitationURLsTeamAllowlist, + maxConvSize, + intraListing, + disabledAPIVersions, + conversationCodeURI, + concurrentDeletionEvents, + deleteConvThrottleMillis, + federationDomain, + mlsPrivateKeyPaths, + featureFlags, defConcurrentDeletionEvents, defDeleteConvThrottleMillis, defFanoutLimit, JournalOpts (JournalOpts), - awsQueueName, - awsEndpoint, + queueName, + endpoint, Opts, - optGalley, - optCassandra, - optBrig, - optGundeck, - optSpar, - optFederator, - optRabbitmq, - optDiscoUrl, - optSettings, - optJournal, - optLogLevel, - optLogNetStrings, - optLogFormat, + galley, + cassandra, + brig, + gundeck, + spar, + federator, + rabbitmq, + discoUrl, + settings, + journal, + logLevel, + logNetStrings, + logFormat, ) where @@ -66,35 +66,35 @@ import Galley.Types.Teams import Imports import Network.AMQP.Extended import System.Logger.Extended (Level, LogFormat) -import Util.Options +import Util.Options hiding (endpoint) import Util.Options.Common import Wire.API.Routes.Version import Wire.API.Team.Member data Settings = Settings { -- | Number of connections for the HTTP client pool - _setHttpPoolSize :: !Int, + _httpPoolSize :: !Int, -- | Max number of members in a team. NOTE: This must be in sync with Brig - _setMaxTeamSize :: !Word32, + _maxTeamSize :: !Word32, -- | Max number of team members users to fanout events to. For teams larger than -- this value, team events and user updates will no longer be sent to team users. -- This defaults to setMaxTeamSize and cannot be > HardTruncationLimit. Useful -- to tune mainly for testing purposes. - _setMaxFanoutSize :: !(Maybe (Range 1 HardTruncationLimit Int32)), + _maxFanoutSize :: !(Maybe (Range 1 HardTruncationLimit Int32)), -- | List of teams for which the invitation URL can be added to the list of all -- invitations retrievable by team admins. See also: -- 'ExposeInvitationURLsToTeamAdminConfig'. - _setExposeInvitationURLsTeamAllowlist :: !(Maybe [TeamId]), + _exposeInvitationURLsTeamAllowlist :: !(Maybe [TeamId]), -- | Max number of members in a conversation. NOTE: This must be in sync with Brig - _setMaxConvSize :: !Word16, + _maxConvSize :: !Word16, -- | Whether to call Brig for device listing - _setIntraListing :: !Bool, + _intraListing :: !Bool, -- | URI prefix for conversations with access mode @code@ - _setConversationCodeURI :: !HttpsUrl, + _conversationCodeURI :: !HttpsUrl, -- | Throttling: limits to concurrent deletion events - _setConcurrentDeletionEvents :: !(Maybe Int), + _concurrentDeletionEvents :: !(Maybe Int), -- | Throttling: delay between sending events upon team deletion - _setDeleteConvThrottleMillis :: !(Maybe Int), + _deleteConvThrottleMillis :: !(Maybe Int), -- | FederationDomain is required, even when not wanting to federate with other backends -- (in that case the 'allowedDomains' can be set to empty in Federator) -- Federation domain is used to qualify local IDs and handles, @@ -107,16 +107,16 @@ data Settings = Settings -- allowedDomains: -- - wire.com -- - example.com - _setFederationDomain :: !Domain, + _federationDomain :: !Domain, -- | When true, galley will assume data in `billing_team_member` table is -- consistent and use it for billing. -- When false, billing information for large teams is not guaranteed to have all -- the owners. -- Defaults to false. - _setMlsPrivateKeyPaths :: !(Maybe MLSPrivateKeyPaths), + _mlsPrivateKeyPaths :: !(Maybe MLSPrivateKeyPaths), -- | FUTUREWORK: 'setFeatureFlags' should be renamed to 'setFeatureConfigs' in all types. - _setFeatureFlags :: !FeatureFlags, - _setDisabledAPIVersions :: Maybe (Set Version) + _featureFlags :: !FeatureFlags, + _disabledAPIVersions :: Maybe (Set Version) } deriving (Show, Generic) @@ -135,9 +135,9 @@ defFanoutLimit = unsafeRange hardTruncationLimit data JournalOpts = JournalOpts { -- | SQS queue name to send team events - _awsQueueName :: !Text, + _queueName :: !Text, -- | AWS endpoint - _awsEndpoint :: !AWSEndpoint + _endpoint :: !AWSEndpoint } deriving (Show, Generic) @@ -147,34 +147,34 @@ makeLenses ''JournalOpts data Opts = Opts { -- | Host and port to bind to - _optGalley :: !Endpoint, + _galley :: !Endpoint, -- | Cassandra settings - _optCassandra :: !CassandraOpts, + _cassandra :: !CassandraOpts, -- | Brig endpoint - _optBrig :: !Endpoint, + _brig :: !Endpoint, -- | Gundeck endpoint - _optGundeck :: !Endpoint, + _gundeck :: !Endpoint, -- | Spar endpoint - _optSpar :: !Endpoint, + _spar :: !Endpoint, -- | Federator endpoint - _optFederator :: !(Maybe Endpoint), + _federator :: !(Maybe Endpoint), -- | RabbitMQ settings, required when federation is enabled. - _optRabbitmq :: !(Maybe RabbitMqOpts), + _rabbitmq :: !(Maybe RabbitMqOpts), -- | Disco URL - _optDiscoUrl :: !(Maybe Text), + _discoUrl :: !(Maybe Text), -- | Other settings - _optSettings :: !Settings, + _settings :: !Settings, -- | Journaling options ('Nothing' -- disables journaling) -- Logging - _optJournal :: !(Maybe JournalOpts), + _journal :: !(Maybe JournalOpts), -- | Log level (Debug, Info, etc) - _optLogLevel :: !Level, + _logLevel :: !Level, -- | Use netstrings encoding -- - _optLogNetStrings :: !(Maybe (Last Bool)), + _logNetStrings :: !(Maybe (Last Bool)), -- | What log format to use - _optLogFormat :: !(Maybe (Last LogFormat)) + _logFormat :: !(Maybe (Last LogFormat)) } deriveFromJSON toOptionFieldName ''Opts diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index 88c92783ba5..bd2ad81994a 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -69,12 +69,12 @@ import Wire.API.Routes.Version.Wai run :: Opts -> IO () run opts = lowerCodensity $ do (app, env) <- mkApp opts - settings <- + settings' <- lift $ newSettings $ defaultServer - (unpack $ opts ^. optGalley . epHost) - (portNumber $ fromIntegral $ opts ^. optGalley . epPort) + (unpack $ opts ^. galley . host) + (portNumber $ fromIntegral $ opts ^. galley . port) (env ^. App.applog) (env ^. monitor) @@ -83,17 +83,17 @@ run opts = lowerCodensity $ do void $ Codensity $ Async.withAsync $ runApp env deleteLoop void $ Codensity $ Async.withAsync $ runApp env refreshMetrics - lift $ finally (runSettingsWithShutdown settings app Nothing) (closeApp env) + lift $ finally (runSettingsWithShutdown settings' app Nothing) (closeApp env) mkApp :: Opts -> Codensity IO (Application, Env) mkApp opts = do - logger <- lift $ mkLogger (opts ^. optLogLevel) (opts ^. optLogNetStrings) (opts ^. optLogFormat) + logger <- lift $ mkLogger (opts ^. logLevel) (opts ^. logNetStrings) (opts ^. logFormat) metrics <- lift $ M.metrics env <- lift $ App.createEnv metrics opts logger lift $ runClient (env ^. cstate) $ versionCheck schemaVersion let middlewares = - versionMiddleware (opts ^. optSettings . setDisabledAPIVersions . traverse) + versionMiddleware (opts ^. settings . disabledAPIVersions . traverse) . servantPlusWAIPrometheusMiddleware API.sitemap (Proxy @CombinedAPI) . GZip.gunzip . GZip.gzip GZip.def @@ -111,7 +111,7 @@ mkApp opts = let e = reqId .~ lookupReqId r $ e0 in Servant.serveWithContext (Proxy @CombinedAPI) - ( view (options . optSettings . setFederationDomain) e + ( view (options . settings . federationDomain) e :. customFormatters :. Servant.EmptyContext ) diff --git a/services/galley/src/Galley/Validation.hs b/services/galley/src/Galley/Validation.hs index f87db6df4bf..964963e4e65 100644 --- a/services/galley/src/Galley/Validation.hs +++ b/services/galley/src/Galley/Validation.hs @@ -62,7 +62,7 @@ checkedConvSize :: Sem r (ConvSizeChecked f a) checkedConvSize o x = do let minV :: Integer = 0 - limit = o ^. optSettings . setMaxConvSize - 1 + limit = o ^. settings . maxConvSize - 1 if length x <= fromIntegral limit then pure (ConvSizeChecked x) else throwErr (errorMsg minV limit "") diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index c753e81a5fc..24038e03605 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -69,9 +69,10 @@ import Data.Time.Clock (getCurrentTime) import Federator.Discovery (DiscoveryFailure (..)) import Federator.MockServer import Galley.API.Mapping -import Galley.Options (optFederator, optRabbitmq) +import Galley.Options (federator, rabbitmq) import Galley.Types.Conversations.Members -import Imports +import Imports hiding (id) +import Imports qualified as I import Network.HTTP.Types.Status qualified as HTTP import Network.Wai.Utilities.Error import Test.QuickCheck (arbitrary, generate) @@ -84,6 +85,7 @@ import TestSetup import Util.Options (Endpoint (Endpoint)) import Wire.API.Connection import Wire.API.Conversation +import Wire.API.Conversation qualified as C import Wire.API.Conversation.Action import Wire.API.Conversation.Code hiding (Value) import Wire.API.Conversation.Protocol @@ -93,10 +95,8 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Brig -import Wire.API.Federation.API.Brig qualified as F import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley -import Wire.API.Federation.API.Galley qualified as F import Wire.API.Internal.Notification import Wire.API.Message import Wire.API.Message qualified as Message @@ -515,33 +515,32 @@ postConvWithRemoteUsersOk rbs = do -- assertions on the conversation.create event triggering federation request let fedReqsCreated = filter (\r -> frRPC r == "on-conversation-created") federatedRequests fedReqCreatedBodies <- for fedReqsCreated $ assertRight . parseFedRequest - forM_ fedReqCreatedBodies $ \fedReqCreatedBody -> liftIO $ do - F.ccOrigUserId fedReqCreatedBody @?= alice - F.ccCnvId fedReqCreatedBody @?= cid - F.ccCnvType fedReqCreatedBody @?= RegularConv - F.ccCnvAccess fedReqCreatedBody @?= [InviteAccess] - F.ccCnvAccessRoles fedReqCreatedBody + forM_ fedReqCreatedBodies $ \(fedReqCreatedBody :: ConversationCreated ConvId) -> liftIO $ do + fedReqCreatedBody.origUserId @?= alice + fedReqCreatedBody.cnvId @?= cid + fedReqCreatedBody.cnvType @?= RegularConv + fedReqCreatedBody.cnvAccess @?= [InviteAccess] + fedReqCreatedBody.cnvAccessRoles @?= Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole, ServiceAccessRole] - F.ccCnvName fedReqCreatedBody @?= Just convName + fedReqCreatedBody.cnvName @?= Just convName assertBool "Notifying an incorrect set of conversation members" $ - minimalShouldBePresentSet `Set.isSubsetOf` F.ccNonCreatorMembers fedReqCreatedBody - F.ccMessageTimer fedReqCreatedBody @?= Nothing - F.ccReceiptMode fedReqCreatedBody @?= Nothing + minimalShouldBePresentSet `Set.isSubsetOf` fedReqCreatedBody.nonCreatorMembers + fedReqCreatedBody.messageTimer @?= Nothing + fedReqCreatedBody.receiptMode @?= Nothing -- assertions on the conversation.member-join event triggering federation request let fedReqsAdd = filter (\r -> frRPC r == "on-conversation-updated") federatedRequests fedReqAddBodies <- for fedReqsAdd $ assertRight . parseFedRequest - forM_ fedReqAddBodies $ \fedReqAddBody -> liftIO $ do - F.cuOrigUserId fedReqAddBody @?= qAlice - F.cuConvId fedReqAddBody @?= cid + forM_ fedReqAddBodies $ \(fedReqAddBody :: ConversationUpdate) -> liftIO $ do + fedReqAddBody.cuOrigUserId @?= qAlice + fedReqAddBody.cuConvId @?= cid -- This remote backend must already have their users in the conversation, -- otherwise they should not be receiving the conversation update message assertBool "The list of already present users should be non-empty" . not . null - . F.cuAlreadyPresentUsers - $ fedReqAddBody - case F.cuAction fedReqAddBody of + $ fedReqAddBody.cuAlreadyPresentUsers + case fedReqAddBody.cuAction of SomeConversationAction SConversationJoinTag _action -> pure () _ -> assertFailure @() "Unexpected update action" where @@ -576,13 +575,13 @@ postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do let t = 5 # Second -- Missing eve let m1 = [(bob, bc, "ciphertext1")] - postOtrMessage id alice ac conv m1 !!! do + postOtrMessage I.id alice ac conv m1 !!! do const 412 === statusCode assertMismatch [(eve, Set.singleton ec)] [] [] -- Complete WS.bracketR2 c bob eve $ \(wsB, wsE) -> do let m2 = [(bob, bc, toBase64Text "ciphertext2"), (eve, ec, toBase64Text "ciphertext2")] - postOtrMessage id alice ac conv m2 !!! do + postOtrMessage I.id alice ac conv m2 !!! do const 201 === statusCode assertMismatch [] [] [] void . liftIO $ WS.assertMatch t wsB (wsAssertOtr qconv qalice ac bc (toBase64Text "ciphertext2")) @@ -590,7 +589,7 @@ postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do -- Redundant self WS.bracketR3 c alice bob eve $ \(wsA, wsB, wsE) -> do let m3 = [(alice, ac, toBase64Text "ciphertext3"), (bob, bc, toBase64Text "ciphertext3"), (eve, ec, toBase64Text "ciphertext3")] - postOtrMessage id alice ac conv m3 !!! do + postOtrMessage I.id alice ac conv m3 !!! do const 201 === statusCode assertMismatch [] [(alice, Set.singleton ac)] [] void . liftIO $ WS.assertMatch t wsB (wsAssertOtr qconv qalice ac bc (toBase64Text "ciphertext3")) @@ -604,7 +603,7 @@ postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do WS.assertMatch_ (5 # WS.Second) wsE $ wsAssertClientRemoved ec let m4 = [(bob, bc, toBase64Text "ciphertext4"), (eve, ec, toBase64Text "ciphertext4")] - postOtrMessage id alice ac conv m4 !!! do + postOtrMessage I.id alice ac conv m4 !!! do const 201 === statusCode assertMismatch [] [] [(eve, Set.singleton ec)] void . liftIO $ WS.assertMatch t wsB (wsAssertOtr qconv qalice ac bc (toBase64Text "ciphertext4")) @@ -613,7 +612,7 @@ postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do -- Deleted eve & redundant self WS.bracketR3 c alice bob eve $ \(wsA, wsB, wsE) -> do let m5 = [(bob, bc, toBase64Text "ciphertext5"), (eve, ec, toBase64Text "ciphertext5"), (alice, ac, toBase64Text "ciphertext5")] - postOtrMessage id alice ac conv m5 !!! do + postOtrMessage I.id alice ac conv m5 !!! do const 201 === statusCode assertMismatch [] [(alice, Set.singleton ac)] [(eve, Set.singleton ec)] void . liftIO $ WS.assertMatch t wsB (wsAssertOtr qconv qalice ac bc (toBase64Text "ciphertext5")) @@ -622,7 +621,7 @@ postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do assertNoMsg wsE (wsAssertOtr qconv qalice ac ec (toBase64Text "ciphertext5")) -- Missing Bob, deleted eve & redundant self let m6 = [(eve, ec, toBase64Text "ciphertext6"), (alice, ac, toBase64Text "ciphertext6")] - postOtrMessage id alice ac conv m6 !!! do + postOtrMessage I.id alice ac conv m6 !!! do const 412 === statusCode assertMismatch [(bob, Set.singleton bc)] @@ -636,7 +635,7 @@ postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do -- The second client listens only for his own messages WS.bracketR (c . queryItem "client" (toByteString' bc2)) bob $ \wsB2 -> do let m7 = [(bob, bc, cipher), (bob, bc2, cipher)] - postOtrMessage id alice ac conv m7 !!! do + postOtrMessage I.id alice ac conv m7 !!! do const 201 === statusCode assertMismatch [] [] [] -- Bob's first client gets both messages @@ -661,7 +660,7 @@ postCryptoMessageVerifyRejectMissingClientAndRepondMissingPrekeysJson = do -- Missing eve let m = [(bob, bc, toBase64Text "hello bob")] r1 <- - postOtrMessage id alice ac conv m do let msgToAllIncludingChad = [(bob, bc, toBase64Text "ciphertext2"), (eve, ec, toBase64Text "ciphertext2"), (chad, cc, toBase64Text "ciphertext2")] - postOtrMessage id alice ac conversationWithAllButChad msgToAllIncludingChad !!! const 201 === statusCode + postOtrMessage I.id alice ac conversationWithAllButChad msgToAllIncludingChad !!! const 201 === statusCode let checkBobGetsMsg = void . liftIO $ WS.assertMatch (5 # Second) wsBob (wsAssertOtr qconv qalice ac bc (toBase64Text "ciphertext2")) let checkEveGetsMsg = void . liftIO $ WS.assertMatch (5 # Second) wsEve (wsAssertOtr qconv qalice ac ec (toBase64Text "ciphertext2")) let checkChadDoesNotGetMsg = assertNoMsg wsChad (wsAssertOtr qconv qalice ac ac (toBase64Text "ciphertext2")) @@ -763,12 +762,12 @@ postMessageRejectIfMissingClients = do let msgMissingClients = mkMsg "hello!" <$> drop 1 allReceivers let checkSendToAllClientShouldBeSuccessful = - postOtrMessage id sender senderClient conv msgToAllClients !!! do + postOtrMessage I.id sender senderClient conv msgToAllClients !!! do const 201 === statusCode assertMismatch [] [] [] let checkSendWitMissingClientsShouldFail = - postOtrMessage id sender senderClient conv msgMissingClients !!! do + postOtrMessage I.id sender senderClient conv msgMissingClients !!! do const 412 === statusCode assertMismatch [(receiver1, Set.singleton receiverClient1)] [] [] @@ -795,7 +794,7 @@ postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam = do let msgMissingChadAndEve = [(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 + postOtrMessage I.id alice ac conv msgMissingChadAndEve !!! const 412 === statusCode postOtrMessage (queryItem "ignore_missing" "false") alice ac conv msgMissingChadAndEve !!! const 412 === statusCode @@ -816,10 +815,10 @@ postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam = do postOtrMessage' (Just [bob]) (queryItem "report_missing" (listToByteString [eve, chad])) alice ac conv msgMissingChadAndEve !!! const 201 === statusCode -- Set it only in the body of the message - postOtrMessage' (Just [bob]) id alice ac conv msgMissingChadAndEve + postOtrMessage' (Just [bob]) I.id alice ac conv msgMissingChadAndEve !!! const 201 === statusCode -- Let's make sure that protobuf works too, when specified in the body only - postProtoOtrMessage' (Just [bob]) id alice ac conv m' + postProtoOtrMessage' (Just [bob]) I.id alice ac conv m' !!! const 201 === statusCode reportEveAndChad <- -- send message with no clients @@ -956,12 +955,12 @@ postMessageQualifiedLocalOwningBackendRedundantAndDeletedClients = do -- FUTUREWORK: Mock federator and ensure that a message to Dee is sent let brigMock = do guardRPC "get-user-clients" - getUserClients <- getRequestBody + getUserClients <- getRequestBody @GetUserClients let lookupClients uid | uid == deeRemoteUnqualified = Just (uid, Set.fromList [PubClient deeClient Nothing]) | uid == nonMemberRemoteUnqualified = Just (uid, Set.fromList [PubClient nonMemberRemoteClient Nothing]) | otherwise = Nothing - mockReply $ UserMap . Map.fromList . mapMaybe lookupClients $ F.gucUsers getUserClients + mockReply $ UserMap . Map.fromList $ mapMaybe lookupClients getUserClients.users galleyMock = "on-message-sent" ~> EmptyResponse (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchReportAll (brigMock <|> galleyMock) @@ -1274,7 +1273,7 @@ postMessageQualifiedRemoteOwningBackendSuccess = do Message.mssFailedToConfirmClients = mempty } message = [(bobOwningDomain, bobClient, "text-for-bob"), (deeRemote, deeClient, "text-for-dee")] - mock = "send-message" ~> F.MessageSendResponse (Right mss) + mock = "send-message" ~> MessageSendResponse (Right mss) (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchReportAll mock @@ -1705,8 +1704,8 @@ testAccessUpdateGuestRemoved = do compareLists ( map ( \fr -> do - cu <- eitherDecode (frBody fr) - pure (F.cuOrigUserId cu, F.cuAction cu) + cu <- eitherDecode @ConversationUpdate (frBody fr) + pure (cu.cuOrigUserId, cu.cuAction) ) ( filter ( \fr -> @@ -1792,8 +1791,8 @@ testAccessUpdateGuestRemovedRemotesUnavailable = do compareLists ( map ( \fr -> do - cu <- eitherDecode (frBody fr) - pure (F.cuOrigUserId cu, F.cuAction cu) + cu <- eitherDecode @ConversationUpdate (frBody fr) + pure (cu.cuOrigUserId, cu.cuAction) ) ( filter ( \fr -> @@ -1963,8 +1962,8 @@ getConvsOk2 = do liftIO . forM_ [(cnv1, c1), (cnv2, c2)] $ \(expected, actual) -> do assertEqual "name mismatch" - (Just $ cnvName expected) - (cnvName <$> actual) + (Just $ C.cnvName expected) + (C.cnvName <$> actual) assertEqual "self member mismatch" (Just . cmSelf $ cnvMembers expected) @@ -2074,12 +2073,12 @@ paginateConvListIds = do replicateM_ 25 $ do conv <- randomId let cu = - F.ConversationUpdate - { F.cuTime = now, - F.cuOrigUserId = qChad, - F.cuConvId = conv, - F.cuAlreadyPresentUsers = [], - F.cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) + ConversationUpdate + { cuTime = now, + cuOrigUserId = qChad, + cuConvId = conv, + cuAlreadyPresentUsers = [], + cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient chadDomain cu @@ -2090,12 +2089,12 @@ paginateConvListIds = do replicateM_ 31 $ do conv <- randomId let cu = - F.ConversationUpdate - { F.cuTime = now, - F.cuOrigUserId = qDee, - F.cuConvId = conv, - F.cuAlreadyPresentUsers = [], - F.cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) + ConversationUpdate + { cuTime = now, + cuOrigUserId = qDee, + cuConvId = conv, + cuAlreadyPresentUsers = [], + cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient deeDomain cu @@ -2135,12 +2134,12 @@ paginateConvListIdsPageEndingAtLocalsAndDomain = do replicateM_ 16 $ do conv <- randomId let cu = - F.ConversationUpdate - { F.cuTime = now, - F.cuOrigUserId = qChad, - F.cuConvId = conv, - F.cuAlreadyPresentUsers = [], - F.cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) + ConversationUpdate + { cuTime = now, + cuOrigUserId = qChad, + cuConvId = conv, + cuAlreadyPresentUsers = [], + cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient chadDomain cu @@ -2153,12 +2152,12 @@ paginateConvListIdsPageEndingAtLocalsAndDomain = do replicateM_ 16 $ do conv <- randomId let cu = - F.ConversationUpdate - { F.cuTime = now, - F.cuOrigUserId = qDee, - F.cuConvId = conv, - F.cuAlreadyPresentUsers = [], - F.cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) + ConversationUpdate + { cuTime = now, + cuOrigUserId = qDee, + cuConvId = conv, + cuAlreadyPresentUsers = [], + cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient deeDomain cu @@ -2355,8 +2354,8 @@ postConvQualifiedFederationNotEnabled = do connectWithRemoteUser alice bob let federatorNotConfigured o = o - & optFederator .~ Nothing - & optRabbitmq .~ Nothing + & federator .~ Nothing + & rabbitmq .~ Nothing withSettingsOverrides federatorNotConfigured $ do g <- viewGalley unreachable :: UnreachableBackends <- @@ -2436,10 +2435,10 @@ putConvAcceptOk = do putConvAccept bob (qUnqualified qcnv) !!! const 200 === statusCode getConvQualified alice qcnv !!! do const 200 === statusCode - const (Just One2OneConv) === fmap cnvType . responseJsonUnsafe + const (Just One2OneConv) === fmap C.cnvType . responseJsonUnsafe getConvQualified bob qcnv !!! do const 200 === statusCode - const (Just One2OneConv) === fmap cnvType . responseJsonUnsafe + const (Just One2OneConv) === fmap C.cnvType . responseJsonUnsafe putConvAcceptRetry :: TestM () putConvAcceptRetry = do @@ -2476,45 +2475,45 @@ postRepeatConnectConvCancel = do rsp1 <- postConnectConv alice bob "A" "a" Nothing getConvQualified bob qconvId liftIO $ do - ConnectConv @=? cnvType cnvX - Just "B" @=? cnvName cnvX - privateAccess @=? cnvAccess cnvX + ConnectConv @=? C.cnvType cnvX + Just "B" @=? C.cnvName cnvX + privateAccess @=? C.cnvAccess cnvX -- Alice accepts, finally turning it into a 1-1 putConvAccept alice convId !!! const 200 === statusCode cnv4 <- responseJsonUnsafeWithMsg "conversation" <$> getConvQualified alice qconvId liftIO $ do - One2OneConv @=? cnvType cnv4 - Just "B" @=? cnvName cnv4 - privateAccess @=? cnvAccess cnv4 + One2OneConv @=? C.cnvType cnv4 + Just "B" @=? C.cnvName cnv4 + privateAccess @=? C.cnvAccess cnv4 where cancel u c = do g <- viewGalley @@ -2734,8 +2733,8 @@ testGetQualifiedLocalConv = do convId <- decodeQualifiedConvId <$> postConv alice [] (Just "gossip") [] Nothing Nothing conv :: Conversation <- fmap responseJsonUnsafe $ getConvQualified alice convId mockedFederatedGalleyResponse @@ -3337,7 +3336,7 @@ leaveRemoteConvQualifiedOk = do (resp, fedRequests) <- withTempMockFederator' mockResponses $ deleteMemberQualified alice qAlice qconv - let leaveRequest = + let leaveRequest :: LeaveConversationRequest = fromJust . decode . frBody . Imports.head $ fedRequests liftIO $ do @@ -3345,8 +3344,8 @@ leaveRemoteConvQualifiedOk = do case responseJsonEither resp of Left err -> assertFailure err Right e -> assertLeaveEvent qconv qAlice [qAlice] e - F.lcConvId leaveRequest @?= conv - F.lcLeaver leaveRequest @?= alice + leaveRequest.convId @?= conv + leaveRequest.leaver @?= alice -- Alice tries to leave a non-existent remote conversation leaveNonExistentRemoteConv :: TestM () @@ -3358,20 +3357,20 @@ leaveNonExistentRemoteConv = do let mockResponses = do guardComponent Galley mockReply $ - F.LeaveConversationResponse (Left F.RemoveFromConversationErrorNotFound) + LeaveConversationResponse (Left RemoveFromConversationErrorNotFound) (resp, fedRequests) <- withTempMockFederator' mockResponses $ responseJsonError =<< deleteMemberQualified (qUnqualified alice) alice conv do let e = List1.head (WS.unpackPayload n) @@ -3546,9 +3545,9 @@ putQualifiedConvRenameWithRemotesUnavailable = do frTargetDomain req @?= remoteDomain frComponent req @?= Galley frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu @?= SomeConversationAction (sing @'ConversationRenameTag) (ConversationRename "gossip++") + Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req + cu.cuConvId @?= qUnqualified qconv + cu.cuAction @?= SomeConversationAction (sing @'ConversationRenameTag) (ConversationRename "gossip++") void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do let e = List1.head (WS.unpackPayload n) @@ -3801,7 +3800,7 @@ putRemoteConvMemberOk update = do fedGalleyClient <- view tsFedGalleyClient now <- liftIO getCurrentTime let cu = - F.ConversationUpdate + ConversationUpdate { cuTime = now, cuOrigUserId = qbob, cuConvId = qUnqualified qconv, @@ -3946,7 +3945,7 @@ putRemoteReceiptModeOk = do fedGalleyClient <- view tsFedGalleyClient now <- liftIO getCurrentTime let cuAddAlice = - F.ConversationUpdate + ConversationUpdate { cuTime = now, cuOrigUserId = qbob, cuConvId = qUnqualified qconv, @@ -3961,7 +3960,7 @@ putRemoteReceiptModeOk = do let adam = qUnqualified qadam connectWithRemoteUser adam qbob let cuAddAdam = - F.ConversationUpdate + ConversationUpdate { cuTime = now, cuOrigUserId = qbob, cuConvId = qUnqualified qconv, @@ -3974,7 +3973,7 @@ putRemoteReceiptModeOk = do let newReceiptMode = ReceiptMode 42 let action = ConversationReceiptModeUpdate newReceiptMode let responseConvUpdate = - F.ConversationUpdate + ConversationUpdate { cuTime = now, cuOrigUserId = qalice, cuConvId = qUnqualified qconv, @@ -3995,11 +3994,11 @@ putRemoteReceiptModeOk = do liftIO $ assertEqual "Unexcepected receipt mode in event" newReceiptMode receiptModeEvent cFedReq <- assertOne $ filter (\r -> frTargetDomain r == remoteDomain && frRPC r == "update-conversation") federatedRequests - cFedReqBody <- assertRight $ parseFedRequest cFedReq + cFedReqBody :: ConversationUpdateRequest <- assertRight $ parseFedRequest cFedReq liftIO $ do - curUser cFedReqBody @?= alice - curConvId cFedReqBody @?= qUnqualified qconv - curAction cFedReqBody @?= SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) action + cFedReqBody.user @?= alice + cFedReqBody.convId @?= qUnqualified qconv + cFedReqBody.action @?= SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) action WS.assertMatch_ (5 # Second) wsAdam $ \n -> do liftIO $ wsAssertConvReceiptModeUpdate qconv qalice newReceiptMode n @@ -4031,9 +4030,9 @@ putReceiptModeWithRemotesOk = do frTargetDomain req @?= remoteDomain frComponent req @?= Galley frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu + Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req + cu.cuConvId @?= qUnqualified qconv + cu.cuAction @?= SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) (ConversationReceiptModeUpdate (ReceiptMode 43)) void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do @@ -4073,9 +4072,9 @@ putReceiptModeWithRemotesUnavailable = do frTargetDomain req @?= remoteDomain frComponent req @?= Galley frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu + Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req + cu.cuConvId @?= qUnqualified qconv + cu.cuAction @?= SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) (ConversationReceiptModeUpdate (ReceiptMode 43)) void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do @@ -4272,18 +4271,18 @@ removeUser = do now <- liftIO getCurrentTime fedGalleyClient <- view tsFedGalleyClient let nc cid creator quids = - F.ConversationCreated - { F.ccTime = now, - F.ccOrigUserId = qUnqualified creator, - F.ccCnvId = cid, - F.ccCnvType = RegularConv, - F.ccCnvAccess = [], - F.ccCnvAccessRoles = Set.fromList [], - F.ccCnvName = Just "gossip4", - F.ccNonCreatorMembers = Set.fromList $ createOtherMember <$> quids, - F.ccMessageTimer = Nothing, - F.ccReceiptMode = Nothing, - F.ccProtocol = ProtocolProteus + ConversationCreated + { time = now, + origUserId = qUnqualified creator, + cnvId = cid, + cnvType = RegularConv, + cnvAccess = [], + cnvAccessRoles = Set.fromList [], + cnvName = Just "gossip4", + nonCreatorMembers = Set.fromList $ createOtherMember <$> quids, + messageTimer = Nothing, + receiptMode = Nothing, + protocol = ProtocolProteus } void $ runFedClient @"on-conversation-created" fedGalleyClient bDomain $ nc convB1 bart [alice, alexDel] void $ runFedClient @"on-conversation-created" fedGalleyClient bDomain $ nc convB2 bart [alexDel] @@ -4299,7 +4298,7 @@ removeUser = do throw (DiscoveryFailureSrvNotAvailable "dDomain"), do guard (d `elem` [bDomain, cDomain]) - "leave-conversation" ~> F.LeaveConversationResponse (Right mempty) + "leave-conversation" ~> LeaveConversationResponse (Right mempty) ] (_, fedRequests) <- withTempMockFederator' handler $ @@ -4400,12 +4399,12 @@ testOne2OneConversationRequest shouldBeLocal actor desired = do fedGalleyClient <- view tsFedGalleyClient GetConversationsResponse convs <- runFedClient @"get-conversations" fedGalleyClient (tDomain bob) $ - F.GetConversationsRequest - { F.gcrUserId = tUnqualified bob, - F.gcrConvIds = [qUnqualified convId] + GetConversationsRequest + { userId = tUnqualified bob, + convIds = [qUnqualified convId] } pure - . fmap (map omQualifiedId . rcmOthers . rcnvMembers) + . fmap (map omQualifiedId . (.members.others)) . listToMaybe $ convs liftIO $ case desired of @@ -4417,7 +4416,7 @@ testOne2OneConversationRequest shouldBeLocal actor desired = do found <- do let rconv = mkProteusConv (qUnqualified convId) (tUnqualified bob) roleNameWireAdmin [] (resp, _) <- - withTempMockFederator' (mockReply (F.GetConversationsResponse [rconv])) $ + withTempMockFederator' (mockReply (GetConversationsResponse [rconv])) $ getConvQualified (tUnqualified alice) convId pure $ statusCode resp == 200 liftIO $ found @?= ((actor, desired) == (LocalActor, Included)) diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index c0d83cae44c..73930b4bd59 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -50,6 +50,7 @@ import Test.Tasty.HUnit import TestHelpers import TestSetup import Wire.API.Conversation +import Wire.API.Conversation qualified as Conv import Wire.API.Conversation.Action import Wire.API.Conversation.Role import Wire.API.Event.Conversation @@ -149,21 +150,21 @@ getConversationsAllFound = do (qUnqualified aliceQ) (map qUnqualified [cnv1Id, cnvQualifiedId cnv2]) - let c2 = find ((== qUnqualified (cnvQualifiedId cnv2)) . rcnvId) convs + let c2 = find ((== qUnqualified (cnvQualifiedId cnv2)) . (.id)) convs liftIO $ do assertEqual "name mismatch" - (Just $ cnvName cnv2) - (cnvmName . rcnvMetadata <$> c2) + (Just $ Conv.cnvName cnv2) + ((.metadata.cnvmName) <$> c2) assertEqual "self member role mismatch" (Just . memConvRoleName . cmSelf $ cnvMembers cnv2) - (rcmSelfRole . rcnvMembers <$> c2) + ((.members.selfRole) <$> c2) assertEqual "other members mismatch" (Just (sort [bob, qUnqualified carlQ])) - (fmap (sort . map (qUnqualified . omQualifiedId) . rcmOthers . rcnvMembers) c2) + (fmap (sort . map (qUnqualified . omQualifiedId) . (.members.others)) c2) -- @SF.Federation @TSFI.RESTfulAPI @S2 -- @@ -761,9 +762,9 @@ leaveConversationSuccess = do . json leaveRequest ) ) [(alice, msg aliceC1), (alice, msg aliceC2), (eve, msg eveC)] rm = FedGalley.RemoteMessage - { FedGalley.rmTime = now, - FedGalley.rmData = Nothing, - FedGalley.rmSender = qbob, - FedGalley.rmSenderClient = fromc, - FedGalley.rmConversation = conv, - FedGalley.rmPriority = Nothing, - FedGalley.rmTransient = False, - FedGalley.rmPush = False, - FedGalley.rmRecipients = rcpts + { FedGalley.time = now, + FedGalley._data = Nothing, + FedGalley.sender = qbob, + FedGalley.senderClient = fromc, + FedGalley.conversation = conv, + FedGalley.priority = Nothing, + FedGalley.transient = False, + FedGalley.push = False, + FedGalley.recipients = rcpts } -- send message to alice and check reception @@ -950,9 +951,9 @@ sendMessage = do msg = mkQualifiedOtrPayload bobClient rcpts "" MismatchReportAll msr = FedGalley.ProteusMessageSendRequest - { FedGalley.pmsrConvId = convId, - FedGalley.pmsrSender = bobId, - FedGalley.pmsrRawMessage = Base64ByteString (Protolens.encodeMessage msg) + { FedGalley.convId = convId, + FedGalley.sender = bobId, + FedGalley.rawMessage = Base64ByteString (Protolens.encodeMessage msg) } let mock = do guardComponent Brig @@ -1058,8 +1059,8 @@ onUserDeleted = do (resp, rpcCalls) <- withTempMockFederator' (mockReply EmptyResponse) $ do let udcn = FedGalley.UserDeletedConversationsNotification - { FedGalley.udcvUser = tUnqualified bob, - FedGalley.udcvConversations = + { FedGalley.user = tUnqualified bob, + FedGalley.conversations = unsafeRange [ qUnqualified ooConvId, qUnqualified groupConvId, @@ -1152,9 +1153,9 @@ updateConversationByRemoteAdmin = do -- bob updates the conversation let cnvUpdateRequest = ConversationUpdateRequest - { curUser = qUnqualified qbob, - curConvId = qUnqualified cnv, - curAction = action + { user = qUnqualified qbob, + convId = qUnqualified cnv, + action = action } resp <- do fedGalleyClient <- view tsFedGalleyClient diff --git a/services/galley/test/integration/API/MLS/Mocks.hs b/services/galley/test/integration/API/MLS/Mocks.hs index db80027d402..1b58cd9df51 100644 --- a/services/galley/test/integration/API/MLS/Mocks.hs +++ b/services/galley/test/integration/API/MLS/Mocks.hs @@ -88,7 +88,7 @@ claimKeyPackagesMock kpb = "claim-key-packages" ~> kpb queryGroupStateMock :: ByteString -> Qualified UserId -> Mock LByteString queryGroupStateMock gs qusr = do guardRPC "query-group-info" - uid <- ggireqSender <$> getRequestBody + uid <- (\(a :: GetGroupInfoRequest) -> a.sender) <$> getRequestBody mockReply $ if uid == qUnqualified qusr then GetGroupInfoResponseState (Base64ByteString gs) diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index a34b8bf714c..edebdaf86e1 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -83,9 +83,9 @@ import Wire.API.User.Client.Prekey cid2Str :: ClientIdentity -> String cid2Str cid = - show (ciUser cid) + show cid.ciUser <> ":" - <> T.unpack (client . ciClient $ cid) + <> T.unpack cid.ciClient.client <> "@" <> T.unpack (domainText (ciDomain cid)) @@ -94,10 +94,10 @@ mapRemoteKeyPackageRef :: (Request -> Request) -> KeyPackageBundle -> m () -mapRemoteKeyPackageRef brig bundle = +mapRemoteKeyPackageRef brigCall bundle = void $ put - ( brig + ( brigCall . paths ["i", "mls", "key-package-refs"] . json bundle ) @@ -113,9 +113,9 @@ postMessage :: ByteString -> m ResponseLBS postMessage sender msg = do - galley <- viewGalley + galleyCall <- viewGalley post - ( galley + ( galleyCall . paths ["mls", "messages"] . zUser (ciUser sender) . zClient (ciClient sender) @@ -134,9 +134,9 @@ postCommitBundle :: ByteString -> m ResponseLBS postCommitBundle sender bundle = do - galley <- viewGalley + galleyCall <- viewGalley post - ( galley + ( galleyCall . paths ["mls", "commit-bundles"] . zUser (ciUser sender) . zClient (ciClient sender) @@ -156,9 +156,9 @@ postWelcome :: ByteString -> m ResponseLBS postWelcome uid welcome = do - galley <- view tsUnversionedGalley + galleyCall <- view tsUnversionedGalley post - ( galley + ( galleyCall . paths ["v2", "mls", "welcome"] . zUser uid . zConn "conn" @@ -190,7 +190,7 @@ mkAppAckProposalMessage gid epoch ref mrs priv pub = do saveRemovalKey :: FilePath -> TestM () saveRemovalKey fp = do - keys <- fromJust <$> view (tsGConf . optSettings . setMlsPrivateKeyPaths) + keys <- fromJust <$> view (tsGConf . settings . mlsPrivateKeyPaths) keysByPurpose <- liftIO $ loadAllMLSKeys keys let (_, pub) = fromJust (mlsKeyPair_ed25519 (keysByPurpose RemovalPurpose)) liftIO $ BS.writeFile fp (BA.convert pub) @@ -293,10 +293,10 @@ createLocalMLSClient (tUntagged -> qusr) = do -- set public key pkey <- mlscli qcid ["public-key"] Nothing - brig <- viewBrig + brigCall <- viewBrig let update = defUpdateClient {updateClientMLSPublicKeys = Map.singleton Ed25519 pkey} put - ( brig + ( brigCall . paths ["clients", toByteString' . ciClient $ qcid] . zUser (ciUser qcid) . json update @@ -325,9 +325,9 @@ uploadNewKeyPackage qcid = do (kp, _) <- generateKeyPackage qcid -- upload key package - brig <- viewBrig + brigCall <- viewBrig post - ( brig + ( brigCall . paths ["mls", "key-packages", "self", toByteString' . ciClient $ qcid] . zUser (ciUser qcid) . json (KeyPackageUpload [kp]) @@ -480,10 +480,10 @@ keyPackageFile qcid ref = claimLocalKeyPackages :: HasCallStack => ClientIdentity -> Local UserId -> MLSTest KeyPackageBundle claimLocalKeyPackages qcid lusr = do - brig <- viewBrig + brigCall <- viewBrig responseJsonError =<< post - ( brig + ( brigCall . paths ["mls", "key-packages", "claim", toByteString' (tDomain lusr), toByteString' (tUnqualified lusr)] . zUser (ciUser qcid) ) @@ -503,7 +503,7 @@ getUserClients qusr = do -- | Generate one key package for each client of a remote user claimRemoteKeyPackages :: HasCallStack => Remote UserId -> MLSTest KeyPackageBundle claimRemoteKeyPackages (tUntagged -> qusr) = do - brig <- viewBrig + brigCall <- viewBrig clients <- getUserClients qusr bundle <- fmap (KeyPackageBundle . Set.fromList) $ for clients $ \cid -> do @@ -515,7 +515,7 @@ claimRemoteKeyPackages (tUntagged -> qusr) = do kpbeRef = ref, kpbeKeyPackage = KeyPackageData (rmRaw kp) } - mapRemoteKeyPackageRef brig bundle + mapRemoteKeyPackageRef brigCall bundle pure bundle -- | Claim key package for a local user, or generate and map key packages for remote ones. @@ -997,9 +997,9 @@ getGroupInfo :: Qualified ConvId -> m ResponseLBS getGroupInfo sender qcnv = do - galley <- viewGalley + galleyCall <- viewGalley get - ( galley + ( galleyCall . paths [ "conversations", toByteString' (qDomain qcnv), @@ -1025,4 +1025,4 @@ getSelfConv u = do withMLSDisabled :: HasSettingsOverrides m => m a -> m a withMLSDisabled = withSettingsOverrides noMLS where - noMLS = Opts.optSettings . Opts.setMlsPrivateKeyPaths .~ Nothing + noMLS = Opts.settings . Opts.mlsPrivateKeyPaths .~ Nothing diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index a5a6f11816a..8b217f515f0 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -59,7 +59,7 @@ import Data.UUID.V1 qualified as UUID import Data.Vector qualified as V import GHC.TypeLits (KnownSymbol) import Galley.Env qualified as Galley -import Galley.Options (optSettings, setFeatureFlags, setMaxConvSize, setMaxFanoutSize) +import Galley.Options (featureFlags, maxConvSize, maxFanoutSize, settings) import Galley.Types.Conversations.Roles import Galley.Types.Teams import Imports @@ -415,7 +415,7 @@ testEnableSSOPerTeam = do liftIO $ do assertEqual "bad status" status403 status assertEqual "bad label" "not-implemented" label - featureSSO <- view (tsGConf . optSettings . setFeatureFlags . flagSSO) + featureSSO <- view (tsGConf . settings . featureFlags . flagSSO) case featureSSO of FeatureSSOEnabledByDefault -> check "Teams should start with SSO enabled" Public.FeatureStatusEnabled FeatureSSODisabledByDefault -> check "Teams should start with SSO disabled" Public.FeatureStatusDisabled @@ -1732,8 +1732,8 @@ postCryptoBroadcastMessageFilteredTooLargeTeam bcast = do WS.bracketR (c . queryItem "client" (toByteString' ac)) alice $ \wsA1 -> do -- We change also max conv size due to the invariants that galley forces us to keep let newOpts = - ((optSettings . setMaxFanoutSize) ?~ unsafeRange 4) - . (optSettings . setMaxConvSize .~ 4) + ((settings . maxFanoutSize) ?~ unsafeRange 4) + . (settings . maxConvSize .~ 4) withSettingsOverrides newOpts $ do -- Untargeted, Alice's team is too large Util.postBroadcast (q alice) ac bcast {bMessage = msg} !!! do diff --git a/services/galley/test/integration/API/Teams/Feature.hs b/services/galley/test/integration/API/Teams/Feature.hs index 73408aca64c..5bb48172366 100644 --- a/services/galley/test/integration/API/Teams/Feature.hs +++ b/services/galley/test/integration/API/Teams/Feature.hs @@ -41,7 +41,7 @@ import Data.Schema (ToSchema) import Data.Set qualified as Set import Data.Timeout (TimeoutUnit (Second), (#)) import GHC.TypeLits (KnownSymbol) -import Galley.Options (optSettings, setExposeInvitationURLsTeamAllowlist, setFeatureFlags) +import Galley.Options (exposeInvitationURLsTeamAllowlist, featureFlags, settings) import Galley.Types.Teams import Imports import Network.Wai.Utilities (label) @@ -284,7 +284,7 @@ testSSO setSSOFeature = do assertFlagForbidden $ getTeamFeatureFlag @SSOConfig nonMember tid - featureSSO <- view (tsGConf . optSettings . setFeatureFlags . flagSSO) + featureSSO <- view (tsGConf . settings . featureFlags . flagSSO) case featureSSO of FeatureSSODisabledByDefault -> do -- Test default @@ -331,7 +331,7 @@ testLegalHold setLegalHoldInternal = do assertFlagForbidden $ getTeamFeatureFlag @LegalholdConfig nonMember tid -- FUTUREWORK: run two galleys, like below for custom search visibility. - featureLegalHold <- view (tsGConf . optSettings . setFeatureFlags . flagLegalHold) + featureLegalHold <- view (tsGConf . settings . featureFlags . flagLegalHold) case featureLegalHold of FeatureLegalHoldDisabledByDefault -> do -- Test default @@ -483,7 +483,7 @@ testClassifiedDomainsDisabled = do let classifiedDomainsDisabled opts = opts & over - (optSettings . setFeatureFlags . flagClassifiedDomains) + (settings . featureFlags . flagClassifiedDomains) (\(ImplicitLockStatus s) -> ImplicitLockStatus (s & setStatus FeatureStatusDisabled & setConfig (ClassifiedDomainsConfig []))) withSettingsOverrides classifiedDomainsDisabled $ do getClassifiedDomains member tid expected @@ -841,8 +841,8 @@ testSelfDeletingMessages = do defLockStatus :: LockStatus <- view ( tsGConf - . optSettings - . setFeatureFlags + . settings + . featureFlags . flagSelfDeletingMessages . unDefaults . to wsLockStatus @@ -996,8 +996,8 @@ testAllFeatures = do defLockStatus :: LockStatus <- view ( tsGConf - . optSettings - . setFeatureFlags + . settings + . featureFlags . flagSelfDeletingMessages . unDefaults . to wsLockStatus @@ -1283,7 +1283,7 @@ testExposeInvitationURLsToTeamAdminTeamIdInAllowList = do tid <- createBindingTeamInternal "foo" owner assertTeamActivate "create team" tid void $ - withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist ?~ [tid]) $ do + withSettingsOverrides (\opts -> opts & settings . exposeInvitationURLsTeamAllowlist ?~ [tid]) $ do g <- viewGalley assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled LockStatusUnlocked let enabled = WithStatusNoLock FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited @@ -1298,7 +1298,7 @@ testExposeInvitationURLsToTeamAdminEmptyAllowList = do tid <- createBindingTeamInternal "foo" owner assertTeamActivate "create team" tid void $ - withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist .~ Nothing) $ do + withSettingsOverrides (\opts -> opts & settings . exposeInvitationURLsTeamAllowlist .~ Nothing) $ do g <- viewGalley assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled LockStatusLocked let enabled = WithStatusNoLock FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited @@ -1310,7 +1310,7 @@ testExposeInvitationURLsToTeamAdminEmptyAllowList = do -- | Ensure that the server config takes precedence over a saved team config. -- -- In other words: When a team id is no longer in the --- `setExposeInvitationURLsTeamAllowlist` the +-- `exposeInvitationURLsTeamAllowlist` the -- `ExposeInvitationURLsToTeamAdminConfig` is always disabled (even tough it -- might have been enabled before). testExposeInvitationURLsToTeamAdminServerConfigTakesPrecedence :: TestM () @@ -1319,7 +1319,7 @@ testExposeInvitationURLsToTeamAdminServerConfigTakesPrecedence = do tid <- createBindingTeamInternal "foo" owner assertTeamActivate "create team" tid void $ - withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist ?~ [tid]) $ do + withSettingsOverrides (\opts -> opts & settings . exposeInvitationURLsTeamAllowlist ?~ [tid]) $ do g <- viewGalley assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled LockStatusUnlocked let enabled = WithStatusNoLock FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited @@ -1328,7 +1328,7 @@ testExposeInvitationURLsToTeamAdminServerConfigTakesPrecedence = do const 200 === statusCode assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusEnabled LockStatusUnlocked void $ - withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist .~ Nothing) $ do + withSettingsOverrides (\opts -> opts & settings . exposeInvitationURLsTeamAllowlist .~ Nothing) $ do g <- viewGalley assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled LockStatusLocked let enabled = WithStatusNoLock FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited diff --git a/services/galley/test/integration/API/Teams/LegalHold.hs b/services/galley/test/integration/API/Teams/LegalHold.hs index 9f37a4a35bc..d9efba2945c 100644 --- a/services/galley/test/integration/API/Teams/LegalHold.hs +++ b/services/galley/test/integration/API/Teams/LegalHold.hs @@ -50,7 +50,7 @@ import Galley.Cassandra.Client (lookupClients) import Galley.Cassandra.LegalHold import Galley.Cassandra.LegalHold qualified as LegalHoldData import Galley.Env qualified as Galley -import Galley.Options (optSettings, setFeatureFlags) +import Galley.Options (featureFlags, settings) import Galley.Types.Clients qualified as Clients import Galley.Types.Teams import Imports @@ -83,7 +83,7 @@ import Wire.API.User.Client qualified as Client onlyIfLhWhitelisted :: TestM () -> TestM () onlyIfLhWhitelisted action = do - featureLegalHold <- view (tsGConf . optSettings . setFeatureFlags . flagLegalHold) + featureLegalHold <- view (tsGConf . settings . featureFlags . flagLegalHold) case featureLegalHold of FeatureLegalHoldDisabledPermanently -> liftIO $ hPutStrLn stderr errmsg diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs index 1a31e38d5ee..835f2100709 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/Util.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -584,8 +584,8 @@ assertMatchChan c match = go [] getLHWhitelistedTeam :: HasCallStack => TeamId -> TestM ResponseLBS getLHWhitelistedTeam tid = do - galley <- viewGalley - getLHWhitelistedTeam' galley tid + galleyCall <- viewGalley + getLHWhitelistedTeam' galleyCall tid getLHWhitelistedTeam' :: (HasCallStack, MonadHttp m) => GalleyR -> TeamId -> m ResponseLBS getLHWhitelistedTeam' g tid = do @@ -596,8 +596,8 @@ getLHWhitelistedTeam' g tid = do putLHWhitelistTeam :: HasCallStack => TeamId -> TestM ResponseLBS putLHWhitelistTeam tid = do - galley <- viewGalley - putLHWhitelistTeam' galley tid + galleyCall <- viewGalley + putLHWhitelistTeam' galleyCall tid putLHWhitelistTeam' :: (HasCallStack, MonadHttp m) => GalleyR -> TeamId -> m ResponseLBS putLHWhitelistTeam' g tid = do @@ -608,8 +608,8 @@ putLHWhitelistTeam' g tid = do _deleteLHWhitelistTeam :: HasCallStack => TeamId -> TestM ResponseLBS _deleteLHWhitelistTeam tid = do - galley <- viewGalley - deleteLHWhitelistTeam' galley tid + galleyCall <- viewGalley + deleteLHWhitelistTeam' galleyCall tid deleteLHWhitelistTeam' :: (HasCallStack, MonadHttp m) => GalleyR -> TeamId -> m ResponseLBS deleteLHWhitelistTeam' g tid = do @@ -642,7 +642,7 @@ instance IsTest LHTest where run :: OptionSet -> LHTest -> (Progress -> IO ()) -> IO Result run _ (LHTest expectedFlag setupAction testAction) _ = do setup <- setupAction - let featureLegalHold = setup ^. tsGConf . optSettings . setFeatureFlags . flagLegalHold + let featureLegalHold = setup ^. tsGConf . settings . featureFlags . flagLegalHold if featureLegalHold == expectedFlag then do hunitResult <- try $ void . flip runReaderT setup . runTestM $ testAction diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 87276bd4358..b126c0aca72 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -25,8 +25,6 @@ import API.SQS qualified as SQS import Bilge hiding (timeout) import Bilge.Assert import Bilge.TestSession -import Brig.Types.Connection -import Brig.Types.Intra import Control.Applicative import Control.Concurrent.Async import Control.Exception (throw) @@ -107,6 +105,7 @@ import Util.Options import Web.Cookie import Wire.API.Connection import Wire.API.Conversation +import Wire.API.Conversation qualified as Conv import Wire.API.Conversation.Action import Wire.API.Conversation.Code hiding (Value) import Wire.API.Conversation.Protocol @@ -193,12 +192,12 @@ symmPermissions p = let s = Set.fromList p in fromJust (newPermissions s s) createBindingTeam :: HasCallStack => TestM (UserId, TeamId) createBindingTeam = do - first userId <$> createBindingTeam' + first Wire.API.User.userId <$> createBindingTeam' createBindingTeam' :: HasCallStack => TestM (User, TeamId) createBindingTeam' = do owner <- randomTeamCreator' - teams <- getTeams (userId owner) [] + teams <- getTeams owner.userId [] let [team] = view teamListTeams teams let tid = view teamId team SQS.assertTeamActivate "create team" tid @@ -367,7 +366,7 @@ getTeamMembersPaginated usr tid n mPs = do . paths ["teams", toByteString' tid, "members"] . zUser usr . queryItem "maxResults" (C.pack $ show n) - . maybe id (queryItem "pagingState" . cs) mPs + . maybe Imports.id (queryItem "pagingState" . cs) mPs ) Maybe Role -> UserId -> TeamId -> TestM addUserToTeamWithRole role inviter tid = do (inv, rsp2) <- addUserToTeamWithRole' role inviter tid let invitee :: User = responseJsonUnsafe rsp2 - inviteeId = userId invitee + inviteeId = invitee.userId let invmeta = Just (inviter, inCreatedAt inv) mem <- getTeamMember inviter tid inviteeId liftIO $ assertEqual "Member has no/wrong invitation metadata" invmeta (mem ^. Team.invitation) @@ -484,8 +483,7 @@ addUserToTeamWithRole' role inviter tid = do addUserToTeamWithSSO :: HasCallStack => Bool -> TeamId -> TestM TeamMember addUserToTeamWithSSO hasEmail tid = do let ssoid = UserSSOId mkSimpleSampleUref - user <- responseJsonError =<< postSSOUser "SSO User" hasEmail ssoid tid - let uid = userId user + uid <- fmap (\(u :: User) -> u.userId) $ responseJsonError =<< postSSOUser "SSO User" hasEmail ssoid tid getTeamMember uid tid uid makeOwner :: HasCallStack => UserId -> TeamMember -> TeamId -> TestM () @@ -726,7 +724,7 @@ postConvQualified u c n = do g . path "/conversations" . zUser u - . maybe id zClient c + . maybe Imports.id zClient c . zConn "conn" . zType "access" . json n @@ -933,7 +931,7 @@ data Broadcast = Broadcast } instance Default Broadcast where - def = Broadcast BroadcastLegacyQueryParams BroadcastJSON mempty "ZXhhbXBsZQ==" mempty id + def = Broadcast BroadcastLegacyQueryParams BroadcastJSON mempty "ZXhhbXBsZQ==" mempty Imports.id postBroadcast :: (MonadIO m, MonadHttp m, HasGalley m) => @@ -945,8 +943,8 @@ postBroadcast lu c b = do let u = qUnqualified lu g <- viewGalley let (bodyReport, queryReport) = case bAPI b of - BroadcastLegacyQueryParams -> (Nothing, maybe id mkOtrReportMissing (bReport b)) - _ -> (bReport b, id) + BroadcastLegacyQueryParams -> (Nothing, maybe Imports.id mkOtrReportMissing (bReport b)) + _ -> (bReport b, Imports.id) let bdy = case (bAPI b, bType b) of (BroadcastQualified, BroadcastJSON) -> error "JSON not supported for the qualified broadcast API" (BroadcastQualified, BroadcastProto) -> @@ -998,7 +996,7 @@ mkOtrMessage (usr, clt, m) = (fn usr, HashMap.singleton (fn clt) m) fn = fromJust . fromByteString . toByteString' postProtoOtrMessage :: UserId -> ClientId -> ConvId -> OtrRecipients -> TestM ResponseLBS -postProtoOtrMessage = postProtoOtrMessage' Nothing id +postProtoOtrMessage = postProtoOtrMessage' Nothing Imports.id postProtoOtrMessage' :: Maybe [UserId] -> (Request -> Request) -> UserId -> ClientId -> ConvId -> OtrRecipients -> TestM ResponseLBS postProtoOtrMessage' reportMissing modif u d c rec = do @@ -1583,17 +1581,17 @@ registerRemoteConv convId originUser name othMembers = do void $ runFedClient @"on-conversation-created" fedGalleyClient (qDomain convId) $ ConversationCreated - { ccTime = now, - ccOrigUserId = originUser, - ccCnvId = qUnqualified convId, - ccCnvType = RegularConv, - ccCnvAccess = [], - ccCnvAccessRoles = Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole], - ccCnvName = name, - ccNonCreatorMembers = othMembers, - ccMessageTimer = Nothing, - ccReceiptMode = Nothing, - ccProtocol = ProtocolProteus + { time = now, + origUserId = originUser, + cnvId = qUnqualified convId, + cnvType = RegularConv, + cnvAccess = [], + cnvAccessRoles = Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole], + cnvName = name, + nonCreatorMembers = othMembers, + messageTimer = Nothing, + receiptMode = Nothing, + protocol = ProtocolProteus } getFeatureStatusMulti :: forall cfg. KnownSymbol (FeatureSymbol cfg) => Multi.TeamFeatureNoConfigMultiRequest -> TestM ResponseLBS @@ -1629,15 +1627,15 @@ assertNotConvMember u c = assertConvEquals :: (HasCallStack, MonadIO m) => Conversation -> Conversation -> m () assertConvEquals c1 c2 = liftIO $ do - assertEqual "id" (cnvQualifiedId c1) (cnvQualifiedId c2) - assertEqual "type" (cnvType c1) (cnvType c2) - assertEqual "creator" (cnvCreator c1) (cnvCreator c2) + assertEqual "id" c1.cnvQualifiedId c2.cnvQualifiedId + assertEqual "type" (Conv.cnvType c1) (Conv.cnvType c2) + assertEqual "creator" (Conv.cnvCreator c1) (Conv.cnvCreator c2) assertEqual "access" (accessSet c1) (accessSet c2) - assertEqual "name" (cnvName c1) (cnvName c2) + assertEqual "name" (Conv.cnvName c1) (Conv.cnvName c2) assertEqual "self member" (selfMember c1) (selfMember c2) assertEqual "other members" (otherMembers c1) (otherMembers c2) where - accessSet = Set.fromList . toList . cnvAccess + accessSet = Set.fromList . toList . Conv.cnvAccess selfMember = cmSelf . cnvMembers otherMembers = Set.fromList . cmOthers . cnvMembers @@ -1670,9 +1668,9 @@ assertConvWithRole r t c s us n mt role = do let _self = cmSelf (cnvMembers cnv) let others = cmOthers (cnvMembers cnv) liftIO $ do - assertEqual "id" cId (qUnqualified (cnvQualifiedId cnv)) - assertEqual "name" n (cnvName cnv) - assertEqual "type" t (cnvType cnv) + assertEqual "id" cId (qUnqualified cnv.cnvQualifiedId) + assertEqual "name" n (Conv.cnvName cnv) + assertEqual "type" t (Conv.cnvType cnv) assertEqual "creator" c (cnvCreator cnv) assertEqual "message_timer" mt (cnvMessageTimer cnv) assertEqual "self" s (memId _self) @@ -1683,9 +1681,9 @@ assertConvWithRole r t c s us n mt role = do assertBool "otr archived not false" (not (memOtrArchived _self)) assertBool "otr archived ref not empty" (isNothing (memOtrArchivedRef _self)) case t of - SelfConv -> assertEqual "access" privateAccess (cnvAccess cnv) - ConnectConv -> assertEqual "access" privateAccess (cnvAccess cnv) - One2OneConv -> assertEqual "access" privateAccess (cnvAccess cnv) + SelfConv -> assertEqual "access" privateAccess (Conv.cnvAccess cnv) + ConnectConv -> assertEqual "access" privateAccess (Conv.cnvAccess cnv) + One2OneConv -> assertEqual "access" privateAccess (Conv.cnvAccess cnv) _ -> pure () pure (cnvQualifiedId cnv) @@ -2026,7 +2024,7 @@ connectUsersUnchecked :: UserId -> List1 UserId -> TestM (List1 (Response (Maybe Lazy.ByteString), Response (Maybe Lazy.ByteString))) -connectUsersUnchecked = connectUsersWith id +connectUsersUnchecked = connectUsersWith Imports.id connectUsersWith :: (Request -> Request) -> @@ -2199,7 +2197,7 @@ ephemeralUser = do let p = object ["name" .= name] r <- post (b . path "/register" . json p) UserId -> LastPrekey -> TestM ClientId randomClient uid lk = randomClientWithCaps uid lk Nothing @@ -2339,11 +2337,11 @@ fromBS bs = convRange :: Maybe (Either [ConvId] ConvId) -> Maybe Int32 -> Request -> Request convRange range size = - maybe id (queryItem "size" . C.pack . show) size + maybe Imports.id (queryItem "size" . C.pack . show) size . case range of Just (Left l) -> queryItem "ids" (C.intercalate "," $ map toByteString' l) Just (Right c) -> queryItem "start" (toByteString' c) - Nothing -> id + Nothing -> Imports.id privateAccess :: [Access] privateAccess = [PrivateAccess] @@ -2391,7 +2389,7 @@ assertMismatchWithMessage mmsg missing redundant deleted = do userClients = UserClients . Map.fromList formatMessage :: String -> String - formatMessage = maybe id (\msg -> ((msg <> "\n") <>)) mmsg + formatMessage = maybe Imports.id (\msg -> ((msg <> "\n") <>)) mmsg assertMismatch :: HasCallStack => @@ -2707,7 +2705,7 @@ withTempMockFederator' resp action = do [("Content-Type", "application/json")] mock $ \mockPort -> do - withSettingsOverrides (\opts -> opts & Opts.optFederator ?~ Endpoint "127.0.0.1" (fromIntegral mockPort)) action + withSettingsOverrides (\opts -> opts & Opts.federator ?~ Endpoint "127.0.0.1" (fromIntegral mockPort)) action -- Starts a servant Application in Network.Wai.Test session and runs the -- FederatedRequest against it. diff --git a/services/galley/test/integration/API/Util/TeamFeature.hs b/services/galley/test/integration/API/Util/TeamFeature.hs index e2d04068bb7..cb98a9aa9c4 100644 --- a/services/galley/test/integration/API/Util/TeamFeature.hs +++ b/services/galley/test/integration/API/Util/TeamFeature.hs @@ -33,7 +33,7 @@ import Data.ByteString.Conversion (toByteString') import Data.Id (ConvId, TeamId, UserId) import Data.Schema import GHC.TypeLits (KnownSymbol) -import Galley.Options (optSettings, setFeatureFlags) +import Galley.Options (featureFlags, settings) import Galley.Types.Teams import Imports import TestSetup @@ -41,7 +41,7 @@ import Wire.API.Team.Feature qualified as Public withCustomSearchFeature :: FeatureTeamSearchVisibilityAvailability -> TestM () -> TestM () withCustomSearchFeature flag action = do - Util.withSettingsOverrides (\opts -> opts & optSettings . setFeatureFlags . flagTeamSearchVisibility .~ flag) action + Util.withSettingsOverrides (\opts -> opts & settings . featureFlags . flagTeamSearchVisibility .~ flag) action getTeamSearchVisibilityAvailable :: HasCallStack => (Request -> Request) -> UserId -> TeamId -> MonadHttp m => m ResponseLBS getTeamSearchVisibilityAvailable = getTeamFeatureFlagWithGalley @Public.SearchVisibilityAvailableConfig diff --git a/services/galley/test/integration/Federation.hs b/services/galley/test/integration/Federation.hs index 6e3d853f7d1..2971b59ecc8 100644 --- a/services/galley/test/integration/Federation.hs +++ b/services/galley/test/integration/Federation.hs @@ -58,7 +58,7 @@ isConvMemberLTests :: TestM () isConvMemberLTests = do s <- ask let opts = s ^. tsGConf - localDomain = opts ^. optSettings . setFederationDomain + localDomain = opts ^. settings . federationDomain remoteDomain = Domain "far-away.example.com" convId = Id $ fromJust $ UUID.fromString "8cc34301-6949-46c5-bb93-00a72268e2f5" convLocalMembers = [LocalMember userId defMemberStatus Nothing roleNameWireMember] @@ -96,8 +96,8 @@ updateFedDomainsTestNoop' = do let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates remoteDomain = Domain "far-away.example.com" remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. optSettings . setFederationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. optSettings . setFederationDomain + liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain + liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain -- Setup a conversation for a known remote domain. -- Include that domain in the old and new lists so -- if the function is acting up we know it will be @@ -115,8 +115,8 @@ updateFedDomainsTestAddRemote' = do let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates remoteDomain = Domain "far-away.example.com" remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. optSettings . setFederationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. optSettings . setFederationDomain + liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain + liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain -- Adding a new federation domain, this too should be a no-op updateFedDomainsAddRemote env remoteDomain remoteDomain2 interval @@ -132,8 +132,8 @@ updateFedDomainsTestRemoveRemoteFromLocal' = do let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates remoteDomain = Domain "far-away.example.com" remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. optSettings . setFederationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. optSettings . setFederationDomain + liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain + liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain -- Remove a remote domain from local conversations updateFedDomainRemoveRemoteFromLocal env remoteDomain remoteDomain2 interval @@ -149,8 +149,8 @@ updateFedDomainsTestRemoveLocalFromRemote' = do let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates remoteDomain = Domain "far-away.example.com" remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. optSettings . setFederationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. optSettings . setFederationDomain + liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain + liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain -- Remove a local domain from remote conversations updateFedDomainRemoveLocalFromRemote env remoteDomain interval @@ -312,7 +312,7 @@ updateFedDomainsAddRemote :: Env -> Domain -> Domain -> Int -> TestM () updateFedDomainsAddRemote env remoteDomain remoteDomain2 interval = do s <- ask let opts = s ^. tsGConf - localDomain = opts ^. optSettings . setFederationDomain + localDomain = opts ^. settings . federationDomain old = FederationDomainConfigs AllowDynamic [FederationDomainConfig remoteDomain FullSearch] interval new = old {remotes = FederationDomainConfig remoteDomain2 FullSearch : remotes old} -- Just check against the domains, as the search @@ -350,7 +350,7 @@ updateFedDomainsTestNoop :: Env -> Domain -> Int -> TestM () updateFedDomainsTestNoop env remoteDomain interval = do s <- ask let opts = s ^. tsGConf - localDomain = opts ^. optSettings . setFederationDomain + localDomain = opts ^. settings . federationDomain old = FederationDomainConfigs AllowDynamic [FederationDomainConfig remoteDomain FullSearch] interval new = old qalice <- randomQualifiedUser diff --git a/services/galley/test/integration/Main.hs b/services/galley/test/integration/Main.hs index 7c211f068a3..6d465089cbe 100644 --- a/services/galley/test/integration/Main.hs +++ b/services/galley/test/integration/Main.hs @@ -22,7 +22,8 @@ where import API qualified import API.SQS qualified as SQS -import Bilge hiding (body, header) +import Bilge hiding (body, header, host, port) +import Bilge qualified import Cassandra.Util import Control.Lens import Data.ByteString.Char8 qualified as BS @@ -37,7 +38,8 @@ import Data.Yaml (decodeFileEither) import Federation import Galley.API (sitemap) import Galley.Aws qualified as Aws -import Galley.Options +import Galley.Options hiding (endpoint) +import Galley.Options qualified as O import Imports hiding (local) import Network.HTTP.Client (responseTimeoutMicro) import Network.HTTP.Client.TLS (tlsManagerSettings) @@ -107,34 +109,34 @@ main = withOpenSSL $ runTests go ] getOpts gFile iFile = do m <- newManager tlsManagerSettings {managerResponseTimeout = responseTimeoutMicro 300000000} - let local p = Endpoint {_epHost = "127.0.0.1", _epPort = p} + let local p = Endpoint {_host = "127.0.0.1", _port = p} gConf <- handleParseError =<< decodeFileEither gFile iConf <- handleParseError =<< decodeFileEither iFile -- FUTUREWORK: we don't support process env setup any more, so both gconf and iConf -- must be 'Just'. the following code could be simplified a lot, but this should -- probably happen after (or at least while) unifying the integration test suites into -- a single library. - galleyEndpoint <- optOrEnv galley iConf (local . read) "GALLEY_WEB_PORT" + galleyEndpoint <- optOrEnv (.galley) iConf (local . read) "GALLEY_WEB_PORT" let g = mkRequest galleyEndpoint - b <- mkRequest <$> optOrEnv brig iConf (local . read) "BRIG_WEB_PORT" - c <- mkRequest <$> optOrEnv cannon iConf (local . read) "CANNON_WEB_PORT" + b <- mkRequest <$> optOrEnv (.brig) iConf (local . read) "BRIG_WEB_PORT" + c <- mkRequest <$> optOrEnv (.cannon) iConf (local . read) "CANNON_WEB_PORT" -- unset this env variable in galley's config to disable testing SQS team events - q <- join <$> optOrEnvSafe queueName gConf (Just . pack) "GALLEY_SQS_TEAM_EVENTS" - e <- join <$> optOrEnvSafe endpoint gConf (fromByteString . BS.pack) "GALLEY_SQS_ENDPOINT" + q <- join <$> optOrEnvSafe queueName' gConf (Just . pack) "GALLEY_SQS_TEAM_EVENTS" + e <- join <$> optOrEnvSafe endpoint' gConf (fromByteString . BS.pack) "GALLEY_SQS_ENDPOINT" convMaxSize <- optOrEnv maxSize gConf read "CONV_MAX_SIZE" awsEnv <- initAwsEnv e q -- Initialize cassandra - let ch = fromJust gConf ^. optCassandra . casEndpoint . epHost - let cp = fromJust gConf ^. optCassandra . casEndpoint . epPort - let ck = fromJust gConf ^. optCassandra . casKeyspace + let ch = fromJust gConf ^. cassandra . endpoint . host + let cp = fromJust gConf ^. cassandra . endpoint . port + let ck = fromJust gConf ^. cassandra . keyspace lg <- Logger.new Logger.defSettings db <- defInitCassandra ck ch cp lg teamEventWatcher <- sequence $ SQS.watchSQSQueue <$> ((^. Aws.awsEnv) <$> awsEnv) <*> q pure $ TestSetup (fromJust gConf) (fromJust iConf) m g b c awsEnv convMaxSize db (FedClient m galleyEndpoint) teamEventWatcher - queueName = fmap (view awsQueueName) . view optJournal - endpoint = fmap (view awsEndpoint) . view optJournal - maxSize = view (optSettings . setMaxConvSize) + queueName' = fmap (view queueName) . view journal + endpoint' = fmap (view O.endpoint) . view journal + maxSize = view (settings . maxConvSize) initAwsEnv (Just e) (Just q) = Just <$> SQS.mkAWSEnv (JournalOpts q e) initAwsEnv _ _ = pure Nothing releaseOpts _ = pure () - mkRequest (Endpoint h p) = host (encodeUtf8 h) . port p + mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p diff --git a/services/galley/test/integration/TestHelpers.hs b/services/galley/test/integration/TestHelpers.hs index cad9342ec46..fdfbb8c309a 100644 --- a/services/galley/test/integration/TestHelpers.hs +++ b/services/galley/test/integration/TestHelpers.hs @@ -24,7 +24,7 @@ import Control.Monad.Catch (MonadMask) import Control.Retry import Data.Domain (Domain) import Data.Qualified -import Galley.Options (optSettings, setFederationDomain) +import Galley.Options (federationDomain, settings) import Imports import Test.Tasty (TestName, TestTree) import Test.Tasty.HUnit (Assertion, testCase) @@ -39,7 +39,7 @@ test s n h = testCase n runTest void . flip runReaderT setup . runTestM $ h viewFederationDomain :: TestM Domain -viewFederationDomain = view (tsGConf . optSettings . setFederationDomain) +viewFederationDomain = view (tsGConf . settings . federationDomain) qualifyLocal :: a -> TestM (Local a) qualifyLocal x = do diff --git a/services/galley/test/integration/TestSetup.hs b/services/galley/test/integration/TestSetup.hs index f3869492956..70ff6fc5356 100644 --- a/services/galley/test/integration/TestSetup.hs +++ b/services/galley/test/integration/TestSetup.hs @@ -142,15 +142,15 @@ runFedClient :: FedClient comp -> Domain -> Servant.Client m api -runFedClient (FedClient mgr endpoint) domain = +runFedClient (FedClient mgr ep) domain = Servant.hoistClient (Proxy @api) (servantClientMToHttp domain) $ Servant.clientIn (Proxy @api) (Proxy @Servant.ClientM) where servantClientMToHttp :: Domain -> Servant.ClientM a -> m a servantClientMToHttp originDomain action = liftIO $ do - let host = Text.unpack $ endpoint ^. epHost - port = fromInteger . toInteger $ endpoint ^. epPort - baseUrl = Servant.BaseUrl Servant.Http host port "/federation" + let h = Text.unpack $ ep ^. host + p = fromInteger . toInteger $ ep ^. port + baseUrl = Servant.BaseUrl Servant.Http h p "/federation" clientEnv = Servant.ClientEnv mgr baseUrl Nothing (makeClientRequest originDomain) eitherRes <- Servant.runClientM action clientEnv case eitherRes of diff --git a/services/galley/test/unit/Test/Galley/Mapping.hs b/services/galley/test/unit/Test/Galley/Mapping.hs index fa296cc6b72..c18bb63f903 100644 --- a/services/galley/test/unit/Test/Galley/Mapping.hs +++ b/services/galley/test/unit/Test/Galley/Mapping.hs @@ -83,19 +83,19 @@ tests = testProperty "self user role in remote conversation view is correct" $ \(ConvWithRemoteUser c ruid) dom -> qDomain (tUntagged ruid) /= dom ==> - fmap (rcmSelfRole . rcnvMembers) (conversationToRemote dom ruid c) + fmap (selfRole . members) (conversationToRemote dom ruid c) == Just roleNameWireMember, testProperty "remote conversation view metadata is correct" $ \(ConvWithRemoteUser c ruid) dom -> qDomain (tUntagged ruid) /= dom ==> - fmap rcnvMetadata (conversationToRemote dom ruid c) + fmap (.metadata) (conversationToRemote dom ruid c) == Just (Data.convMetadata c), testProperty "remote conversation view does not contain self" $ \(ConvWithRemoteUser c ruid) dom -> case conversationToRemote dom ruid c of Nothing -> False Just rcnv -> tUntagged ruid - `notElem` map omQualifiedId (rcmOthers (rcnvMembers rcnv)) + `notElem` map omQualifiedId rcnv.members.others ] cnvUids :: Conversation -> [Qualified UserId] diff --git a/services/gundeck/src/Gundeck/Aws.hs b/services/gundeck/src/Gundeck/Aws.hs index be9caa56973..ab8dd27c69c 100644 --- a/services/gundeck/src/Gundeck/Aws.hs +++ b/services/gundeck/src/Gundeck/Aws.hs @@ -24,6 +24,8 @@ module Gundeck.Aws mkEnv, Amazon, execute, + Gundeck.Aws.region, + Gundeck.Aws.account, -- * Errors Error (..), @@ -79,7 +81,8 @@ import Data.Text.Lazy qualified as LT import Gundeck.Aws.Arn import Gundeck.Aws.Sns import Gundeck.Instances () -import Gundeck.Options +import Gundeck.Options (Opts) +import Gundeck.Options qualified as O import Gundeck.Types.Push hiding (token) import Gundeck.Types.Push qualified as Push import Imports @@ -152,10 +155,10 @@ mkEnv lgr opts mgr = do e <- mkAwsEnv g - (mkEndpoint SQS.defaultService (opts ^. optAws . awsSqsEndpoint)) - (mkEndpoint SNS.defaultService (opts ^. optAws . awsSnsEndpoint)) - q <- getQueueUrl e (opts ^. optAws . awsQueueName) - pure (Env e g q (opts ^. optAws . awsRegion) (opts ^. optAws . awsAccount)) + (mkEndpoint SQS.defaultService (opts ^. O.aws . O.sqsEndpoint)) + (mkEndpoint SNS.defaultService (opts ^. O.aws . O.snsEndpoint)) + q <- getQueueUrl e (opts ^. O.aws . O.queueName) + pure (Env e g q (opts ^. O.aws . O.region) (opts ^. O.aws . O.account)) where mkEndpoint svc e = AWS.setEndpoint (e ^. awsSecure) (e ^. awsHost) (e ^. awsPort) svc mkAwsEnv g sqs sns = do @@ -166,7 +169,7 @@ mkEnv lgr opts mgr = do pure $ baseEnv { AWS.logger = awsLogger g, - AWS.region = opts ^. optAws . awsRegion, + AWS.region = opts ^. O.aws . O.region, AWS.retryCheck = retryCheck, AWS.manager = mgr } @@ -291,7 +294,7 @@ createEndpoint :: UserId -> Push.Transport -> ArnEnv -> AppName -> Push.Token -> createEndpoint u tr arnEnv app token = do env <- ask let top = mkAppTopic arnEnv tr app - let arn = mkSnsArn (env ^. region) (env ^. account) top + let arn = mkSnsArn env._region env._account top let tkn = Push.tokenText token let req = SNS.newCreatePlatformEndpoint (toText arn) tkn diff --git a/services/gundeck/src/Gundeck/Env.hs b/services/gundeck/src/Gundeck/Env.hs index bc2ee1d2676..f6e205e74e5 100644 --- a/services/gundeck/src/Gundeck/Env.hs +++ b/services/gundeck/src/Gundeck/Env.hs @@ -19,7 +19,7 @@ module Gundeck.Env where -import Bilge +import Bilge hiding (host, port) import Cassandra (ClientState, Keyspace (..)) import Cassandra qualified as C import Cassandra.Settings qualified as C @@ -36,7 +36,8 @@ import Data.Time.Clock import Data.Time.Clock.POSIX import Database.Redis qualified as Redis import Gundeck.Aws qualified as Aws -import Gundeck.Options as Opt +import Gundeck.Options as Opt hiding (host, port) +import Gundeck.Options qualified as O import Gundeck.Redis qualified as Redis import Gundeck.Redis.HedisExtensions qualified as Redis import Gundeck.ThreadBudget @@ -68,23 +69,23 @@ schemaVersion = 7 createEnv :: Metrics -> Opts -> IO ([Async ()], Env) createEnv m o = do - l <- Logger.mkLogger (o ^. optLogLevel) (o ^. optLogNetStrings) (o ^. optLogFormat) + l <- Logger.mkLogger (o ^. logLevel) (o ^. logNetStrings) (o ^. logFormat) c <- maybe - (C.initialContactsPlain (o ^. optCassandra . casEndpoint . epHost)) + (C.initialContactsPlain (o ^. cassandra . endpoint . host)) (C.initialContactsDisco "cassandra_gundeck" . unpack) - (o ^. optDiscoUrl) + (o ^. discoUrl) n <- newManager tlsManagerSettings - { managerConnCount = o ^. optSettings . setHttpPoolSize, - managerIdleConnectionCount = 3 * (o ^. optSettings . setHttpPoolSize), + { managerConnCount = o ^. settings . httpPoolSize, + managerIdleConnectionCount = 3 * (o ^. settings . httpPoolSize), managerResponseTimeout = responseTimeoutMicro 5000000 } - (rThread, r) <- createRedisPool l (o ^. optRedis) "main-redis" + (rThread, r) <- createRedisPool l (o ^. redis) "main-redis" - (rAdditionalThreads, rAdditional) <- case o ^. optRedisAdditionalWrite of + (rAdditionalThreads, rAdditional) <- case o ^. redisAdditionalWrite of Nothing -> pure ([], Nothing) Just additionalRedis -> do (rAddThread, rAdd) <- createRedisPool l additionalRedis "additional-write-redis" @@ -94,15 +95,15 @@ createEnv m o = do C.init $ C.setLogger (C.mkLogger (Logger.clone (Just "cassandra.gundeck") l)) . C.setContacts (NE.head c) (NE.tail c) - . C.setPortNumber (fromIntegral $ o ^. optCassandra . casEndpoint . epPort) - . C.setKeyspace (Keyspace (o ^. optCassandra . casKeyspace)) + . C.setPortNumber (fromIntegral $ o ^. cassandra . endpoint . port) + . C.setKeyspace (Keyspace (o ^. cassandra . keyspace)) . C.setMaxConnections 4 . C.setMaxStreams 128 . C.setPoolStripes 4 . C.setSendTimeout 3 . C.setResponseTimeout 10 . C.setProtocolVersion C.V4 - . C.setPolicy (C.dcFilterPolicyIfConfigured l (o ^. optCassandra . casFilterNodesByDatacentre)) + . C.setPolicy (C.dcFilterPolicyIfConfigured l (o ^. cassandra . filterNodesByDatacentre)) $ C.defSettings a <- Aws.mkEnv l o n io <- @@ -110,7 +111,7 @@ createEnv m o = do defaultUpdateSettings { updateAction = Ms . round . (* 1000) <$> getPOSIXTime } - mtbs <- mkThreadBudgetState `mapM` (o ^. optSettings . setMaxConcurrentNativePushes) + mtbs <- mkThreadBudgetState `mapM` (o ^. settings . maxConcurrentNativePushes) pure $! (rThread : rAdditionalThreads,) $! Env def m o l n p r rAdditional a io mtbs reqIdMsg :: RequestId -> Logger.Msg -> Logger.Msg @@ -118,21 +119,21 @@ reqIdMsg = ("request" Logger..=) . unRequestId {-# INLINE reqIdMsg #-} createRedisPool :: Logger.Logger -> RedisEndpoint -> ByteString -> IO (Async (), Redis.RobustConnection) -createRedisPool l endpoint identifier = do +createRedisPool l ep identifier = do let redisConnInfo = Redis.defaultConnectInfo - { Redis.connectHost = unpack $ endpoint ^. rHost, - Redis.connectPort = Redis.PortNumber (fromIntegral $ endpoint ^. rPort), + { Redis.connectHost = unpack $ ep ^. O.host, + Redis.connectPort = Redis.PortNumber (fromIntegral $ ep ^. O.port), Redis.connectTimeout = Just (secondsToNominalDiffTime 5), Redis.connectMaxConnections = 100 } Log.info l $ Log.msg (Log.val $ "starting connection to " <> identifier <> "...") - . Log.field "connectionMode" (show $ endpoint ^. rConnectionMode) + . Log.field "connectionMode" (show $ ep ^. O.connectionMode) . Log.field "connInfo" (show redisConnInfo) let connectWithRetry = Redis.connectRobust l (capDelay 1000000 (exponentialBackoff 50000)) - r <- case endpoint ^. rConnectionMode of + r <- case ep ^. O.connectionMode of Master -> connectWithRetry $ Redis.checkedConnect redisConnInfo Cluster -> connectWithRetry $ Redis.checkedConnectCluster redisConnInfo Log.info l $ Log.msg (Log.val $ "Established connection to " <> identifier <> ".") diff --git a/services/gundeck/src/Gundeck/Notification.hs b/services/gundeck/src/Gundeck/Notification.hs index 92136f7bec2..5f41a7ba5cf 100644 --- a/services/gundeck/src/Gundeck/Notification.hs +++ b/services/gundeck/src/Gundeck/Notification.hs @@ -33,11 +33,11 @@ import Data.Time.Clock.POSIX import Data.UUID qualified as UUID import Gundeck.Monad import Gundeck.Notification.Data qualified as Data -import Gundeck.Options +import Gundeck.Options hiding (host, port) import Imports hiding (getLast) import System.Logger.Class import System.Logger.Class qualified as Log -import Util.Options +import Util.Options hiding (host, port) import Wire.API.Internal.Notification data PaginateResult = PaginateResult @@ -64,7 +64,7 @@ paginate uid since mclt size = do updateActivity :: UserId -> ClientId -> Gundeck () updateActivity uid clt = do r <- do - Endpoint h p <- view $ options . optBrig + Endpoint h p <- view $ options . brig post ( host (toByteString' h) . port p diff --git a/services/gundeck/src/Gundeck/Notification/Data.hs b/services/gundeck/src/Gundeck/Notification/Data.hs index f8bbe165b78..d680e68f2c4 100644 --- a/services/gundeck/src/Gundeck/Notification/Data.hs +++ b/services/gundeck/src/Gundeck/Notification/Data.hs @@ -37,7 +37,7 @@ import Data.Range (Range, fromRange) import Data.Sequence (Seq, ViewL ((:<))) import Data.Sequence qualified as Seq import Gundeck.Env -import Gundeck.Options (NotificationTTL (..), optSettings, setInternalPageSize, setMaxPayloadLoadSize) +import Gundeck.Options (NotificationTTL (..), internalPageSize, maxPayloadLoadSize, settings) import Gundeck.Push.Native.Serialise () import Imports hiding (cs) import UnliftIO (pooledForConcurrentlyN_) @@ -119,7 +119,7 @@ fetchId u n c = runMaybeT $ do fetchLast :: (MonadReader Env m, MonadClient m) => UserId -> Maybe ClientId -> m (Maybe QueuedNotification) fetchLast u c = do - pageSize <- fromMaybe 100 <$> asks (^. options . optSettings . setInternalPageSize) + pageSize <- fromMaybe 100 <$> asks (^. options . settings . internalPageSize) go (Page True [] (firstPage pageSize)) where go page = case result page of @@ -219,12 +219,12 @@ mkResultPage size more ns = fetch :: (MonadReader Env m, MonadClient m, MonadUnliftIO m) => UserId -> Maybe ClientId -> Maybe NotificationId -> Range 100 10000 Int32 -> m ResultPage fetch u c Nothing (fromIntegral . fromRange -> size) = do - pageSize <- fromMaybe 100 <$> asks (^. options . optSettings . setInternalPageSize) + pageSize <- fromMaybe 100 <$> asks (^. options . settings . internalPageSize) let page1 = retry x1 $ paginate cqlStart (paramsP LocalQuorum (Identity u) pageSize) -- We always need to look for one more than requested in order to correctly -- report whether there are more results. - maxPayloadLoadSize <- fromMaybe (5 * 1024 * 1024) <$> asks (^. options . optSettings . setMaxPayloadLoadSize) - (ns, more) <- collect c Seq.empty True (size + 1) maxPayloadLoadSize page1 + maxPayloadSize <- fromMaybe (5 * 1024 * 1024) <$> asks (^. options . settings . maxPayloadLoadSize) + (ns, more) <- collect c Seq.empty True (size + 1) maxPayloadSize page1 -- Drop the extra element at the end if present pure $! mkResultPage size more ns where @@ -235,7 +235,7 @@ fetch u c Nothing (fromIntegral . fromRange -> size) = do \WHERE user = ? \ \ORDER BY id ASC" fetch u c (Just since) (fromIntegral . fromRange -> size) = do - pageSize <- fromMaybe 100 <$> asks (^. options . optSettings . setInternalPageSize) + pageSize <- fromMaybe 100 <$> asks (^. options . settings . internalPageSize) let page1 = retry x1 $ paginate cqlSince (paramsP LocalQuorum (u, TimeUuid (toUUID since)) pageSize) @@ -243,8 +243,8 @@ fetch u c (Just since) (fromIntegral . fromRange -> size) = do -- notification corresponding to the `since` argument itself. The second is -- to get an accurate `hasMore`, just like in the case above. - maxPayloadLoadSize <- fromMaybe (5 * 1024 * 1024) <$> asks (^. options . optSettings . setMaxPayloadLoadSize) - (ns, more) <- collect c Seq.empty True (size + 2) maxPayloadLoadSize page1 + maxPayloadSize <- fromMaybe (5 * 1024 * 1024) <$> asks (^. options . settings . maxPayloadLoadSize) + (ns, more) <- collect c Seq.empty True (size + 2) maxPayloadSize page1 -- Remove notification corresponding to the `since` argument, and record if it is found. let (ns', sinceFound) = case Seq.viewl ns of x :< xs | since == x ^. queuedNotificationId -> (xs, True) diff --git a/services/gundeck/src/Gundeck/Options.hs b/services/gundeck/src/Gundeck/Options.hs index ec90c651c56..ac484ec8331 100644 --- a/services/gundeck/src/Gundeck/Options.hs +++ b/services/gundeck/src/Gundeck/Options.hs @@ -36,15 +36,15 @@ newtype NotificationTTL = NotificationTTL data AWSOpts = AWSOpts { -- | AWS account - _awsAccount :: !Account, + _account :: !Account, -- | AWS region name - _awsRegion :: !Region, + _region :: !Region, -- | Environment name to scope ARNs to - _awsArnEnv :: !ArnEnv, + _arnEnv :: !ArnEnv, -- | SQS queue name - _awsQueueName :: !Text, - _awsSqsEndpoint :: !AWSEndpoint, - _awsSnsEndpoint :: !AWSEndpoint + _queueName :: !Text, + _sqsEndpoint :: !AWSEndpoint, + _snsEndpoint :: !AWSEndpoint } deriving (Show, Generic) @@ -54,18 +54,18 @@ makeLenses ''AWSOpts data Settings = Settings { -- | Number of connections to keep open in the http-client pool - _setHttpPoolSize :: !Int, + _httpPoolSize :: !Int, -- | TTL (seconds) of stored notifications - _setNotificationTTL :: !NotificationTTL, + _notificationTTL :: !NotificationTTL, -- | Use this option to group push notifications and send them in bulk to Cannon, instead -- of in individual requests - _setBulkPush :: !Bool, + _bulkPush :: !Bool, -- | Maximum number of concurrent threads calling SNS. - _setMaxConcurrentNativePushes :: !(Maybe MaxConcurrentNativePushes), + _maxConcurrentNativePushes :: !(Maybe MaxConcurrentNativePushes), -- | Maximum number of parallel requests to SNS and cassandra -- during native push processing (per incoming push request) -- defaults to unbounded, if unset. - _setPerNativePushConcurrency :: !(Maybe Int), + _perNativePushConcurrency :: !(Maybe Int), -- | The amount of time in milliseconds to wait after reading from an SQS queue -- returns no message, before asking for messages from SQS again. -- defaults to 'defSqsThrottleMillis'. @@ -74,25 +74,25 @@ data Settings = Settings -- ensures that there is only one request every 20 seconds. -- However, that parameter is not honoured when using fake-sqs -- (where throttling can thus make sense) - _setSqsThrottleMillis :: !(Maybe Int), - _setDisabledAPIVersions :: !(Maybe (Set Version)), + _sqsThrottleMillis :: !(Maybe Int), + _disabledAPIVersions :: !(Maybe (Set Version)), -- | Maximum number of bytes loaded into memory when fetching (referenced) payloads. -- Gundeck will return a truncated page if the whole page's payload sizes would exceed this limit in total. -- Inlined payloads can cause greater payload sizes to be loaded into memory regardless of this setting. - _setMaxPayloadLoadSize :: Maybe Int32, + _maxPayloadLoadSize :: Maybe Int32, -- | Cassandra page size for fetching notifications. Does not directly -- effect the page size request in the client API. A lower number will -- reduce the amount by which setMaxPayloadLoadSize is exceeded when loading -- notifications from the database if notifications have inlined payloads. - _setInternalPageSize :: Maybe Int32 + _internalPageSize :: Maybe Int32 } deriving (Show, Generic) data MaxConcurrentNativePushes = MaxConcurrentNativePushes { -- | more than this number of threads will not be allowed - _limitHard :: !(Maybe Int), + _hard :: !(Maybe Int), -- | more than this number of threads will be warned about - _limitSoft :: !(Maybe Int) + _soft :: !(Maybe Int) } deriving (Show, Generic) @@ -108,9 +108,9 @@ data RedisConnectionMode deriveJSON defaultOptions {constructorTagModifier = map toLower} ''RedisConnectionMode data RedisEndpoint = RedisEndpoint - { _rHost :: !Text, - _rPort :: !Word16, - _rConnectionMode :: !RedisConnectionMode + { _host :: !Text, + _port :: !Word16, + _connectionMode :: !RedisConnectionMode } deriving (Show, Generic) @@ -124,22 +124,22 @@ deriveFromJSON toOptionFieldName ''Settings data Opts = Opts { -- | Hostname and port to bind to - _optGundeck :: !Endpoint, - _optBrig :: !Endpoint, - _optCassandra :: !CassandraOpts, - _optRedis :: !RedisEndpoint, - _optRedisAdditionalWrite :: !(Maybe RedisEndpoint), - _optAws :: !AWSOpts, - _optDiscoUrl :: !(Maybe Text), - _optSettings :: !Settings, + _gundeck :: !Endpoint, + _brig :: !Endpoint, + _cassandra :: !CassandraOpts, + _redis :: !RedisEndpoint, + _redisAdditionalWrite :: !(Maybe RedisEndpoint), + _aws :: !AWSOpts, + _discoUrl :: !(Maybe Text), + _settings :: !Settings, -- Logging -- | Log level (Debug, Info, etc) - _optLogLevel :: !Level, + _logLevel :: !Level, -- | Use netstrings encoding: -- - _optLogNetStrings :: !(Maybe (Last Bool)), - _optLogFormat :: !(Maybe (Last LogFormat)) + _logNetStrings :: !(Maybe (Last Bool)), + _logFormat :: !(Maybe (Last LogFormat)) } deriving (Show, Generic) diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 5b282837ecd..02c6984873b 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -72,7 +72,7 @@ import Wire.API.Push.Token qualified as Public push :: [Push] -> Gundeck () push ps = do - bulk :: Bool <- view (options . optSettings . setBulkPush) + bulk :: Bool <- view (options . settings . bulkPush) rs <- if bulk then (Right <$> pushAll ps) `catch` (pure . Left . Seq.singleton) @@ -95,7 +95,7 @@ class MonadThrow m => MonadPushAll m where mpaRunWithBudget :: Int -> a -> m a -> m a instance MonadPushAll Gundeck where - mpaNotificationTTL = view (options . optSettings . setNotificationTTL) + mpaNotificationTTL = view (options . settings . notificationTTL) mpaMkNotificationId = mkNotificationId mpaListAllPresences = runWithDefaultRedis . Presence.listAll mpaBulkPush = Web.bulkPush @@ -126,7 +126,7 @@ class Monad m => MonadMapAsync m where mntgtPerPushConcurrency :: m (Maybe Int) instance MonadMapAsync Gundeck where - mntgtPerPushConcurrency = view (options . optSettings . setPerNativePushConcurrency) + mntgtPerPushConcurrency = view (options . settings . perNativePushConcurrency) mntgtMapAsync f l = do perPushConcurrency <- mntgtPerPushConcurrency case perPushConcurrency of @@ -451,9 +451,9 @@ addToken uid cid newtok = mpaRunWithBudget 1 (Left Public.AddTokenErrorNoBudget) let trp = t ^. tokenTransport let app = t ^. tokenApp let tok = t ^. token - env <- view (options . optAws . awsArnEnv) - aws <- view awsEnv - ept <- Aws.execute aws (Aws.createEndpoint uid trp env app tok) + env <- view (options . aws . arnEnv) + aws' <- view awsEnv + ept <- Aws.execute aws' (Aws.createEndpoint uid trp env app tok) case ept of Left (Aws.EndpointInUse arn) -> do Log.info $ "arn" .= toText arn ~~ msg (val "ARN in use") @@ -483,8 +483,8 @@ addToken uid cid newtok = mpaRunWithBudget 1 (Left Public.AddTokenErrorNoBudget) when (n >= 3) $ do Log.err $ msg (val "AWS SNS inconsistency w.r.t. " +++ toText arn) throwM (mkError status500 "server-error" "Server Error") - aws <- view awsEnv - ept <- Aws.execute aws (Aws.lookupEndpoint arn) + aws' <- view awsEnv + ept <- Aws.execute aws' (Aws.lookupEndpoint arn) case ept of Nothing -> create (n + 1) t Just ep -> diff --git a/services/gundeck/src/Gundeck/Push/Native.hs b/services/gundeck/src/Gundeck/Push/Native.hs index 7156463f283..752351340d4 100644 --- a/services/gundeck/src/Gundeck/Push/Native.hs +++ b/services/gundeck/src/Gundeck/Push/Native.hs @@ -52,7 +52,7 @@ push :: NativePush -> [Address] -> Gundeck () push _ [] = pure () push m [a] = push1 m a push m addrs = do - perPushConcurrency <- view (options . optSettings . setPerNativePushConcurrency) + perPushConcurrency <- view (options . settings . perNativePushConcurrency) case perPushConcurrency of -- send all at once Nothing -> void $ mapConcurrently (push1 m) addrs @@ -123,9 +123,9 @@ push1 = push1' 0 let trp = t ^. tokenTransport let app = t ^. tokenApp let tok = t ^. token - env <- view (options . optAws . awsArnEnv) - aws <- view awsEnv - ept <- Aws.execute aws (Aws.createEndpoint uid trp env app tok) + env <- view (options . aws . arnEnv) + aws' <- view awsEnv + ept <- Aws.execute aws' (Aws.createEndpoint uid trp env app tok) case ept of Left (Aws.EndpointInUse arn) -> Log.info $ "arn" .= toText arn ~~ msg (val "ARN in use") @@ -157,7 +157,7 @@ push1 = push1' 0 let r = singleton (target (a ^. addrUser) & targetClients .~ [c]) let t = a ^. addrPushToken let p = singletonPayload (PushRemove t) - Stream.add i r p =<< view (options . optSettings . setNotificationTTL) + Stream.add i r p =<< view (options . settings . notificationTTL) publish :: NativePush -> Address -> Aws.Amazon Result publish m a = flip catches pushException $ do @@ -194,7 +194,7 @@ publish m a = flip catches pushException $ do -- migrated to the token and endpoint of the new address. deleteTokens :: [Address] -> Maybe Address -> Gundeck () deleteTokens tokens new = do - aws <- view awsEnv + aws' <- view awsEnv forM_ tokens $ \a -> do Log.info $ field "user" (UUID.toASCIIBytes (toUUID (a ^. addrUser))) @@ -202,30 +202,30 @@ deleteTokens tokens new = do ~~ field "arn" (toText (a ^. addrEndpoint)) ~~ msg (val "Deleting push token") Data.delete (a ^. addrUser) (a ^. addrTransport) (a ^. addrApp) (a ^. addrToken) - ept <- Aws.execute aws (Aws.lookupEndpoint (a ^. addrEndpoint)) + ept <- Aws.execute aws' (Aws.lookupEndpoint (a ^. addrEndpoint)) for_ ept $ \ep -> let us = Set.delete (a ^. addrUser) (ep ^. Aws.endpointUsers) in if Set.null us - then delete aws a + then delete aws' a else case new of - Nothing -> update aws a us + Nothing -> update aws' a us Just a' -> do mapM_ (migrate a a') us - update aws a' (ep ^. Aws.endpointUsers) - delete aws a + update aws' a' (ep ^. Aws.endpointUsers) + delete aws' a where - delete aws a = do + delete aws' a = do Log.info $ field "user" (UUID.toASCIIBytes (toUUID (a ^. addrUser))) ~~ field "arn" (toText (a ^. addrEndpoint)) ~~ msg (val "Deleting SNS endpoint") - Aws.execute aws (Aws.deleteEndpoint (a ^. addrEndpoint)) - update aws a us = do + Aws.execute aws' (Aws.deleteEndpoint (a ^. addrEndpoint)) + update aws' a us = do Log.info $ field "user" (UUID.toASCIIBytes (toUUID (a ^. addrUser))) ~~ field "arn" (toText (a ^. addrEndpoint)) ~~ msg (val "Updating SNS endpoint") - Aws.execute aws (Aws.updateEndpoint us (a ^. addrToken) (a ^. addrEndpoint)) + Aws.execute aws' (Aws.updateEndpoint us (a ^. addrToken) (a ^. addrEndpoint)) migrate a a' u = do let oldArn = a ^. addrEndpoint let oldTok = a ^. addrToken diff --git a/services/gundeck/src/Gundeck/React.hs b/services/gundeck/src/Gundeck/React.hs index ac34e68e987..d97f4312ccc 100644 --- a/services/gundeck/src/Gundeck/React.hs +++ b/services/gundeck/src/Gundeck/React.hs @@ -37,7 +37,7 @@ import Gundeck.Env import Gundeck.Instances () import Gundeck.Monad import Gundeck.Notification.Data qualified as Stream -import Gundeck.Options (optSettings, setNotificationTTL) +import Gundeck.Options (notificationTTL, settings) import Gundeck.Push.Data qualified as Push import Gundeck.Push.Native.Types import Gundeck.Push.Websocket qualified as Web @@ -162,7 +162,7 @@ deleteToken u ev tk cl = do n = Notification i False p r = singleton (target u & targetClients .~ [cl]) void $ Web.push n r (Just u) Nothing Set.empty - Stream.add i r p =<< view (options . optSettings . setNotificationTTL) + Stream.add i r p =<< view (options . settings . notificationTTL) Push.delete u (t ^. tokenTransport) (t ^. tokenApp) tk mkPushToken :: Event -> Token -> ClientId -> PushToken diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index 8db0f115f12..3f7c9d1f6f4 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -39,7 +39,7 @@ import Gundeck.Aws qualified as Aws import Gundeck.Env import Gundeck.Env qualified as Env import Gundeck.Monad -import Gundeck.Options +import Gundeck.Options hiding (host, port) import Gundeck.React import Gundeck.ThreadBudget import Imports hiding (head) @@ -62,8 +62,8 @@ run o = do runClient (e ^. cstate) $ versionCheck schemaVersion let l = e ^. applog - s <- newSettings $ defaultServer (unpack $ o ^. optGundeck . epHost) (o ^. optGundeck . epPort) l m - let throttleMillis = fromMaybe defSqsThrottleMillis $ o ^. (optSettings . setSqsThrottleMillis) + s <- newSettings $ defaultServer (unpack $ o ^. gundeck . host) (o ^. gundeck . port) l m + let throttleMillis = fromMaybe defSqsThrottleMillis $ o ^. (settings . sqsThrottleMillis) lst <- Async.async $ Aws.execute (e ^. awsEnv) (Aws.listen throttleMillis (runDirect e . onEvent)) wtbs <- forM (e ^. threadBudgetState) $ \tbs -> Async.async $ runDirect e $ watchThreadBudgetState m tbs 10 @@ -81,7 +81,7 @@ run o = do where middleware :: Env -> Wai.Middleware middleware e = - versionMiddleware (fold (o ^. optSettings . setDisabledAPIVersions)) + versionMiddleware (fold (o ^. settings . disabledAPIVersions)) . waiPrometheusMiddleware sitemap . GZip.gunzip . GZip.gzip GZip.def diff --git a/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs b/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs index 4918560c12e..4f311bb072c 100644 --- a/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs +++ b/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs @@ -135,10 +135,10 @@ runWithBudget' metrics (ThreadBudgetState limits ref) spent fallback action = do key <- liftIO nextRandom (`finally` unregister ref key) $ do oldsize <- allocate ref key spent - let softLimitBreached = maybe False (oldsize >=) (limits ^. limitSoft) - hardLimitBreached = maybe False (oldsize >=) (limits ^. limitHard) + let softLimitBreached = maybe False (oldsize >=) (limits ^. soft) + hardLimitBreached = maybe False (oldsize >=) (limits ^. hard) warnNoBudget softLimitBreached hardLimitBreached oldsize - if maybe True (oldsize <) (limits ^. limitHard) + if maybe True (oldsize <) (limits ^. hard) then go key oldsize else pure fallback where @@ -154,14 +154,14 @@ runWithBudget' metrics (ThreadBudgetState limits ref) spent fallback action = do -- iff soft and/or hard limit are breached, log a warning-level message. warnNoBudget :: Bool -> Bool -> Int -> m () warnNoBudget False False _ = pure () - warnNoBudget soft hard oldsize = do - let limit = if hard then "hard" else "soft" + warnNoBudget soft' hard' oldsize = do + let limit = if hard' then "hard" else "soft" metric = "net.nativepush." <> limit <> "_limit_breached" counterIncr (path metric) metrics LC.warn $ "spent" LC..= show oldsize - LC.~~ "soft-breach" LC..= soft - LC.~~ "hard-breach" LC..= hard + LC.~~ "soft-breach" LC..= soft' + LC.~~ "hard-breach" LC..= hard' LC.~~ LC.msg ("runWithBudget: " <> limit <> " limit reached") -- | Fork a thread that checks with the given frequency if any async handles stored in the @@ -194,9 +194,9 @@ recordMetrics :: recordMetrics metrics limits ref = do (BudgetMap spent _) <- readIORef ref gaugeSet (fromIntegral spent) (path "net.nativepush.thread_budget_allocated") metrics - forM_ (limits ^. limitHard) $ \lim -> + forM_ (limits ^. hard) $ \lim -> gaugeSet (fromIntegral lim) (path "net.nativepush.thread_budget_hard_limit") metrics - forM_ (limits ^. limitSoft) $ \lim -> + forM_ (limits ^. soft) $ \lim -> gaugeSet (fromIntegral lim) (path "net.nativepush.thread_budget_soft_limit") metrics threadDelayNominalDiffTime :: NominalDiffTime -> MonadIO m => m () diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index 5956d356057..930d83fa1c3 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -50,7 +50,8 @@ import Data.Set qualified as Set import Data.Text.Encoding qualified as T import Data.UUID qualified as UUID import Data.UUID.V4 -import Gundeck.Options +import Gundeck.Options hiding (bulkPush) +import Gundeck.Options qualified as O import Gundeck.Types import Gundeck.Types.Common qualified import Imports @@ -407,10 +408,10 @@ targetClientPush = do storeNotificationsEvenWhenRedisIsDown :: TestM () storeNotificationsEvenWhenRedisIsDown = do ally <- randomId - origRedisEndpoint <- view $ tsOpts . optRedis + origRedisEndpoint <- view $ tsOpts . redis let proxyPort = 10112 - redisProxyServer <- liftIO . async $ runRedisProxy (origRedisEndpoint ^. rHost) (origRedisEndpoint ^. rPort) proxyPort - withSettingsOverrides (optRedis .~ RedisEndpoint "localhost" proxyPort (origRedisEndpoint ^. rConnectionMode)) $ do + redisProxyServer <- liftIO . async $ runRedisProxy (origRedisEndpoint ^. O.host) (origRedisEndpoint ^. O.port) proxyPort + withSettingsOverrides (redis .~ RedisEndpoint "localhost" proxyPort (origRedisEndpoint ^. connectionMode)) $ do let pload = textPayload "hello" push = buildPush ally [(ally, RecipientClientsAll)] pload gu <- view tsGundeck @@ -912,7 +913,7 @@ testRedisMigration = do let presence = Presence uid con cannonURI Nothing 1 "" redis2 <- view tsRedis2 - withSettingsOverrides (optRedisAdditionalWrite ?~ redis2) $ do + withSettingsOverrides (redisAdditionalWrite ?~ redis2) $ do g <- view tsGundeck setPresence g presence !!! const 201 @@ -921,7 +922,7 @@ testRedisMigration = do map resource . decodePresence <$> (getPresence g (toByteString' uid) (getPresence g (toByteString' uid) ServiceName -> (Socket -> IO a) -> IO b -runTCPServer mhost port server = withSocketsDo $ do - addr <- resolve Stream mhost port True +runTCPServer mhost port' server = withSocketsDo $ do + addr <- resolve Stream mhost port' True clientThreads <- newTVarIO [] E.bracket (open addr) (cleanupClients clientThreads) (loop clientThreads) where @@ -70,8 +70,8 @@ runTCPServer mhost port server = withSocketsDo $ do mapM_ killThread =<< readTVarIO clientThreads resolve :: SocketType -> Maybe HostName -> ServiceName -> Bool -> IO AddrInfo -resolve socketType mhost port passive = - head <$> getAddrInfo (Just hints) mhost (Just port) +resolve socketType mhost port' passive = + head <$> getAddrInfo (Just hints) mhost (Just port') where hints = defaultHints diff --git a/services/gundeck/test/unit/ThreadBudget.hs b/services/gundeck/test/unit/ThreadBudget.hs index 45182fea0a3..bd06c866dea 100644 --- a/services/gundeck/test/unit/ThreadBudget.hs +++ b/services/gundeck/test/unit/ThreadBudget.hs @@ -276,7 +276,7 @@ postcondition model@(Model (Just _)) cmd@Measure {} resp@(MeasureResponse concre Model (Just state) = transition model cmd resp threadLimit :: Int threadLimit = case opaque state of - (tbs, _, _) -> tbs ^?! Control.Lens.to threadBudgetLimits . limitHard . _Just + (tbs, _, _) -> tbs ^?! Control.Lens.to threadBudgetLimits . hard . _Just -- number of running threads is never above the limit. threadLimitExceeded = Annotate "thread limit exceeded" $ concreteRunning .<= threadLimit -- FUTUREWORK: check that the number of running threads matches the model exactly. looks diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index a6e82976950..5c27ad4407d 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -256,9 +256,9 @@ authreq authreqttl msucc merr idpid = do form@(SAML.FormRedirect _ ((^. SAML.rqID) -> reqid)) <- do idp :: IdP <- IdPConfigStore.getConfig idpid let mbtid :: Maybe TeamId - mbtid = case fromMaybe defWireIdPAPIVersion (idp ^. SAML.idpExtraInfo . wiApiVersion) of + mbtid = case fromMaybe defWireIdPAPIVersion (idp ^. SAML.idpExtraInfo . apiVersion) of WireIdPAPIV1 -> Nothing - WireIdPAPIV2 -> Just $ idp ^. SAML.idpExtraInfo . wiTeam + WireIdPAPIV2 -> Just $ idp ^. SAML.idpExtraInfo . team SAML2.authReq authreqttl (SamlProtocolSettings.spIssuer mbtid) idpid VerdictFormatStore.store authreqttl reqid vformat pure form @@ -370,7 +370,7 @@ idpGetAll :: Sem r IdPList idpGetAll zusr = withDebugLog "idpGetAll" (const Nothing) $ do teamid <- Brig.getZUsrCheckPerm zusr ReadIdp - _idplProviders <- IdPConfigStore.getConfigsByTeam teamid + _providers <- IdPConfigStore.getConfigsByTeam teamid pure IdPList {..} -- | Delete empty IdPs, or if @"purge=true"@ in the HTTP query, delete all users @@ -399,18 +399,18 @@ idpDelete :: Sem r NoContent idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (const Nothing) $ do idp <- IdPConfigStore.getConfig idpid - (zusr, team) <- authorizeIdP mbzusr idp + (zusr, teamId) <- authorizeIdP mbzusr idp let issuer = idp ^. SAML.idpMetadata . SAML.edIssuer whenM (idpDoesAuthSelf idp zusr) $ throwSparSem SparIdPCannotDeleteOwnIdp - SAMLUserStore.getAllByIssuerPaginated issuer >>= assertEmptyOrPurge team + SAMLUserStore.getAllByIssuerPaginated issuer >>= assertEmptyOrPurge teamId updateOldIssuers idp updateReplacingIdP idp -- Delete tokens associated with given IdP (we rely on the fact that -- each IdP has exactly one team so we can look up all tokens -- associated with the team and then filter them) - tokens <- ScimTokenStore.lookupByTeam team + tokens <- ScimTokenStore.lookupByTeam teamId for_ tokens $ \ScimTokenInfo {..} -> - when (stiIdP == Just idpid) $ ScimTokenStore.delete team stiId + when (stiIdP == Just idpid) $ ScimTokenStore.delete teamId stiId -- Delete IdP config do IdPConfigStore.deleteConfig idp @@ -418,11 +418,11 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co pure NoContent where assertEmptyOrPurge :: TeamId -> Cas.Page (SAML.UserRef, UserId) -> Sem r () - assertEmptyOrPurge team page = do + assertEmptyOrPurge teamId page = do forM_ (Cas.result page) $ \(uref, uid) -> do mAccount <- BrigAccess.getAccount NoPendingInvitations uid let mUserTeam = userTeam . accountUser =<< mAccount - when (mUserTeam == Just team) $ do + when (mUserTeam == Just teamId) $ do if purge then do SAMLUserStore.delete uid uref @@ -430,7 +430,7 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co else do throwSparSem SparIdPHasBoundUsers when (Cas.hasMore page) $ - SAMLUserStore.nextPage page >>= assertEmptyOrPurge team + SAMLUserStore.nextPage page >>= assertEmptyOrPurge teamId updateOldIssuers :: IdP -> Sem r () updateOldIssuers _ = pure () @@ -442,11 +442,11 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co -- leave old issuers dangling for now. updateReplacingIdP :: IdP -> Sem r () - updateReplacingIdP idp = forM_ (idp ^. SAML.idpExtraInfo . wiOldIssuers) $ \oldIssuer -> do + updateReplacingIdP idp = forM_ (idp ^. SAML.idpExtraInfo . oldIssuers) $ \oldIssuer -> do iid <- - view SAML.idpId <$> case fromMaybe defWireIdPAPIVersion $ idp ^. SAML.idpExtraInfo . wiApiVersion of + view SAML.idpId <$> case fromMaybe defWireIdPAPIVersion $ idp ^. SAML.idpExtraInfo . apiVersion of WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1 oldIssuer - WireIdPAPIV2 -> IdPConfigStore.getIdPByIssuerV2 oldIssuer (idp ^. SAML.idpExtraInfo . wiTeam) + WireIdPAPIV2 -> IdPConfigStore.getIdPByIssuerV2 oldIssuer (idp ^. SAML.idpExtraInfo . team) IdPConfigStore.clearReplacedBy $ Replaced iid idpDoesAuthSelf :: IdP -> UserId -> Sem r Bool @@ -497,8 +497,9 @@ idpCreateXML zusr raw idpmeta mReplaces (fromMaybe defWireIdPAPIVersion -> apive teamid <- Brig.getZUsrCheckPerm zusr CreateUpdateDeleteIdp GalleyAccess.assertSSOEnabled teamid assertNoScimOrNoIdP teamid - handle <- maybe (IdPConfigStore.newHandle teamid) (pure . IdPHandle . fromRange) mHandle - idp <- validateNewIdP apiversion idpmeta teamid mReplaces handle + idp <- + maybe (IdPConfigStore.newHandle teamid) (pure . IdPHandle . fromRange) mHandle + >>= validateNewIdP apiversion idpmeta teamid mReplaces IdPRawMetadataStore.store (idp ^. SAML.idpId) raw IdPConfigStore.insertConfig idp forM_ mReplaces $ \replaces -> @@ -558,21 +559,21 @@ validateNewIdP :: Maybe SAML.IdPId -> IdPHandle -> m IdP -validateNewIdP apiversion _idpMetadata teamId mReplaces handle = withDebugLog "validateNewIdP" (Just . show . (^. SAML.idpId)) $ do +validateNewIdP apiversion _idpMetadata teamId mReplaces idHandle = withDebugLog "validateNewIdP" (Just . show . (^. SAML.idpId)) $ do _idpId <- SAML.IdPId <$> Random.uuid - oldIssuers :: [SAML.Issuer] <- case mReplaces of + oldIssuersList :: [SAML.Issuer] <- case mReplaces of Nothing -> pure [] Just replaces -> do idp <- IdPConfigStore.getConfig replaces - pure $ (idp ^. SAML.idpMetadata . SAML.edIssuer) : (idp ^. SAML.idpExtraInfo . wiOldIssuers) + pure $ (idp ^. SAML.idpMetadata . SAML.edIssuer) : (idp ^. SAML.idpExtraInfo . oldIssuers) let requri = _idpMetadata ^. SAML.edRequestURI - _idpExtraInfo = WireIdP teamId (Just apiversion) oldIssuers Nothing handle + _idpExtraInfo = WireIdP teamId (Just apiversion) oldIssuersList Nothing idHandle enforceHttps requri mbIdp <- case apiversion of WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1Maybe (_idpMetadata ^. SAML.edIssuer) WireIdPAPIV2 -> IdPConfigStore.getIdPByIssuerV2Maybe (_idpMetadata ^. SAML.edIssuer) teamId Logger.log Logger.Debug $ show (apiversion, _idpMetadata, teamId, mReplaces) - Logger.log Logger.Debug $ show (_idpId, oldIssuers, mbIdp) + Logger.log Logger.Debug $ show (_idpId, oldIssuersList, mbIdp) let failWithIdPClash :: m () failWithIdPClash = throwSparSem . SparNewIdPAlreadyInUse $ case apiversion of @@ -625,7 +626,7 @@ idpUpdateXML zusr raw idpmeta idpid mHandle = withDebugLog "idpUpdateXML" (Just GalleyAccess.assertSSOEnabled teamid IdPRawMetadataStore.store (idp ^. SAML.idpId) raw let idp' :: IdP = case mHandle of - Just idpHandle -> idp & (SAML.idpExtraInfo . wiHandle) .~ IdPHandle (fromRange idpHandle) + Just idpHandle -> idp & (SAML.idpExtraInfo . handle) .~ IdPHandle (fromRange idpHandle) Nothing -> idp -- (if raw metadata is stored and then spar goes out, raw metadata won't match the -- structured idp config. since this will lead to a 5xx response, the client is expected to @@ -633,10 +634,10 @@ idpUpdateXML zusr raw idpmeta idpid mHandle = withDebugLog "idpUpdateXML" (Just IdPConfigStore.insertConfig idp' -- if the IdP issuer is updated, the old issuer must be removed explicitly. -- if this step is ommitted (due to a crash) resending the update request should fix the inconsistent state. - let mbteamid = case fromMaybe defWireIdPAPIVersion $ idp' ^. SAML.idpExtraInfo . wiApiVersion of + let mbteamid = case fromMaybe defWireIdPAPIVersion $ idp' ^. SAML.idpExtraInfo . apiVersion of WireIdPAPIV1 -> Nothing WireIdPAPIV2 -> Just teamid - forM_ (idp' ^. SAML.idpExtraInfo . wiOldIssuers) (flip IdPConfigStore.deleteIssuer mbteamid) + forM_ (idp' ^. SAML.idpExtraInfo . oldIssuers) (flip IdPConfigStore.deleteIssuer mbteamid) pure idp' -- | Check that: idp id is valid; calling user is admin in that idp's home team; team id in @@ -660,7 +661,7 @@ validateIdPUpdate :: validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateIdPUpdate" (Just . show . (_2 %~ (^. SAML.idpId))) $ do previousIdP <- IdPConfigStore.getConfig _idpId (_, teamId) <- authorizeIdP zusr previousIdP - unless (previousIdP ^. SAML.idpExtraInfo . wiTeam == teamId) $ + unless (previousIdP ^. SAML.idpExtraInfo . team == teamId) $ throw errUnknownIdP _idpExtraInfo <- do let previousIssuer = previousIdP ^. SAML.idpMetadata . SAML.edIssuer @@ -671,7 +672,7 @@ validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateIdPUpdate" (J pure $ previousIdP ^. SAML.idpExtraInfo else do idpIssuerInUse <- - ( case fromMaybe defWireIdPAPIVersion $ previousIdP ^. SAML.idpExtraInfo . wiApiVersion of + ( case fromMaybe defWireIdPAPIVersion $ previousIdP ^. SAML.idpExtraInfo . apiVersion of WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1Maybe newIssuer WireIdPAPIV2 -> IdPConfigStore.getIdPByIssuerV2Maybe newIssuer teamId ) @@ -681,7 +682,7 @@ validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateIdPUpdate" (J ) if idpIssuerInUse then throwSparSem SparIdPIssuerInUse - else pure $ previousIdP ^. SAML.idpExtraInfo & wiOldIssuers %~ nub . (previousIssuer :) + else pure $ previousIdP ^. SAML.idpExtraInfo & oldIssuers %~ nub . (previousIssuer :) let requri = _idpMetadata ^. SAML.edRequestURI enforceHttps requri @@ -712,7 +713,7 @@ authorizeIdP :: Sem r (UserId, TeamId) authorizeIdP Nothing _ = throw (SAML.CustomError $ SparNoPermission (cs $ show CreateUpdateDeleteIdp)) authorizeIdP (Just zusr) idp = do - let teamid = idp ^. SAML.idpExtraInfo . wiTeam + let teamid = idp ^. SAML.idpExtraInfo . team GalleyAccess.assertHasPermission teamid CreateUpdateDeleteIdp zusr pure (zusr, teamid) @@ -736,8 +737,8 @@ internalDeleteTeam :: ) => TeamId -> Sem r NoContent -internalDeleteTeam team = do - deleteTeam team +internalDeleteTeam teamId = do + deleteTeam teamId pure NoContent internalPutSsoSettings :: diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index 2dceac0b0eb..f32f221433d 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -205,18 +205,18 @@ autoprovisionSamlUser :: autoprovisionSamlUser idp buid suid = do guardReplacedIdP guardScimTokens - createSamlUserWithId (idp ^. idpExtraInfo . wiTeam) buid suid defaultRole + createSamlUserWithId (idp ^. idpExtraInfo . team) buid suid defaultRole where -- Replaced IdPs are not allowed to create new wire accounts. guardReplacedIdP :: Sem r () guardReplacedIdP = do - unless (isNothing $ idp ^. idpExtraInfo . wiReplacedBy) $ do + unless (isNothing $ idp ^. idpExtraInfo . replacedBy) $ do throwSparSem $ SparCannotCreateUsersOnReplacedIdP (cs . SAML.idPIdToST $ idp ^. idpId) -- IdPs in teams with scim tokens are not allowed to auto-provision. guardScimTokens :: Sem r () guardScimTokens = do - let teamid = idp ^. idpExtraInfo . wiTeam + let teamid = idp ^. idpExtraInfo . team scimtoks <- ScimTokenStore.lookupByTeam teamid unless (null scimtoks) $ do throwSparSem SparSamlCredentialsNotFound @@ -361,7 +361,7 @@ getUserByUrefViaOldIssuerUnsafe idp (SAML.UserRef _ subject) = do where uref = SAML.UserRef oldIssuer subject - foldM tryFind Nothing (idp ^. idpExtraInfo . wiOldIssuers) + foldM tryFind Nothing (idp ^. idpExtraInfo . oldIssuers) -- | After a user has been found using 'findUserWithOldIssuer', update it everywhere so that -- the old IdP is not needed any more next time. @@ -397,18 +397,18 @@ verdictHandlerResultCore idp = \case pure $ VerifyHandlerDenied reasons SAML.AccessGranted uref -> do uid :: UserId <- do - let team = idp ^. idpExtraInfo . wiTeam + let team' = idp ^. idpExtraInfo . team err = SparUserRefInNoOrMultipleTeams . cs . show $ uref getUserByUrefUnsafe uref >>= \case Just usr -> do - if userTeam usr == Just team + if userTeam usr == Just team' then pure (userId usr) else throwSparSem err Nothing -> do getUserByUrefViaOldIssuerUnsafe idp uref >>= \case Just (olduref, usr) -> do let uid = userId usr - if userTeam usr == Just team + if userTeam usr == Just team' then moveUserToNewIssuer olduref uref uid >> pure uid else throwSparSem err Nothing -> do @@ -572,11 +572,11 @@ deleteTeam :: ) => TeamId -> Sem r () -deleteTeam team = do - ScimTokenStore.deleteByTeam team +deleteTeam team' = do + ScimTokenStore.deleteByTeam team' -- Since IdPs are not shared between teams, we can look at the set of IdPs -- used by the team, and remove everything related to those IdPs, too. - idps <- IdPConfigStore.getConfigsByTeam team + idps <- IdPConfigStore.getConfigsByTeam team' for_ idps $ \idp -> do let issuer = idp ^. SAML.idpMetadata . SAML.edIssuer SAMLUserStore.deleteByIssuer issuer diff --git a/services/spar/src/Spar/Run.hs b/services/spar/src/Spar/Run.hs index 8c6df422ab8..58681ad8974 100644 --- a/services/spar/src/Spar/Run.hs +++ b/services/spar/src/Spar/Run.hs @@ -28,7 +28,7 @@ module Spar.Run ) where -import Bilge +import qualified Bilge import Cassandra as Cas import qualified Cassandra.Schema as Cas import qualified Cassandra.Settings as Cas @@ -52,7 +52,7 @@ import Spar.Options import Spar.Orphans () import System.Logger.Class (Logger) import qualified System.Logger.Extended as Log -import Util.Options (casEndpoint, casFilterNodesByDatacentre, casKeyspace, epHost, epPort) +import Util.Options (endpoint, filterNodesByDatacentre, host, keyspace, port) import Wire.API.Routes.Version.Wai import Wire.Sem.Logger.TinyLog @@ -64,7 +64,7 @@ initCassandra opts lgr = do let cassOpts = cassandra opts connectString <- maybe - (Cas.initialContactsPlain (cassOpts ^. casEndpoint . epHost)) + (Cas.initialContactsPlain (cassOpts ^. endpoint . host)) (Cas.initialContactsDisco "cassandra_spar" . cs) (discoUrl opts) cas <- @@ -72,15 +72,15 @@ initCassandra opts lgr = do Cas.defSettings & Cas.setLogger (Cas.mkLogger (Log.clone (Just "cassandra.spar") lgr)) & Cas.setContacts (NE.head connectString) (NE.tail connectString) - & Cas.setPortNumber (fromIntegral $ cassOpts ^. casEndpoint . epPort) - & Cas.setKeyspace (Keyspace $ cassOpts ^. casKeyspace) + & Cas.setPortNumber (fromIntegral $ cassOpts ^. endpoint . port) + & Cas.setKeyspace (Keyspace $ cassOpts ^. keyspace) & Cas.setMaxConnections 4 & Cas.setMaxStreams 128 & Cas.setPoolStripes 4 & Cas.setSendTimeout 3 & Cas.setResponseTimeout 10 & Cas.setProtocolVersion V4 - & Cas.setPolicy (Cas.dcFilterPolicyIfConfigured lgr (cassOpts ^. casFilterNodesByDatacentre)) + & Cas.setPolicy (Cas.dcFilterPolicyIfConfigured lgr (cassOpts ^. filterNodesByDatacentre)) runClient cas $ Cas.versionCheck Data.schemaVersion pure cas @@ -104,14 +104,14 @@ mkApp sparCtxOpts = do let logLevel = samlToLevel $ saml sparCtxOpts ^. SAML.cfgLogLevel sparCtxLogger <- Log.mkLogger logLevel (logNetStrings sparCtxOpts) (logFormat sparCtxOpts) sparCtxCas <- initCassandra sparCtxOpts sparCtxLogger - sparCtxHttpManager <- newManager defaultManagerSettings + sparCtxHttpManager <- Bilge.newManager Bilge.defaultManagerSettings let sparCtxHttpBrig = - Bilge.host (sparCtxOpts ^. to brig . epHost . to cs) - . Bilge.port (sparCtxOpts ^. to brig . epPort) + Bilge.host (sparCtxOpts ^. to brig . host . to cs) + . Bilge.port (sparCtxOpts ^. to brig . port) $ Bilge.empty let sparCtxHttpGalley = - Bilge.host (sparCtxOpts ^. to galley . epHost . to cs) - . Bilge.port (sparCtxOpts ^. to galley . epPort) + Bilge.host (sparCtxOpts ^. to galley . host . to cs) + . Bilge.port (sparCtxOpts ^. to galley . port) $ Bilge.empty let wrappedApp = versionMiddleware (fold (disabledAPIVersions sparCtxOpts)) @@ -131,9 +131,9 @@ mkApp sparCtxOpts = do if Wai.requestMethod req == "POST" && Wai.pathInfo req == ["sso", "finalize-login"] then Just out else Nothing - pure (wrappedApp, let sparCtxRequestId = RequestId "N/A" in Env {..}) + pure (wrappedApp, let sparCtxRequestId = Bilge.RequestId "N/A" in Env {..}) -lookupRequestIdMiddleware :: (RequestId -> Application) -> Application +lookupRequestIdMiddleware :: (Bilge.RequestId -> Application) -> Application lookupRequestIdMiddleware mkapp req cont = do - let reqid = maybe def RequestId $ lookupRequestId req + let reqid = maybe def Bilge.RequestId $ lookupRequestId req mkapp reqid req cont diff --git a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs index 30c985dd9df..56f60c6c4f3 100644 --- a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs @@ -38,7 +38,8 @@ import Spar.Data.Instances () import Spar.Error import Spar.Sem.IdPConfigStore (IdPConfigStore (..), Replaced (..), Replacing (..)) import URI.ByteString -import Wire.API.User.IdentityProvider +import Wire.API.User.IdentityProvider hiding (apiVersion, oldIssuers, replacedBy, team) +import qualified Wire.API.User.IdentityProvider as IP idPToCassandra :: forall m r a. @@ -59,8 +60,8 @@ idPToCassandra = DeleteConfig idp -> let idpid = idp ^. SAML.idpId issuer = idp ^. SAML.idpMetadata . SAML.edIssuer - team = idp ^. SAML.idpExtraInfo . wiTeam - in embed @m $ deleteIdPConfig idpid issuer team + team' = idp ^. SAML.idpExtraInfo . IP.team + in embed @m $ deleteIdPConfig idpid issuer team' SetReplacedBy r r11 -> embed @m $ setReplacedBy r r11 ClearReplacedBy r -> embed @m $ clearReplacedBy r DeleteIssuer i t -> embed @m $ deleteIssuer i t @@ -85,32 +86,32 @@ insertIdPConfig idp = do NL.head (idp ^. SAML.idpMetadata . SAML.edCertAuthnResponse), NL.tail (idp ^. SAML.idpMetadata . SAML.edCertAuthnResponse), -- (the 'List1' is split up into head and tail to make migration from one-element-only easier.) - idp ^. SAML.idpExtraInfo . wiTeam, - idp ^. SAML.idpExtraInfo . wiApiVersion, - idp ^. SAML.idpExtraInfo . wiOldIssuers, - idp ^. SAML.idpExtraInfo . wiReplacedBy, - Just (unIdPHandle $ idp ^. SAML.idpExtraInfo . wiHandle) + idp ^. SAML.idpExtraInfo . IP.team, + idp ^. SAML.idpExtraInfo . IP.apiVersion, + idp ^. SAML.idpExtraInfo . IP.oldIssuers, + idp ^. SAML.idpExtraInfo . IP.replacedBy, + Just (unIdPHandle $ idp ^. SAML.idpExtraInfo . handle) ) addPrepQuery byIssuer ( idp ^. SAML.idpMetadata . SAML.edIssuer, - idp ^. SAML.idpExtraInfo . wiTeam, + idp ^. SAML.idpExtraInfo . IP.team, idp ^. SAML.idpId ) addPrepQuery byTeam ( idp ^. SAML.idpId, - idp ^. SAML.idpExtraInfo . wiTeam + idp ^. SAML.idpExtraInfo . IP.team ) where ensureDoNotMixApiVersions :: m () ensureDoNotMixApiVersions = do - let thisVersion = fromMaybe defWireIdPAPIVersion $ idp ^. SAML.idpExtraInfo . wiApiVersion + let thisVersion = fromMaybe defWireIdPAPIVersion $ idp ^. SAML.idpExtraInfo . IP.apiVersion issuer = idp ^. SAML.idpMetadata . SAML.edIssuer failIfNot :: WireIdPAPIVersion -> IdP -> m () failIfNot expectedVersion idp' = do - let actualVersion = fromMaybe defWireIdPAPIVersion $ idp' ^. SAML.idpExtraInfo . wiApiVersion + let actualVersion = fromMaybe defWireIdPAPIVersion $ idp' ^. SAML.idpExtraInfo . IP.apiVersion unless (actualVersion == expectedVersion) $ throwError InsertIdPConfigCannotMixApiVersions @@ -144,10 +145,10 @@ newUniqueHandle = newUniqueHandle' 1 where newUniqueHandle' :: Int -> [Text] -> Text newUniqueHandle' n handles = - let handle = "IdP " <> pack (show n) - in if handle `elem` handles + let handle' = "IdP " <> pack (show n) + in if handle' `elem` handles then newUniqueHandle' (n + 1) handles - else handle + else handle' getIdPConfig :: forall m. @@ -293,7 +294,7 @@ doNotMixApiVersions :: IdP -> m () doNotMixApiVersions expectVersion idp = do - let actualVersion = fromMaybe defWireIdPAPIVersion (idp ^. SAML.idpExtraInfo . wiApiVersion) + let actualVersion = fromMaybe defWireIdPAPIVersion (idp ^. SAML.idpExtraInfo . IP.apiVersion) unless (actualVersion == expectVersion) $ do throwError $ case expectVersion of WireIdPAPIV1 -> AttemptToGetV1IssuerViaV2API diff --git a/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs b/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs index 2c3a5ad0b02..a7f0ff7c4fb 100644 --- a/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs +++ b/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs @@ -75,7 +75,7 @@ insertConfig iw = . M.filter ( \iw' -> (iw' ^. SAML.idpMetadata . SAML.edIssuer /= iw ^. SAML.idpMetadata . SAML.edIssuer) - || (iw' ^. SAML.idpExtraInfo . IP.wiTeam /= iw ^. SAML.idpExtraInfo . IP.wiTeam) + || (iw' ^. SAML.idpExtraInfo . IP.team /= iw ^. SAML.idpExtraInfo . IP.team) ) getConfig :: SAML.IdPId -> TypedState -> IP.IdP @@ -106,14 +106,14 @@ getIdByIssuerWithTeamMaybe iss team mp = fl :: IP.IdP -> Bool fl idp = idp ^. SAML.idpMetadata . SAML.edIssuer == iss - && idp ^. SAML.idpExtraInfo . IP.wiTeam == team + && idp ^. SAML.idpExtraInfo . IP.team == team getConfigsByTeam :: TeamId -> TypedState -> [IP.IdP] getConfigsByTeam team = filter fl . M.elems where fl :: IP.IdP -> Bool - fl idp = idp ^. SAML.idpExtraInfo . IP.wiTeam == team + fl idp = idp ^. SAML.idpExtraInfo . IP.team == team deleteConfig :: IP.IdP -> TypedState -> TypedState deleteConfig idp = @@ -126,7 +126,7 @@ updateReplacedBy :: Maybe SAML.IdPId -> SAML.IdPId -> IP.IdP -> IP.IdP updateReplacedBy mbReplacing replaced idp = idp & if idp ^. SAML.idpId == replaced - then SAML.idpExtraInfo . IP.wiReplacedBy .~ mbReplacing + then SAML.idpExtraInfo . IP.replacedBy .~ mbReplacing else id deleteIssuer :: SAML.Issuer -> TypedState -> TypedState diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 3ec850ab8c6..2da4aea9ea1 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -258,12 +258,12 @@ specFinalizeLogin = do context "happy flow" $ do it "responds with a very peculiar 'allowed' HTTP response" $ do env <- ask - let apiVersion = env ^. teWireIdPAPIVersion + let apiVer = env ^. teWireIdPAPIVersion (_, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta - liftIO $ fromMaybe defWireIdPAPIVersion (idp ^. idpExtraInfo . wiApiVersion) `shouldBe` apiVersion + liftIO $ fromMaybe defWireIdPAPIVersion (idp ^. idpExtraInfo . apiVersion) `shouldBe` apiVer spmeta <- getTestSPMetadata tid authnreq <- negotiateAuthnRequest idp - let audiencePath = case apiVersion of + let audiencePath = case apiVer of WireIdPAPIV1 -> "/sso/finalize-login" WireIdPAPIV2 -> "/sso/finalize-login/" <> toByteString' tid liftIO $ authnreq ^. rqIssuer . fromIssuer . to URI.uriPath `shouldBe` audiencePath @@ -621,7 +621,7 @@ specCRUDIdentityProvider = do (owner :: UserId, _teamid :: TeamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) callIdpGetAll (env ^. teSpar) (Just owner) - `shouldRespondWith` (null . _idplProviders) + `shouldRespondWith` (null . _providers) context "some idps are registered" $ do context "client is team owner with email" $ do it "returns a non-empty empty list" $ do @@ -629,7 +629,7 @@ specCRUDIdentityProvider = do (SampleIdP metadata _ _ _) <- makeSampleIdPMetadata (owner, _, _) <- registerTestIdPFrom metadata (env ^. teMgr) (env ^. teBrig) (env ^. teGalley) (env ^. teSpar) callIdpGetAll (env ^. teSpar) (Just owner) - `shouldRespondWith` (not . null . _idplProviders) + `shouldRespondWith` (not . null . _providers) context "client is team owner without email" $ do it "returns a non-empty empty list" $ do env <- ask @@ -637,7 +637,7 @@ specCRUDIdentityProvider = do (firstOwner, tid, idp) <- registerTestIdPFrom metadata (env ^. teMgr) (env ^. teBrig) (env ^. teGalley) (env ^. teSpar) ssoOwner <- mkSsoOwner firstOwner tid idp privcreds callIdpGetAll (env ^. teSpar) (Just ssoOwner) - `shouldRespondWith` (not . null . _idplProviders) + `shouldRespondWith` (not . null . _providers) describe "DELETE /identity-providers/:idp" $ do testGetPutDelete (\o t i _ -> callIdpDelete' o t i) context "zuser has wrong team" $ do @@ -722,12 +722,12 @@ specCRUDIdentityProvider = do (SampleIdP metadata _ _ _) <- makeSampleIdPMetadata idp <- call $ callIdpCreate (env ^. teWireIdPAPIVersion) (env ^. teSpar) (Just owner) metadata callIdpGet (env ^. teSpar) (Just owner) (idp ^. idpId) - `shouldRespondWith` ((== IdPHandle "IdP 1") . (\idp' -> idp' ^. (SAML.idpExtraInfo . wiHandle))) + `shouldRespondWith` ((== IdPHandle "IdP 1") . (\idp' -> idp' ^. (SAML.idpExtraInfo . handle))) let expected = IdPHandle "kukku mukku" callIdpUpdateWithHandle (env ^. teSpar) (Just owner) (idp ^. idpId) (IdPMetadataValue (cs $ SAML.encode metadata) undefined) expected `shouldRespondWith` ((== 200) . statusCode) callIdpGet (env ^. teSpar) (Just owner) (idp ^. idpId) - `shouldRespondWith` ((== expected) . (\idp' -> idp' ^. (SAML.idpExtraInfo . wiHandle))) + `shouldRespondWith` ((== expected) . (\idp' -> idp' ^. (SAML.idpExtraInfo . handle))) it "updates IdP metadata and creates a new IdP with the first metadata" $ do env <- ask (owner, _) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) @@ -785,8 +785,8 @@ specCRUDIdentityProvider = do idp <- runSpar $ IdPEffect.getConfig idpid1 liftIO $ do (idp ^. idpMetadata . edIssuer) `shouldBe` (idpmeta1 ^. edIssuer) - (idp ^. idpExtraInfo . wiOldIssuers) `shouldBe` [] - (idp ^. idpExtraInfo . wiReplacedBy) `shouldBe` Nothing + (idp ^. idpExtraInfo . oldIssuers) `shouldBe` [] + (idp ^. idpExtraInfo . replacedBy) `shouldBe` Nothing let -- change idp metadata (only issuer, to be precise), and look at new issuer and -- old issuers. @@ -798,8 +798,8 @@ specCRUDIdentityProvider = do idp <- runSpar $ IdPEffect.getConfig idpid1 liftIO $ do (idp ^. idpMetadata . edIssuer) `shouldBe` (new ^. edIssuer) - sort (idp ^. idpExtraInfo . wiOldIssuers) `shouldBe` sort (olds <&> (^. edIssuer)) - (idp ^. idpExtraInfo . wiReplacedBy) `shouldBe` Nothing + sort (idp ^. idpExtraInfo . oldIssuers) `shouldBe` sort (olds <&> (^. edIssuer)) + (idp ^. idpExtraInfo . replacedBy) `shouldBe` Nothing -- update the name a few times, ending up with the original one. change idpmeta1' [idpmeta1] @@ -819,7 +819,7 @@ specCRUDIdentityProvider = do liftIO $ do statusCode resp `shouldBe` 200 idp ^. idpMetadata . edIssuer `shouldBe` issuer2 - idp ^. idpExtraInfo . wiOldIssuers `shouldBe` [idpmeta1 ^. edIssuer] + idp ^. idpExtraInfo . oldIssuers `shouldBe` [idpmeta1 ^. edIssuer] it "migrates old users to new idp on their next login (auto-prov)" $ do env <- ask (owner1, _, idp1@((^. idpId) -> idpid1), (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta @@ -916,7 +916,7 @@ specCRUDIdentityProvider = do check :: HasCallStack => Bool -> Int -> String -> Either String () -> TestSpar () check useNewPrivKey expectedStatus expectedHtmlTitle expectedCookie = do (idp, oldPrivKey, newPrivKey) <- initidp - let tid = idp ^. idpExtraInfo . wiTeam + let tid = idp ^. idpExtraInfo . team env <- ask (_, authnreq) <- call $ callAuthnReq (env ^. teSpar) (idp ^. idpId) spmeta <- getTestSPMetadata tid @@ -1024,14 +1024,14 @@ specCRUDIdentityProvider = do liftIO $ do idp1 `shouldBe` idp1' idp2 `shouldBe` idp2' - (idp1 ^. (SAML.idpExtraInfo . wiHandle)) `shouldBe` IdPHandle "IdP 1" - (idp2 ^. (SAML.idpExtraInfo . wiHandle)) `shouldBe` IdPHandle "IdP 2" + (idp1 ^. (SAML.idpExtraInfo . handle)) `shouldBe` IdPHandle "IdP 1" + (idp2 ^. (SAML.idpExtraInfo . handle)) `shouldBe` IdPHandle "IdP 2" it "explicitly set handle on IdP create" $ do env <- ask (owner, _) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) (SampleIdP metadata _ _ _) <- makeSampleIdPMetadata let expected = IdPHandle "kukku mukku" - actual <- (\idp -> idp ^. (SAML.idpExtraInfo . wiHandle)) <$> call (callIdpCreateWithHandle (env ^. teWireIdPAPIVersion) (env ^. teSpar) (Just owner) metadata expected) + actual <- (\idp -> idp ^. (SAML.idpExtraInfo . handle)) <$> call (callIdpCreateWithHandle (env ^. teWireIdPAPIVersion) (env ^. teSpar) (Just owner) metadata expected) liftIO $ actual `shouldBe` expected context "client is owner without email" $ do it "responds with 2xx; makes IdP available for GET /identity-providers/" $ do @@ -1065,22 +1065,22 @@ specCRUDIdentityProvider = do idp1' <- call $ callIdpGet (env ^. teSpar) (Just owner1) (idp1 ^. SAML.idpId) idp2' <- call $ callIdpGet (env ^. teSpar) (Just owner1) (idp2 ^. SAML.idpId) liftIO $ do - (idp1 & idpExtraInfo . wiReplacedBy .~ (idp1' ^. idpExtraInfo . wiReplacedBy)) `shouldBe` idp1' + (idp1 & idpExtraInfo . replacedBy .~ (idp1' ^. idpExtraInfo . replacedBy)) `shouldBe` idp1' idp2 `shouldBe` idp2' idp1 ^. idpMetadata . SAML.edIssuer `shouldBe` (idpmeta1 ^. SAML.edIssuer) idp2 ^. idpMetadata . SAML.edIssuer `shouldBe` issuer2 idp2 ^. idpId `shouldNotBe` idp1 ^. idpId - idp2 ^. idpExtraInfo . wiOldIssuers `shouldBe` [idpmeta1 ^. edIssuer] - idp1' ^. idpExtraInfo . wiReplacedBy `shouldBe` Just (idp2 ^. idpId) + idp2 ^. idpExtraInfo . oldIssuers `shouldBe` [idpmeta1 ^. edIssuer] + idp1' ^. idpExtraInfo . replacedBy `shouldBe` Just (idp2 ^. idpId) -- erase everything that is supposed to be different between idp1, idp2, and make -- sure the result is equal. let erase :: IdP -> IdP erase = (idpId .~ (idp1 ^. idpId)) . (idpMetadata . edIssuer .~ (idp1 ^. idpMetadata . edIssuer)) - . (idpExtraInfo . wiOldIssuers .~ (idp1 ^. idpExtraInfo . wiOldIssuers)) - . (idpExtraInfo . wiReplacedBy .~ (idp1 ^. idpExtraInfo . wiReplacedBy)) - . (idpExtraInfo . wiHandle .~ (idp1 ^. idpExtraInfo . wiHandle)) + . (idpExtraInfo . oldIssuers .~ (idp1 ^. idpExtraInfo . oldIssuers)) + . (idpExtraInfo . replacedBy .~ (idp1 ^. idpExtraInfo . replacedBy)) + . (idpExtraInfo . handle .~ (idp1 ^. idpExtraInfo . handle)) erase idp1 `shouldBe` erase idp2 it "users can still login on old idp as before" $ do env <- ask @@ -1211,7 +1211,7 @@ specDeleteCornerCases = describe "delete corner cases" $ do createViaSamlResp :: HasCallStack => IdP -> SignPrivCreds -> SAML.UserRef -> TestSpar ResponseLBS createViaSamlResp idp privCreds (SAML.UserRef _ subj) = do - let tid = idp ^. idpExtraInfo . wiTeam + let tid = idp ^. idpExtraInfo . team authnReq <- negotiateAuthnRequest idp spmeta <- getTestSPMetadata tid authnResp <- runSimpleSP $ mkAuthnResponseWithSubj subj privCreds idp spmeta authnReq True @@ -1297,7 +1297,7 @@ specScimAndSAML = do mkNameID subj (Just "https://federation.foobar.com/nidp/saml2/metadata") (Just "https://prod-nginz-https.wire.com/sso/finalize-login") Nothing authnreq <- negotiateAuthnRequest idp - spmeta <- getTestSPMetadata (idp ^. idpExtraInfo . wiTeam) + spmeta <- getTestSPMetadata (idp ^. idpExtraInfo . team) authnresp :: SignedAuthnResponse <- runSimpleSP $ mkAuthnResponseWithSubj subjectWithQualifier privcreds idp spmeta authnreq True ssoid <- getSsoidViaAuthResp authnresp @@ -1565,8 +1565,8 @@ specReAuthSsoUserWithPassword = if withIdp then do SampleIdP idpmeta _privkey _ _ <- makeSampleIdPMetadata - apiVersion <- view teWireIdPAPIVersion - idp <- call $ callIdpCreate apiVersion (env ^. teSpar) (Just owner) idpmeta + apiVer <- view teWireIdPAPIVersion + idp <- call $ callIdpCreate apiVer (env ^. teSpar) (Just owner) idpmeta pure $ Just (idp ^. idpId) else pure Nothing -- then user gets upgraded to scim with or without SAML diff --git a/services/spar/test-integration/Test/Spar/AppSpec.hs b/services/spar/test-integration/Test/Spar/AppSpec.hs index a0aa834dd0b..6009dd511e5 100644 --- a/services/spar/test-integration/Test/Spar/AppSpec.hs +++ b/services/spar/test-integration/Test/Spar/AppSpec.hs @@ -152,7 +152,7 @@ requestAccessVerdict idp isGranted mkAuthnReq = do raw <- mkAuthnReq (idp ^. SAML.idpId) bdy <- maybe (error "authreq") pure $ responseBody raw either (error . show) pure $ Servant.mimeUnrender (Servant.Proxy @SAML.HTML) bdy - spmeta <- getTestSPMetadata (idp ^. idpExtraInfo . User.wiTeam) + spmeta <- getTestSPMetadata (idp ^. idpExtraInfo . User.team) (privKey, _, _) <- DSig.mkSignCredsWithCert Nothing 96 authnresp :: SAML.AuthnResponse <- do case authnreq of diff --git a/services/spar/test-integration/Test/Spar/DataSpec.hs b/services/spar/test-integration/Test/Spar/DataSpec.hs index 46f7fe88e64..b81715f6f89 100644 --- a/services/spar/test-integration/Test/Spar/DataSpec.hs +++ b/services/spar/test-integration/Test/Spar/DataSpec.hs @@ -154,7 +154,7 @@ spec = do it "getIdPByIssuer works" $ do idp <- makeTestIdP () <- runSpar $ IdPEffect.insertConfig idp - midp <- getIdPByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . wiTeam) + midp <- getIdPByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . team) liftIO $ midp `shouldBe` Just idp it "getIdPConfigsByTeam works" $ do skipIdPAPIVersions [WireIdPAPIV1] @@ -176,10 +176,10 @@ spec = do idpOrError <- runSparE $ IdPEffect.getConfig (idp ^. idpId) liftIO $ idpOrError `shouldBe` Left (SAML.CustomError $ IdpDbError IdpNotFound) do - midp <- getIdPByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . wiTeam) + midp <- getIdPByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . team) liftIO $ midp `shouldBe` Nothing do - midp <- getIdPByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . wiTeam) + midp <- getIdPByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . team) liftIO $ midp `shouldBe` Nothing do idps <- runSpar $ IdPEffect.getConfigsByTeam teamid @@ -274,7 +274,7 @@ testDeleteTeam = it "cleans up all the right tables after deletion" $ do -- The config from 'issuer_idp': do let issuer = idp ^. SAML.idpMetadata . SAML.edIssuer - mbIdp <- getIdPByIssuer issuer (idp ^. SAML.idpExtraInfo . wiTeam) + mbIdp <- getIdPByIssuer issuer (idp ^. SAML.idpExtraInfo . team) liftIO $ mbIdp `shouldBe` Nothing -- The config from 'team_idp': do diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index fae49de6ac4..709a659088d 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -1057,7 +1057,7 @@ samlUserShouldSatisfy uref property = do createViaSamlResp :: HasCallStack => IdP -> SAML.SignPrivCreds -> SAML.UserRef -> TestSpar ResponseLBS createViaSamlResp idp privCreds (SAML.UserRef _ subj) = do authnReq <- negotiateAuthnRequest idp - let tid = idp ^. SAML.idpExtraInfo . User.wiTeam + let tid = idp ^. SAML.idpExtraInfo . User.team spmeta <- getTestSPMetadata tid authnResp <- runSimpleSP $ @@ -1135,7 +1135,7 @@ testCreateUserTimeout = do tryquery (filterBy "externalId" $ fromEmail email) waitUserExpiration = do - timeoutSecs <- view (teTstOpts . to cfgBrigSettingsTeamInvitationTimeout) + timeoutSecs <- view (teTstOpts . to brigSettingsTeamInvitationTimeout) Control.Exception.assert (timeoutSecs < 30) $ do threadDelay $ (timeoutSecs + 1) * 1_000_000 diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index bc3342f6e79..2a13b750abf 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -139,7 +139,7 @@ module Util.Core ) where -import Bilge hiding (getCookie) -- we use Web.Cookie instead of the http-client type +import Bilge hiding (getCookie, host, port) -- we use Web.Cookie instead of the http-client type import qualified Bilge import Bilge.Assert (Assertions, (!!!), ( -- would be a good place to look for code to steal. mkEnv :: HasCallStack => IntegrationConfig -> Opts -> IO TestEnv -mkEnv _teTstOpts _teOpts = do - _teMgr :: Manager <- newManager defaultManagerSettings - sparCtxLogger <- Log.mkLogger (samlToLevel $ saml _teOpts ^. SAML.cfgLogLevel) (logNetStrings _teOpts) (logFormat _teOpts) - _teCql :: ClientState <- initCassandra _teOpts sparCtxLogger - let _teBrig = endpointToReq (cfgBrig _teTstOpts) - _teGalley = endpointToReq (cfgGalley _teTstOpts) - _teSpar = endpointToReq (cfgSpar _teTstOpts) - _teSparEnv = Spar.Env {..} - _teWireIdPAPIVersion = WireIdPAPIV2 - sparCtxOpts = _teOpts - sparCtxCas = _teCql - sparCtxHttpManager = _teMgr - sparCtxHttpBrig = _teBrig empty - sparCtxHttpGalley = _teGalley empty +mkEnv tstOpts opts = do + mgr :: Manager <- newManager defaultManagerSettings + sparCtxLogger <- Log.mkLogger (samlToLevel $ saml opts ^. SAML.cfgLogLevel) (logNetStrings opts) (logFormat opts) + cql :: ClientState <- initCassandra opts sparCtxLogger + let brig = endpointToReq tstOpts.brig + galley = endpointToReq tstOpts.galley + spar = endpointToReq tstOpts.spar + sparEnv = Spar.Env {..} + wireIdPAPIVersion = WireIdPAPIV2 + sparCtxOpts = opts + sparCtxCas = cql + sparCtxHttpManager = mgr + sparCtxHttpBrig = brig empty + sparCtxHttpGalley = galley empty sparCtxRequestId = RequestId "" - pure TestEnv {..} + pure $ + TestEnv + mgr + cql + brig + galley + spar + sparEnv + opts + tstOpts + wireIdPAPIVersion destroyEnv :: HasCallStack => TestEnv -> IO () destroyEnv _ = pure () @@ -377,9 +387,9 @@ createUserWithTeamDisableSSO brg gly = do ] bdy <- selfUser . responseJsonUnsafe <$> post (brg . path "/i/users" . contentJson . body p) let (uid, Just tid) = (userId bdy, userTeam bdy) - (team : _) <- (^. Galley.teamListTeams) <$> getTeams uid gly + (team' : _) <- (^. Galley.teamListTeams) <$> getTeams uid gly () <- - Control.Exception.assert {- "Team ID in registration and team table do not match" -} (tid == team ^. Galley.teamId) $ + Control.Exception.assert {- "Team ID in registration and team table do not match" -} (tid == team' ^. Galley.teamId) $ pure () selfTeam <- userTeam . selfUser <$> getSelfProfile brg uid () <- @@ -728,22 +738,22 @@ zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" endpointToReq :: Endpoint -> (Bilge.Request -> Bilge.Request) -endpointToReq ep = Bilge.host (ep ^. epHost . to cs) . Bilge.port (ep ^. epPort) +endpointToReq ep = Bilge.host (ep ^. host . to cs) . Bilge.port (ep ^. port) endpointToSettings :: Endpoint -> Warp.Settings -endpointToSettings endpoint = +endpointToSettings ep = Warp.defaultSettings - { Warp.settingsHost = Imports.fromString . cs $ endpoint ^. epHost, - Warp.settingsPort = fromIntegral $ endpoint ^. epPort + { Warp.settingsHost = Imports.fromString . cs $ ep ^. host, + Warp.settingsPort = fromIntegral $ ep ^. port } endpointToURL :: MonadIO m => Endpoint -> Text -> m URI -endpointToURL endpoint urlpath = either err pure url +endpointToURL ep urlpath = either err pure url where url = parseURI' ("http://" <> urlhost <> ":" <> urlport) <&> (=/ urlpath) - urlhost = cs $ endpoint ^. epHost - urlport = cs . show $ endpoint ^. epPort - err = liftIO . throwIO . ErrorCall . show . (,(endpoint, url)) + urlhost = cs $ ep ^. host + urlport = cs . show $ ep ^. port + err = liftIO . throwIO . ErrorCall . show . (,(ep, url)) -- spar specifics @@ -821,10 +831,10 @@ registerTestIdPFrom :: SparReq -> m (UserId, TeamId, IdP) registerTestIdPFrom metadata mgr brig galley spar = do - apiVersion <- view teWireIdPAPIVersion + apiVer <- view teWireIdPAPIVersion liftIO . runHttpT mgr $ do (uid, tid) <- createUserWithTeam brig galley - (uid,tid,) <$> callIdpCreate apiVersion spar (Just uid) metadata + (uid,tid,) <$> callIdpCreate apiVer spar (Just uid) metadata getCookie :: KnownSymbol name => proxy name -> ResponseLBS -> Either String (SAML.SimpleSetCookie name) getCookie proxy rsp = do @@ -850,7 +860,7 @@ hasPersistentCookieHeader rsp = do tryLogin :: HasCallStack => SignPrivCreds -> IdP -> NameID -> TestSpar SAML.UserRef tryLogin privkey idp userSubject = do env <- ask - let tid = idp ^. idpExtraInfo . wiTeam + let tid = idp ^. idpExtraInfo . team spmeta <- getTestSPMetadata tid (_, authnreq) <- call $ callAuthnReq (env ^. teSpar) (idp ^. SAML.idpId) idpresp <- runSimpleSP $ mkAuthnResponseWithSubj userSubject privkey idp spmeta authnreq True @@ -865,7 +875,7 @@ tryLogin privkey idp userSubject = do tryLoginFail :: HasCallStack => SignPrivCreds -> IdP -> NameID -> String -> TestSpar () tryLoginFail privkey idp userSubject bodyShouldContain = do env <- ask - let tid = idp ^. idpExtraInfo . wiTeam + let tid = idp ^. idpExtraInfo . team spmeta <- getTestSPMetadata tid (_, authnreq) <- call $ callAuthnReq (env ^. teSpar) (idp ^. SAML.idpId) idpresp <- runSimpleSP $ mkAuthnResponseWithSubj userSubject privkey idp spmeta authnreq True @@ -946,7 +956,7 @@ loginCreatedSsoUser :: m (UserId, Cookie) loginCreatedSsoUser nameid idp privCreds = do env <- ask - let tid = idp ^. idpExtraInfo . wiTeam + let tid = idp ^. idpExtraInfo . team authnReq <- negotiateAuthnRequest idp spmeta <- getTestSPMetadata tid authnResp <- runSimpleSP $ mkAuthnResponseWithSubj nameid privCreds idp spmeta authnReq True diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index e070ff6b7e7..ef714ed42e3 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -62,7 +62,7 @@ import qualified Web.Scim.Schema.User.Phone as Phone import qualified Wire.API.Team.Member as Member import Wire.API.Team.Role (Role, defaultRole) import Wire.API.User -import Wire.API.User.IdentityProvider +import Wire.API.User.IdentityProvider hiding (team) import Wire.API.User.RichInfo import Wire.API.User.Scim diff --git a/services/spar/test-integration/Util/Types.hs b/services/spar/test-integration/Util/Types.hs index ecdf4db4aff..777470f2bb2 100644 --- a/services/spar/test-integration/Util/Types.hs +++ b/services/spar/test-integration/Util/Types.hs @@ -50,7 +50,6 @@ import Data.Aeson import qualified Data.Aeson as Aeson import Data.Aeson.TH import Imports -import SAML2.WebSSO.Types.TH (deriveJSONOptions) import Spar.API () import qualified Spar.App as Spar import Spar.Options @@ -93,14 +92,14 @@ data TestEnv = TestEnv type Select = TestEnv -> (Request -> Request) data IntegrationConfig = IntegrationConfig - { cfgBrig :: Endpoint, - cfgGalley :: Endpoint, - cfgSpar :: Endpoint, - cfgBrigSettingsTeamInvitationTimeout :: Int + { brig :: Endpoint, + galley :: Endpoint, + spar :: Endpoint, + brigSettingsTeamInvitationTimeout :: Int } deriving (Show, Generic) -deriveFromJSON deriveJSONOptions ''IntegrationConfig +deriveFromJSON Aeson.defaultOptions ''IntegrationConfig makeLenses ''TestEnv diff --git a/services/spar/test/Arbitrary.hs b/services/spar/test/Arbitrary.hs index 7480a1fcdc0..d8b2daf6839 100644 --- a/services/spar/test/Arbitrary.hs +++ b/services/spar/test/Arbitrary.hs @@ -1,5 +1,4 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeSynonymInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-redundant-constraints #-} @@ -39,9 +38,7 @@ import Wire.API.User.IdentityProvider import Wire.API.User.Saml instance Arbitrary IdPList where - arbitrary = do - _idplProviders <- arbitrary - pure $ IdPList {..} + arbitrary = IdPList <$> arbitrary instance Arbitrary WireIdP where arbitrary = WireIdP <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 4da3fe838dd..fda4370e032 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -78,7 +78,7 @@ start o = do Server.runSettingsWithShutdown s (servantApp e) Nothing where server :: Env -> Server.Server - server e = Server.defaultServer (unpack $ stern o ^. epHost) (stern o ^. epPort) (e ^. applog) (e ^. metrics) + server e = Server.defaultServer (unpack $ stern o ^. host) (stern o ^. port) (e ^. applog) (e ^. metrics) servantApp :: Env -> Application servantApp e = diff --git a/tools/stern/src/Stern/App.hs b/tools/stern/src/Stern/App.hs index a2fb31b6ba9..1e4a1f2bfde 100644 --- a/tools/stern/src/Stern/App.hs +++ b/tools/stern/src/Stern/App.hs @@ -74,7 +74,7 @@ newEnv o = do Env (mkRequest $ O.brig o) (mkRequest $ O.galley o) (mkRequest $ O.gundeck o) (mkRequest $ O.ibis o) (mkRequest $ O.galeb o) l mt def <$> newManager where - mkRequest s = Bilge.host (encodeUtf8 (s ^. epHost)) . Bilge.port (s ^. epPort) $ Bilge.empty + mkRequest s = Bilge.host (encodeUtf8 (s ^. host)) . Bilge.port (s ^. port) $ Bilge.empty newManager = Bilge.newManager (Bilge.defaultManagerSettings {Bilge.managerResponseTimeout = responseTimeoutMicro 10000000}) -- Monads diff --git a/tools/stern/src/Stern/Types.hs b/tools/stern/src/Stern/Types.hs index aa3e9af280a..b62994c943d 100644 --- a/tools/stern/src/Stern/Types.hs +++ b/tools/stern/src/Stern/Types.hs @@ -141,8 +141,8 @@ newtype MarketoResult = MarketoResult deriving (Eq, Show, ToJSON, FromJSON) data ConsentLogAndMarketo = ConsentLogAndMarketo - { clamConsentLog :: ConsentLog, - clamMarketo :: MarketoResult + { consentLog :: ConsentLog, + marketo :: MarketoResult } deriving (Eq, Show) diff --git a/tools/stern/test/integration/Main.hs b/tools/stern/test/integration/Main.hs index 08fb5b41e07..6c95115b870 100644 --- a/tools/stern/test/integration/Main.hs +++ b/tools/stern/test/integration/Main.hs @@ -36,7 +36,7 @@ import System.Logger qualified as Logger import Test.Tasty import Test.Tasty.Options import TestSetup -import Util.Options +import Util.Options (Endpoint (Endpoint)) import Util.Test data IntegrationConfig = IntegrationConfig From db05435c476df3d8370200d254616a8bb8225ec1 Mon Sep 17 00:00:00 2001 From: fisx Date: Tue, 22 Aug 2023 15:53:41 +0200 Subject: [PATCH 096/225] nit-picks (#3519) * Remove unneeded -Wwarn (re-enabeling -Werror in those modules). * Makefile: fix hspec_options overloading in .envrc.local. --- Makefile | 2 +- libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs | 2 +- services/cannon/src/Cannon/Run.hs | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 91a31e617e3..d013fde4041 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ EXE_SCHEMA := ./dist/$(package)-schema # Additionally, if stack is being used with nix, environment variables do not # make it into the shell where hspec is run, to tackle that this variable is # also exported in stack-deps.nix. -export HSPEC_OPTIONS = --fail-on-focused +export HSPEC_OPTIONS ?= --fail-on-focused default: install diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs index c6f14ce1352..8a4ab10fd72 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs @@ -1,4 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -15,7 +16,6 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# OPTIONS_GHC -Wwarn #-} module Test.Wire.API.Roundtrip.MLS (tests) where diff --git a/services/cannon/src/Cannon/Run.hs b/services/cannon/src/Cannon/Run.hs index 49b3d4edb69..df5f6e06a66 100644 --- a/services/cannon/src/Cannon/Run.hs +++ b/services/cannon/src/Cannon/Run.hs @@ -15,8 +15,6 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# OPTIONS -Wwarn #-} - module Cannon.Run ( run, CombinedAPI, From ed2bed64847ec2acfcc6fbd0adc6ac563327f35c Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 23 Aug 2023 14:42:28 +0200 Subject: [PATCH 097/225] integration: Fix testAddingUserNonFullyConnectedFederation and testNotificationsForOfflineBackends (#3529) * integration: Fix testAddingUserNonFullyConnectedFederation * integration: Don't allow adding users to conv when one of the pariticipating backends is down * integration: Add retries to get around problem of federation domain sync threads --- integration/test/API/BrigInternal.hs | 8 ++++++ integration/test/API/Galley.hs | 34 +++++++++++++--------- integration/test/Test/Conversation.hs | 39 ++++++++++++++++++------- integration/test/Test/Federation.hs | 41 +++++++++++++-------------- integration/test/Testlib/HTTP.hs | 2 +- 5 files changed, 78 insertions(+), 46 deletions(-) diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index fdd6b0587d5..ef09e08a560 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -110,6 +110,14 @@ deleteFedConn' owndom dom = do req <- rawBaseRequest owndom Brig Unversioned ("/i/federation/remotes/" <> dom) submit "DELETE" req +deleteAllFedConns :: (HasCallStack, MakesValue dom) => dom -> App () +deleteAllFedConns dom = do + readFedConns dom >>= \resp -> + resp.json %. "remotes" + & asList + >>= traverse (\v -> v %. "domain" & asString) + >>= mapM_ (deleteFedConn dom) + registerOAuthClient :: (HasCallStack, MakesValue user, MakesValue name, MakesValue url) => user -> name -> url -> App Response registerOAuthClient user name url = do req <- baseRequest user Brig Unversioned "i/oauth/clients" diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index d03ba1322c9..967786f86c0 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -171,22 +171,30 @@ postProteusMessage user conv msgs = do submit "POST" (addProtobuf bytes req) mkProteusRecipient :: (HasCallStack, MakesValue user, MakesValue client) => user -> client -> String -> App Proto.QualifiedUserEntry -mkProteusRecipient user client msg = do - userDomain <- objDomain user - userId <- LBS.toStrict . UUID.toByteString . fromJust . UUID.fromString <$> objId user - clientId <- (^?! hex) <$> objId client +mkProteusRecipient user client = mkProteusRecipients user [(user, [client])] + +mkProteusRecipients :: (HasCallStack, MakesValue domain, MakesValue user, MakesValue client) => domain -> [(user, [client])] -> String -> App Proto.QualifiedUserEntry +mkProteusRecipients dom userClients msg = do + userDomain <- asString =<< objDomain dom + userEntries <- mapM mkUserEntry userClients pure $ Proto.defMessage & #domain .~ fromString userDomain - & #entries - .~ [ Proto.defMessage - & #user . #uuid .~ userId - & #clients - .~ [ Proto.defMessage - & #client . #client .~ clientId - & #text .~ fromString msg - ] - ] + & #entries .~ userEntries + where + mkUserEntry (user, clients) = do + userId <- LBS.toStrict . UUID.toByteString . fromJust . UUID.fromString <$> objId user + clientEntries <- mapM mkClientEntry clients + pure $ + Proto.defMessage + & #user . #uuid .~ userId + & #clients .~ clientEntries + mkClientEntry client = do + clientId <- (^?! hex) <$> objId client + pure $ + Proto.defMessage + & #client . #client .~ clientId + & #text .~ fromString msg getGroupInfo :: (HasCallStack, MakesValue user, MakesValue conv) => diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 3324089b0c2..3d38b417307 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -1,4 +1,5 @@ {-# OPTIONS_GHC -Wno-ambiguous-fields #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} module Test.Conversation where @@ -410,19 +411,35 @@ testAddUnreachable = do testAddingUserNonFullyConnectedFederation :: HasCallStack => App () testAddingUserNonFullyConnectedFederation = do let overrides = - def {dbBrig = setField "optSettings.setFederationStrategy" "allowAll"} - <> fullSearchWithAll - startDynamicBackends [overrides] $ \domains -> do - own <- make OwnDomain & asString - other <- make OtherDomain & asString - [alice, alex, bob, charlie] <- - createAndConnectUsers $ [own, own, other] <> domains - - let newConv = defProteus {qualifiedUsers = [alex]} + def + { dbBrig = + setField "optSettings.setFederationStrategy" "allowDynamic" + >=> removeField "optSettings.setFederationDomainConfigs" + } + startDynamicBackends [overrides] $ \[dynBackend] -> do + own <- asString OwnDomain + other <- asString OtherDomain + + -- Ensure that dynamic backend only federates with own domain, but not other + -- domain. + -- + -- FUTUREWORK: deleteAllFedConns at the time of acquiring a backend, so + -- tests don't affect each other. + deleteAllFedConns dynBackend + void $ createFedConn dynBackend (FedConn own "full_search") + + alice <- randomUser own def + bob <- randomUser other def + charlie <- randomUser dynBackend def + -- We use retryT here so the dynamic federated connection changes can take + -- some time to be propagated. Remove after fixing https://wearezeta.atlassian.net/browse/WPB-3797 + mapM_ (retryT . connectUsers alice) [bob, charlie] + + let newConv = defProteus {qualifiedUsers = []} conv <- postConversation alice newConv >>= getJSON 201 bobId <- bob %. "qualified_id" charlieId <- charlie %. "qualified_id" - bindResponse (addMembers alex conv [bobId, charlieId]) $ \resp -> do + bindResponse (addMembers alice conv [bobId, charlieId]) $ \resp -> do resp.status `shouldMatchInt` 409 - resp.json %. "non_federating_backends" `shouldMatchSet` (other : domains) + resp.json %. "non_federating_backends" `shouldMatchSet` [other, dynBackend] diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs index 2636827be65..d7cf2c3a1e3 100644 --- a/integration/test/Test/Federation.hs +++ b/integration/test/Test/Federation.hs @@ -22,9 +22,10 @@ testNotificationsForOfflineBackends :: HasCallStack => App () testNotificationsForOfflineBackends = do resourcePool <- asks (.resourcePool) -- `delUser` will eventually get deleted. - [delUser, otherUser] <- createAndConnectUsers [OwnDomain, OtherDomain] + [delUser, otherUser, otherUser2] <- createAndConnectUsers [OwnDomain, OtherDomain, OtherDomain] delClient <- objId $ bindResponse (API.addClient delUser def) $ getJSON 201 otherClient <- objId $ bindResponse (API.addClient otherUser def) $ getJSON 201 + otherClient2 <- objId $ bindResponse (API.addClient otherUser2 def) $ getJSON 201 -- We call it 'downBackend' because it is down for the most of this test -- except for setup and assertions. Perhaps there is a better name. @@ -36,18 +37,18 @@ testNotificationsForOfflineBackends = do connectUsers delUser downUser1 connectUsers delUser downUser2 connectUsers otherUser downUser1 - upBackendConv <- bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, downUser1]})) $ getJSON 201 + upBackendConv <- bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, otherUser2, downUser1]})) $ getJSON 201 downBackendConv <- bindResponse (postConversation downUser1 (defProteus {qualifiedUsers = [otherUser, delUser]})) $ getJSON 201 pure (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) -- Even when a participating backend is down, messages to conversations -- owned by other backends should go. - successfulMsgForOtherUser <- mkProteusRecipient otherUser otherClient "success message for other user" + successfulMsgForOtherUsers <- mkProteusRecipients otherUser [(otherUser, [otherClient]), (otherUser2, [otherClient2])] "success message for other user" successfulMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "success message for down user" let successfulMsg = Proto.defMessage @Proto.QualifiedNewOtrMessage & #sender . Proto.client .~ (delClient ^?! hex) - & #recipients .~ [successfulMsgForOtherUser, successfulMsgForDownUser] + & #recipients .~ [successfulMsgForOtherUsers, successfulMsgForDownUser] & #reportAll .~ Proto.defMessage bindResponse (postProteusMessage delUser upBackendConv successfulMsg) assertSuccess @@ -68,12 +69,13 @@ testNotificationsForOfflineBackends = do bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, downUser1]})) $ \resp -> resp.status `shouldMatchInt` 533 - -- Adding users to an up backend conversation should work even when one of - -- the participating backends is down - otherUser2 <- randomUser OtherDomain def - connectUsers delUser otherUser2 - bindResponse (addMembers delUser upBackendConv [otherUser2]) $ \resp -> - resp.status `shouldMatchInt` 200 + -- Adding users to an up backend conversation should not work when one of + -- the participating backends is down. This is due to not being able to + -- check non-fully connected graph between all participating backends + otherUser3 <- randomUser OtherDomain def + connectUsers delUser otherUser3 + bindResponse (addMembers delUser upBackendConv [otherUser3]) $ \resp -> + resp.status `shouldMatchInt` 533 -- Adding users from down backend to a conversation should also fail bindResponse (addMembers delUser upBackendConv [downUser2]) $ \resp -> @@ -86,14 +88,17 @@ testNotificationsForOfflineBackends = do -- User deletions should eventually make it to the other backend. deleteUser delUser + + let isOtherUser2LeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser otherUser2] + isDelUserLeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser delUser] + do newMsgNotif <- awaitNotification otherUser otherClient noValue 1 isNewMessageNotif newMsgNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv newMsgNotif %. "payload.0.data.text" `shouldMatchBase64` "success message for other user" - memberJoinNotif <- awaitNotification otherUser otherClient (Just newMsgNotif) 1 isMemberJoinNotif - memberJoinNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv - asListOf objQidObject (memberJoinNotif %. "payload.0.data.users") `shouldMatch` mapM objQidObject [otherUser2] + void $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isOtherUser2LeaveUpConvNotif + void $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDelUserLeaveUpConvNotif delUserDeletedNotif <- nPayload $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDeleteUserNotif objQid delUserDeletedNotif `shouldMatch` objQid delUser @@ -103,11 +108,6 @@ testNotificationsForOfflineBackends = do newMsgNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv newMsgNotif %. "payload.0.data.text" `shouldMatchBase64` "success message for down user" - -- FUTUREWORK: Uncomment after fixing this bug: https://wearezeta.atlassian.net/browse/WPB-3664 - -- memberJoinNotif <- awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isMemberJoinNotif - -- memberJoinNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv - -- asListOf objQidObject (memberJoinNotif %. "payload.0.data.users") `shouldMatch` mapM objQidObject [downUser2] - let isDelUserLeaveDownConvNotif = allPreds [ isConvLeaveNotif, @@ -115,11 +115,10 @@ testNotificationsForOfflineBackends = do isNotifForUser delUser ] void $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isDelUserLeaveDownConvNotif - void $ awaitNotification otherUser otherClient noValue 1 isDelUserLeaveDownConvNotif -- FUTUREWORK: Uncomment after fixing this bug: https://wearezeta.atlassian.net/browse/WPB-3664 - -- void $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 (allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser otherUser]) - -- void $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 (allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser delUser]) + -- void $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isOtherUser2LeaveUpConvNotif + -- void $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDelUserLeaveDownConvNotif delUserDeletedNotif <- nPayload $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isDeleteUserNotif objQid delUserDeletedNotif `shouldMatch` objQid delUser diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index 4168c709e6a..5dde2f9a187 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -100,7 +100,7 @@ getJSON status resp = withResponse resp $ \r -> do r.status `shouldMatch` status r.json -assertSuccess :: Response -> App () +assertSuccess :: HasCallStack => Response -> App () assertSuccess resp = withResponse resp $ \r -> r.status `shouldMatchRange` (200, 299) onFailureAddResponse :: HasCallStack => Response -> App a -> App a From 1892827b9fd4e36a3eadf79f79a9ad6721aa3051 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 23 Aug 2023 15:56:07 +0200 Subject: [PATCH 098/225] Introduce API v5 (#3527) * Introduce development version 5 * Specialise API to a specific version * Use versioned swagger for galley * Use version swagger for all other services * Collect all service Swaggers into a typeclass * Fix swagger integration tests * Revert any changes to API versions before 5 * Remove promotion of isDevelopmentVersion * Add CHANGELOG entry --- changelog.d/1-api-changes/introduce-v5 | 1 + .../src/developer/developer/api-versioning.md | 3 +- integration/test/Test/Brig.hs | 2 +- libs/wire-api/src/Wire/API/Error.hs | 9 ++ .../src/Wire/API/MakesFederatedCall.hs | 5 + libs/wire-api/src/Wire/API/Routes/API.hs | 17 ++- libs/wire-api/src/Wire/API/Routes/Bearer.hs | 5 + libs/wire-api/src/Wire/API/Routes/Cookies.hs | 5 + .../src/Wire/API/Routes/LowLevelStream.hs | 5 + libs/wire-api/src/Wire/API/Routes/Public.hs | 49 ++++++--- .../src/Wire/API/Routes/Public/Brig.hs | 8 +- .../src/Wire/API/Routes/Public/Brig/OAuth.hs | 9 +- .../src/Wire/API/Routes/Public/Cannon.hs | 11 +- .../src/Wire/API/Routes/Public/Cargohold.hs | 16 +-- .../src/Wire/API/Routes/Public/Galley.hs | 12 +- .../src/Wire/API/Routes/Public/Gundeck.hs | 10 +- .../src/Wire/API/Routes/Public/Proxy.hs | 10 +- .../src/Wire/API/Routes/Public/Spar.hs | 14 ++- .../src/Wire/API/Routes/QualifiedCapture.hs | 5 + libs/wire-api/src/Wire/API/Routes/Version.hs | 103 ++++++++++++++++-- .../wire-api/src/Wire/API/Routes/Versioned.hs | 4 + .../wire-api/src/Wire/API/Routes/WebSocket.hs | 3 + libs/wire-api/src/Wire/API/VersionInfo.hs | 7 -- services/brig/src/Brig/API/Public.hs | 53 ++++++--- services/brig/test/integration/API/Swagger.hs | 2 +- services/cannon/src/Cannon/API/Public.hs | 2 +- services/cannon/src/Cannon/Run.hs | 4 +- .../cargohold/src/CargoHold/API/Public.hs | 2 +- services/cargohold/src/CargoHold/Run.hs | 4 +- .../galley/src/Galley/API/Public/Servant.hs | 2 +- services/galley/src/Galley/Run.hs | 4 +- services/spar/src/Spar/API.hs | 6 +- services/spar/src/Spar/Run.hs | 4 +- services/spar/test/Test/Spar/APISpec.hs | 4 +- tools/fedcalls/src/Main.hs | 33 +++--- 35 files changed, 305 insertions(+), 128 deletions(-) create mode 100644 changelog.d/1-api-changes/introduce-v5 diff --git a/changelog.d/1-api-changes/introduce-v5 b/changelog.d/1-api-changes/introduce-v5 new file mode 100644 index 00000000000..0d2498b3d31 --- /dev/null +++ b/changelog.d/1-api-changes/introduce-v5 @@ -0,0 +1 @@ +Introduce v5 development version diff --git a/docs/src/developer/developer/api-versioning.md b/docs/src/developer/developer/api-versioning.md index 053c8307496..3cf36134547 100644 --- a/docs/src/developer/developer/api-versioning.md +++ b/docs/src/developer/developer/api-versioning.md @@ -110,8 +110,7 @@ are several steps to make apart from deciding what endpoint changes are part of the version: - In `wire-api` extend the `Version` type with a new version by appending the - new version to the end, e.g., by adding `V4`. Make sure to update its - `ToSchema` instance, + new version to the end, e.g., by adding `V4`. - In the same `Version` module update the `developmentVersions` value to list only the new version, - Consider updating the `backendApiVersion` value in Stern, which is diff --git a/integration/test/Test/Brig.hs b/integration/test/Test/Brig.hs index 98e046fa6da..3380d8400ca 100644 --- a/integration/test/Test/Brig.hs +++ b/integration/test/Test/Brig.hs @@ -139,7 +139,7 @@ testCrudOAuthClient = do testSwagger :: HasCallStack => App () testSwagger = do let existingVersions :: [Int] - existingVersions = [0, 1, 2, 3, 4] + existingVersions = [0, 1, 2, 3, 4, 5] internalApis :: [String] internalApis = ["brig", "cannon", "cargohold", "cannon", "spar"] diff --git a/libs/wire-api/src/Wire/API/Error.hs b/libs/wire-api/src/Wire/API/Error.hs index 1edf2eda329..304f11596ec 100644 --- a/libs/wire-api/src/Wire/API/Error.hs +++ b/libs/wire-api/src/Wire/API/Error.hs @@ -68,6 +68,7 @@ import Servant import Servant.Swagger import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named) +import Wire.API.Routes.Version -- | Runtime representation of a statically-known error. data DynError = DynError @@ -167,6 +168,10 @@ instance RoutesToPaths api => RoutesToPaths (CanThrow err :> api) where instance RoutesToPaths api => RoutesToPaths (CanThrowMany errs :> api) where getRoutes = getRoutes @api +type instance + SpecialiseToVersion v (CanThrow e :> api) = + CanThrow e :> SpecialiseToVersion v api + instance (HasServer api ctx) => HasServer (CanThrow e :> api) ctx where type ServerT (CanThrow e :> api) m = ServerT api m @@ -185,6 +190,10 @@ instance where toSwagger _ = addToSwagger @e (toSwagger (Proxy @api)) +type instance + SpecialiseToVersion v (CanThrowMany es :> api) = + CanThrowMany es :> SpecialiseToVersion v api + instance HasSwagger api => HasSwagger (CanThrowMany '() :> api) where toSwagger _ = toSwagger (Proxy @api) diff --git a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs index f1f571106aa..fbc133d6728 100644 --- a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs +++ b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs @@ -48,6 +48,7 @@ import Servant.Swagger import Test.QuickCheck (Arbitrary) import TransitiveAnns.Types import Unsafe.Coerce (unsafeCoerce) +import Wire.API.Routes.Version import Wire.Arbitrary (GenericUniform (..)) -- | This function exists only to provide a convenient place for the @@ -151,6 +152,10 @@ type family ShowComponent (x :: Component) = (res :: Symbol) | res -> x where ShowComponent 'Galley = "galley" ShowComponent 'Cargohold = "cargohold" +type instance + SpecialiseToVersion v (MakesFederatedCall comp name :> api) = + MakesFederatedCall comp name :> SpecialiseToVersion v api + -- | 'MakesFederatedCall' annotates the swagger documentation with an extension -- tag @x-wire-makes-federated-calls-to@. instance (HasSwagger api, KnownSymbol name, KnownSymbol (ShowComponent comp)) => HasSwagger (MakesFederatedCall comp name :> api :: Type) where diff --git a/libs/wire-api/src/Wire/API/Routes/API.hs b/libs/wire-api/src/Wire/API/Routes/API.hs index 607933e2ed4..b43569bf76f 100644 --- a/libs/wire-api/src/Wire/API/Routes/API.hs +++ b/libs/wire-api/src/Wire/API/Routes/API.hs @@ -16,7 +16,8 @@ -- with this program. If not, see . module Wire.API.Routes.API - ( API, + ( ServiceAPI (..), + API, hoistAPIHandler, hoistAPI, mkAPI, @@ -29,14 +30,28 @@ module Wire.API.Routes.API where import Data.Domain +import Data.Kind import Data.Proxy +import Data.Swagger qualified as S import Imports import Polysemy import Polysemy.Error import Polysemy.Internal import Servant hiding (Union) +import Servant.Swagger import Wire.API.Error import Wire.API.Routes.Named +import Wire.API.Routes.Version + +class ServiceAPI service (v :: Version) where + type ServiceAPIRoutes service + type SpecialisedAPIRoutes v service :: Type + type SpecialisedAPIRoutes v service = SpecialiseToVersion v (ServiceAPIRoutes service) + serviceSwagger :: HasSwagger (SpecialisedAPIRoutes v service) => S.Swagger + serviceSwagger = toSwagger (Proxy @(SpecialisedAPIRoutes v service)) + +instance ServiceAPI VersionAPITag v where + type ServiceAPIRoutes VersionAPITag = VersionAPI -- | A Servant handler on a polysemy stack. This is used to help with type inference. newtype API api r = API {unAPI :: ServerT api (Sem r)} diff --git a/libs/wire-api/src/Wire/API/Routes/Bearer.hs b/libs/wire-api/src/Wire/API/Routes/Bearer.hs index b2b0c1918eb..545db5254df 100644 --- a/libs/wire-api/src/Wire/API/Routes/Bearer.hs +++ b/libs/wire-api/src/Wire/API/Routes/Bearer.hs @@ -26,6 +26,7 @@ import Data.Text.Encoding qualified as T import Imports import Servant import Servant.Swagger +import Wire.API.Routes.Version newtype Bearer a = Bearer {unBearer :: a} @@ -42,6 +43,10 @@ type BearerQueryParam = [Lenient, Description "Access token"] "access_token" +type instance + SpecialiseToVersion v (Bearer a :> api) = + Bearer a :> SpecialiseToVersion v api + instance HasSwagger api => HasSwagger (Bearer a :> api) where toSwagger _ = toSwagger (Proxy @api) diff --git a/libs/wire-api/src/Wire/API/Routes/Cookies.hs b/libs/wire-api/src/Wire/API/Routes/Cookies.hs index 644435d205d..10383904ccb 100644 --- a/libs/wire-api/src/Wire/API/Routes/Cookies.hs +++ b/libs/wire-api/src/Wire/API/Routes/Cookies.hs @@ -29,6 +29,7 @@ import Imports import Servant import Servant.Swagger import Web.Cookie (parseCookies) +import Wire.API.Routes.Version data (:::) a b @@ -58,6 +59,10 @@ newtype CookieTuple cs = CookieTuple {unCookieTuple :: NP I (CookieTypes cs)} type CookieMap = Map ByteString (NonEmpty ByteString) +type instance + SpecialiseToVersion v (Cookies cs :> api) = + Cookies cs :> SpecialiseToVersion v api + instance HasSwagger api => HasSwagger (Cookies cs :> api) where toSwagger _ = toSwagger (Proxy @api) diff --git a/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs b/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs index 209afbf64a9..d9287bd5fa9 100644 --- a/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs +++ b/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs @@ -37,6 +37,7 @@ import Servant.Server hiding (respond) import Servant.Server.Internal import Servant.Swagger as S import Servant.Swagger.Internal as S +import Wire.API.Routes.Version -- FUTUREWORK: make it possible to generate headers at runtime data LowLevelStream method status (headers :: [(Symbol, Symbol)]) desc ctype @@ -84,6 +85,10 @@ instance status = statusFromNat (Proxy :: Proxy status) extraHeaders = renderHeaders @headers +type instance + SpecialiseToVersion v (LowLevelStream m s h d t) = + LowLevelStream m s h d t + instance (Accept ctype, KnownNat status, KnownSymbol desc, SwaggerMethod method) => HasSwagger (LowLevelStream method status headers desc ctype) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index befd0855009..8ad302b6dd8 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -55,6 +55,7 @@ import Servant.Server.Internal.DelayedIO import Servant.Server.Internal.Router (Router) import Servant.Swagger (HasSwagger (toSwagger)) import Wire.API.OAuth qualified as OAuth +import Wire.API.Routes.Version mapRequestArgument :: forall mods a b. @@ -196,20 +197,29 @@ type ZOptHostHeader = instance HasSwagger api => HasSwagger (ZHostOpt :> api) where toSwagger _ = toSwagger (Proxy @api) +type instance SpecialiseToVersion v (ZHostOpt :> api) = ZHostOpt :> SpecialiseToVersion v api + +addZAuthSwagger :: Swagger -> Swagger +addZAuthSwagger s = + s + & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) + & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "ZAuth" []] + where + secScheme = + SecurityScheme + { _securitySchemeType = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader), + _securitySchemeDescription = Just "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'." + } + +type instance + SpecialiseToVersion v (ZAuthServant t opts :> api) = + ZAuthServant t opts :> SpecialiseToVersion v api + instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts :> api) where - toSwagger _ = - toSwagger (Proxy @api) - & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) - & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "ZAuth" []] - where - secScheme = - SecurityScheme - { _securitySchemeType = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader), - _securitySchemeDescription = Just "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'." - } + toSwagger _ = addZAuthSwagger (toSwagger (Proxy @api)) instance HasSwagger api => HasSwagger (ZAuthServant 'ZLocalAuthUser opts :> api) where - toSwagger _ = toSwagger (Proxy @(ZAuthServant 'ZAuthUser opts :> api)) + toSwagger _ = addZAuthSwagger (toSwagger (Proxy @api)) instance HasLink endpoint => HasLink (ZAuthServant usr opts :> endpoint) where type MkLink (ZAuthServant _ _ :> endpoint) a = MkLink endpoint a @@ -286,11 +296,18 @@ instance ToSchema a => ToSchema (Headers ls a) where data DescriptionOAuthScope (scope :: OAuth.OAuthScope) -instance (HasSwagger api, OAuth.IsOAuthScope scope) => HasSwagger (DescriptionOAuthScope scope :> api) where - toSwagger _ = toSwagger (Proxy @api) & addScopeDescription - where - addScopeDescription :: Swagger -> Swagger - addScopeDescription = allOperations . description %~ Just . (<> "\nOAuth scope: `" <> cs (toByteString (OAuth.toOAuthScope @scope)) <> "`") . fold +type instance + SpecialiseToVersion v (DescriptionOAuthScope scope :> api) = + DescriptionOAuthScope scope :> SpecialiseToVersion v api + +instance + (HasSwagger api, OAuth.IsOAuthScope scope) => + HasSwagger (DescriptionOAuthScope scope :> api) + where + toSwagger _ = addScopeDescription @scope (toSwagger (Proxy @api)) + +addScopeDescription :: forall scope. OAuth.IsOAuthScope scope => Swagger -> Swagger +addScopeDescription = allOperations . description %~ Just . (<> "\nOAuth scope: `" <> cs (toByteString (OAuth.toOAuthScope @scope)) <> "`") . fold instance (HasServer api ctx) => HasServer (DescriptionOAuthScope scope :> api) ctx where type ServerT (DescriptionOAuthScope scope :> api) m = ServerT api m diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index ceec7e951c0..b99aa0e7298 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -39,7 +39,6 @@ import Imports hiding (head) import Network.Wai.Utilities import Servant (JSON) import Servant hiding (Handler, JSON, addHeader, respond) -import Servant.Swagger (HasSwagger (toSwagger)) import Servant.Swagger.Internal.Orphans () import Wire.API.Call.Config (RTCConfiguration) import Wire.API.Connection hiding (MissingLegalholdConsent) @@ -51,6 +50,7 @@ import Wire.API.MLS.Servant import Wire.API.MakesFederatedCall import Wire.API.OAuth import Wire.API.Properties +import Wire.API.Routes.API import Wire.API.Routes.Bearer import Wire.API.Routes.Cookies import Wire.API.Routes.MultiVerb @@ -93,8 +93,10 @@ type BrigAPI = :<|> SystemSettingsAPI :<|> OAuthAPI -brigSwagger :: Swagger -brigSwagger = toSwagger (Proxy @BrigAPI) +data BrigAPITag + +instance ServiceAPI BrigAPITag v where + type ServiceAPIRoutes BrigAPITag = BrigAPI ------------------------------------------------------------------------------- -- User API diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs index 2a37bd71c66..0a4adf52401 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs @@ -19,14 +19,13 @@ module Wire.API.Routes.Public.Brig.OAuth where import Data.Id as Id import Data.SOP -import Data.Swagger (Swagger) import Imports hiding (exp, head) import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger import Servant.Swagger.Internal.Orphans () import Wire.API.Error import Wire.API.OAuth +import Wire.API.Routes.API import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named (..)) import Wire.API.Routes.Public @@ -156,5 +155,7 @@ instance AsUnion CreateOAuthAuthorizationCodeResponses CreateOAuthCodeResponse w fromUnion (S (S (S (S (Z (I _)))))) = CreateOAuthCodeRedirectUrlMissMatch fromUnion (S (S (S (S (S x))))) = case x of {} -swaggerDoc :: Swagger -swaggerDoc = toSwagger (Proxy @OAuthAPI) +data OAuthAPITag + +instance ServiceAPI OAuthAPITag v where + type ServiceAPIRoutes OAuthAPITag = OAuthAPI diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Cannon.hs b/libs/wire-api/src/Wire/API/Routes/Public/Cannon.hs index ceacf45518a..eda1f01a8e3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Cannon.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Cannon.hs @@ -18,14 +18,13 @@ module Wire.API.Routes.Public.Cannon where import Data.Id -import Data.Swagger import Servant -import Servant.Swagger +import Wire.API.Routes.API import Wire.API.Routes.Named import Wire.API.Routes.Public (ZConn, ZUser) import Wire.API.Routes.WebSocket -type PublicAPI = +type CannonAPI = Named "await-notifications" ( Summary "Establish websocket connection" @@ -43,5 +42,7 @@ type PublicAPI = :> WebSocketPending ) -swaggerDoc :: Swagger -swaggerDoc = toSwagger (Proxy @PublicAPI) +data CannonAPITag + +instance ServiceAPI CannonAPITag v where + type ServiceAPIRoutes CannonAPITag = CannonAPI diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs index 2260d3953e5..1ce9dd600cc 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs @@ -22,16 +22,15 @@ import Data.Kind import Data.Metrics.Servant import Data.Qualified import Data.SOP -import Data.Swagger qualified as Swagger import Imports import Servant -import Servant.Swagger.Internal import Servant.Swagger.Internal.Orphans () import URI.ByteString import Wire.API.Asset import Wire.API.Error import Wire.API.Error.Cargohold import Wire.API.MakesFederatedCall +import Wire.API.Routes.API import Wire.API.Routes.AssetBody import Wire.API.Routes.MultiVerb import Wire.API.Routes.Public @@ -56,8 +55,9 @@ type instance ApplyPrincipalPath 'BotPrincipalTag api = ZBot :> "bot" :> "assets type instance ApplyPrincipalPath 'ProviderPrincipalTag api = ZProvider :> "provider" :> "assets" :> api -instance HasSwagger (ApplyPrincipalPath tag api) => HasSwagger (tag :> api) where - toSwagger _ = toSwagger (Proxy @(ApplyPrincipalPath tag api)) +type instance + SpecialiseToVersion v ((tag :: PrincipalTag) :> api) = + SpecialiseToVersion v (ApplyPrincipalPath tag api) instance HasServer (ApplyPrincipalPath tag api) ctx => HasServer (tag :> api) ctx where type ServerT (tag :> api) m = ServerT (ApplyPrincipalPath tag api) m @@ -90,7 +90,7 @@ type GetAsset = '[ErrorResponse 'AssetNotFound, AssetRedirect] (Maybe (AssetLocation Absolute)) -type ServantAPI = +type CargoholdAPI = ( Summary "Renew an asset token" :> Until 'V2 :> CanThrow 'AssetNotFound @@ -315,5 +315,7 @@ type MainAPI = () ) -swaggerDoc :: Swagger.Swagger -swaggerDoc = toSwagger (Proxy @ServantAPI) +data CargoholdAPITag + +instance ServiceAPI CargoholdAPITag v where + type ServiceAPIRoutes CargoholdAPITag = CargoholdAPI 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 ff1b80fe0b3..d24c473738e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -20,11 +20,9 @@ module Wire.API.Routes.Public.Galley where -import Data.SOP -import Data.Swagger qualified as Swagger import Servant hiding (WithStatus) -import Servant.Swagger.Internal import Servant.Swagger.Internal.Orphans () +import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Bot import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Galley.CustomBackend @@ -37,7 +35,7 @@ import Wire.API.Routes.Public.Galley.TeamConversation import Wire.API.Routes.Public.Galley.TeamMember import Wire.API.Routes.Public.Galley.TeamNotification (TeamNotificationAPI) -type ServantAPI = +type GalleyAPI = ConversationAPI :<|> TeamConversationAPI :<|> MessagingAPI @@ -50,5 +48,7 @@ type ServantAPI = :<|> TeamMemberAPI :<|> TeamNotificationAPI -swaggerDoc :: Swagger.Swagger -swaggerDoc = toSwagger (Proxy @ServantAPI) +data GalleyAPITag + +instance ServiceAPI GalleyAPITag v where + type ServiceAPIRoutes GalleyAPITag = GalleyAPI diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Gundeck.hs b/libs/wire-api/src/Wire/API/Routes/Public/Gundeck.hs index 7fad8afba98..b2d0d329dae 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Gundeck.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Gundeck.hs @@ -19,15 +19,13 @@ module Wire.API.Routes.Public.Gundeck where import Data.Id (ClientId) import Data.Range -import Data.SOP -import Data.Swagger qualified as Swagger import Imports import Servant -import Servant.Swagger import Wire.API.Error import Wire.API.Error.Gundeck as E import Wire.API.Notification import Wire.API.Push.V2.Token +import Wire.API.Routes.API import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -132,5 +130,7 @@ type NotificationAPI = (Maybe QueuedNotificationList) ) -swaggerDoc :: Swagger.Swagger -swaggerDoc = toSwagger (Proxy @GundeckAPI) +data GundeckAPITag + +instance ServiceAPI GundeckAPITag v where + type ServiceAPIRoutes GundeckAPITag = GundeckAPI diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Proxy.hs b/libs/wire-api/src/Wire/API/Routes/Public/Proxy.hs index 4f507e1635c..4fa0e100c83 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Proxy.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Proxy.hs @@ -17,11 +17,9 @@ module Wire.API.Routes.Public.Proxy where -import Data.SOP -import Data.Swagger qualified as Swagger import Servant import Servant.API.Extended.RawM (RawM) -import Servant.Swagger +import Wire.API.Routes.API import Wire.API.Routes.Named type ProxyAPI = @@ -50,6 +48,8 @@ type family ProxyAPISummary name where ProxyAPISummary "gmaps-path" = "[DEPRECATED] proxy: `get /proxy/googlemaps/maps/api/geocode/:path`; see google maps API docs" +data ProxyAPITag + -- | FUTUREWORK(fisx): (1) the verb could be added to the swagger docs in the appropriate -- place here; it's always defined in the `Summary`, but the `RawM` doesn't allow to constrain -- it. (2) there should be a way to make this more type-safe: `assertMethod` in @@ -58,5 +58,5 @@ type family ProxyAPISummary name where -- "api" :> "token" :> OnlyMethod "POST" :> RawM`, and then the `ServerT` instance for -- `OnlyMethod` requires a proxy argument in the handler of the same type. Or something. (am -- i massifly over-engineering things here?) -swaggerDoc :: Swagger.Swagger -swaggerDoc = toSwagger (Proxy @ProxyAPI) +instance ServiceAPI ProxyAPITag v where + type ServiceAPIRoutes ProxyAPITag = ProxyAPI diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs index adaf1c3b729..59b98ddc9eb 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -20,19 +20,19 @@ module Wire.API.Routes.Public.Spar where import Data.Id import Data.Proxy import Data.Range -import Data.Swagger (Swagger) import Imports import SAML2.WebSSO qualified as SAML import Servant import Servant.API.Extended import Servant.Multipart -import Servant.Swagger (toSwagger) +import Servant.Swagger import URI.ByteString qualified as URI import Web.Scim.Capabilities.MetaSchema as Scim.Meta import Web.Scim.Class.Auth as Scim.Auth import Web.Scim.Class.User as Scim.User import Wire.API.Error import Wire.API.Error.Brig +import Wire.API.Routes.API import Wire.API.Routes.Internal.Spar import Wire.API.Routes.Public import Wire.API.SwaggerServant @@ -45,7 +45,7 @@ import Wire.API.User.Scim -- FUTUREWORK: use https://hackage.haskell.org/package/servant-0.14.1/docs/Servant-API-Generic.html? -type API = +type SparAPI = "sso" :> APISSO :<|> "identity-providers" :> APIIDP :<|> "scim" :> APIScim @@ -186,5 +186,9 @@ type APIScimTokenDelete = type APIScimTokenList = Get '[JSON] ScimTokenList -swaggerDoc :: Swagger -swaggerDoc = toSwagger (Proxy @API) +data SparAPITag + +instance ServiceAPI SparAPITag v where + type ServiceAPIRoutes SparAPITag = SparAPI + type SpecialisedAPIRoutes v SparAPITag = SparAPI + serviceSwagger = toSwagger (Proxy @SparAPI) diff --git a/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs b/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs index 4fd030267d0..f54cccf4f36 100644 --- a/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs +++ b/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs @@ -34,6 +34,7 @@ import Servant.API.Modifiers import Servant.Client.Core.HasClient import Servant.Server.Internal.ErrorFormatter import Servant.Swagger +import Wire.API.Routes.Version -- | Capture a value qualified by a domain, with modifiers. data QualifiedCapture' (mods :: [Type]) (capture :: Symbol) (a :: Type) @@ -50,6 +51,10 @@ type WithDomain mods capture a api = :> Capture' mods capture a :> api +type instance + SpecialiseToVersion v (QualifiedCapture' mods capture a :> api) = + QualifiedCapture' mods capture a :> SpecialiseToVersion v api + instance ( Typeable a, ToParamSchema a, diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index 95fba403022..826da8873a5 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -22,8 +22,8 @@ module Wire.API.Routes.Version ( -- * API version endpoint VersionAPI, + VersionAPITag, VersionInfo (..), - versionSwagger, versionHeader, VersionHeader, @@ -31,11 +31,15 @@ module Wire.API.Routes.Version Version (..), VersionNumber (..), supportedVersions, + isDevelopmentVersion, developmentVersions, -- * Servant combinators Until, From, + + -- * Swagger instances + SpecialiseToVersion, ) where @@ -49,13 +53,15 @@ import Data.ByteString.Conversion (ToByteString (builder), toByteString') import Data.ByteString.Lazy qualified as LBS import Data.Domain import Data.Schema -import Data.Singletons.TH +import Data.Singletons.Base.TH import Data.Swagger qualified as S -import Data.Text as Text +import Data.Text qualified as Text import Data.Text.Encoding as Text +import GHC.TypeLits import Imports import Servant -import Servant.Swagger +import Servant.API.Extended.RawM +import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.VersionInfo import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform)) @@ -68,7 +74,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform)) -- and 'developmentVersions' stay in sync; everything else here should keep working without -- change. See also documentation in the *docs* directory. -- https://docs.wire.com/developer/developer/api-versioning.html#version-bump-checklist -data Version = V0 | V1 | V2 | V3 | V4 +data Version = V0 | V1 | V2 | V3 | V4 | V5 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (FromJSON, ToJSON) via (Schema Version) deriving (Arbitrary) via (GenericUniform Version) @@ -85,12 +91,10 @@ versionInt V1 = 1 versionInt V2 = 2 versionInt V3 = 3 versionInt V4 = 4 +versionInt V5 = 5 supportedVersions :: [Version] -supportedVersions = [minBound .. V4] - -developmentVersions :: [Version] -developmentVersions = [V4] +supportedVersions = [minBound .. maxBound] ---------------------------------------------------------------------- @@ -179,7 +183,84 @@ type VersionAPI = :> Get '[JSON] VersionInfo ) -versionSwagger :: S.Swagger -versionSwagger = toSwagger (Proxy @VersionAPI) +data VersionAPITag + +-- Development versions $(genSingletons [''Version]) + +isDevelopmentVersion :: Version -> Bool +isDevelopmentVersion V0 = False +isDevelopmentVersion V1 = False +isDevelopmentVersion V2 = False +isDevelopmentVersion V3 = False +isDevelopmentVersion _ = True + +developmentVersions :: [Version] +developmentVersions = filter isDevelopmentVersion supportedVersions + +-- Version-aware swagger generation + +$(promoteOrdInstances [''Version]) + +type family SpecialiseToVersion (v :: Version) api + +type instance + SpecialiseToVersion v (From w :> api) = + If (v < w) EmptyAPI (SpecialiseToVersion v api) + +type instance + SpecialiseToVersion v (Until w :> api) = + If (v < w) (SpecialiseToVersion v api) EmptyAPI + +type instance + SpecialiseToVersion v ((s :: Symbol) :> api) = + s :> SpecialiseToVersion v api + +type instance + SpecialiseToVersion v (Named n api) = + Named n (SpecialiseToVersion v api) + +type instance + SpecialiseToVersion v (Capture' mod sym a :> api) = + Capture' mod sym a :> SpecialiseToVersion v api + +type instance + SpecialiseToVersion v (Summary s :> api) = + Summary s :> SpecialiseToVersion v api + +type instance + SpecialiseToVersion v (Verb m s t r) = + Verb m s t r + +type instance + SpecialiseToVersion v (MultiVerb m t r x) = + MultiVerb m t r x + +type instance SpecialiseToVersion v RawM = RawM + +type instance + SpecialiseToVersion v (ReqBody t x :> api) = + ReqBody t x :> SpecialiseToVersion v api + +type instance + SpecialiseToVersion v (QueryParam' mods l x :> api) = + QueryParam' mods l x :> SpecialiseToVersion v api + +type instance + SpecialiseToVersion v (Header' opts l x :> api) = + Header' opts l x :> SpecialiseToVersion v api + +type instance + SpecialiseToVersion v (Description desc :> api) = + Description desc :> SpecialiseToVersion v api + +type instance + SpecialiseToVersion v (StreamBody' opts f t x :> api) = + StreamBody' opts f t x :> SpecialiseToVersion v api + +type instance SpecialiseToVersion v EmptyAPI = EmptyAPI + +type instance + SpecialiseToVersion v (api1 :<|> api2) = + SpecialiseToVersion v api1 :<|> SpecialiseToVersion v api2 diff --git a/libs/wire-api/src/Wire/API/Routes/Versioned.hs b/libs/wire-api/src/Wire/API/Routes/Versioned.hs index be309b77fd9..1ca7bac0587 100644 --- a/libs/wire-api/src/Wire/API/Routes/Versioned.hs +++ b/libs/wire-api/src/Wire/API/Routes/Versioned.hs @@ -57,6 +57,10 @@ instance route _p ctx d = route (Proxy :: Proxy (ReqBody cts (Versioned v a) :> api)) ctx (fmap (. unVersioned) d) +type instance + SpecialiseToVersion w (VersionedReqBody v cts a :> api) = + VersionedReqBody v cts a :> SpecialiseToVersion w api + instance ( S.ToSchema (Versioned v a), HasSwagger api, diff --git a/libs/wire-api/src/Wire/API/Routes/WebSocket.hs b/libs/wire-api/src/Wire/API/Routes/WebSocket.hs index 756605366ad..72354d95bc1 100644 --- a/libs/wire-api/src/Wire/API/Routes/WebSocket.hs +++ b/libs/wire-api/src/Wire/API/Routes/WebSocket.hs @@ -31,6 +31,7 @@ import Servant.Server.Internal.Delayed import Servant.Server.Internal.RouteResult import Servant.Server.Internal.Router import Servant.Swagger +import Wire.API.Routes.Version -- | A websocket that relates to a 'PendingConnection' -- Copied and adapted from: @@ -62,6 +63,8 @@ instance HasServer WebSocketPending ctx where errHeaders = mempty } +type instance SpecialiseToVersion v WebSocketPending = WebSocketPending + instance HasSwagger WebSocketPending where toSwagger _ = mempty diff --git a/libs/wire-api/src/Wire/API/VersionInfo.hs b/libs/wire-api/src/Wire/API/VersionInfo.hs index 0fa210b01eb..1d05a55e027 100644 --- a/libs/wire-api/src/Wire/API/VersionInfo.hs +++ b/libs/wire-api/src/Wire/API/VersionInfo.hs @@ -42,7 +42,6 @@ import Servant import Servant.Client.Core import Servant.Server.Internal.Delayed import Servant.Server.Internal.DelayedIO -import Servant.Swagger import Wire.API.Routes.ClientAlgebra vinfoObjectSchema :: ValueSchema NamedSwaggerDoc v -> ObjectSchema SwaggerDoc [v] @@ -108,9 +107,6 @@ instance clientWithRoute pm (Proxy @api) req hoistClientMonad pm _ f = hoistClientMonad pm (Proxy @api) f -instance HasSwagger (Until v :> api) where - toSwagger _ = mempty - instance RoutesToPaths api => RoutesToPaths (Until v :> api) where getRoutes = getRoutes @api @@ -159,8 +155,5 @@ instance clientWithRoute pm (Proxy @api) req hoistClientMonad pm _ f = hoistClientMonad pm (Proxy @api) f -instance HasSwagger api => HasSwagger (From v :> api) where - toSwagger _ = toSwagger (Proxy @api) - instance RoutesToPaths api => RoutesToPaths (From v :> api) where getRoutes = getRoutes @api diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 188db1b0ff7..b27c4094448 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -113,6 +113,7 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Federation.API import Wire.API.Federation.Error import Wire.API.Properties qualified as Public +import Wire.API.Routes.API import Wire.API.Routes.Internal.Brig qualified as BrigInternalAPI import Wire.API.Routes.Internal.Cannon qualified as CannonInternalAPI import Wire.API.Routes.Internal.Cargohold qualified as CargoholdInternalAPI @@ -121,13 +122,13 @@ import Wire.API.Routes.Internal.Spar qualified as SparInternalAPI import Wire.API.Routes.MultiTablePaging qualified as Public import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig -import Wire.API.Routes.Public.Brig.OAuth qualified as OAuth -import Wire.API.Routes.Public.Cannon qualified as CannonAPI -import Wire.API.Routes.Public.Cargohold qualified as CargoholdAPI -import Wire.API.Routes.Public.Galley qualified as GalleyAPI -import Wire.API.Routes.Public.Gundeck qualified as GundeckAPI -import Wire.API.Routes.Public.Proxy qualified as ProxyAPI -import Wire.API.Routes.Public.Spar qualified as SparAPI +import Wire.API.Routes.Public.Brig.OAuth +import Wire.API.Routes.Public.Cannon +import Wire.API.Routes.Public.Cargohold +import Wire.API.Routes.Public.Galley +import Wire.API.Routes.Public.Gundeck +import Wire.API.Routes.Public.Proxy +import Wire.API.Routes.Public.Spar import Wire.API.Routes.Public.Util qualified as Public import Wire.API.Routes.Version import Wire.API.SwaggerHelper (cleanupSwagger) @@ -166,17 +167,32 @@ docsAPI = -- -- Dual to `internalEndpointsSwaggerDocsAPI`. versionedSwaggerDocsAPI :: Servant.Server VersionedSwaggerDocsAPI +versionedSwaggerDocsAPI (Just (VersionNumber V5)) = + swaggerSchemaUIServer $ + ( serviceSwagger @VersionAPITag @'V5 + <> serviceSwagger @BrigAPITag @'V5 + <> serviceSwagger @GalleyAPITag @'V5 + <> serviceSwagger @SparAPITag @'V5 + <> serviceSwagger @CargoholdAPITag @'V5 + <> serviceSwagger @CannonAPITag @'V5 + <> serviceSwagger @GundeckAPITag @'V5 + <> serviceSwagger @ProxyAPITag @'V5 + <> serviceSwagger @OAuthAPITag @'V5 + ) + & S.info . S.title .~ "Wire-Server API" + & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger.md") + & cleanupSwagger versionedSwaggerDocsAPI (Just (VersionNumber V4)) = swaggerSchemaUIServer $ - ( brigSwagger - <> versionSwagger - <> GalleyAPI.swaggerDoc - <> SparAPI.swaggerDoc - <> CargoholdAPI.swaggerDoc - <> CannonAPI.swaggerDoc - <> GundeckAPI.swaggerDoc - <> ProxyAPI.swaggerDoc - <> OAuth.swaggerDoc + ( serviceSwagger @VersionAPITag @'V4 + <> serviceSwagger @BrigAPITag @'V4 + <> serviceSwagger @GalleyAPITag @'V4 + <> serviceSwagger @SparAPITag @'V4 + <> serviceSwagger @CargoholdAPITag @'V4 + <> serviceSwagger @CannonAPITag @'V4 + <> serviceSwagger @GundeckAPITag @'V4 + <> serviceSwagger @ProxyAPITag @'V4 + <> serviceSwagger @OAuthAPITag @'V4 ) & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger.md") @@ -218,6 +234,11 @@ internalEndpointsSwaggerDocsAPI :: PortNumber -> S.Swagger -> Servant.Server (VersionedSwaggerDocsAPIBase service) +internalEndpointsSwaggerDocsAPI service examplePort swagger (Just (VersionNumber V5)) = + swaggerSchemaUIServer $ + swagger + & adjustSwaggerForInternalEndpoint service examplePort + & cleanupSwagger internalEndpointsSwaggerDocsAPI service examplePort swagger (Just (VersionNumber V4)) = swaggerSchemaUIServer $ swagger diff --git a/services/brig/test/integration/API/Swagger.hs b/services/brig/test/integration/API/Swagger.hs index 2d2525dcdc6..ec5ec230e96 100644 --- a/services/brig/test/integration/API/Swagger.hs +++ b/services/brig/test/integration/API/Swagger.hs @@ -48,7 +48,7 @@ tests p _opts brigNoImplicitVersion = [ test p "toc" $ do forM_ ["/api/swagger-ui", "/api/swagger-ui/index.html", "/api/swagger.json"] $ \pth -> do r <- get (brigNoImplicitVersion . path pth . expect2xx) - liftIO $ assertEqual "toc is intact" (responseBody r) (Just "

please pick an api version

/v0/api/swagger-ui/
/v1/api/swagger-ui/
/v2/api/swagger-ui/
/v3/api/swagger-ui/
/v4/api/swagger-ui/
") + liftIO $ assertEqual "toc is intact" (Just "

please pick an api version

/v0/api/swagger-ui/
/v1/api/swagger-ui/
/v2/api/swagger-ui/
/v3/api/swagger-ui/
/v4/api/swagger-ui/
/v5/api/swagger-ui/
") (responseBody r) -- are all versions listed? forM_ [minBound :: Version ..] $ \v -> liftIO $ assertBool (show v) ((cs (toQueryParam v) :: String) `isInfixOf` (cs . fromJust . responseBody $ r)) -- FUTUREWORK: maybe test that no invalid versions are listed? (that wouldn't diff --git a/services/cannon/src/Cannon/API/Public.hs b/services/cannon/src/Cannon/API/Public.hs index 0eb81bf5fb1..4a559f9f17c 100644 --- a/services/cannon/src/Cannon/API/Public.hs +++ b/services/cannon/src/Cannon/API/Public.hs @@ -31,7 +31,7 @@ import Servant import Wire.API.Routes.Named import Wire.API.Routes.Public.Cannon -publicAPIServer :: ServerT PublicAPI Cannon +publicAPIServer :: ServerT CannonAPI Cannon publicAPIServer = Named @"await-notifications" streamData streamData :: UserId -> ConnId -> Maybe ClientId -> PendingConnection -> Cannon () diff --git a/services/cannon/src/Cannon/Run.hs b/services/cannon/src/Cannon/Run.hs index df5f6e06a66..b0657c59f8f 100644 --- a/services/cannon/src/Cannon/Run.hs +++ b/services/cannon/src/Cannon/Run.hs @@ -58,7 +58,7 @@ import Wire.API.Routes.Internal.Cannon qualified as Internal import Wire.API.Routes.Public.Cannon import Wire.API.Routes.Version.Wai -type CombinedAPI = PublicAPI :<|> Internal.API +type CombinedAPI = CannonAPI :<|> Internal.API run :: Opts -> IO () run o = do @@ -88,7 +88,7 @@ run o = do app = middleware (serve (Proxy @CombinedAPI) server) server :: Servant.Server CombinedAPI server = - hoistServer (Proxy @PublicAPI) (runCannonToServant e) publicAPIServer + hoistServer (Proxy @CannonAPI) (runCannonToServant e) publicAPIServer :<|> hoistServer (Proxy @Internal.API) (runCannonToServant e) internalServer tid <- myThreadId E.handle uncaughtExceptionHandler $ do diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 280e00b02b8..a896025bdc6 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -41,7 +41,7 @@ import Wire.API.Routes.AssetBody import Wire.API.Routes.Internal.Cargohold import Wire.API.Routes.Public.Cargohold -servantSitemap :: ServerT ServantAPI Handler +servantSitemap :: ServerT CargoholdAPI Handler servantSitemap = renewTokenV3 :<|> deleteTokenV3 diff --git a/services/cargohold/src/CargoHold/Run.hs b/services/cargohold/src/CargoHold/Run.hs index 556cabf3679..ae43e1e96ae 100644 --- a/services/cargohold/src/CargoHold/Run.hs +++ b/services/cargohold/src/CargoHold/Run.hs @@ -55,7 +55,7 @@ import Wire.API.Routes.Internal.Cargohold import Wire.API.Routes.Public.Cargohold import Wire.API.Routes.Version.Wai -type CombinedAPI = FederationAPI :<|> ServantAPI :<|> InternalAPI +type CombinedAPI = FederationAPI :<|> CargoholdAPI :<|> InternalAPI run :: Opts -> IO () run o = lowerCodensity $ do @@ -89,7 +89,7 @@ mkApp o = Codensity $ \k -> (Proxy @CombinedAPI) ((o ^. settings . federationDomain) :. Servant.EmptyContext) ( hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap - :<|> hoistServerWithDomain @ServantAPI (toServantHandler e) servantSitemap + :<|> hoistServerWithDomain @CargoholdAPI (toServantHandler e) servantSitemap :<|> hoistServerWithDomain @InternalAPI (toServantHandler e) internalSitemap ) r diff --git a/services/galley/src/Galley/API/Public/Servant.hs b/services/galley/src/Galley/API/Public/Servant.hs index 86e223c522a..ea777ec4992 100644 --- a/services/galley/src/Galley/API/Public/Servant.hs +++ b/services/galley/src/Galley/API/Public/Servant.hs @@ -32,7 +32,7 @@ import Galley.App import Wire.API.Routes.API import Wire.API.Routes.Public.Galley -servantSitemap :: API ServantAPI GalleyEffects +servantSitemap :: API GalleyAPI GalleyEffects servantSitemap = conversationAPI <@> teamConversationAPI diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index bd2ad81994a..60924f451f8 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -63,7 +63,7 @@ import System.Logger qualified as Log import System.Logger.Extended (mkLogger) import Util.Options import Wire.API.Routes.API -import Wire.API.Routes.Public.Galley qualified as GalleyAPI +import Wire.API.Routes.Public.Galley import Wire.API.Routes.Version.Wai run :: Opts -> IO () @@ -151,7 +151,7 @@ bodyParserErrorFormatter' _ _ errMsg = } type CombinedAPI = - GalleyAPI.ServantAPI + GalleyAPI :<|> InternalAPI :<|> FederationAPI :<|> Servant.Raw diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 5c27ad4407d..4107e44910f 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -32,7 +32,7 @@ module Spar.API api, -- * API types - API, + SparAPI, -- ** Individual API pieces APIAuthReqPrecheck, @@ -113,7 +113,7 @@ import qualified Wire.Sem.Random as Random app :: Env -> Application app ctx = SAML.setHttpCachePolicy $ - serve (Proxy @API) (hoistServer (Proxy @API) (runSparToHandler ctx) (api $ sparCtxOpts ctx) :: Server API) + serve (Proxy @SparAPI) (hoistServer (Proxy @SparAPI) (runSparToHandler ctx) (api $ sparCtxOpts ctx) :: Server SparAPI) api :: ( Member GalleyAccess r, @@ -144,7 +144,7 @@ api :: Member (Logger (Msg -> Msg)) r ) => Opts -> - ServerT API (Sem r) + ServerT SparAPI (Sem r) api opts = apiSSO opts :<|> apiIDP diff --git a/services/spar/src/Spar/Run.hs b/services/spar/src/Spar/Run.hs index 58681ad8974..5331fddabc9 100644 --- a/services/spar/src/Spar/Run.hs +++ b/services/spar/src/Spar/Run.hs @@ -44,7 +44,7 @@ import qualified Network.Wai.Handler.Warp as Warp import Network.Wai.Utilities.Request (lookupRequestId) import qualified Network.Wai.Utilities.Server as WU import qualified SAML2.WebSSO as SAML -import Spar.API (API, app) +import Spar.API (SparAPI, app) import Spar.App import qualified Spar.Data as Data import Spar.Data.Instances () @@ -116,7 +116,7 @@ mkApp sparCtxOpts = do let wrappedApp = versionMiddleware (fold (disabledAPIVersions sparCtxOpts)) . WU.heavyDebugLogging heavyLogOnly logLevel sparCtxLogger - . servantPrometheusMiddleware (Proxy @API) + . servantPrometheusMiddleware (Proxy @SparAPI) . WU.catchErrors sparCtxLogger [] -- Error 'Response's are usually not thrown as exceptions, but logged in -- 'renderSparErrorWithLogging' before the 'Application' can construct a 'Response' diff --git a/services/spar/test/Test/Spar/APISpec.hs b/services/spar/test/Test/Spar/APISpec.hs index ceefb59e489..07bfdb8fde3 100644 --- a/services/spar/test/Test/Spar/APISpec.hs +++ b/services/spar/test/Test/Spar/APISpec.hs @@ -37,9 +37,9 @@ import Wire.API.User.Saml (SsoSettings) spec :: Spec spec = do -- Note: SCIM types are not validated because their content-type is 'SCIM'. - validateEveryToJSON (Proxy @API.API) + validateEveryToJSON (Proxy @API.SparAPI) it "api consistency" $ do - pathsConsistencyCheck (routesToPaths @API.API) `shouldBe` mempty + pathsConsistencyCheck (routesToPaths @API.SparAPI) `shouldBe` mempty it "roundtrip: IdPMetadataInfo" . property $ \(val :: IdPMetadataInfo) -> do let withoutRaw (IdPMetadataValue _ x) = x (withoutRaw <$> (Aeson.eitherDecode . Aeson.encode) val) `shouldBe` Right (withoutRaw val) diff --git a/tools/fedcalls/src/Main.hs b/tools/fedcalls/src/Main.hs index 79ac0e78878..c1b4471da9f 100644 --- a/tools/fedcalls/src/Main.hs +++ b/tools/fedcalls/src/Main.hs @@ -42,17 +42,16 @@ import Data.Swagger ) import Imports import Language.Dot as D +import Wire.API.Routes.API import Wire.API.Routes.Internal.Brig qualified as BrigIRoutes -import Wire.API.Routes.Public.Brig qualified as BrigRoutes -import Wire.API.Routes.Public.Cannon qualified as CannonRoutes -import Wire.API.Routes.Public.Cargohold qualified as CargoholdRoutes -import Wire.API.Routes.Public.Galley qualified as GalleyRoutes -import Wire.API.Routes.Public.Gundeck qualified as GundeckRoutes -import Wire.API.Routes.Public.Proxy qualified as ProxyRoutes --- import qualified Wire.API.Routes.Internal.Cannon as CannonIRoutes --- import qualified Wire.API.Routes.Internal.Cargohold as CargoholdIRoutes --- import qualified Wire.API.Routes.Internal.LegalHold as LegalHoldIRoutes -import Wire.API.Routes.Public.Spar qualified as SparRoutes +import Wire.API.Routes.Public.Brig +import Wire.API.Routes.Public.Cannon +import Wire.API.Routes.Public.Cargohold +import Wire.API.Routes.Public.Galley +import Wire.API.Routes.Public.Gundeck +import Wire.API.Routes.Public.Proxy +import Wire.API.Routes.Public.Spar +import Wire.API.Routes.Version ------------------------------ @@ -72,13 +71,13 @@ swaggers = -- services, use that in /services/brig/src/Brig/API/Public.hs instead of -- doing it by hand. - BrigRoutes.brigSwagger, -- TODO: s/brigSwagger/swaggerDoc/ like everybody else! - CannonRoutes.swaggerDoc, - CargoholdRoutes.swaggerDoc, - GalleyRoutes.swaggerDoc, - GundeckRoutes.swaggerDoc, - ProxyRoutes.swaggerDoc, - SparRoutes.swaggerDoc, + serviceSwagger @BrigAPITag @'V5, + serviceSwagger @CannonAPITag @'V5, + serviceSwagger @CargoholdAPITag @'V5, + serviceSwagger @GalleyAPITag @'V5, + serviceSwagger @GundeckAPITag @'V5, + serviceSwagger @ProxyAPITag @'V5, + serviceSwagger @SparAPITag @'V5, -- TODO: collect all internal apis somewhere else (brig?), and expose them -- via an internal swagger api end-point. From e0ea81e596561b589bb07163267bf37f9dc47fd6 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 24 Aug 2023 10:13:26 +0200 Subject: [PATCH 099/225] stern: Optimize RAM usage of /i/users/meta-info (#3522) * stern: Fetch only the notifications that are needed * stern: Fetch only the conversations that are needed --- changelog.d/5-internal/optimize-stern | 1 + tools/stern/src/Stern/API.hs | 4 +-- tools/stern/src/Stern/Intra.hs | 52 ++++++++++++++------------- 3 files changed, 30 insertions(+), 27 deletions(-) create mode 100644 changelog.d/5-internal/optimize-stern diff --git a/changelog.d/5-internal/optimize-stern b/changelog.d/5-internal/optimize-stern new file mode 100644 index 00000000000..ad7c8c83a55 --- /dev/null +++ b/changelog.d/5-internal/optimize-stern @@ -0,0 +1 @@ +stern: Optimize RAM usage of /i/users/meta-info \ No newline at end of file diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index fda4370e032..c559d0e6f20 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -402,9 +402,9 @@ getUserData :: UserId -> Maybe Int -> Maybe Int -> Handler UserMetaInfo getUserData uid mMaxConvs mMaxNotifs = do account <- Intra.getUserProfiles (Left [uid]) >>= noSuchUser . listToMaybe conns <- Intra.getUserConnections uid - convs <- Intra.getUserConversations uid <&> take (fromMaybe 1 mMaxConvs) + convs <- Intra.getUserConversations uid (fromMaybe 1 mMaxConvs) clts <- Intra.getUserClients uid - notfs <- (Intra.getUserNotifications uid <&> take (fromMaybe 10 mMaxNotifs) <&> toJSON @[QueuedNotification]) `catchE` (pure . String . cs . show) + notfs <- (Intra.getUserNotifications uid (fromMaybe 10 mMaxNotifs) <&> toJSON @[QueuedNotification]) `catchE` (pure . String . cs . show) consent <- (Intra.getUserConsentValue uid <&> toJSON @ConsentValue) `catchE` (pure . String . cs . show) consentLog <- (Intra.getUserConsentLog uid <&> toJSON @ConsentLog) `catchE` (pure . String . cs . show) cookies <- Intra.getUserCookies uid diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index f064691c51f..8ab4c1e3a86 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -749,25 +749,27 @@ getUserCookies uid = do ) parseResponse (mkError status502 "bad-upstream") r -getUserConversations :: UserId -> Handler [Conversation] -getUserConversations uid = do +getUserConversations :: UserId -> Int -> Handler [Conversation] +getUserConversations uid maxConvs = do info $ msg "Getting user conversations" - fetchAll [] Nothing + fetchAll [] Nothing maxConvs where - fetchAll xs start = do - userConversationList <- fetchBatch start + fetchAll :: [Conversation] -> Maybe ConvId -> Int -> Handler [Conversation] + fetchAll xs start remaining = do + userConversationList <- fetchBatch start (min 100 remaining) let batch = convList userConversationList - if (not . null) batch && convHasMore userConversationList - then fetchAll (batch ++ xs) (Just . qUnqualified . cnvQualifiedId $ last batch) + remaining' = remaining - length batch + if (not . null) batch && convHasMore userConversationList && remaining' > 0 + then fetchAll (batch ++ xs) (Just . qUnqualified . cnvQualifiedId $ last batch) remaining' else pure (batch ++ xs) - fetchBatch :: Maybe ConvId -> Handler (ConversationList Conversation) - fetchBatch start = do - b <- view galley + fetchBatch :: Maybe ConvId -> Int -> Handler (ConversationList Conversation) + fetchBatch start batchSize = do + baseReq <- view galley r <- catchRpcErrors $ rpc' "galley" - b + baseReq ( method GET . header "Z-User" (toByteString' uid) . versionedPath "conversations" @@ -776,7 +778,6 @@ getUserConversations uid = do . expect2xx ) unVersioned @'V2 <$> parseResponse (mkError status502 "bad-upstream") r - batchSize = 100 :: Int getUserClients :: UserId -> Handler [Client] getUserClients uid = do @@ -829,25 +830,27 @@ getUserProperties uid = do value <- parseResponse (mkError status502 "bad-upstream") r fetchProperty b xs (Map.insert x value acc) -getUserNotifications :: UserId -> Handler [QueuedNotification] -getUserNotifications uid = do +getUserNotifications :: UserId -> Int -> Handler [QueuedNotification] +getUserNotifications uid maxNotifs = do info $ msg "Getting user notifications" - fetchAll [] Nothing + fetchAll [] Nothing maxNotifs where - fetchAll xs start = do - userNotificationList <- fetchBatch start + fetchAll :: [QueuedNotification] -> Maybe NotificationId -> Int -> ExceptT Error App [QueuedNotification] + fetchAll xs start remaining = do + userNotificationList <- fetchBatch start (min 100 remaining) let batch = view queuedNotifications userNotificationList - if (not . null) batch && view queuedHasMore userNotificationList - then fetchAll (batch ++ xs) (Just . view queuedNotificationId $ last batch) + remaining' = remaining - length batch + if (not . null) batch && view queuedHasMore userNotificationList && remaining' > 0 + then fetchAll (batch ++ xs) (Just . view queuedNotificationId $ last batch) remaining' else pure (batch ++ xs) - fetchBatch :: Maybe NotificationId -> Handler QueuedNotificationList - fetchBatch start = do - b <- view gundeck + fetchBatch :: Maybe NotificationId -> Int -> Handler QueuedNotificationList + fetchBatch start batchSize = do + baseReq <- view gundeck r <- catchRpcErrors $ rpc' - "galley" - b + "gundeck" + baseReq ( method GET . header "Z-User" (toByteString' uid) . versionedPath "notifications" @@ -861,7 +864,6 @@ getUserNotifications uid = do 200 -> parseResponse (mkError status502 "bad-upstream") r 404 -> parseResponse (mkError status502 "bad-upstream") r _ -> throwE (mkError status502 "bad-upstream" "") - batchSize = 100 :: Int getSsoDomainRedirect :: Text -> Handler (Maybe CustomBackend) getSsoDomainRedirect domain = do From 119fe8ab4eb724af5039f3f29686043b92174cd9 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 24 Aug 2023 15:08:56 +0200 Subject: [PATCH 100/225] Integration tests: use static ports (#3536) --- integration/integration.cabal | 2 + integration/test/API/Cargohold.hs | 7 +- integration/test/API/GalleyInternal.hs | 6 +- integration/test/SetupHelpers.hs | 8 +- integration/test/Test/AssetDownload.hs | 68 ++-- integration/test/Test/Brig.hs | 36 +- integration/test/Test/Conversation.hs | 40 +-- integration/test/Test/Defederation.hs | 6 +- integration/test/Test/Demo.hs | 55 ++- integration/test/Test/Federation.hs | 4 +- integration/test/Testlib/App.hs | 1 + integration/test/Testlib/Cannon.hs | 1 + integration/test/Testlib/Env.hs | 37 +-- integration/test/Testlib/HTTP.hs | 1 + integration/test/Testlib/ModService.hs | 407 +++++++++-------------- integration/test/Testlib/Ports.hs | 39 +++ integration/test/Testlib/Prelude.hs | 2 + integration/test/Testlib/ResourcePool.hs | 122 +++++-- integration/test/Testlib/Run.hs | 1 + integration/test/Testlib/RunServices.hs | 84 +---- integration/test/Testlib/Service.hs | 49 +++ integration/test/Testlib/Types.hs | 101 +++--- 22 files changed, 507 insertions(+), 570 deletions(-) create mode 100644 integration/test/Testlib/Ports.hs create mode 100644 integration/test/Testlib/Service.hs diff --git a/integration/integration.cabal b/integration/integration.cabal index 8e29915dbb2..7053817cbf6 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -119,6 +119,7 @@ library Testlib.JSON Testlib.ModService Testlib.Options + Testlib.Ports Testlib.Prekeys Testlib.Prelude Testlib.Printing @@ -126,6 +127,7 @@ library Testlib.ResourcePool Testlib.Run Testlib.RunServices + Testlib.Service Testlib.Types build-depends: diff --git a/integration/test/API/Cargohold.hs b/integration/test/API/Cargohold.hs index 34276fecb38..5e08d84d794 100644 --- a/integration/test/API/Cargohold.hs +++ b/integration/test/API/Cargohold.hs @@ -122,11 +122,8 @@ buildMultipartBody header body bodyMimeType = MIME.mime_val_content = MIME.Single ((decodeUtf8 . LBS.toStrict) c) } -downloadAsset :: (HasCallStack, MakesValue user, MakesValue assetDomain, MakesValue key) => user -> assetDomain -> key -> (HTTP.Request -> HTTP.Request) -> App Response -downloadAsset user assetDomain key trans = downloadAsset' user assetDomain key "nginz-https.example.com" trans - -downloadAsset' :: (HasCallStack, MakesValue user, MakesValue key, MakesValue assetDomain) => user -> assetDomain -> key -> String -> (HTTP.Request -> HTTP.Request) -> App Response -downloadAsset' user assetDomain key zHostHeader trans = do +downloadAsset :: (HasCallStack, MakesValue user, MakesValue key, MakesValue assetDomain) => user -> assetDomain -> key -> String -> (HTTP.Request -> HTTP.Request) -> App Response +downloadAsset user assetDomain key zHostHeader trans = do uid <- objId user domain <- objDomain assetDomain key' <- asString key diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index bfdff81b50a..0d7f8c9f602 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -32,9 +32,9 @@ putTeamMember user team perms = do ] req -getTeamFeature :: HasCallStack => String -> String -> App Response -getTeamFeature featureName tid = do - req <- baseRequest OwnDomain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName] +getTeamFeature :: (HasCallStack, MakesValue domain_) => domain_ -> String -> String -> App Response +getTeamFeature domain_ featureName tid = do + req <- baseRequest domain_ Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName] submit "GET" $ req getFederationStatus :: diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index f6ba6f46768..8851e258900 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -106,7 +106,7 @@ addFullSearchFor domains val = fullSearchWithAll :: ServiceOverrides fullSearchWithAll = def - { dbBrig = \val -> do + { brigCfg = \val -> do ownDomain <- asString =<< val %. "optSettings.setFederationDomain" env <- ask let remoteDomains = List.delete ownDomain $ [env.domain1, env.domain2] <> env.dynamicDomains @@ -120,9 +120,9 @@ withFederatingBackendsAllowDynamic n k = do >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends - [ def {dbBrig = setFederationConfig}, - def {dbBrig = setFederationConfig}, - def {dbBrig = setFederationConfig} + [ def {brigCfg = setFederationConfig}, + def {brigCfg = setFederationConfig}, + def {brigCfg = setFederationConfig} ] $ \dynDomains -> do domains@[domainA, domainB, domainC] <- pure dynDomains diff --git a/integration/test/Test/AssetDownload.hs b/integration/test/Test/AssetDownload.hs index e9d7b968b4c..c595c84f0e7 100644 --- a/integration/test/Test/AssetDownload.hs +++ b/integration/test/Test/AssetDownload.hs @@ -15,7 +15,7 @@ testDownloadAsset = do resp.status `shouldMatchInt` 201 resp.json %. "key" - bindResponse (downloadAsset user user key id) $ \resp -> do + bindResponse (downloadAsset user user key "nginz-https.example.com" id) $ \resp -> do resp.status `shouldMatchInt` 200 assertBool ("Expect 'Hello World!' as text asset content. Got: " ++ show resp.body) @@ -27,37 +27,53 @@ testDownloadAssetMultiIngressS3DownloadUrl = do -- multi-ingress disabled key <- doUploadAsset user - checkAssetDownload user key - withModifiedService Cargohold modifyConfig $ \_ -> do - -- multi-ingress enabled - key' <- doUploadAsset user - checkAssetDownload user key' - where - checkAssetDownload :: HasCallStack => Value -> Value -> App () - checkAssetDownload user key = withModifiedService Cargohold modifyConfig $ \_ -> do - bindResponse (downloadAsset user user key noRedirects) $ \resp -> do - resp.status `shouldMatchInt` 404 - bindResponse (downloadAsset' user user key "red.example.com" noRedirects) $ \resp -> do - resp.status `shouldMatchInt` 302 - locationHeaderHost resp `shouldMatch` "s3-download.red.example.com" - bindResponse (downloadAsset' user user key "green.example.com" noRedirects) $ \resp -> do - resp.status `shouldMatchInt` 302 - locationHeaderHost resp `shouldMatch` "s3-download.green.example.com" - bindResponse (downloadAsset' user user key "unknown.example.com" noRedirects) $ \resp -> do - resp.status `shouldMatchInt` 404 - resp.json %. "label" `shouldMatch` "not-found" + bindResponse (downloadAsset user user key "nginz-https.example.com" noRedirects) $ \resp -> do + resp.status `shouldMatchInt` 302 + + bindResponse (downloadAsset user user key "red.example.com" noRedirects) $ \resp -> do + resp.status `shouldMatchInt` 302 + + bindResponse (downloadAsset user user key "green.example.com" noRedirects) $ \resp -> do + resp.status `shouldMatchInt` 302 + + bindResponse (downloadAsset user user key "unknown.example.com" noRedirects) $ \resp -> do + resp.status `shouldMatchInt` 302 + + -- multi-ingress enabled + withModifiedBackend modifyConfig $ \domain -> do + user' <- randomUser domain def + key' <- doUploadAsset user' + bindResponse (downloadAsset user' user' key' "nginz-https.example.com" noRedirects) $ \resp -> do + resp.status `shouldMatchInt` 404 + resp.json %. "label" `shouldMatch` "not-found" + + bindResponse (downloadAsset user' user' key' "red.example.com" noRedirects) $ \resp -> do + resp.status `shouldMatchInt` 302 + locationHeaderHost resp `shouldMatch` "s3-download.red.example.com" + + bindResponse (downloadAsset user' user' key' "green.example.com" noRedirects) $ \resp -> do + resp.status `shouldMatchInt` 302 + locationHeaderHost resp `shouldMatch` "s3-download.green.example.com" + + bindResponse (downloadAsset user' user' key' "unknown.example.com" noRedirects) $ \resp -> do + resp.status `shouldMatchInt` 404 + resp.json %. "label" `shouldMatch` "not-found" + where noRedirects :: HTTP.Request -> HTTP.Request noRedirects req = (req {redirectCount = 0}) - modifyConfig :: Value -> App Value + modifyConfig :: ServiceOverrides modifyConfig = - setField "aws.multiIngress" $ - object - [ "red.example.com" .= "http://s3-download.red.example.com", - "green.example.com" .= "http://s3-download.green.example.com" - ] + def + { cargoholdCfg = + setField "aws.multiIngress" $ + object + [ "red.example.com" .= "http://s3-download.red.example.com", + "green.example.com" .= "http://s3-download.green.example.com" + ] + } doUploadAsset :: HasCallStack => Value -> App Value doUploadAsset user = bindResponse (uploadAsset user) $ \resp -> do diff --git a/integration/test/Test/Brig.hs b/integration/test/Test/Brig.hs index 3380d8400ca..a229f8cfd6d 100644 --- a/integration/test/Test/Brig.hs +++ b/integration/test/Test/Brig.hs @@ -30,11 +30,13 @@ testCrudFederationRemotes :: HasCallStack => App () testCrudFederationRemotes = do otherDomain <- asString OtherDomain let overrides = - ( setField - "optSettings.setFederationDomainConfigs" - [object ["domain" .= otherDomain, "search_policy" .= "full_search"]] - ) - withModifiedService Brig overrides $ \_ -> do + def + { brigCfg = + setField + "optSettings.setFederationDomainConfigs" + [object ["domain" .= otherDomain, "search_policy" .= "full_search"]] + } + withModifiedBackend overrides $ \ownDomain -> do let parseFedConns :: HasCallStack => Response -> App [Value] parseFedConns resp = -- Pick out the list of federation domain configs @@ -45,38 +47,38 @@ testCrudFederationRemotes = do addOnce :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => fedConn -> [fedConn2] -> App () addOnce fedConn want = do - bindResponse (Internal.createFedConn OwnDomain fedConn) $ \res -> do + bindResponse (Internal.createFedConn ownDomain fedConn) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 200 - res2 <- parseFedConns =<< Internal.readFedConns OwnDomain + res2 <- parseFedConns =<< Internal.readFedConns ownDomain sort res2 `shouldMatch` sort want addFail :: HasCallStack => MakesValue fedConn => fedConn -> App () addFail fedConn = do - bindResponse (Internal.createFedConn' OwnDomain fedConn) $ \res -> do + bindResponse (Internal.createFedConn' ownDomain fedConn) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 533 deleteOnce :: (Ord fedConn, ToJSON fedConn, MakesValue fedConn) => String -> [fedConn] -> App () deleteOnce domain want = do - bindResponse (Internal.deleteFedConn OwnDomain domain) $ \res -> do + bindResponse (Internal.deleteFedConn ownDomain domain) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 200 - res2 <- parseFedConns =<< Internal.readFedConns OwnDomain + res2 <- parseFedConns =<< Internal.readFedConns ownDomain sort res2 `shouldMatch` sort want deleteFail :: HasCallStack => String -> App () deleteFail del = do - bindResponse (Internal.deleteFedConn' OwnDomain del) $ \res -> do + bindResponse (Internal.deleteFedConn' ownDomain del) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 533 updateOnce :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => String -> fedConn -> [fedConn2] -> App () updateOnce domain fedConn want = do - bindResponse (Internal.updateFedConn OwnDomain domain fedConn) $ \res -> do + bindResponse (Internal.updateFedConn ownDomain domain fedConn) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 200 - res2 <- parseFedConns =<< Internal.readFedConns OwnDomain + res2 <- parseFedConns =<< Internal.readFedConns ownDomain sort res2 `shouldMatch` sort want updateFail :: (MakesValue fedConn, HasCallStack) => String -> fedConn -> App () updateFail domain fedConn = do - bindResponse (Internal.updateFedConn' OwnDomain domain fedConn) $ \res -> do + bindResponse (Internal.updateFedConn' ownDomain domain fedConn) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 533 dom1 :: String <- (<> ".example.com") . UUID.toString <$> liftIO UUID.nextRandom @@ -93,8 +95,8 @@ testCrudFederationRemotes = do remote1J <- make remote1 remote1J' <- make remote1' - resetFedConns OwnDomain - cfgRemotes <- parseFedConns =<< Internal.readFedConns OwnDomain + resetFedConns ownDomain + cfgRemotes <- parseFedConns =<< Internal.readFedConns ownDomain cfgRemotes `shouldMatch` [cfgRemotesExpect] -- entries present in the config file can be idempotently added if identical, but cannot be -- updated, deleted or updated. @@ -185,7 +187,7 @@ testRemoteUserSearch = do setField "optSettings.setFederationStrategy" "allowDynamic" >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - startDynamicBackends [def {dbBrig = overrides}, def {dbBrig = overrides}] $ \dynDomains -> do + startDynamicBackends [def {brigCfg = overrides}, def {brigCfg = overrides}] $ \dynDomains -> do domains@[d1, d2] <- pure dynDomains connectAllDomainsAndWaitToSync 1 domains [u1, u2] <- createAndConnectUsers [d1, d2] diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 3d38b417307..7e11aff0700 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -18,7 +18,7 @@ import Testlib.Prelude testDynamicBackendsFullyConnectedWhenAllowAll :: HasCallStack => App () testDynamicBackendsFullyConnectedWhenAllowAll = do let overrides = - def {dbBrig = setField "optSettings.setFederationStrategy" "allowAll"} + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} <> fullSearchWithAll startDynamicBackends [overrides, overrides, overrides] $ \dynDomains -> do [domainA, domainB, domainC] <- pure dynDomains @@ -41,7 +41,7 @@ testDynamicBackendsNotFederating :: HasCallStack => App () testDynamicBackendsNotFederating = do let overrides = def - { dbBrig = + { brigCfg = setField "optSettings.setFederationStrategy" "allowNone" } startDynamicBackends [overrides, overrides, overrides] $ @@ -62,9 +62,9 @@ testDynamicBackendsFullyConnectedWhenAllowDynamic = do >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends - [ def {dbBrig = overrides}, - def {dbBrig = overrides}, - def {dbBrig = overrides} + [ def {brigCfg = overrides}, + def {brigCfg = overrides}, + def {brigCfg = overrides} ] $ \dynDomains -> do domains@[domainA, domainB, domainC] <- pure dynDomains @@ -86,7 +86,7 @@ testDynamicBackendsNotFullyConnected :: HasCallStack => App () testDynamicBackendsNotFullyConnected = do let overrides = def - { dbBrig = + { brigCfg = setField "optSettings.setFederationStrategy" "allowDynamic" >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) @@ -140,9 +140,9 @@ testCreateConversationFullyConnected = do >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends - [ def {dbBrig = setFederationConfig}, - def {dbBrig = setFederationConfig}, - def {dbBrig = setFederationConfig} + [ def {brigCfg = setFederationConfig}, + def {brigCfg = setFederationConfig}, + def {brigCfg = setFederationConfig} ] $ \dynDomains -> do domains@[domainA, domainB, domainC] <- pure dynDomains @@ -158,9 +158,9 @@ testCreateConversationNonFullyConnected = do >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends - [ def {dbBrig = setFederationConfig}, - def {dbBrig = setFederationConfig}, - def {dbBrig = setFederationConfig} + [ def {brigCfg = setFederationConfig}, + def {brigCfg = setFederationConfig}, + def {brigCfg = setFederationConfig} ] $ \dynDomains -> do domains@[domainA, domainB, domainC] <- pure dynDomains @@ -181,8 +181,8 @@ testDefederationGroupConversation = do >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends - [ def {dbBrig = setFederationConfig}, - def {dbBrig = setFederationConfig} + [ def {brigCfg = setFederationConfig}, + def {brigCfg = setFederationConfig} ] $ \dynDomains -> do domains@[domainA, domainB] <- pure dynDomains @@ -247,8 +247,8 @@ testDefederationOneOnOne = do >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends - [ def {dbBrig = setFederationConfig}, - def {dbBrig = setFederationConfig} + [ def {brigCfg = setFederationConfig}, + def {brigCfg = setFederationConfig} ] $ \dynDomains -> do domains@[domainA, domainB] <- pure dynDomains @@ -342,7 +342,7 @@ testAddMembersNonFullyConnectedProteus = do testConvWithUnreachableRemoteUsers :: HasCallStack => App () testConvWithUnreachableRemoteUsers = do let overrides = - def {dbBrig = setField "optSettings.setFederationStrategy" "allowAll"} + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} <> fullSearchWithAll ([alice, alex, bob, charlie, dylan], domains) <- startDynamicBackends [overrides, overrides] $ \domains -> do @@ -363,7 +363,7 @@ testConvWithUnreachableRemoteUsers = do testAddReachableWithUnreachableRemoteUsers :: HasCallStack => App () testAddReachableWithUnreachableRemoteUsers = do let overrides = - def {dbBrig = setField "optSettings.setFederationStrategy" "allowAll"} + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} <> fullSearchWithAll ([alex, bob], conv, domains) <- startDynamicBackends [overrides, overrides] $ \domains -> do @@ -389,7 +389,7 @@ testAddReachableWithUnreachableRemoteUsers = do testAddUnreachable :: HasCallStack => App () testAddUnreachable = do let overrides = - def {dbBrig = setField "optSettings.setFederationStrategy" "allowAll"} + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} <> fullSearchWithAll ([alex, charlie], [charlieDomain, dylanDomain], conv) <- startDynamicBackends [overrides, overrides] $ \domains -> do @@ -412,7 +412,7 @@ testAddingUserNonFullyConnectedFederation :: HasCallStack => App () testAddingUserNonFullyConnectedFederation = do let overrides = def - { dbBrig = + { brigCfg = setField "optSettings.setFederationStrategy" "allowDynamic" >=> removeField "optSettings.setFederationDomainConfigs" } diff --git a/integration/test/Test/Defederation.hs b/integration/test/Test/Defederation.hs index 73399d96280..513efe535f6 100644 --- a/integration/test/Test/Defederation.hs +++ b/integration/test/Test/Defederation.hs @@ -33,9 +33,9 @@ testDefederationNonFullyConnectedGraph = do >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends - [ def {dbBrig = setFederationConfig}, - def {dbBrig = setFederationConfig}, - def {dbBrig = setFederationConfig} + [ def {brigCfg = setFederationConfig}, + def {brigCfg = setFederationConfig}, + def {brigCfg = setFederationConfig} ] $ \dynDomains -> do domains@[domainA, domainB, domainC] <- pure dynDomains diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 45d240c1548..69d3cf1b0c4 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -5,9 +5,7 @@ import API.Brig qualified as Public import API.BrigInternal qualified as Internal import API.GalleyInternal qualified as Internal import API.Nginz qualified as Nginz -import Control.Monad.Codensity import Control.Monad.Cont -import Data.Map qualified as Map import GHC.Stack import SetupHelpers import Testlib.Prelude @@ -34,11 +32,10 @@ testDeleteUnknownClient = do testModifiedBrig :: HasCallStack => App () testModifiedBrig = do - withModifiedService - Brig - (setField "optSettings.setFederationDomain" "overridden.example.com") - $ \_domain -> do - bindResponse (Public.getAPIVersion OwnDomain) + withModifiedBackend + (def {brigCfg = setField "optSettings.setFederationDomain" "overridden.example.com"}) + $ \domain -> do + bindResponse (Public.getAPIVersion domain) $ \resp -> do resp.status `shouldMatchInt` 200 (resp.json %. "domain") `shouldMatch` "overridden.example.com" @@ -47,54 +44,56 @@ testModifiedGalley :: HasCallStack => App () testModifiedGalley = do (_user, tid) <- createTeam OwnDomain - let getFeatureStatus = do - bindResponse (Internal.getTeamFeature "searchVisibility" tid) $ \res -> do + let getFeatureStatus :: (MakesValue domain) => domain -> String -> App Value + getFeatureStatus domain team = do + bindResponse (Internal.getTeamFeature domain "searchVisibility" team) $ \res -> do res.status `shouldMatchInt` 200 res.json %. "status" - do - getFeatureStatus `shouldMatch` "disabled" + getFeatureStatus OwnDomain tid `shouldMatch` "disabled" - withModifiedService - Galley - (setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default") - $ \_ -> getFeatureStatus `shouldMatch` "enabled" + withModifiedBackend + def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default"} + $ \domain -> do + (_user, tid') <- createTeam domain + getFeatureStatus domain tid' `shouldMatch` "enabled" testModifiedCannon :: HasCallStack => App () testModifiedCannon = do - withModifiedService Cannon pure $ \_ -> pure () + withModifiedBackend def $ \_ -> pure () testModifiedGundeck :: HasCallStack => App () testModifiedGundeck = do - withModifiedService Gundeck pure $ \_ -> pure () + withModifiedBackend def $ \_ -> pure () testModifiedCargohold :: HasCallStack => App () testModifiedCargohold = do - withModifiedService Cargohold pure $ \_ -> pure () + withModifiedBackend def $ \_ -> pure () testModifiedSpar :: HasCallStack => App () testModifiedSpar = do - withModifiedService Spar pure $ \_ -> pure () + withModifiedBackend def $ \_ -> pure () testModifiedServices :: HasCallStack => App () testModifiedServices = do let serviceMap = - Map.fromList - [ (Brig, setField "optSettings.setFederationDomain" "overridden.example.com"), - (Galley, setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default") - ] - runCodensity (withModifiedServices serviceMap) $ \_domain -> do - (_user, tid) <- createTeam OwnDomain - bindResponse (Internal.getTeamFeature "searchVisibility" tid) $ \res -> do + def + { brigCfg = setField "optSettings.setFederationDomain" "overridden.example.com", + galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default" + } + + withModifiedBackend serviceMap $ \domain -> do + (_user, tid) <- createTeam domain + bindResponse (Internal.getTeamFeature domain "searchVisibility" tid) $ \res -> do res.status `shouldMatchInt` 200 res.json %. "status" `shouldMatch` "enabled" - bindResponse (Public.getAPIVersion OwnDomain) $ + bindResponse (Public.getAPIVersion domain) $ \resp -> do resp.status `shouldMatchInt` 200 (resp.json %. "domain") `shouldMatch` "overridden.example.com" - bindResponse (Nginz.getSystemSettingsUnAuthorized OwnDomain) $ + bindResponse (Nginz.getSystemSettingsUnAuthorized domain) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "setRestrictUserCreation" `shouldMatch` False diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs index d7cf2c3a1e3..a16b3e9077f 100644 --- a/integration/test/Test/Federation.hs +++ b/integration/test/Test/Federation.hs @@ -30,7 +30,7 @@ testNotificationsForOfflineBackends = do -- We call it 'downBackend' because it is down for the most of this test -- except for setup and assertions. Perhaps there is a better name. runCodensity (acquireResources 1 resourcePool) $ \[downBackend] -> do - (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) <- runCodensity (startDynamicBackend downBackend mempty mempty) $ \_ -> do + (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) <- runCodensity (startDynamicBackend downBackend mempty) $ \_ -> do downUser1 <- randomUser downBackend.berDomain def downUser2 <- randomUser downBackend.berDomain def downClient1 <- objId $ bindResponse (API.addClient downUser1 def) $ getJSON 201 @@ -103,7 +103,7 @@ testNotificationsForOfflineBackends = do delUserDeletedNotif <- nPayload $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDeleteUserNotif objQid delUserDeletedNotif `shouldMatch` objQid delUser - runCodensity (startDynamicBackend downBackend mempty mempty) $ \_ -> do + runCodensity (startDynamicBackend downBackend mempty) $ \_ -> do newMsgNotif <- awaitNotification downUser1 downClient1 noValue 5 isNewMessageNotif newMsgNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv newMsgNotif %. "payload.0.data.text" `shouldMatchBase64` "success message for down user" diff --git a/integration/test/Testlib/App.hs b/integration/test/Testlib/App.hs index a7018959898..b219f3da9e1 100644 --- a/integration/test/Testlib/App.hs +++ b/integration/test/Testlib/App.hs @@ -12,6 +12,7 @@ import GHC.Stack (HasCallStack) import System.FilePath import Testlib.Env import Testlib.JSON +import Testlib.Service import Testlib.Types import Prelude diff --git a/integration/test/Testlib/Cannon.hs b/integration/test/Testlib/Cannon.hs index 4fefd3dfe3f..162dd58fb0f 100644 --- a/integration/test/Testlib/Cannon.hs +++ b/integration/test/Testlib/Cannon.hs @@ -68,6 +68,7 @@ import Testlib.Env import Testlib.HTTP import Testlib.JSON import Testlib.Printing +import Testlib.Service import Testlib.Types import Prelude diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index d0a6a541850..1390ceed213 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -23,6 +23,7 @@ import System.IO import System.IO.Temp import Testlib.Prekeys import Testlib.ResourcePool +import Testlib.Service import Prelude -- | Initialised once per test. @@ -107,42 +108,6 @@ data HostPort = HostPort instance FromJSON HostPort -data Service = Brig | Galley | Cannon | Gundeck | Cargohold | Nginz | Spar | BackgroundWorker | Stern | FederatorInternal - deriving - ( Show, - Eq, - Ord, - Enum, - Bounded - ) - -serviceName :: Service -> String -serviceName = \case - Brig -> "brig" - Galley -> "galley" - Cannon -> "cannon" - Gundeck -> "gundeck" - Cargohold -> "cargohold" - Nginz -> "nginz" - Spar -> "spar" - BackgroundWorker -> "backgroundWorker" - Stern -> "stern" - FederatorInternal -> "federator" - --- | Converts the service name to kebab-case. -configName :: Service -> String -configName = \case - Brig -> "brig" - Galley -> "galley" - Cannon -> "cannon" - Gundeck -> "gundeck" - Cargohold -> "cargohold" - Nginz -> "nginz" - Spar -> "spar" - BackgroundWorker -> "background-worker" - Stern -> "stern" - FederatorInternal -> "federator" - serviceHostPort :: ServiceMap -> Service -> HostPort serviceHostPort m Brig = m.brig serviceHostPort m Galley = m.galley diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index 5dde2f9a187..e07bac156a9 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -25,6 +25,7 @@ import Network.URI (URI (..), URIAuth (..), parseURI) import Testlib.Assertions import Testlib.Env import Testlib.JSON +import Testlib.Service import Testlib.Types import Prelude diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 44606a6f238..5b2ce20dc5e 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -1,15 +1,13 @@ {-# LANGUAGE OverloadedStrings #-} module Testlib.ModService - ( withModifiedService, - withModifiedServices, + ( withModifiedBackend, startDynamicBackend, startDynamicBackends, traverseConcurrentlyCodensity, ) where -import Control.Applicative ((<|>)) import Control.Concurrent import Control.Concurrent.Async import Control.Exception (finally) @@ -20,8 +18,7 @@ import Control.Monad.Extra import Control.Monad.Reader import Control.Retry (fibonacciBackoff, limitRetriesByCumulativeDelay, retrying) import Data.Aeson hiding ((.=)) -import Data.Attoparsec.ByteString.Char8 -import Data.Either.Extra (eitherToMaybe) +import Data.Default import Data.Foldable import Data.Function import Data.Functor @@ -37,11 +34,9 @@ import Data.Word (Word16) import Data.Yaml qualified as Yaml import GHC.Stack import Network.HTTP.Client qualified as HTTP -import Network.Socket qualified as N import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, listDirectory, removeDirectoryRecursive, removeFile) import System.FilePath import System.IO -import System.IO.Error qualified as Error import System.IO.Temp (createTempDirectory, writeTempFile) import System.Posix (killProcess, signalProcess) import System.Process (CreateProcess (..), ProcessHandle, StdStream (..), createProcess, getPid, proc, terminateProcess, waitForProcess) @@ -52,17 +47,14 @@ import Testlib.HTTP import Testlib.JSON import Testlib.Printing import Testlib.ResourcePool +import Testlib.Service import Testlib.Types import Text.RawString.QQ import Prelude -withModifiedService :: - Service -> - -- | function that edits the config - (Value -> App Value) -> - (String -> App a) -> - App a -withModifiedService srv modConfig = runCodensity $ withModifiedServices (Map.singleton srv modConfig) +withModifiedBackend :: HasCallStack => ServiceOverrides -> (HasCallStack => String -> App a) -> App a +withModifiedBackend overrides k = + startDynamicBackends [overrides] (\domains -> k (head domains)) copyDirectoryRecursively :: FilePath -> FilePath -> IO () copyDirectoryRecursively from to = do @@ -145,169 +137,108 @@ startDynamicBackends beOverrides k = when (Prelude.length beOverrides > 3) $ lift $ failApp "Too many backends. Currently only 3 are supported." pool <- asks (.resourcePool) resources <- acquireResources (Prelude.length beOverrides) pool - void $ traverseConcurrentlyCodensity (\(res, overrides) -> startDynamicBackend res mempty overrides) (zip resources beOverrides) + void $ traverseConcurrentlyCodensity (uncurry startDynamicBackend) (zip resources beOverrides) pure $ map (.berDomain) resources ) k -startDynamicBackend :: HasCallStack => BackendResource -> Map.Map Service Word16 -> ServiceOverrides -> Codensity App (Env -> Env) -startDynamicBackend resource staticPorts beOverrides = do - defDomain <- asks (.domain1) - let services = - withOverrides beOverrides $ - Map.mapWithKey - ( \srv conf -> - conf - >=> setKeyspace srv - >=> setEsIndex srv - >=> setFederationSettings srv - >=> setAwsConfigs srv - >=> setLogLevel srv - ) - defaultServiceOverridesToMap - startBackend - resource.berDomain - staticPorts - (Just resource.berNginzSslPort) - (Just setFederatorConfig) - services - ( \ports sm -> do - let templateBackend = fromMaybe (error "no default domain found in backends") $ sm & Map.lookup defDomain - in Map.insert resource.berDomain (setFederatorPorts resource $ updateServiceMap ports templateBackend) sm - ) +startDynamicBackend :: HasCallStack => BackendResource -> ServiceOverrides -> Codensity App (Env -> Env) +startDynamicBackend resource beOverrides = do + let overrides = + mconcat + [ setKeyspace, + setEsIndex, + setFederationSettings, + setAwsConfigs, + setLogLevel, + beOverrides + ] + startBackend resource overrides allServices where - setAwsConfigs :: Service -> Value -> App Value - setAwsConfigs = \case - Brig -> - setField "aws.userJournalQueue" resource.berAwsUserJournalQueue - >=> setField "aws.prekeyTable" resource.berAwsPrekeyTable - >=> setField "internalEvents.queueName" resource.berBrigInternalEvents - >=> setField "emailSMS.email.sesQueue" resource.berEmailSMSSesQueue - >=> setField "emailSMS.general.emailSender" resource.berEmailSMSEmailSender - Cargohold -> setField "aws.s3Bucket" resource.berAwsS3Bucket - Gundeck -> setField "aws.queueName" resource.berAwsQueueName - Galley -> - setField "journal.queueName" resource.berGalleyJournal - >=> setField "rabbitmq.vHost" resource.berVHost - BackgroundWorker -> setField "rabbitmq.vHost" resource.berVHost - _ -> pure - - setFederationSettings :: Service -> Value -> App Value + setAwsConfigs :: ServiceOverrides + setAwsConfigs = + def + { brigCfg = + setField "aws.userJournalQueue" resource.berAwsUserJournalQueue + >=> setField "aws.prekeyTable" resource.berAwsPrekeyTable + >=> setField "internalEvents.queueName" resource.berBrigInternalEvents + >=> setField "emailSMS.email.sesQueue" resource.berEmailSMSSesQueue + >=> setField "emailSMS.general.emailSender" resource.berEmailSMSEmailSender, + cargoholdCfg = setField "aws.s3Bucket" resource.berAwsS3Bucket, + gundeckCfg = setField "aws.queueName" resource.berAwsQueueName, + galleyCfg = setField "journal.queueName" resource.berGalleyJournal + } + + setFederationSettings :: ServiceOverrides setFederationSettings = - \case - Brig -> - setField "optSettings.setFederationDomain" resource.berDomain - >=> setField "optSettings.setFederationDomainConfigs" ([] :: [Value]) - >=> setField "federatorInternal.port" resource.berFederatorInternal - >=> setField "federatorInternal.host" ("127.0.0.1" :: String) - >=> setField "rabbitmq.vHost" resource.berVHost - Cargohold -> - setField "settings.federationDomain" resource.berDomain - >=> setField "federator.host" ("127.0.0.1" :: String) - >=> setField "federator.port" resource.berFederatorInternal - Galley -> - setField "settings.federationDomain" resource.berDomain - >=> setField "settings.featureFlags.classifiedDomains.config.domains" [resource.berDomain] - >=> setField "federator.host" ("127.0.0.1" :: String) - >=> setField "federator.port" resource.berFederatorInternal - >=> setField "rabbitmq.vHost" resource.berVHost - Gundeck -> setField "settings.federationDomain" resource.berDomain - BackgroundWorker -> - setField "federatorInternal.port" resource.berFederatorInternal - >=> setField "federatorInternal.host" ("127.0.0.1" :: String) - >=> setField "rabbitmq.vHost" resource.berVHost - _ -> pure - - setFederatorConfig :: Value -> App Value - setFederatorConfig = - setField "federatorInternal.port" resource.berFederatorInternal - >=> setField "federatorExternal.port" resource.berFederatorExternal - >=> setField "optSettings.setFederationDomain" resource.berDomain - - setKeyspace :: Service -> Value -> App Value - setKeyspace = \case - Galley -> setField "cassandra.keyspace" resource.berGalleyKeyspace - Brig -> setField "cassandra.keyspace" resource.berBrigKeyspace - Spar -> setField "cassandra.keyspace" resource.berSparKeyspace - Gundeck -> setField "cassandra.keyspace" resource.berGundeckKeyspace - -- other services do not have a DB - _ -> pure - - setEsIndex :: Service -> Value -> App Value - setEsIndex = \case - Brig -> setField "elasticsearch.index" resource.berElasticsearchIndex - -- other services do not have an ES index - _ -> pure - - setLogLevel :: Service -> Value -> App Value - setLogLevel = \case - Spar -> setField "saml.logLevel" ("Warn" :: String) - _ -> setField "logLevel" ("Warn" :: String) - -setFederatorPorts :: BackendResource -> ServiceMap -> ServiceMap -setFederatorPorts resource sm = - sm - { federatorInternal = sm.federatorInternal {host = "127.0.0.1", port = resource.berFederatorInternal}, - federatorExternal = sm.federatorExternal {host = "127.0.0.1", port = resource.berFederatorExternal} - } - -withModifiedServices :: Map.Map Service (Value -> App Value) -> Codensity App String -withModifiedServices services = do - domain <- lift $ asks (.domain1) - void $ - startBackend domain mempty Nothing Nothing services (\ports -> Map.adjust (updateServiceMap ports) domain) - pure domain - -updateServiceMap :: Map.Map Service Word16 -> ServiceMap -> ServiceMap -updateServiceMap ports serviceMap = - Map.foldrWithKey - ( \srv newPort sm -> - case srv of - Brig -> sm {brig = sm.brig {host = "127.0.0.1", port = newPort}} - Galley -> sm {galley = sm.galley {host = "127.0.0.1", port = newPort}} - Cannon -> sm {cannon = sm.cannon {host = "127.0.0.1", port = newPort}} - Gundeck -> sm {gundeck = sm.gundeck {host = "127.0.0.1", port = newPort}} - Cargohold -> sm {cargohold = sm.cargohold {host = "127.0.0.1", port = newPort}} - Nginz -> sm {nginz = sm.nginz {host = "127.0.0.1", port = newPort}} - Spar -> sm {spar = sm.spar {host = "127.0.0.1", port = newPort}} - BackgroundWorker -> sm {backgroundWorker = sm.backgroundWorker {host = "127.0.0.1", port = newPort}} - Stern -> sm {stern = sm.stern {host = "127.0.0.1", port = newPort}} - FederatorInternal -> sm {federatorInternal = sm.federatorInternal {host = "127.0.0.1", port = newPort}} - ) - serviceMap - ports + def + { brigCfg = + setField "optSettings.setFederationDomain" resource.berDomain + >=> setField "optSettings.setFederationDomainConfigs" ([] :: [Value]) + >=> setField "federatorInternal.port" resource.berFederatorInternal + >=> setField "federatorInternal.host" ("127.0.0.1" :: String) + >=> setField "rabbitmq.vHost" resource.berVHost, + cargoholdCfg = + setField "settings.federationDomain" resource.berDomain + >=> setField "federator.host" ("127.0.0.1" :: String) + >=> setField "federator.port" resource.berFederatorInternal, + galleyCfg = + setField "settings.federationDomain" resource.berDomain + >=> setField "settings.featureFlags.classifiedDomains.config.domains" [resource.berDomain] + >=> setField "federator.host" ("127.0.0.1" :: String) + >=> setField "federator.port" resource.berFederatorInternal + >=> setField "rabbitmq.vHost" resource.berVHost, + gundeckCfg = setField "settings.federationDomain" resource.berDomain, + backgroundWorkerCfg = + setField "federatorInternal.port" resource.berFederatorInternal + >=> setField "federatorInternal.host" ("127.0.0.1" :: String) + >=> setField "rabbitmq.vHost" resource.berVHost, + federatorInternalCfg = + setField "federatorInternal.port" resource.berFederatorInternal + >=> setField "federatorExternal.port" resource.berFederatorExternal + >=> setField "optSettings.setFederationDomain" resource.berDomain + } + + setKeyspace :: ServiceOverrides + setKeyspace = + def + { galleyCfg = setField "cassandra.keyspace" resource.berGalleyKeyspace, + brigCfg = setField "cassandra.keyspace" resource.berBrigKeyspace, + sparCfg = setField "cassandra.keyspace" resource.berSparKeyspace, + gundeckCfg = setField "cassandra.keyspace" resource.berGundeckKeyspace + } + + setEsIndex :: ServiceOverrides + setEsIndex = + def + { brigCfg = setField "elasticsearch.index" resource.berElasticsearchIndex + } + + setLogLevel :: ServiceOverrides + setLogLevel = + def + { sparCfg = setField "saml.logLevel" ("Warn" :: String), + brigCfg = setField "logLevel" ("Warn" :: String), + cannonCfg = setField "logLevel" ("Warn" :: String), + cargoholdCfg = setField "logLevel" ("Warn" :: String), + galleyCfg = setField "logLevel" ("Warn" :: String), + gundeckCfg = setField "logLevel" ("Warn" :: String), + nginzCfg = setField "logLevel" ("Warn" :: String), + backgroundWorkerCfg = setField "logLevel" ("Warn" :: String), + sternCfg = setField "logLevel" ("Warn" :: String), + federatorInternalCfg = setField "logLevel" ("Warn" :: String) + } startBackend :: HasCallStack => - String -> - Map.Map Service Word16 -> - Maybe Word16 -> - Maybe (Value -> App Value) -> - Map.Map Service (Value -> App Value) -> - (Map.Map Service Word16 -> Map.Map String ServiceMap -> Map.Map String ServiceMap) -> + BackendResource -> + ServiceOverrides -> + [Service] -> Codensity App (Env -> Env) -startBackend domain staticPorts nginzSslPort mFederatorOverrides services modifyBackends = do - -- We already close sockets before starting any services that want to bind to - -- it, because if done later some services might already connect to the - -- dummy sockets (e.g. federator connecting to nginz) and blocking the ports - -- from being bindable - ports <- - Map.traverseWithKey - ( \srv _ -> - case Map.lookup srv staticPorts of - Just port -> pure port - Nothing -> do - (port, sock) <- liftIO openFreePort - liftIO $ N.close sock - pure (fromIntegral port) - ) - services - nginzHttp2Port <- liftIO $ do - (port, sock) <- openFreePort - N.close sock - pure (fromIntegral port) +startBackend resource overrides services = do + let domain = resource.berDomain - let updateServiceMapInConfig :: Maybe Service -> Value -> App Value + let updateServiceMapInConfig :: Service -> Value -> App Value updateServiceMapInConfig forSrv config = foldlM ( \c (srv, port) -> do @@ -323,7 +254,7 @@ startBackend domain staticPorts nginzSslPort mFederatorOverrides services modify ) ) case (srv, forSrv) of - (Spar, Just Spar) -> do + (Spar, Spar) -> do overridden -- FUTUREWORK: override "saml.spAppUri" and "saml.spSsoUri" with correct port, too? & setField "saml.spHost" ("127.0.0.1" :: String) @@ -331,63 +262,63 @@ startBackend domain staticPorts nginzSslPort mFederatorOverrides services modify _ -> pure overridden ) config - (Map.assocs ports) - - -- close all sockets before starting the services - stopInstances <- lift $ do - fedInstance <- - case mFederatorOverrides of - Nothing -> pure [] - Just override -> - readServiceConfig' "federator" - >>= updateServiceMapInConfig Nothing - >>= override - >>= startProcess' domain "federator" - <&> (: []) - - otherInstances <- for (Map.assocs $ Map.filterWithKey (\s _ -> s /= FederatorInternal) services) $ \case - (Nginz, _) -> do + [(srv, berInternalServicePorts resource srv :: Int) | srv <- services] + + let serviceMap = + let g srv = HostPort "127.0.0.1" (berInternalServicePorts resource srv) + in ServiceMap + { brig = g Brig, + backgroundWorker = g BackgroundWorker, + cannon = g Cannon, + cargohold = g Cargohold, + federatorInternal = g FederatorInternal, + federatorExternal = HostPort "127.0.0.1" resource.berFederatorExternal, + galley = g Galley, + gundeck = g Gundeck, + nginz = g Nginz, + spar = g Spar, + -- FUTUREWORK: Set to g Proxy, when we add Proxy to spawned services + proxy = HostPort "127.0.0.1" 9087, + stern = g Stern + } + + instances <- lift $ do + for services $ \case + Nginz -> do env <- ask - sm <- maybe (failApp "the impossible in withServices happened") pure (Map.lookup domain (modifyBackends (fromIntegral <$> ports) env.serviceMap)) - port <- maybe (failApp "the impossible in withServices happened") (pure . fromIntegral) (Map.lookup Nginz ports) case env.servicesCwdBase of - Nothing -> startNginzK8s domain sm - Just _ -> startNginzLocal domain port nginzHttp2Port nginzSslPort sm - (srv, modifyConfig) -> do + Nothing -> startNginzK8s domain serviceMap + Just _ -> startNginzLocal domain resource.berNginzSslPort resource.berNginzSslPort serviceMap + srv -> do readServiceConfig srv - >>= updateServiceMapInConfig (Just srv) - >>= modifyConfig + >>= updateServiceMapInConfig srv + >>= lookupConfigOverride overrides srv >>= startProcess domain srv - let instances = fedInstance <> otherInstances - - let stopInstances = liftIO $ do - -- Running waitForProcess would hang for 30 seconds when the test suite - -- is run from within ghci, so we don't wait here. - for_ instances $ \(ph, path) -> do - terminateProcess ph - timeout 50000 (waitForProcess ph) >>= \case - Just _ -> pure () - Nothing -> do - timeout 100000 (waitForProcess ph) >>= \case - Just _ -> pure () - Nothing -> do - mPid <- getPid ph - for_ mPid (signalProcess killProcess) - void $ waitForProcess ph - whenM (doesFileExist path) $ removeFile path - whenM (doesDirectoryExist path) $ removeDirectoryRecursive path - - pure stopInstances - - let modifyEnv env = - env {serviceMap = modifyBackends (fromIntegral <$> ports) env.serviceMap} + let stopInstances = liftIO $ do + -- Running waitForProcess would hang for 30 seconds when the test suite + -- is run from within ghci, so we don't wait here. + for_ instances $ \(ph, path) -> do + terminateProcess ph + timeout 50000 (waitForProcess ph) >>= \case + Just _ -> pure () + Nothing -> do + timeout 100000 (waitForProcess ph) >>= \case + Just _ -> pure () + Nothing -> do + mPid <- getPid ph + for_ mPid (signalProcess killProcess) + void $ waitForProcess ph + whenM (doesFileExist path) $ removeFile path + whenM (doesDirectoryExist path) $ removeDirectoryRecursive path + + let modifyEnv env = env {serviceMap = Map.insert resource.berDomain serviceMap env.serviceMap} Codensity $ \action -> local modifyEnv $ do waitForService <- appToIOKleisli (waitUntilServiceUp domain) ioAction <- appToIO (action ()) liftIO $ - (mapConcurrently_ waitForService (Map.keys ports) >> ioAction) + (mapConcurrently_ waitForService services >> ioAction) `finally` stopInstances pure modifyEnv @@ -455,29 +386,6 @@ waitUntilServiceUp domain = \case unless isUp $ failApp ("Time out for service " <> show srv <> " to come up") --- | Open a TCP socket on a random free port. This is like 'warp''s --- openFreePort. --- --- Since 0.0.0.1 -openFreePort :: IO (Int, N.Socket) -openFreePort = - E.bracketOnError (N.socket N.AF_INET N.Stream N.defaultProtocol) N.close $ - \sock -> do - N.bind sock $ N.SockAddrInet 0 $ N.tupleToHostAddress (127, 0, 0, 1) - N.getSocketName sock >>= \case - N.SockAddrInet port _ -> do - pure (fromIntegral port, sock) - addr -> - E.throwIO $ - Error.mkIOError - Error.userErrorType - ( "openFreePort was unable to create socket with a SockAddrInet. " - <> "Got " - <> show addr - ) - Nothing - Nothing - startNginzK8s :: String -> ServiceMap -> App (ProcessHandle, FilePath) startNginzK8s domain sm = do tmpDir <- liftIO $ createTempDirectory "/tmp" ("nginz" <> "-" <> domain) @@ -501,8 +409,8 @@ startNginzK8s domain sm = do ph <- startNginz domain nginxConfFile "/" pure (ph, tmpDir) -startNginzLocal :: String -> Word16 -> Word16 -> Maybe Word16 -> ServiceMap -> App (ProcessHandle, FilePath) -startNginzLocal domain localPort http2Port mSslPort sm = do +startNginzLocal :: String -> Word16 -> Word16 -> ServiceMap -> App (ProcessHandle, FilePath) +startNginzLocal domain http2Port sslPort sm = do -- Create a whole temporary directory and copy all nginx's config files. -- This is necessary because nginx assumes local imports are relative to -- the location of the main configuration file. @@ -527,25 +435,6 @@ startNginzLocal domain localPort http2Port mSslPort sm = do & Text.replace "access_log /dev/stdout" "access_log /dev/null" ) - conf <- Prelude.lines <$> liftIO (readFile integrationConfFile) - let sslPortParser = do - _ <- string "listen" - _ <- many1 space - p <- many1 digit - _ <- many1 space - _ <- string "ssl" - _ <- many1 space - _ <- string "http2" - _ <- many1 space - _ <- char ';' - pure (read p :: Word16) - - let mParsedPort = - mapMaybe (eitherToMaybe . parseOnly sslPortParser . cs) conf - & (\case [] -> Nothing; (p : _) -> Just p) - - sslPort <- maybe (failApp "could not determine nginz's ssl port") pure (mSslPort <|> mParsedPort) - -- override port configuration let portConfigTemplate = [r|listen {localPort}; @@ -555,7 +444,7 @@ listen [::]:{ssl_port} ssl http2; |] let portConfig = portConfigTemplate - & Text.replace "{localPort}" (cs $ show localPort) + & Text.replace "{localPort}" (cs $ show (sm.nginz.port)) & Text.replace "{http2_port}" (cs $ show http2Port) & Text.replace "{ssl_port}" (cs $ show sslPort) diff --git a/integration/test/Testlib/Ports.hs b/integration/test/Testlib/Ports.hs new file mode 100644 index 00000000000..05f48483c2b --- /dev/null +++ b/integration/test/Testlib/Ports.hs @@ -0,0 +1,39 @@ +module Testlib.Ports where + +import Testlib.Service qualified as Service +import Prelude + +data PortNamespace + = NginzSSL + | NginzHttp2 + | FederatorExternal + | ServiceInternal Service.Service + +port :: Num a => PortNamespace -> Service.BackendName -> a +port NginzSSL bn = mkPort 8443 bn +port NginzHttp2 bn = mkPort 8099 bn +port FederatorExternal bn = mkPort 8098 bn +port (ServiceInternal Service.BackgroundWorker) bn = mkPort 8089 bn +port (ServiceInternal Service.Brig) bn = mkPort 8082 bn +port (ServiceInternal Service.Cannon) bn = mkPort 8083 bn +port (ServiceInternal Service.Cargohold) bn = mkPort 8084 bn +port (ServiceInternal Service.FederatorInternal) bn = mkPort 8097 bn +port (ServiceInternal Service.Galley) bn = mkPort 8085 bn +port (ServiceInternal Service.Gundeck) bn = mkPort 8086 bn +port (ServiceInternal Service.Nginz) bn = mkPort 8080 bn +port (ServiceInternal Service.Spar) bn = mkPort 8088 bn +port (ServiceInternal Service.Stern) bn = mkPort 8091 bn + +portForDyn :: Num a => PortNamespace -> Int -> a +portForDyn ns i = port ns (Service.DynamicBackend i) + +mkPort :: Num a => Int -> Service.BackendName -> a +mkPort basePort bn = + let i = case bn of + Service.BackendA -> 0 + Service.BackendB -> 1 + (Service.DynamicBackend k) -> 1 + k + in fromIntegral basePort + (fromIntegral i) * 1000 + +internalServicePorts :: Num a => Service.BackendName -> Service.Service -> a +internalServicePorts backend service = port (ServiceInternal service) backend diff --git a/integration/test/Testlib/Prelude.hs b/integration/test/Testlib/Prelude.hs index 05a04f366a3..ce21e7ac6f0 100644 --- a/integration/test/Testlib/Prelude.hs +++ b/integration/test/Testlib/Prelude.hs @@ -8,6 +8,7 @@ module Testlib.Prelude module Testlib.HTTP, module Testlib.JSON, module Testlib.PTest, + module Testlib.Service, module Data.Aeson, module Prelude, module Control.Applicative, @@ -120,6 +121,7 @@ import Testlib.HTTP import Testlib.JSON import Testlib.ModService import Testlib.PTest +import Testlib.Service import Testlib.Types import UnliftIO.Exception import Prelude diff --git a/integration/test/Testlib/ResourcePool.hs b/integration/test/Testlib/ResourcePool.hs index ae498c4eabf..06b05f17904 100644 --- a/integration/test/Testlib/ResourcePool.hs +++ b/integration/test/Testlib/ResourcePool.hs @@ -5,6 +5,8 @@ module Testlib.ResourcePool backendResources, createBackendResourcePool, acquireResources, + backendA, + backendB, ) where @@ -23,6 +25,8 @@ import Data.Word import GHC.Generics import GHC.Stack (HasCallStack) import System.IO +import Testlib.Ports qualified as Ports +import Testlib.Service import Prelude data ResourcePool a = ResourcePool @@ -52,7 +56,8 @@ createBackendResourcePool dynConfs = <*> newIORef resources data BackendResource = BackendResource - { berBrigKeyspace :: String, + { berName :: BackendName, + berBrigKeyspace :: String, berGalleyKeyspace :: String, berSparKeyspace :: String, berGundeckKeyspace :: String, @@ -69,9 +74,16 @@ data BackendResource = BackendResource berEmailSMSEmailSender :: String, berGalleyJournal :: String, berVHost :: String, - berNginzSslPort :: Word16 + berNginzSslPort :: Word16, + berNginzHttp2Port :: Word16, + berInternalServicePorts :: forall a. Num a => Service -> a } - deriving (Show, Eq, Ord) + +instance Eq BackendResource where + a == b = a.berName == b.berName + +instance Ord BackendResource where + a `compare` b = a.berName `compare` b.berName data DynamicBackendConfig = DynamicBackendConfig { domain :: String, @@ -85,35 +97,87 @@ backendResources :: [DynamicBackendConfig] -> Set.Set BackendResource backendResources dynConfs = (zip dynConfs [1 ..]) <&> ( \(dynConf, i) -> - BackendResource - { berBrigKeyspace = "brig_test_dyn_" <> show i, - berGalleyKeyspace = "galley_test_dyn_" <> show i, - berSparKeyspace = "spar_test_dyn_" <> show i, - berGundeckKeyspace = "gundeck_test_dyn_" <> show i, - berElasticsearchIndex = "directory_dyn_" <> show i <> "_test", - berFederatorInternal = federatorInternalPort i, - berFederatorExternal = dynConf.federatorExternalPort, - berDomain = dynConf.domain, - berAwsUserJournalQueue = "integration-user-events.fifo" <> suffix i, - berAwsPrekeyTable = "integration-brig-prekeys" <> suffix i, - berAwsS3Bucket = "dummy-bucket" <> suffix i, - berAwsQueueName = "integration-gundeck-events" <> suffix i, - berBrigInternalEvents = "integration-brig-events-internal" <> suffix i, - berEmailSMSSesQueue = "integration-brig-events" <> suffix i, - berEmailSMSEmailSender = "backend-integration" <> suffix i <> "@wire.com", - berGalleyJournal = "integration-team-events.fifo" <> suffix i, - berVHost = dynConf.domain, - berNginzSslPort = mkNginzSslPort i - } + let name = DynamicBackend i + in BackendResource + { berName = name, + berBrigKeyspace = "brig_test_dyn_" <> show i, + berGalleyKeyspace = "galley_test_dyn_" <> show i, + berSparKeyspace = "spar_test_dyn_" <> show i, + berGundeckKeyspace = "gundeck_test_dyn_" <> show i, + berElasticsearchIndex = "directory_dyn_" <> show i <> "_test", + berFederatorInternal = Ports.portForDyn (Ports.ServiceInternal FederatorInternal) i, + berFederatorExternal = dynConf.federatorExternalPort, + berDomain = dynConf.domain, + berAwsUserJournalQueue = "integration-user-events.fifo" <> suffix i, + berAwsPrekeyTable = "integration-brig-prekeys" <> suffix i, + berAwsS3Bucket = "dummy-bucket" <> suffix i, + berAwsQueueName = "integration-gundeck-events" <> suffix i, + berBrigInternalEvents = "integration-brig-events-internal" <> suffix i, + berEmailSMSSesQueue = "integration-brig-events" <> suffix i, + berEmailSMSEmailSender = "backend-integration" <> suffix i <> "@wire.com", + berGalleyJournal = "integration-team-events.fifo" <> suffix i, + berVHost = dynConf.domain, + berNginzSslPort = Ports.portForDyn Ports.NginzSSL i, + berNginzHttp2Port = Ports.portForDyn Ports.NginzHttp2 i, + berInternalServicePorts = Ports.internalServicePorts name + } ) & Set.fromList where - suffix :: Word16 -> String + suffix :: (Show a, Num a) => a -> String suffix i = show $ i + 2 - mkNginzSslPort :: Word16 -> Word16 - mkNginzSslPort i = 8443 + ((1 + i) * 1000) +backendA :: BackendResource +backendA = + BackendResource + { berName = BackendA, + berBrigKeyspace = "brig_test", + berGalleyKeyspace = "galley_test", + berSparKeyspace = "spar_test", + berGundeckKeyspace = "gundeck_test", + berElasticsearchIndex = "directory_test", + berFederatorInternal = Ports.port (Ports.ServiceInternal FederatorInternal) BackendA, + berFederatorExternal = Ports.port Ports.FederatorExternal BackendA, + berDomain = "example.com", + berAwsUserJournalQueue = "integration-user-events.fifo", + berAwsPrekeyTable = "integration-brig-prekeys", + berAwsS3Bucket = "dummy-bucket", + berAwsQueueName = "integration-gundeck-events", + berBrigInternalEvents = "integration-brig-events-internal", + berEmailSMSSesQueue = "integration-brig-events", + berEmailSMSEmailSender = "backend-integration@wire.com", + berGalleyJournal = "integration-team-events.fifo", + berVHost = "backendA", + berNginzSslPort = Ports.port Ports.NginzSSL BackendA, + berInternalServicePorts = Ports.internalServicePorts BackendA, + berNginzHttp2Port = Ports.port Ports.NginzHttp2 BackendA + } - -- Fixed internal port for federator, e.g. for dynamic backends: 1 -> 10097, 2 -> 11097, etc. - federatorInternalPort :: Num a => a -> a - federatorInternalPort i = 8097 + ((1 + i) * 1000) +backendB :: BackendResource +backendB = + BackendResource + { berName = BackendB, + berBrigKeyspace = "brig_test2", + berGalleyKeyspace = "galley_test2", + berSparKeyspace = "spar_test2", + berGundeckKeyspace = "gundeck_test2", + berElasticsearchIndex = "directory2_test", + berFederatorInternal = Ports.port (Ports.ServiceInternal FederatorInternal) BackendB, + berFederatorExternal = Ports.port Ports.FederatorExternal BackendB, + berDomain = "b.example.com", + berAwsUserJournalQueue = "integration-user-events.fifo2", + berAwsPrekeyTable = "integration-brig-prekeys2", + berAwsS3Bucket = "dummy-bucket2", + berAwsQueueName = "integration-gundeck-events2", + berBrigInternalEvents = "integration-brig-events-internal2", + berEmailSMSSesQueue = "integration-brig-events2", + berEmailSMSEmailSender = "backend-integration2@wire.com", + berGalleyJournal = "integration-team-events.fifo2", + -- FUTUREWORK: set up vhosts in dev/ci for example.com and b.example.com + -- in case we want backendA and backendB to federate with a third backend + -- (because otherwise both queues will overlap) + berVHost = "backendB", + berNginzSslPort = Ports.port Ports.NginzSSL BackendB, + berInternalServicePorts = Ports.internalServicePorts BackendB, + berNginzHttp2Port = Ports.port Ports.NginzHttp2 BackendB + } diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index b53ec16cab6..ee6b76f531c 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -23,6 +23,7 @@ import Testlib.Env import Testlib.JSON import Testlib.Options import Testlib.Printing +import Testlib.Service import Testlib.Types import Text.Printf import UnliftIO.Async diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index 3991a607b59..5f2b5a3eb34 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -4,7 +4,6 @@ module Testlib.RunServices where import Control.Concurrent import Control.Monad.Codensity (lowerCodensity) -import Data.Map qualified as Map import SetupHelpers import System.Directory import System.Environment (getArgs) @@ -16,83 +15,6 @@ import Testlib.Prelude import Testlib.ResourcePool import Testlib.Run (createGlobalEnv) -backendA :: BackendResource -backendA = - BackendResource - { berBrigKeyspace = "brig_test", - berGalleyKeyspace = "galley_test", - berSparKeyspace = "spar_test", - berGundeckKeyspace = "gundeck_test", - berElasticsearchIndex = "directory_test", - berFederatorInternal = 8097, - berFederatorExternal = 8098, - berDomain = "example.com", - berAwsUserJournalQueue = "integration-user-events.fifo", - berAwsPrekeyTable = "integration-brig-prekeys", - berAwsS3Bucket = "dummy-bucket", - berAwsQueueName = "integration-gundeck-events", - berBrigInternalEvents = "integration-brig-events-internal", - berEmailSMSSesQueue = "integration-brig-events", - berEmailSMSEmailSender = "backend-integration@wire.com", - berGalleyJournal = "integration-team-events.fifo", - berVHost = "backendA", - berNginzSslPort = 8443 - } - -staticPortsA :: Map.Map Service Word16 -staticPortsA = - Map.fromList - [ (Brig, 8082), - (Galley, 8085), - (Gundeck, 8086), - (Cannon, 8083), - (Cargohold, 8084), - (Spar, 8088), - (BackgroundWorker, 8089), - (Nginz, 8080), - (Stern, 8091) - ] - -backendB :: BackendResource -backendB = - BackendResource - { berBrigKeyspace = "brig_test2", - berGalleyKeyspace = "galley_test2", - berSparKeyspace = "spar_test2", - berGundeckKeyspace = "gundeck_test2", - berElasticsearchIndex = "directory2_test", - berFederatorInternal = 9097, - berFederatorExternal = 9098, - berDomain = "b.example.com", - berAwsUserJournalQueue = "integration-user-events.fifo2", - berAwsPrekeyTable = "integration-brig-prekeys2", - berAwsS3Bucket = "dummy-bucket2", - berAwsQueueName = "integration-gundeck-events2", - berBrigInternalEvents = "integration-brig-events-internal2", - berEmailSMSSesQueue = "integration-brig-events2", - berEmailSMSEmailSender = "backend-integration2@wire.com", - berGalleyJournal = "integration-team-events.fifo2", - -- FUTUREWORK: set up vhosts in dev/ci for example.com and b.example.com - -- in case we want backendA and backendB to federate with a third backend - -- (because otherwise both queues will overlap) - berVHost = "backendB", - berNginzSslPort = 9443 - } - -staticPortsB :: Map.Map Service Word16 -staticPortsB = - Map.fromList - [ (Brig, 9082), - (Galley, 9085), - (Gundeck, 9086), - (Cannon, 9083), - (Cargohold, 9084), - (Spar, 9088), - (BackgroundWorker, 9089), - (Nginz, 9080), - (Stern, 9091) - ] - parentDir :: FilePath -> Maybe FilePath parentDir path = let dirs = splitPath path @@ -140,10 +62,10 @@ main = do lowerCodensity $ do _modifyEnv <- traverseConcurrentlyCodensity - ( \(res, staticPorts) -> + ( \resource -> -- We add the 'fullSerachWithAll' overrrides is a hack to get -- around https://wearezeta.atlassian.net/browse/WPB-3796 - startDynamicBackend res staticPorts fullSearchWithAll + startDynamicBackend resource fullSearchWithAll ) - [(backendA, staticPortsA), (backendB, staticPortsB)] + [backendA, backendB] liftIO run diff --git a/integration/test/Testlib/Service.hs b/integration/test/Testlib/Service.hs new file mode 100644 index 00000000000..a921858051d --- /dev/null +++ b/integration/test/Testlib/Service.hs @@ -0,0 +1,49 @@ +module Testlib.Service where + +import Prelude + +data Service = Brig | Galley | Cannon | Gundeck | Cargohold | Nginz | Spar | BackgroundWorker | Stern | FederatorInternal + deriving + ( Show, + Eq, + Ord, + Enum, + Bounded + ) + +serviceName :: Service -> String +serviceName = \case + Brig -> "brig" + Galley -> "galley" + Cannon -> "cannon" + Gundeck -> "gundeck" + Cargohold -> "cargohold" + Nginz -> "nginz" + Spar -> "spar" + BackgroundWorker -> "backgroundWorker" + Stern -> "stern" + FederatorInternal -> "federator" + +-- | Converts the service name to kebab-case. +configName :: Service -> String +configName = \case + Brig -> "brig" + Galley -> "galley" + Cannon -> "cannon" + Gundeck -> "gundeck" + Cargohold -> "cargohold" + Nginz -> "nginz" + Spar -> "spar" + BackgroundWorker -> "background-worker" + Stern -> "stern" + FederatorInternal -> "federator" + +data BackendName + = BackendA + | BackendB + | -- | The index of dynamic backends begin with 1 + DynamicBackend Int + deriving (Show, Eq, Ord) + +allServices :: [Service] +allServices = [minBound .. maxBound] diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 25ed9c640ed..d6a2f590f1f 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -14,7 +14,6 @@ import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Lazy qualified as L import Data.CaseInsensitive qualified as CI import Data.Default -import Data.Function ((&)) import Data.Functor import Data.Hex import Data.IORef @@ -29,6 +28,7 @@ import Network.HTTP.Types qualified as HTTP import Network.URI import Testlib.Env import Testlib.Printing +import Testlib.Service import UnliftIO (MonadUnliftIO) import Prelude @@ -127,7 +127,7 @@ appToIOKleisli k = do env <- ask pure $ \a -> runAppWithEnv env (k a) -getServiceMap :: String -> App ServiceMap +getServiceMap :: HasCallStack => String -> App ServiceMap getServiceMap fedDomain = do env <- ask assertJust ("Could not find service map for federation domain: " <> fedDomain) (Map.lookup fedDomain (env.serviceMap)) @@ -192,15 +192,16 @@ modifyFailure modifyAssertion action = do ) data ServiceOverrides = ServiceOverrides - { dbBrig :: Value -> App Value, - dbCannon :: Value -> App Value, - dbCargohold :: Value -> App Value, - dbGalley :: Value -> App Value, - dbGundeck :: Value -> App Value, - dbNginz :: Value -> App Value, - dbSpar :: Value -> App Value, - dbBackgroundWorker :: Value -> App Value, - dbStern :: Value -> App Value + { brigCfg :: Value -> App Value, + cannonCfg :: Value -> App Value, + cargoholdCfg :: Value -> App Value, + galleyCfg :: Value -> App Value, + gundeckCfg :: Value -> App Value, + nginzCfg :: Value -> App Value, + sparCfg :: Value -> App Value, + backgroundWorkerCfg :: Value -> App Value, + sternCfg :: Value -> App Value, + federatorInternalCfg :: Value -> App Value } instance Default ServiceOverrides where @@ -209,15 +210,16 @@ instance Default ServiceOverrides where instance Semigroup ServiceOverrides where a <> b = ServiceOverrides - { dbBrig = dbBrig a >=> dbBrig b, - dbCannon = dbCannon a >=> dbCannon b, - dbCargohold = dbCargohold a >=> dbCargohold b, - dbGalley = dbGalley a >=> dbGalley b, - dbGundeck = dbGundeck a >=> dbGundeck b, - dbNginz = dbNginz a >=> dbNginz b, - dbSpar = dbSpar a >=> dbSpar b, - dbBackgroundWorker = dbBackgroundWorker a >=> dbBackgroundWorker b, - dbStern = dbStern a >=> dbStern b + { brigCfg = brigCfg a >=> brigCfg b, + cannonCfg = cannonCfg a >=> cannonCfg b, + cargoholdCfg = cargoholdCfg a >=> cargoholdCfg b, + galleyCfg = galleyCfg a >=> galleyCfg b, + gundeckCfg = gundeckCfg a >=> gundeckCfg b, + nginzCfg = nginzCfg a >=> nginzCfg b, + sparCfg = sparCfg a >=> sparCfg b, + backgroundWorkerCfg = backgroundWorkerCfg a >=> backgroundWorkerCfg b, + sternCfg = sternCfg a >=> sternCfg b, + federatorInternalCfg = federatorInternalCfg a >=> federatorInternalCfg b } instance Monoid ServiceOverrides where @@ -226,42 +228,27 @@ instance Monoid ServiceOverrides where defaultServiceOverrides :: ServiceOverrides defaultServiceOverrides = ServiceOverrides - { dbBrig = pure, - dbCannon = pure, - dbCargohold = pure, - dbGalley = pure, - dbGundeck = pure, - dbNginz = pure, - dbSpar = pure, - dbBackgroundWorker = pure, - dbStern = pure + { brigCfg = pure, + cannonCfg = pure, + cargoholdCfg = pure, + galleyCfg = pure, + gundeckCfg = pure, + nginzCfg = pure, + sparCfg = pure, + backgroundWorkerCfg = pure, + sternCfg = pure, + federatorInternalCfg = pure } -defaultServiceOverridesToMap :: Map.Map Service (Value -> App Value) -defaultServiceOverridesToMap = ([minBound .. maxBound] <&> (,pure)) & Map.fromList - --- | Overrides the service configurations with the given overrides. --- e.g. --- `let overrides = --- def --- { dbBrig = --- setField "optSettings.setFederationStrategy" "allowDynamic" --- >=> removeField "optSettings.setFederationDomainConfigs" --- } --- withOverrides overrides defaultServiceOverridesToMap` -withOverrides :: ServiceOverrides -> Map.Map Service (Value -> App Value) -> Map.Map Service (Value -> App Value) -withOverrides overrides = - Map.mapWithKey - ( \svr f -> - case svr of - Brig -> f >=> overrides.dbBrig - Cannon -> f >=> overrides.dbCannon - Cargohold -> f >=> overrides.dbCargohold - Galley -> f >=> overrides.dbGalley - Gundeck -> f >=> overrides.dbGundeck - Nginz -> f >=> overrides.dbNginz - Spar -> f >=> overrides.dbSpar - BackgroundWorker -> f >=> overrides.dbBackgroundWorker - Stern -> f >=> overrides.dbStern - FederatorInternal -> f - ) +lookupConfigOverride :: ServiceOverrides -> Service -> (Value -> App Value) +lookupConfigOverride overrides = \case + Brig -> overrides.brigCfg + Cannon -> overrides.cannonCfg + Cargohold -> overrides.cargoholdCfg + Galley -> overrides.galleyCfg + Gundeck -> overrides.gundeckCfg + Nginz -> overrides.nginzCfg + Spar -> overrides.sparCfg + BackgroundWorker -> overrides.backgroundWorkerCfg + Stern -> overrides.sternCfg + FederatorInternal -> overrides.federatorInternalCfg From bec112aedae094c54e3df59c65750acb6586a697 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 24 Aug 2023 16:07:03 +0200 Subject: [PATCH 101/225] [WPB-3799] cannot fetch conversation details after connection request (#3538) --- changelog.d/5-internal/pr-3538 | 1 + integration/default.nix | 6 ++ integration/integration.cabal | 4 + integration/test/API/Brig.hs | 8 +- integration/test/API/BrigInternal.hs | 11 +++ integration/test/API/Nginz.hs | 27 +++++++ integration/test/Test/Conversation.hs | 21 +++++- integration/test/Testlib/HTTP.hs | 4 + integration/test/Testlib/One2One.hs | 102 ++++++++++++++++++++++++++ 9 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 changelog.d/5-internal/pr-3538 create mode 100644 integration/test/Testlib/One2One.hs diff --git a/changelog.d/5-internal/pr-3538 b/changelog.d/5-internal/pr-3538 new file mode 100644 index 00000000000..37868aea9ef --- /dev/null +++ b/changelog.d/5-internal/pr-3538 @@ -0,0 +1 @@ +Additional integration test for federated connections diff --git a/integration/default.nix b/integration/default.nix index e02e43a5c6f..3d0bd4907ee 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -15,8 +15,10 @@ , Cabal , case-insensitive , containers +, cryptonite , data-default , directory +, errors , exceptions , extra , filepath @@ -28,6 +30,7 @@ , lens , lens-aeson , lib +, memory , mime , monad-control , mtl @@ -76,8 +79,10 @@ mkDerivation { bytestring-conversion case-insensitive containers + cryptonite data-default directory + errors exceptions extra filepath @@ -87,6 +92,7 @@ mkDerivation { kan-extensions lens lens-aeson + memory mime monad-control mtl diff --git a/integration/integration.cabal b/integration/integration.cabal index 7053817cbf6..b00ed4317af 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -118,6 +118,7 @@ library Testlib.HTTP Testlib.JSON Testlib.ModService + Testlib.One2One Testlib.Options Testlib.Ports Testlib.Prekeys @@ -142,8 +143,10 @@ library , bytestring-conversion , case-insensitive , containers + , cryptonite , data-default , directory + , errors , exceptions , extra , filepath @@ -153,6 +156,7 @@ library , kan-extensions , lens , lens-aeson + , memory , mime , monad-control , mtl diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 6fd779b9f31..82abfb5a6bb 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -1,6 +1,7 @@ module API.Brig where import API.Common +import Data.Aeson qualified as Aeson import Data.ByteString.Base64 qualified as Base64 import Data.Foldable import Data.Function @@ -218,7 +219,12 @@ putConnection userFrom userTo status = do baseRequest userFrom Brig Versioned $ joinHttpPath ["/connections", userToDomain, userToId] statusS <- asString status - submit "POST" (req & addJSONObject ["status" .= statusS]) + submit "PUT" (req & addJSONObject ["status" .= statusS]) + +getConnections :: (HasCallStack, MakesValue user) => user -> App Response +getConnections user = do + req <- baseRequest user Brig Versioned "/list-connections" + submit "POST" (req & addJSONObject ["size" .= Aeson.Number 500]) uploadKeyPackage :: ClientIdentity -> ByteString -> App Response uploadKeyPackage cid kp = do diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index ef09e08a560..d2788c64bb4 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -150,3 +150,14 @@ refreshIndex domain = do req <- baseRequest domain Brig Unversioned "i/index/refresh" res <- submit "POST" req res.status `shouldMatchInt` 200 + +connectWithRemoteUser :: (MakesValue userFrom, MakesValue userTo) => userFrom -> userTo -> App () +connectWithRemoteUser userFrom userTo = do + userFromId <- objId userFrom + qUserTo <- make userTo + let body = ["tag" .= "CreateConnectionForTest", "user" .= userFromId, "other" .= qUserTo] + req <- + baseRequest userFrom Brig Unversioned $ + joinHttpPath ["i", "connections", "connection-update"] + res <- submit "PUT" (req & addJSONObject body) + res.status `shouldMatchInt` 200 diff --git a/integration/test/API/Nginz.hs b/integration/test/API/Nginz.hs index e01e3505e79..4c34ef639d3 100644 --- a/integration/test/API/Nginz.hs +++ b/integration/test/API/Nginz.hs @@ -6,3 +6,30 @@ getSystemSettingsUnAuthorized :: (HasCallStack, MakesValue domain) => domain -> getSystemSettingsUnAuthorized domain = do req <- baseRequest domain Nginz Versioned "/system/settings/unauthorized" submit "GET" req + +login :: (HasCallStack, MakesValue domain, MakesValue email, MakesValue password) => domain -> email -> password -> App Response +login domain email pw = do + req <- rawBaseRequest domain Nginz Unversioned "/login" + emailStr <- make email >>= asString + pwStr <- make pw >>= asString + submit "POST" (req & addJSONObject ["email" .= emailStr, "password" .= pwStr, "label" .= "auth"]) + +access :: (HasCallStack, MakesValue domain, MakesValue cookie) => domain -> cookie -> App Response +access domain cookie = do + req <- rawBaseRequest domain Nginz Unversioned "/access" + cookieStr <- make cookie >>= asString + submit "POST" (req & setCookie cookieStr) + +logout :: (HasCallStack, MakesValue domain, MakesValue cookie, MakesValue token) => domain -> cookie -> token -> App Response +logout d c t = do + req <- rawBaseRequest d Nginz Unversioned "/access/logout" + cookie <- make c & asString + token <- make t & asString + submit "POST" (req & setCookie cookie & addHeader "Authorization" ("Bearer " <> token)) + +getConversation :: (HasCallStack, MakesValue user, MakesValue qcnv, MakesValue token) => user -> qcnv -> token -> App Response +getConversation user qcnv t = do + (domain, cnv) <- objQid qcnv + token <- make t & asString + req <- rawBaseRequest user Nginz Versioned (joinHttpPath ["conversations", domain, cnv]) + submit "GET" (req & addHeader "Authorization" ("Bearer " <> token)) diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 7e11aff0700..ef1ef920c38 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -3,7 +3,7 @@ module Test.Conversation where -import API.Brig (getConnection) +import API.Brig (getConnection, getConnections, postConnection) import API.BrigInternal import API.Galley import API.GalleyInternal @@ -13,6 +13,7 @@ import Control.Concurrent (threadDelay) import Data.Aeson qualified as Aeson import GHC.Stack import SetupHelpers +import Testlib.One2One (generateRemoteAndConvIdWithDomain) import Testlib.Prelude testDynamicBackendsFullyConnectedWhenAllowAll :: HasCallStack => App () @@ -443,3 +444,21 @@ testAddingUserNonFullyConnectedFederation = do bindResponse (addMembers alice conv [bobId, charlieId]) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "non_federating_backends" `shouldMatchSet` [other, dynBackend] + +testGetOneOnOneConvInStatusSentFromRemote :: App () +testGetOneOnOneConvInStatusSentFromRemote = do + d1User <- randomUser OwnDomain def + let shouldBeLocal = True + (d2Usr, d2ConvId) <- generateRemoteAndConvIdWithDomain OtherDomain (not shouldBeLocal) d1User + bindResponse (postConnection d1User d2Usr) $ \r -> do + r.status `shouldMatchInt` 201 + r.json %. "status" `shouldMatch` "sent" + bindResponse (listConversationIds d1User def) $ \r -> do + r.status `shouldMatchInt` 200 + convIds <- r.json %. "qualified_conversations" & asList + filter ((==) d2ConvId) convIds `shouldMatch` [d2ConvId] + bindResponse (getConnections d1User) $ \r -> do + qConvIds <- r.json %. "connections" & asList >>= traverse (%. "qualified_conversation") + filter ((==) d2ConvId) qConvIds `shouldMatch` [d2ConvId] + resp <- getConversation d1User d2ConvId + resp.status `shouldMatchInt` 200 diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index e07bac156a9..1aa0b80ca75 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -70,6 +70,10 @@ addHeader :: String -> String -> HTTP.Request -> HTTP.Request addHeader name value req = req {HTTP.requestHeaders = (CI.mk . C8.pack $ name, C8.pack value) : HTTP.requestHeaders req} +setCookie :: String -> HTTP.Request -> HTTP.Request +setCookie c r = + addHeader "Cookie" (cs c) r + addQueryParams :: [(String, String)] -> HTTP.Request -> HTTP.Request addQueryParams params req = HTTP.setQueryString (map (\(k, v) -> (cs k, Just (cs v))) params) req diff --git a/integration/test/Testlib/One2One.hs b/integration/test/Testlib/One2One.hs new file mode 100644 index 00000000000..ebecfd46ee9 --- /dev/null +++ b/integration/test/Testlib/One2One.hs @@ -0,0 +1,102 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 . + +-- This is a duplicate of `Galley.Types.Conversations.One2One` +-- and is needed because we do not have access to galley code in the integration tests +module Testlib.One2One (generateRemoteAndConvIdWithDomain) where + +import Control.Error (atMay) +import Crypto.Hash qualified as Crypto +import Data.Bits +import Data.ByteArray (convert) +import Data.ByteString +import Data.ByteString qualified as B +import Data.ByteString.Conversion +import Data.ByteString.Lazy qualified as L +import Data.UUID as UUID +import SetupHelpers (randomUser) +import Testlib.Prelude + +generateRemoteAndConvIdWithDomain :: (MakesValue domain, MakesValue a) => domain -> Bool -> a -> App (Value, Value) +generateRemoteAndConvIdWithDomain remoteDomain shouldBeLocal lUserId = do + (localDomain, localUser) <- objQid lUserId + otherUsr <- randomUser remoteDomain def >>= objId + otherDomain <- asString remoteDomain + let (cId, cDomain) = + one2OneConvId + (fromMaybe (error "invalid UUID") (UUID.fromString localUser), localDomain) + (fromMaybe (error "invalid UUID") (UUID.fromString otherUsr), otherDomain) + isLocal = localDomain == cDomain + if shouldBeLocal == isLocal + then + pure $ + ( object ["id" .= (otherUsr), "domain" .= otherDomain], + object ["id" .= (UUID.toString cId), "domain" .= cDomain] + ) + else generateRemoteAndConvIdWithDomain remoteDomain shouldBeLocal lUserId + +one2OneConvId :: (UUID, String) -> (UUID, String) -> (UUID, String) +one2OneConvId a@(a1, dom1) b@(a2, dom2) = case compare (dom1, a1) (dom2, a2) of + GT -> one2OneConvId b a + _ -> + let c = + mconcat + [ L.toStrict (UUID.toByteString namespace), + quidToByteString a, + quidToByteString b + ] + x = hash c + result = + toUuidV5 + . mkV5 + . fromMaybe nil + . UUID.fromByteString + . L.fromStrict + . B.take 16 + $ x + domain + | fromMaybe 0 (atMay (B.unpack x) 16) .&. 0x80 == 0 = dom1 + | otherwise = dom2 + in (result, domain) + where + hash :: ByteString -> ByteString + hash = convert . Crypto.hash @ByteString @Crypto.SHA256 + + namespace :: UUID + namespace = fromWords 0x9a51edb8 0x060c0d9a 0x0c2950a8 0x5d152982 + + quidToByteString :: (UUID, String) -> ByteString + quidToByteString (uid, domain) = toASCIIBytes uid <> toByteString' domain + +newtype UuidV5 = UuidV5 {toUuidV5 :: UUID} + deriving (Eq, Ord, Show) + +mkV5 :: UUID -> UuidV5 +mkV5 u = UuidV5 $ + case toWords u of + (x0, x1, x2, x3) -> + fromWords + x0 + (retainVersion 5 x1) + (retainVariant 2 x2) + x3 + where + retainVersion :: Word32 -> Word32 -> Word32 + retainVersion v x = (x .&. 0xFFFF0FFF) .|. (v `shiftL` 12) + + retainVariant :: Word32 -> Word32 -> Word32 + retainVariant v x = (x .&. 0x3FFFFFFF) .|. (v `shiftL` 30) From f356578be5c2127b048fe1327800ef10ca2a4c3f Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 24 Aug 2023 16:44:24 +0200 Subject: [PATCH 102/225] brig-integration: Fix flaky tests for API.Federation (#3539) * brig-integration: Don't assume only 1 result in search by display name Display names are random strings from 2 to 128 characters. If a 2 string name gets generated it is likely that it matches some name generated in another test. * brig-integration: Mark test not flaky It didn't fail after runnning it 1000 times. --- .../brig/test/integration/API/Federation.hs | 39 +++++++++++-------- services/brig/test/integration/Util.hs | 5 +++ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/services/brig/test/integration/API/Federation.hs b/services/brig/test/integration/API/Federation.hs index d69cc968046..c58d022c901 100644 --- a/services/brig/test/integration/API/Federation.hs +++ b/services/brig/test/integration/API/Federation.hs @@ -1,4 +1,3 @@ -{-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-} -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -68,7 +67,7 @@ tests m opts brig cannon fedBrigClient = test m "POST /federation/search-users : Found (multiple users)" (testFulltextSearchMultipleUsers opts brig), test m "POST /federation/search-users : NotFound" (testSearchNotFound opts), test m "POST /federation/search-users : Empty Input - NotFound" (testSearchNotFoundEmpty opts), - flakyTest m "POST /federation/search-users : configured restrictions" (testSearchRestrictions opts brig), + test m "POST /federation/search-users : configured restrictions" (testSearchRestrictions opts brig), test m "POST /federation/get-user-by-handle : configured restrictions" (testGetUserByHandleRestrictions opts brig), test m "POST /federation/get-user-by-handle : Found" (testGetUserByHandleSuccess opts brig), test m "POST /federation/get-user-by-handle : NotFound" (testGetUserByHandleNotFound opts), @@ -80,7 +79,7 @@ tests m opts brig cannon fedBrigClient = test m "POST /federation/claim-multi-prekey-bundle : 200" (testClaimMultiPrekeyBundleSuccess brig fedBrigClient), test m "POST /federation/get-user-clients : 200" (testGetUserClients brig fedBrigClient), test m "POST /federation/get-user-clients : Not Found" (testGetUserClientsNotFound fedBrigClient), - flakyTest m "POST /federation/on-user-deleted-connections : 200" (testRemoteUserGetsDeleted opts brig cannon fedBrigClient), + test m "POST /federation/on-user-deleted-connections : 200" (testRemoteUserGetsDeleted opts brig cannon fedBrigClient), test m "POST /federation/api-version : 200" (testAPIVersion brig fedBrigClient) ] @@ -116,11 +115,11 @@ testFulltextSearchSuccess opts brig = do searchResponse <- withSettingsOverrides (allowFullSearch domain opts) $ do runWaiTestFedClient domain $ createWaiTestFedClient @"search-users" @'Brig $ - SearchRequest ((fromName . userDisplayName) user) + SearchRequest (fromName $ userDisplayName user) liftIO $ do let contacts = contactQualifiedId <$> S.contacts searchResponse - assertEqual "should return the user id" [quid] contacts + assertElem "should return the user id" quid contacts testFulltextSearchMultipleUsers :: Opt.Opts -> Brig -> Http () testFulltextSearchMultipleUsers opts brig = do @@ -190,22 +189,30 @@ testSearchRestrictions opts brig = do FD.FederationDomainConfig domainFullSearch FullSearch ] - let expectSearch :: HasCallStack => Domain -> Text -> [Qualified UserId] -> FederatedUserSearchPolicy -> WaiTest.Session () - expectSearch domain squery expectedUsers expectedSearchPolicy = do + let expectSearch :: HasCallStack => Domain -> Either Handle Name -> Maybe (Qualified UserId) -> FederatedUserSearchPolicy -> WaiTest.Session () + expectSearch domain handleOrName mExpectedUser expectedSearchPolicy = do + let squery = either fromHandle fromName handleOrName searchResponse <- runWaiTestFedClient domain $ createWaiTestFedClient @"search-users" @'Brig (SearchRequest squery) - liftIO $ assertEqual "Unexpected search result" expectedUsers (contactQualifiedId <$> S.contacts searchResponse) - liftIO $ assertEqual "Unexpected search result" expectedSearchPolicy (S.searchPolicy searchResponse) + liftIO $ do + case (mExpectedUser, handleOrName) of + (Just expectedUser, Right _) -> + assertElem "Unexpected search result" expectedUser (contactQualifiedId <$> S.contacts searchResponse) + (Nothing, Right _) -> + assertEqual "Unexpected search result" [] (contactQualifiedId <$> S.contacts searchResponse) + _ -> + assertEqual "Unexpected search result" (maybeToList mExpectedUser) (contactQualifiedId <$> S.contacts searchResponse) + assertEqual "Unexpected search result" expectedSearchPolicy (S.searchPolicy searchResponse) withSettingsOverrides opts' $ do - expectSearch domainNoSearch (fromHandle handle) [] NoSearch - expectSearch domainExactHandle (fromHandle handle) [quid] ExactHandleSearch - expectSearch domainExactHandle (fromName (userDisplayName user)) [] ExactHandleSearch - expectSearch domainFullSearch (fromHandle handle) [quid] FullSearch - expectSearch domainFullSearch (fromName (userDisplayName user)) [quid] FullSearch - expectSearch domainUnconfigured (fromHandle handle) [] NoSearch - expectSearch domainUnconfigured (fromName (userDisplayName user)) [] NoSearch + expectSearch domainNoSearch (Left handle) Nothing NoSearch + expectSearch domainExactHandle (Left handle) (Just quid) ExactHandleSearch + expectSearch domainExactHandle (Right (userDisplayName user)) Nothing ExactHandleSearch + expectSearch domainFullSearch (Left handle) (Just quid) FullSearch + expectSearch domainFullSearch (Right (userDisplayName user)) (Just quid) FullSearch + expectSearch domainUnconfigured (Left handle) Nothing NoSearch + expectSearch domainUnconfigured (Right (userDisplayName user)) Nothing NoSearch testGetUserByHandleRestrictions :: Opt.Opts -> Brig -> Http () testGetUserByHandleRestrictions opts brig = do diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 3c7ee83324a..6aafab8a991 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -1333,3 +1333,8 @@ spawn cp minput = do assertJust :: (HasCallStack, MonadIO m) => Maybe a -> m a assertJust (Just a) = pure a assertJust Nothing = liftIO $ error "Expected Just, got Nothing" + +assertElem :: (HasCallStack, Eq a, Show a) => String -> a -> [a] -> Assertion +assertElem msg x xs = + unless (x `elem` xs) $ + assertFailure (msg <> "\nExpected to find: \n" <> show x <> "\nin:\n" <> show xs) From bb4d4df3c611c0f57f4d7e5ade210140bda7726a Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 25 Aug 2023 11:56:35 +0200 Subject: [PATCH 103/225] Integration suite: Fix bug in local setup: wrong port for nginz http2 (#3543) --- integration/test/Testlib/ModService.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 5b2ce20dc5e..003fb9e2df3 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -288,7 +288,7 @@ startBackend resource overrides services = do env <- ask case env.servicesCwdBase of Nothing -> startNginzK8s domain serviceMap - Just _ -> startNginzLocal domain resource.berNginzSslPort resource.berNginzSslPort serviceMap + Just _ -> startNginzLocal domain resource.berNginzHttp2Port resource.berNginzSslPort serviceMap srv -> do readServiceConfig srv >>= updateServiceMapInConfig srv From 2de7d9c6c0cdcb1542a1470f00b6cdfb2d498324 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 29 Aug 2023 11:23:40 +0200 Subject: [PATCH 104/225] [WPB-662] servantify brig provider bot api (#3540) --- changelog.d/5-internal/pr-3540 | 1 + libs/types-common/src/Data/Id.hs | 7 +- .../wire-api/src/Wire/API/Conversation/Bot.hs | 90 +++----- libs/wire-api/src/Wire/API/Error/Brig.hs | 15 ++ libs/wire-api/src/Wire/API/Provider/Bot.hs | 57 ++--- libs/wire-api/src/Wire/API/Routes/Public.hs | 71 ++++-- .../src/Wire/API/Routes/Public/Brig.hs | 2 + .../src/Wire/API/Routes/Public/Brig/Bot.hs | 168 +++++++++++++++ libs/wire-api/src/Wire/API/User/Client.hs | 19 +- libs/wire-api/wire-api.cabal | 1 + services/brig/src/Brig/API.hs | 2 - services/brig/src/Brig/API/Public.hs | 8 +- services/brig/src/Brig/Provider/API.hs | 204 +++++------------- .../brig/test/integration/API/Provider.hs | 131 ++++++++++- 14 files changed, 490 insertions(+), 286 deletions(-) create mode 100644 changelog.d/5-internal/pr-3540 create mode 100644 libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs diff --git a/changelog.d/5-internal/pr-3540 b/changelog.d/5-internal/pr-3540 new file mode 100644 index 00000000000..f1c0ef4f559 --- /dev/null +++ b/changelog.d/5-internal/pr-3540 @@ -0,0 +1 @@ +The bot API is now migrated to servant diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index 4fccc60e942..cae7c686ce7 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -354,10 +354,11 @@ newtype BotId = BotId FromHttpApiData, Hashable, NFData, - FromJSON, - ToJSON, - Generic + Generic, + ToParamSchema ) + deriving newtype (ToSchema) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema BotId instance Show BotId where show = show . botUserId diff --git a/libs/wire-api/src/Wire/API/Conversation/Bot.hs b/libs/wire-api/src/Wire/API/Conversation/Bot.hs index 4b4da2f0466..2fd4a442cb3 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Bot.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Bot.hs @@ -26,9 +26,10 @@ module Wire.API.Conversation.Bot ) where -import Data.Aeson +import Data.Aeson qualified as A import Data.Id -import Data.Json.Util ((#)) +import Data.Schema +import Data.Swagger qualified as S import Imports import Wire.API.Event.Conversation (Event) import Wire.API.User.Client.Prekey (Prekey) @@ -46,21 +47,15 @@ data AddBot = AddBot } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform AddBot) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema AddBot -instance ToJSON AddBot where - toJSON n = - object $ - "provider" .= addBotProvider n - # "service" .= addBotService n - # "locale" .= addBotLocale n - # [] - -instance FromJSON AddBot where - parseJSON = withObject "NewBot" $ \o -> - AddBot - <$> o .: "provider" - <*> o .: "service" - <*> o .:? "locale" +instance ToSchema AddBot where + schema = + object "AddBot" $ + AddBot + <$> addBotProvider .= field "provider" schema + <*> addBotService .= field "service" schema + <*> addBotLocale .= maybe_ (optField "locale" schema) data AddBotResponse = AddBotResponse { rsAddBotId :: BotId, @@ -72,27 +67,18 @@ data AddBotResponse = AddBotResponse } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform AddBotResponse) - -instance ToJSON AddBotResponse where - toJSON r = - object - [ "id" .= rsAddBotId r, - "client" .= rsAddBotClient r, - "name" .= rsAddBotName r, - "accent_id" .= rsAddBotColour r, - "assets" .= rsAddBotAssets r, - "event" .= rsAddBotEvent r - ] - -instance FromJSON AddBotResponse where - parseJSON = withObject "AddBotResponse" $ \o -> - AddBotResponse - <$> o .: "id" - <*> o .: "client" - <*> o .: "name" - <*> o .: "accent_id" - <*> o .: "assets" - <*> o .: "event" + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema AddBotResponse + +instance ToSchema AddBotResponse where + schema = + object "AddBotResponse" $ + AddBotResponse + <$> rsAddBotId .= field "id" schema + <*> rsAddBotClient .= field "client" schema + <*> rsAddBotName .= field "name" schema + <*> rsAddBotColour .= field "accent_id" schema + <*> rsAddBotAssets .= field "assets" (array schema) + <*> rsAddBotEvent .= field "event" schema -------------------------------------------------------------------------------- -- RemoveBot @@ -104,16 +90,13 @@ newtype RemoveBotResponse = RemoveBotResponse } deriving stock (Eq, Show) deriving newtype (Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema RemoveBotResponse -instance ToJSON RemoveBotResponse where - toJSON r = - object - [ "event" .= rsRemoveBotEvent r - ] - -instance FromJSON RemoveBotResponse where - parseJSON = withObject "RemoveBotResponse" $ \o -> - RemoveBotResponse <$> o .: "event" +instance ToSchema RemoveBotResponse where + schema = + object "RemoveBotResponse" $ + RemoveBotResponse + <$> rsRemoveBotEvent .= field "event" schema -------------------------------------------------------------------------------- -- UpdateBotPrekeys @@ -123,13 +106,10 @@ newtype UpdateBotPrekeys = UpdateBotPrekeys } deriving stock (Eq, Show) deriving newtype (Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema UpdateBotPrekeys -instance ToJSON UpdateBotPrekeys where - toJSON u = - object - [ "prekeys" .= updateBotPrekeyList u - ] - -instance FromJSON UpdateBotPrekeys where - parseJSON = withObject "UpdateBotPrekeys" $ \o -> - UpdateBotPrekeys <$> o .: "prekeys" +instance ToSchema UpdateBotPrekeys where + schema = + object "UpdateBotPrekeys" $ + UpdateBotPrekeys + <$> updateBotPrekeyList .= field "prekeys" (array schema) diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 949d3c45725..8544a58d50d 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -80,12 +80,27 @@ data BrigError | NotificationNotFound | PendingInvitationNotFound | ConflictingInvitations + | AccessDenied + | InvalidConversation + | TooManyConversationMembers + | ServiceDisabled + | InvalidBot instance KnownError (MapError e) => IsSwaggerError (e :: BrigError) where addToSwagger = addStaticErrorToSwagger @(MapError e) +type instance MapError 'ServiceDisabled = 'StaticError 403 "service-disabled" "The desired service is currently disabled." + +type instance MapError 'InvalidBot = 'StaticError 403 "invalid-bot" "The targeted user is not a bot." + type instance MapError 'UserNotFound = 'StaticError 404 "not-found" "User not found" +type instance MapError 'InvalidConversation = 'StaticError 403 "invalid-conversation" "The operation is not allowed in this conversation." + +type instance MapError 'TooManyConversationMembers = 'StaticError 403 "too-many-members" "Maximum number of members per conversation reached." + +type instance MapError 'AccessDenied = 'StaticError 403 "access-denied" "Access denied." + type instance MapError 'InvalidUser = 'StaticError 400 "invalid-user" "Invalid user" type instance MapError 'InvalidCode = 'StaticError 403 "invalid-code" "Invalid verification code" diff --git a/libs/wire-api/src/Wire/API/Provider/Bot.hs b/libs/wire-api/src/Wire/API/Provider/Bot.hs index b8aabf3af01..0db3cabeaff 100644 --- a/libs/wire-api/src/Wire/API/Provider/Bot.hs +++ b/libs/wire-api/src/Wire/API/Provider/Bot.hs @@ -31,10 +31,11 @@ module Wire.API.Provider.Bot where import Control.Lens (makeLenses) -import Data.Aeson +import Data.Aeson qualified as A import Data.Handle (Handle) import Data.Id -import Data.Json.Util ((#)) +import Data.Schema +import Data.Swagger qualified as S import Imports import Wire.API.Conversation.Member (OtherMember (..)) import Wire.API.User.Profile (ColourId, Name) @@ -51,25 +52,19 @@ data BotConvView = BotConvView } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform BotConvView) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema BotConvView + +instance ToSchema BotConvView where + schema = + object "BotConvView" $ + BotConvView + <$> _botConvId .= field "id" schema + <*> _botConvName .= maybe_ (optField "name" schema) + <*> _botConvMembers .= field "members" (array schema) botConvView :: ConvId -> Maybe Text -> [OtherMember] -> BotConvView botConvView = BotConvView -instance ToJSON BotConvView where - toJSON c = - object $ - "id" .= _botConvId c - # "name" .= _botConvName c - # "members" .= _botConvMembers c - # [] - -instance FromJSON BotConvView where - parseJSON = withObject "BotConvView" $ \o -> - BotConvView - <$> o .: "id" - <*> o .:? "name" - <*> o .: "members" - -------------------------------------------------------------------------------- -- BotUserView @@ -82,24 +77,16 @@ data BotUserView = BotUserView } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform BotUserView) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema BotUserView -instance ToJSON BotUserView where - toJSON u = - object - [ "id" .= botUserViewId u, - "name" .= botUserViewName u, - "accent_id" .= botUserViewColour u, - "handle" .= botUserViewHandle u, - "team" .= botUserViewTeam u - ] - -instance FromJSON BotUserView where - parseJSON = withObject "BotUserView" $ \o -> - BotUserView - <$> o .: "id" - <*> o .: "name" - <*> o .: "accent_id" - <*> o .:? "handle" - <*> o .:? "team" +instance ToSchema BotUserView where + schema = + object "BotUserView" $ + BotUserView + <$> botUserViewId .= field "id" schema + <*> botUserViewName .= field "name" schema + <*> botUserViewColour .= field "accent_id" schema + <*> botUserViewHandle .= optField "handle" (maybeWithDefault A.Null schema) + <*> botUserViewTeam .= optField "team" (maybeWithDefault A.Null schema) makeLenses ''BotConvView diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 8ad302b6dd8..d6b30283c94 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -30,6 +30,7 @@ module Wire.API.Routes.Public ZBot, ZConversation, ZProvider, + ZAccess, DescriptionOAuthScope, ZHostOpt, ) @@ -85,9 +86,19 @@ data ZType | ZAuthBot | ZAuthConv | ZAuthProvider + | -- | (Typically short-lived) access token. + ZAuthAccess + +class HasTokenType (ztype :: ZType) where + -- | The expected value of the "Z-Type" header. + tokenType :: Maybe ByteString + tokenType = Nothing class - (KnownSymbol (ZHeader ztype), FromHttpApiData (ZParam ztype)) => + ( KnownSymbol (ZHeader ztype), + FromHttpApiData (ZParam ztype), + HasTokenType ztype + ) => IsZType (ztype :: ZType) ctx where type ZHeader ztype :: Symbol @@ -96,12 +107,7 @@ class qualifyZParam :: Context ctx -> ZParam ztype -> ZQualifiedParam ztype -class HasTokenType ztype where - -- | The expected value of the "Z-Type" header. - tokenType :: Maybe ByteString - -instance {-# OVERLAPPABLE #-} HasTokenType ztype where - tokenType = Nothing +instance HasTokenType 'ZLocalAuthUser instance HasContextEntry ctx Domain => IsZType 'ZLocalAuthUser ctx where type ZHeader 'ZLocalAuthUser = "Z-User" @@ -110,6 +116,8 @@ instance HasContextEntry ctx Domain => IsZType 'ZLocalAuthUser ctx where qualifyZParam ctx = toLocalUnsafe (getContextEntry ctx) +instance HasTokenType 'ZAuthUser + instance IsZType 'ZAuthUser ctx where type ZHeader 'ZAuthUser = "Z-User" type ZParam 'ZAuthUser = UserId @@ -117,6 +125,8 @@ instance IsZType 'ZAuthUser ctx where qualifyZParam _ = id +instance HasTokenType 'ZAuthClient + instance IsZType 'ZAuthClient ctx where type ZHeader 'ZAuthClient = "Z-Client" type ZParam 'ZAuthClient = ClientId @@ -124,6 +134,8 @@ instance IsZType 'ZAuthClient ctx where qualifyZParam _ = id +instance HasTokenType 'ZAuthConn + instance IsZType 'ZAuthConn ctx where type ZHeader 'ZAuthConn = "Z-Connection" type ZParam 'ZAuthConn = ConnId @@ -131,6 +143,9 @@ instance IsZType 'ZAuthConn ctx where qualifyZParam _ = id +instance HasTokenType 'ZAuthBot where + tokenType = Just "bot" + instance IsZType 'ZAuthBot ctx where type ZHeader 'ZAuthBot = "Z-Bot" type ZParam 'ZAuthBot = BotId @@ -138,6 +153,8 @@ instance IsZType 'ZAuthBot ctx where qualifyZParam _ = id +instance HasTokenType 'ZAuthConv + instance IsZType 'ZAuthConv ctx where type ZHeader 'ZAuthConv = "Z-Conversation" type ZParam 'ZAuthConv = ConvId @@ -145,8 +162,8 @@ instance IsZType 'ZAuthConv ctx where qualifyZParam _ = id -instance HasTokenType 'ZAuthBot where - tokenType = Just "bot" +instance HasTokenType 'ZAuthProvider where + tokenType = Just "provider" instance IsZType 'ZAuthProvider ctx where type ZHeader 'ZAuthProvider = "Z-Provider" @@ -155,8 +172,15 @@ instance IsZType 'ZAuthProvider ctx where qualifyZParam _ = id -instance HasTokenType 'ZAuthProvider where - tokenType = Just "provider" +instance HasTokenType 'ZAuthAccess where + tokenType = Just "access" + +instance IsZType 'ZAuthAccess ctx where + type ZHeader 'ZAuthAccess = "Z-User" + type ZParam 'ZAuthAccess = UserId + type ZQualifiedParam 'ZAuthAccess = UserId + + qualifyZParam _ = id data ZAuthServant (ztype :: ZType) (opts :: [Type]) @@ -182,6 +206,8 @@ type ZConversation = ZAuthServant 'ZAuthConv InternalAuthDefOpts type ZProvider = ZAuthServant 'ZAuthProvider InternalAuthDefOpts +type ZAccess = ZAuthServant 'ZAuthAccess InternalAuthDefOpts + type ZOptUser = ZAuthServant 'ZAuthUser '[Servant.Optional, Servant.Strict] type ZOptClient = ZAuthServant 'ZAuthClient '[Servant.Optional, Servant.Strict] @@ -270,17 +296,18 @@ instance ) where checkType :: Maybe ByteString -> Wai.Request -> DelayedIO () - checkType token req = case (token, lookup "Z-Type" (Wai.requestHeaders req)) of - (Just t, value) - | value /= Just t -> - delayedFail - ServerError - { errHTTPCode = 403, - errReasonPhrase = "Access denied", - errBody = "", - errHeaders = [] - } - _ -> pure () + checkType token req = + case (token, lookup "Z-Type" (Wai.requestHeaders req)) of + (Just t, value) + | value /= Just t -> + delayedFail + ServerError + { errHTTPCode = 403, + errReasonPhrase = "Access denied", + errBody = "", + errHeaders = [] + } + _ -> pure () hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index b99aa0e7298..d83004bb54b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -56,6 +56,7 @@ import Wire.API.Routes.Cookies import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public +import Wire.API.Routes.Public.Brig.Bot (BotAPI) import Wire.API.Routes.Public.Brig.OAuth (OAuthAPI) import Wire.API.Routes.Public.Util import Wire.API.Routes.QualifiedCapture @@ -92,6 +93,7 @@ type BrigAPI = :<|> TeamsAPI :<|> SystemSettingsAPI :<|> OAuthAPI + :<|> BotAPI data BrigAPITag diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs new file mode 100644 index 00000000000..285202fb993 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs @@ -0,0 +1,168 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.Routes.Public.Brig.Bot where + +import Data.CommaSeparatedList (CommaSeparatedList) +import Data.Id as Id +import Imports +import Servant (JSON) +import Servant hiding (Handler, JSON, Tagged, addHeader, respond) +import Servant.Swagger.Internal.Orphans () +import Wire.API.Conversation.Bot +import Wire.API.Error (CanThrow, ErrorResponse) +import Wire.API.Error.Brig (BrigError (..)) +import Wire.API.Provider.Bot (BotUserView) +import Wire.API.Routes.API (ServiceAPI (..)) +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named (Named (..)) +import Wire.API.Routes.Public +import Wire.API.User +import Wire.API.User.Client +import Wire.API.User.Client.Prekey (PrekeyId) + +type DeleteResponses = + '[ RespondEmpty 204 "", + Respond 200 "User found" RemoveBotResponse + ] + +type GetClientResponses = + '[ ErrorResponse 'ClientNotFound, + Respond 200 "Client found" Client + ] + +type BotAPI = + Named + "add-bot" + ( Summary "Add bot" + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidConversation + :> CanThrow 'TooManyConversationMembers + :> CanThrow 'ServiceDisabled + :> ZAccess + :> ZConn + :> "conversations" + :> Capture "Conversation ID" ConvId + :> "bots" + :> ReqBody '[JSON] AddBot + :> MultiVerb1 'POST '[JSON] (Respond 201 "" AddBotResponse) + ) + :<|> Named + "remove-bot" + ( Summary "Remove bot" + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidConversation + :> ZAccess + :> ZConn + :> "conversations" + :> Capture "Conversation ID" ConvId + :> "bots" + :> Capture "Bot ID" BotId + :> MultiVerb 'DELETE '[JSON] DeleteResponses (Maybe RemoveBotResponse) + ) + :<|> Named + "bot-get-self" + ( Summary "Get self" + :> CanThrow 'UserNotFound + :> CanThrow 'AccessDenied + :> ZBot + :> "bot" + :> "self" + :> Get '[JSON] UserProfile + ) + :<|> Named + "bot-delete-self" + ( Summary "Delete self" + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidBot + :> ZBot + :> ZConversation + :> "bot" + :> "self" + :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 200 "") + ) + :<|> Named + "bot-list-prekeys" + ( Summary "List prekeys for bot" + :> CanThrow 'AccessDenied + :> ZBot + :> "bot" + :> "client" + :> "prekeys" + :> Get '[JSON] [PrekeyId] + ) + :<|> Named + "bot-update-prekeys" + ( Summary "Update prekeys for bot" + :> CanThrow 'AccessDenied + :> CanThrow 'ClientNotFound + :> ZBot + :> "bot" + :> "client" + :> "prekeys" + :> ReqBody '[JSON] UpdateBotPrekeys + :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "") + ) + :<|> Named + "bot-get-client" + ( Summary "Get client for bot" + :> CanThrow 'AccessDenied + :> CanThrow 'ClientNotFound + :> ZBot + :> "bot" + :> "client" + :> MultiVerb 'GET '[JSON] GetClientResponses (Maybe Client) + ) + :<|> Named + "bot-claim-users-prekeys" + ( Summary "Claim users prekeys" + :> CanThrow 'AccessDenied + :> CanThrow 'TooManyClients + :> CanThrow 'MissingLegalholdConsent + :> ZBot + :> "bot" + :> "users" + :> "prekeys" + :> ReqBody '[JSON] UserClients + :> Post '[JSON] UserClientPrekeyMap + ) + :<|> Named + "bot-list-users" + ( Summary "List users" + :> CanThrow 'AccessDenied + :> ZBot + :> "bot" + :> "users" + :> QueryParam' [Required, Strict] "ids" (CommaSeparatedList UserId) + :> Get '[JSON] [BotUserView] + ) + :<|> Named + "bot-get-user-clients" + ( Summary "Get user clients" + :> CanThrow 'AccessDenied + :> ZBot + :> "bot" + :> "users" + :> Capture "User ID" UserId + :> "clients" + :> Get '[JSON] [PubClient] + ) + +data BotAPITag + +instance ServiceAPI BotAPITag v where + type ServiceAPIRoutes BotAPITag = BotAPI diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index 69c1f65e7fb..761d8b7337e 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -529,19 +529,14 @@ data PubClient = PubClient deriving stock (Eq, Show, Generic, Ord) deriving (Arbitrary) via (GenericUniform PubClient) deriving (Swagger.ToSchema) via (CustomSwagger '[FieldLabelModifier (StripPrefix "pubClient", LowerCase)] PubClient) + deriving (FromJSON, ToJSON) via Schema PubClient -instance ToJSON PubClient where - toJSON c = - A.object $ - "id" A..= pubClientId c - # "class" A..= pubClientClass c - # [] - -instance FromJSON PubClient where - parseJSON = A.withObject "PubClient" $ \o -> - PubClient - <$> o A..: "id" - <*> o A..:? "class" +instance ToSchema PubClient where + schema = + object "PubClient" $ + PubClient + <$> pubClientId .= field "id" schema + <*> pubClientClass .= maybe_ (optField "class" schema) -------------------------------------------------------------------------------- -- Client Type/Class diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 05cd5447b97..e488149d735 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -103,6 +103,7 @@ library Wire.API.Routes.Named Wire.API.Routes.Public Wire.API.Routes.Public.Brig + Wire.API.Routes.Public.Brig.Bot Wire.API.Routes.Public.Brig.OAuth Wire.API.Routes.Public.Cannon Wire.API.Routes.Public.Cargohold diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index 3d4ff3ebb98..1c03e0376ca 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -28,13 +28,11 @@ import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Network.Wai.Routing (Routes) import Polysemy -import Wire.Sem.Concurrency sitemap :: forall r p. ( Member BlacklistStore r, Member GalleyProvider r, - Member (Concurrency 'Unsafe) r, Member (UserPendingActivationStore p) r ) => Routes () (Handler r) () diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index b27c4094448..46665c7122f 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -56,6 +56,7 @@ import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Options hiding (internalEvents, sesQueue) +import Brig.Provider.API (botAPI) import Brig.Provider.API qualified as Provider import Brig.Team.API qualified as Team import Brig.Team.Email qualified as Team @@ -122,6 +123,7 @@ import Wire.API.Routes.Internal.Spar qualified as SparInternalAPI import Wire.API.Routes.MultiTablePaging qualified as Public import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig +import Wire.API.Routes.Public.Brig.Bot import Wire.API.Routes.Public.Brig.OAuth import Wire.API.Routes.Public.Cannon import Wire.API.Routes.Public.Cargohold @@ -178,6 +180,7 @@ versionedSwaggerDocsAPI (Just (VersionNumber V5)) = <> serviceSwagger @GundeckAPITag @'V5 <> serviceSwagger @ProxyAPITag @'V5 <> serviceSwagger @OAuthAPITag @'V5 + <> serviceSwagger @BotAPITag @'V5 ) & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger.md") @@ -193,6 +196,7 @@ versionedSwaggerDocsAPI (Just (VersionNumber V4)) = <> serviceSwagger @GundeckAPITag @'V4 <> serviceSwagger @ProxyAPITag @'V4 <> serviceSwagger @OAuthAPITag @'V4 + <> serviceSwagger @BotAPITag @'V4 ) & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger.md") @@ -283,6 +287,7 @@ servantSitemap = :<|> Team.servantAPI :<|> systemSettingsAPI :<|> oauthAPI + :<|> botAPI where userAPI :: ServerT UserAPI (Handler r) userAPI = @@ -424,8 +429,7 @@ servantSitemap = -- - MemberLeave event to members for all conversations the user was in (via galley) sitemap :: - ( Member (Concurrency 'Unsafe) r, - Member GalleyProvider r + ( Member GalleyProvider r ) => Routes () (Handler r) () sitemap = do diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index d30876aa078..a836861f8f5 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -19,6 +19,7 @@ module Brig.Provider.API ( -- * Main stuff routesPublic, routesInternal, + botAPI, -- * Event handlers finishDeleteService, @@ -59,6 +60,7 @@ import Control.Monad.Except import Data.Aeson hiding (json) import Data.ByteString.Conversion import Data.ByteString.Lazy.Char8 qualified as LC8 +import Data.CommaSeparatedList (CommaSeparatedList (fromCommaSeparatedList)) import Data.Conduit (runConduit, (.|)) import Data.Conduit.List qualified as C import Data.Hashable (hash) @@ -79,7 +81,7 @@ import GHC.TypeNats import Imports import Network.HTTP.Types.Status import Network.Wai (Response) -import Network.Wai.Predicate (accept, contentType, def, opt, query) +import Network.Wai.Predicate (accept, def, opt, query) import Network.Wai.Routing import Network.Wai.Utilities.Error ((!>>)) import Network.Wai.Utilities.Error qualified as Wai @@ -92,6 +94,7 @@ import OpenSSL.PEM qualified as SSL import OpenSSL.RSA qualified as SSL import OpenSSL.Random (randBytes) import Polysemy +import Servant (ServerT, (:<|>) (..)) import Ssl.Util qualified as SSL import System.Logger.Class (MonadLogger) import UnliftIO.Async (pooledMapConcurrentlyN_) @@ -113,6 +116,8 @@ import Wire.API.Provider.External qualified as Ext import Wire.API.Provider.Service import Wire.API.Provider.Service qualified as Public import Wire.API.Provider.Service.Tag qualified as Public +import Wire.API.Routes.Named (Named (Named)) +import Wire.API.Routes.Public.Brig.Bot (BotAPI) import Wire.API.Team.Feature qualified as Feature import Wire.API.Team.LegalHold (LegalholdProtectee (UnprotectedBot)) import Wire.API.Team.Permission @@ -124,10 +129,26 @@ import Wire.API.User.Client.Prekey qualified as Public (PrekeyId) import Wire.API.User.Identity qualified as Public (Email) import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe)) -routesPublic :: +botAPI :: ( Member GalleyProvider r, Member (Concurrency 'Unsafe) r ) => + ServerT BotAPI (Handler r) +botAPI = + Named @"add-bot" addBot + :<|> Named @"remove-bot" removeBot + :<|> Named @"bot-get-self" botGetSelf + :<|> Named @"bot-delete-self" botDeleteSelf + :<|> Named @"bot-list-prekeys" botListPrekeys + :<|> Named @"bot-update-prekeys" botUpdatePrekeys + :<|> Named @"bot-get-client" botGetClient + :<|> Named @"bot-claim-users-prekeys" botClaimUsersPrekeys + :<|> Named @"bot-list-users" botListUserProfiles + :<|> Named @"bot-get-user-clients" botGetUserClients + +routesPublic :: + ( Member GalleyProvider r + ) => Routes () (Handler r) () routesPublic = do -- Public API (Unauthenticated) -------------------------------------------- @@ -270,63 +291,6 @@ routesPublic = do .&. capture "tid" .&. jsonRequest @Public.UpdateServiceWhitelist - post "/conversations/:cnv/bots" (continue addBotH) $ - accept "application" "json" - .&> zauth ZAuthAccess - .&> zauthUserId - .&. zauthConnId - .&. capture "cnv" - .&. jsonRequest @Public.AddBot - - delete "/conversations/:cnv/bots/:bot" (continue removeBotH) $ - zauth ZAuthAccess - .&> zauthUserId - .&. zauthConnId - .&. capture "cnv" - .&. capture "bot" - - -- Bot API ----------------------------------------------------------------- - - get "/bot/self" (continue botGetSelfH) $ - accept "application" "json" - .&> zauth ZAuthBot - .&> zauthBotId - - delete "/bot/self" (continue botDeleteSelfH) $ - zauth ZAuthBot - .&> zauthBotId - .&. zauthConvId - - get "/bot/client/prekeys" (continue botListPrekeysH) $ - accept "application" "json" - .&> zauth ZAuthBot - .&> zauthBotId - - post "/bot/client/prekeys" (continue botUpdatePrekeysH) $ - zauth ZAuthBot - .&> zauthBotId - .&. jsonRequest @Public.UpdateBotPrekeys - - get "/bot/client" (continue botGetClientH) $ - contentType "application" "json" - .&> zauth ZAuthBot - .&> zauthBotId - - post "/bot/users/prekeys" (continue botClaimUsersPrekeysH) $ - accept "application" "json" - .&> zauth ZAuthBot - .&> jsonRequest @Public.UserClients - - get "/bot/users" (continue botListUserProfilesH) $ - accept "application" "json" - .&> zauth ZAuthBot - .&> query "ids" - - get "/bot/users/:uid/clients" (continue botGetUserClientsH) $ - accept "application" "json" - .&> zauth ZAuthBot - .&> capture "uid" - routesInternal :: Member GalleyProvider r => Routes a (Handler r) () routesInternal = do get "/i/provider/activation-code" (continue getActivationCodeH) $ @@ -887,13 +851,12 @@ updateServiceWhitelist uid con tid upd = do wrapClientE $ DB.deleteServiceWhitelist (Just tid) pid sid pure UpdateServiceWhitelistRespChanged -addBotH :: Member GalleyProvider r => UserId ::: ConnId ::: ConvId ::: JsonRequest Public.AddBot -> (Handler r) Response -addBotH (zuid ::: zcon ::: cid ::: req) = do - guardSecondFactorDisabled (Just zuid) - setStatus status201 . json <$> (addBot zuid zcon cid =<< parseJsonBody req) +-------------------------------------------------------------------------------- +-- Bot API addBot :: Member GalleyProvider r => UserId -> ConnId -> ConvId -> Public.AddBot -> (Handler r) Public.AddBotResponse addBot zuid zcon cid add = do + guardSecondFactorDisabled (Just zuid) zusr <- lift (wrapClient $ User.lookupUser NoPendingInvitations zuid) >>= maybeInvalidUser let pid = addBotProvider add let sid = addBotService add @@ -910,18 +873,18 @@ addBot zuid zcon cid add = do guardConvAdmin cnv let mems = cnvMembers cnv unless (cnvType cnv == RegularConv) $ - throwStd invalidConv + throwStd (errorToWai @'E.InvalidConversation) maxSize <- fromIntegral . setMaxConvSize <$> view settings unless (length (cmOthers mems) < maxSize - 1) $ - throwStd tooManyMembers + throwStd (errorToWai @'E.TooManyConversationMembers) -- For team conversations: bots are not allowed in -- team-only conversations unless (Set.member ServiceAccessRole (cnvAccessRoles cnv)) $ - throwStd invalidConv + throwStd (errorToWai @'E.InvalidConversation) -- Lookup the relevant service data scon <- wrapClientE (DB.lookupServiceConn pid sid) >>= maybeServiceNotFound unless (sconEnabled scon) $ - throwStd serviceDisabled + throwStd (errorToWai @'E.ServiceDisabled) svp <- wrapClientE (DB.lookupServiceProfile pid sid) >>= maybeServiceNotFound for_ (cnvTeam cnv) $ \tid -> do whitelisted <- wrapClientE $ DB.getServiceWhitelistStatus tid pid sid @@ -975,13 +938,9 @@ addBot zuid zcon cid add = do Public.rsAddBotEvent = ev } -removeBotH :: Member GalleyProvider r => UserId ::: ConnId ::: ConvId ::: BotId -> (Handler r) Response -removeBotH (zusr ::: zcon ::: cid ::: bid) = do - guardSecondFactorDisabled (Just zusr) - maybe (setStatus status204 empty) json <$> removeBot zusr zcon cid bid - removeBot :: Member GalleyProvider r => UserId -> ConnId -> ConvId -> BotId -> (Handler r) (Maybe Public.RemoveBotResponse) removeBot zusr zcon cid bid = do + guardSecondFactorDisabled (Just zusr) -- Get the conversation and check preconditions lcid <- qualifyLocal cid cnv <- lift (liftSem $ GalleyProvider.getConv zusr lcid) >>= maybeConvNotFound @@ -993,7 +952,7 @@ removeBot zusr zcon cid bid = do guardConvAdmin cnv let mems = cnvMembers cnv unless (cnvType cnv == RegularConv) $ - throwStd invalidConv + (throwStd (errorToWai @'E.InvalidConversation)) -- Find the bot in the member list and delete it let busr = botUserId bid let bot = List.find ((== busr) . qUnqualified . omQualifiedId) (cmOthers mems) @@ -1005,49 +964,29 @@ removeBot zusr zcon cid bid = do guardConvAdmin :: Conversation -> ExceptT Error (AppT r) () guardConvAdmin conv = do let selfMember = cmSelf . cnvMembers $ conv - unless (memConvRoleName selfMember == roleNameWireAdmin) $ throwStd accessDenied - --------------------------------------------------------------------------------- --- Bot API - -botGetSelfH :: Member GalleyProvider r => BotId -> (Handler r) Response -botGetSelfH bot = do - guardSecondFactorDisabled (Just (botUserId bot)) - json <$> botGetSelf bot + unless (memConvRoleName selfMember == roleNameWireAdmin) $ (throwStd (errorToWai @'E.AccessDenied)) botGetSelf :: BotId -> (Handler r) Public.UserProfile botGetSelf bot = do p <- lift $ wrapClient $ User.lookupUser NoPendingInvitations (botUserId bot) maybe (throwStd (errorToWai @'E.UserNotFound)) (pure . (`Public.publicProfile` UserLegalHoldNoConsent)) p -botGetClientH :: Member GalleyProvider r => BotId -> (Handler r) Response -botGetClientH bot = do - guardSecondFactorDisabled (Just (botUserId bot)) - maybe (throwStd (errorToWai @'E.ClientNotFound)) (pure . json) =<< lift (botGetClient bot) - -botGetClient :: BotId -> (AppT r) (Maybe Public.Client) -botGetClient bot = - listToMaybe <$> wrapClient (User.lookupClients (botUserId bot)) - -botListPrekeysH :: Member GalleyProvider r => BotId -> (Handler r) Response -botListPrekeysH bot = do +botGetClient :: Member GalleyProvider r => BotId -> (Handler r) (Maybe Public.Client) +botGetClient bot = do guardSecondFactorDisabled (Just (botUserId bot)) - json <$> botListPrekeys bot + lift $ listToMaybe <$> wrapClient (User.lookupClients (botUserId bot)) -botListPrekeys :: BotId -> (Handler r) [Public.PrekeyId] +botListPrekeys :: Member GalleyProvider r => BotId -> (Handler r) [Public.PrekeyId] botListPrekeys bot = do + guardSecondFactorDisabled (Just (botUserId bot)) clt <- lift $ listToMaybe <$> wrapClient (User.lookupClients (botUserId bot)) case clientId <$> clt of Nothing -> pure [] Just ci -> lift (wrapClient $ User.lookupPrekeyIds (botUserId bot) ci) -botUpdatePrekeysH :: Member GalleyProvider r => BotId ::: JsonRequest Public.UpdateBotPrekeys -> (Handler r) Response -botUpdatePrekeysH (bot ::: req) = do - guardSecondFactorDisabled (Just (botUserId bot)) - empty <$ (botUpdatePrekeys bot =<< parseJsonBody req) - -botUpdatePrekeys :: BotId -> Public.UpdateBotPrekeys -> (Handler r) () +botUpdatePrekeys :: Member GalleyProvider r => BotId -> Public.UpdateBotPrekeys -> (Handler r) () botUpdatePrekeys bot upd = do + guardSecondFactorDisabled (Just (botUserId bot)) clt <- lift $ listToMaybe <$> wrapClient (User.lookupClients (botUserId bot)) case clt of Nothing -> throwStd (errorToWai @'E.ClientNotFound) @@ -1055,57 +994,36 @@ botUpdatePrekeys bot upd = do let pks = updateBotPrekeyList upd wrapClientE (User.updatePrekeys (botUserId bot) (clientId c) pks) !>> clientDataError -botClaimUsersPrekeysH :: - ( Member GalleyProvider r, - Member (Concurrency 'Unsafe) r - ) => - JsonRequest Public.UserClients -> - Handler r Response -botClaimUsersPrekeysH req = do - guardSecondFactorDisabled Nothing - json <$> (botClaimUsersPrekeys =<< parseJsonBody req) - botClaimUsersPrekeys :: - Member (Concurrency 'Unsafe) r => + (Member (Concurrency 'Unsafe) r, Member GalleyProvider r) => + BotId -> Public.UserClients -> Handler r Public.UserClientPrekeyMap -botClaimUsersPrekeys body = do +botClaimUsersPrekeys _ body = do + guardSecondFactorDisabled Nothing maxSize <- fromIntegral . setMaxConvSize <$> view settings when (Map.size (Public.userClients body) > maxSize) $ throwStd (errorToWai @'E.TooManyClients) Client.claimLocalMultiPrekeyBundles UnprotectedBot body !>> clientError -botListUserProfilesH :: Member GalleyProvider r => List UserId -> (Handler r) Response -botListUserProfilesH uids = do +botListUserProfiles :: Member GalleyProvider r => BotId -> (CommaSeparatedList UserId) -> (Handler r) [Public.BotUserView] +botListUserProfiles _ uids = do guardSecondFactorDisabled Nothing -- should we check all user ids? - json <$> botListUserProfiles uids - -botListUserProfiles :: List UserId -> (Handler r) [Public.BotUserView] -botListUserProfiles uids = do - us <- lift . wrapClient $ User.lookupUsers NoPendingInvitations (fromList uids) + us <- lift . wrapClient $ User.lookupUsers NoPendingInvitations (fromCommaSeparatedList uids) pure (map mkBotUserView us) -botGetUserClientsH :: Member GalleyProvider r => UserId -> (Handler r) Response -botGetUserClientsH uid = do +botGetUserClients :: Member GalleyProvider r => BotId -> UserId -> (Handler r) [Public.PubClient] +botGetUserClients _ uid = do guardSecondFactorDisabled (Just uid) - json <$> lift (botGetUserClients uid) - -botGetUserClients :: UserId -> (AppT r) [Public.PubClient] -botGetUserClients uid = - pubClient <$$> wrapClient (User.lookupClients uid) + lift $ pubClient <$$> wrapClient (User.lookupClients uid) where pubClient c = Public.PubClient (clientId c) (clientClass c) -botDeleteSelfH :: Member GalleyProvider r => BotId ::: ConvId -> (Handler r) Response -botDeleteSelfH (bid ::: cid) = do - guardSecondFactorDisabled (Just (botUserId bid)) - empty <$ botDeleteSelf bid cid - botDeleteSelf :: Member GalleyProvider r => BotId -> ConvId -> (Handler r) () botDeleteSelf bid cid = do guardSecondFactorDisabled (Just (botUserId bid)) bot <- lift . wrapClient $ User.lookupUser NoPendingInvitations (botUserId bid) - _ <- maybeInvalidBot (userService =<< bot) + _ <- maybe (throwStd (errorToWai @'E.InvalidBot)) pure $ (userService =<< bot) _ <- lift $ wrapHttpClient $ deleteBot (botUserId bid) Nothing bid cid pure () @@ -1120,7 +1038,7 @@ guardSecondFactorDisabled :: ExceptT Error (AppT r) () guardSecondFactorDisabled mbUserId = do enabled <- lift $ liftSem $ (==) Feature.FeatureStatusEnabled . Feature.wsStatus . Feature.afcSndFactorPasswordChallenge <$> GalleyProvider.getAllFeatureConfigsForUser mbUserId - when enabled $ throwStd accessDenied + when enabled $ (throwStd (errorToWai @'E.AccessDenied)) minRsaKeySize :: Int minRsaKeySize = 256 -- Bytes (= 2048 bits) @@ -1231,9 +1149,6 @@ maybeBadCredentials = maybe (throwStd (errorToWai @'E.BadCredentials)) pure maybeInvalidServiceKey :: Monad m => Maybe a -> (ExceptT Error m) a maybeInvalidServiceKey = maybe (throwStd invalidServiceKey) pure -maybeInvalidBot :: Monad m => Maybe a -> (ExceptT Error m) a -maybeInvalidBot = maybe (throwStd invalidBot) pure - maybeInvalidUser :: Monad m => Maybe a -> (ExceptT Error m) a maybeInvalidUser = maybe (throwStd (errorToWai @'E.InvalidUser)) pure @@ -1246,24 +1161,12 @@ invalidServiceKey = Wai.mkError status400 "invalid-service-key" "Invalid service invalidProvider :: Wai.Error invalidProvider = Wai.mkError status403 "invalid-provider" "The provider does not exist." -invalidBot :: Wai.Error -invalidBot = Wai.mkError status403 "invalid-bot" "The targeted user is not a bot." - -invalidConv :: Wai.Error -invalidConv = Wai.mkError status403 "invalid-conversation" "The operation is not allowed in this conversation." - badGateway :: Wai.Error badGateway = Wai.mkError status502 "bad-gateway" "The upstream service returned an invalid response." -tooManyMembers :: Wai.Error -tooManyMembers = Wai.mkError status403 "too-many-members" "Maximum number of members per conversation reached." - tooManyBots :: Wai.Error tooManyBots = Wai.mkError status409 "too-many-bots" "Maximum number of bots for the service reached." -serviceDisabled :: Wai.Error -serviceDisabled = Wai.mkError status403 "service-disabled" "The desired service is currently disabled." - serviceNotWhitelisted :: Wai.Error serviceNotWhitelisted = Wai.mkError status403 "service-not-whitelisted" "The desired service is not on the whitelist of allowed services for this team." @@ -1271,8 +1174,5 @@ serviceError :: RPC.ServiceError -> Wai.Error serviceError RPC.ServiceUnavailable = badGateway serviceError RPC.ServiceBotConflict = tooManyBots -accessDenied :: Wai.Error -accessDenied = Wai.mkError status403 "access-denied" "Access denied." - randServiceToken :: MonadIO m => m Public.ServiceToken randServiceToken = ServiceToken . Ascii.encodeBase64Url <$> liftIO (randBytes 18) diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 2025bd45e86..02d33c55333 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -47,6 +47,7 @@ import Data.Id hiding (client) import Data.Json.Util (toBase64Text) import Data.List1 (List1) import Data.List1 qualified as List1 +import Data.Map qualified as Map import Data.Misc import Data.PEM import Data.Qualified @@ -145,7 +146,10 @@ tests dom conf p db b c g = do [ test p "add-remove" $ testAddRemoveBot conf db b g c, test p "message" $ testMessageBot conf db b g c, test p "bad fingerprint" $ testBadFingerprint conf db b g c, - test p "add bot forbidden" $ testAddBotForbidden conf db b g + test p "add bot forbidden" $ testAddBotForbidden conf db b g, + test p "claim user prekeys" $ testClaimUserPrekeys conf db b g, + test p "list user profiles" $ testListUserProfiles conf db b g, + test p "get user clients" $ testGetUserClients conf db b g ], testGroup "bot-teams" @@ -560,6 +564,57 @@ testAddBotForbidden config db brig galley = withTestService config db brig defSe const 403 === statusCode const (Just "invalid-conversation") === fmap Error.label . responseJsonMaybe +testClaimUserPrekeys :: Config -> DB.ClientState -> Brig -> Galley -> Http () +testClaimUserPrekeys config db brig galley = withTestService config db brig defServiceApp $ \sref _ -> do + (pid, sid, u1, _u2, _h) <- prepareUsers sref brig + cid <- do + rs <- createConv galley u1.userId [] DB.ClientState -> Brig -> Galley -> Http () +testListUserProfiles config db brig galley = withTestService config db brig defServiceApp $ \sref _ -> do + (pid, sid, u1, u2, _h) <- prepareUsers sref brig + cid <- do + rs <- createConv galley u1.userId [] DB.ClientState -> Brig -> Galley -> Http () +testGetUserClients config db brig galley = withTestService config db brig defServiceApp $ \sref _ -> do + (pid, sid, u1, _u2, _h) <- prepareUsers sref brig + cid <- do + rs <- createConv galley u1.userId [] DB.ClientState -> Brig -> Galley -> Http () testAddBotBlocked config db brig galley = withTestService config db brig defServiceApp $ \sref _buf -> do (userId -> u1, _, _, tid, cid, pid, sid) <- prepareBotUsersTeam brig galley sref @@ -1504,6 +1559,68 @@ enabled2ndFaForTeamInternal galley tid = do ) !!! const 200 === statusCode +getBotSelf :: Brig -> BotId -> Http ResponseLBS +getBotSelf brig bid = + get $ + brig + . path "/bot/self" + . header "Z-Type" "bot" + . header "Z-Bot" (toByteString' bid) + +getBotClient :: Brig -> BotId -> Http ResponseLBS +getBotClient brig bid = + get $ + brig + . path "/bot/client" + . header "Z-Type" "bot" + . header "Z-Bot" (toByteString' bid) + . contentJson + +getBotPreKeyIds :: Brig -> BotId -> Http ResponseLBS +getBotPreKeyIds brig bid = + get $ + brig + . path "/bot/client/prekeys" + . header "Z-Type" "bot" + . header "Z-Bot" (toByteString' bid) + +updateBotPrekeys :: Brig -> BotId -> [Prekey] -> Http ResponseLBS +updateBotPrekeys brig bid prekeys = + post $ + brig + . path "/bot/client/prekeys" + . header "Z-Type" "bot" + . header "Z-Bot" (toByteString' bid) + . contentJson + . body (RequestBodyLBS (encode (UpdateBotPrekeys prekeys))) + +claimUsersPrekeys :: Brig -> BotId -> UserClients -> Http ResponseLBS +claimUsersPrekeys brig bid ucs = + post $ + brig + . path "/bot/users/prekeys" + . header "Z-Type" "bot" + . header "Z-Bot" (toByteString' bid) + . contentJson + . body (RequestBodyLBS (encode ucs)) + +listUserProfiles :: Brig -> BotId -> [UserId] -> Http ResponseLBS +listUserProfiles brig bid uids = + get $ + brig + . path "/bot/users" + . header "Z-Type" "bot" + . header "Z-Bot" (toByteString' bid) + . queryItem "ids" (C8.intercalate "," $ toByteString' <$> uids) + +getUserClients :: Brig -> BotId -> UserId -> Http ResponseLBS +getUserClients brig bid uid = + get $ + brig + . paths ["bot", "users", toByteString' uid, "clients"] + . header "Z-Type" "bot" + . header "Z-Bot" (toByteString' bid) + -------------------------------------------------------------------------------- -- DB Operations @@ -2036,9 +2153,17 @@ testAddRemoveBotUtil localDomain pid sid cid u1 u2 h sref buf brig galley cannon let Just rs = responseJsonMaybe _rs bid = rsAddBotId rs qbuid = Qualified (botUserId bid) localDomain + getBotSelf brig bid !!! const 200 === statusCode + (randomId >>= getBotSelf brig . BotId) !!! const 404 === statusCode + botClient :: Client <- responseJsonError =<< getBotClient brig bid >= getBotClient brig . BotId) !!! const 404 === statusCode bot <- svcAssertBotCreated buf bid cid - liftIO $ assertEqual "bot client" (rsAddBotClient rs) (testBotClient bot) + liftIO $ assertEqual "bot client" rs.rsAddBotClient bot.testBotClient liftIO $ assertEqual "bot event" MemberJoin (evtType (rsAddBotEvent rs)) + -- just check that these endpoints works + getBotPreKeyIds brig bid !!! const 200 === statusCode + updateBotPrekeys brig bid bot.testBotPrekeys !!! const 200 === statusCode -- Member join event for both users forM_ [ws1, ws2] $ \ws -> wsAssertMemberJoin ws qcid quid1 [qbuid] -- Member join event for the bot @@ -2068,7 +2193,7 @@ testAddRemoveBotUtil localDomain pid sid cid u1 u2 h sref buf brig galley cannon assertEqual "colour" defaultAccentId (profileAccentId bp) assertEqual "assets" defServiceAssets (profileAssets bp) -- Check that the bot client exists and has prekeys - let isBotPrekey = (`elem` testBotPrekeys bot) . prekeyData + let isBotPrekey = (`elem` bot.testBotPrekeys) . prekeyData getPreKey brig buid buid (rsAddBotClient rs) !!! do const 200 === statusCode const (Just True) === fmap isBotPrekey . responseJsonMaybe From 5f66f8af4ac1cf39c4f597673d029e9b3fcd435d Mon Sep 17 00:00:00 2001 From: Jappie Klooster Date: Wed, 30 Aug 2023 04:44:41 -0400 Subject: [PATCH 105/225] Fix broken "we are hiring" link (#3549) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 159a7431297..92c34edcab3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Wire™ -[![Wire logo](https://github.com/wireapp/wire/blob/master/assets/header-small.png?raw=true)](https://wire.com/jobs/) +[![Wire logo](https://github.com/wireapp/wire/blob/master/assets/header-small.png?raw=true)](https://start.wire.com/careers-en) This repository is part of the source code of Wire. You can find more information at [wire.com](https://wire.com) or by contacting opensource@wire.com. From fed463d8e829176116842d54bf938c70837b4365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Ivankovi=C4=87?= <33936733+Veki301@users.noreply.github.com> Date: Thu, 31 Aug 2023 10:04:00 +0200 Subject: [PATCH 106/225] change chart version nr --- charts/outlook-addin/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/outlook-addin/Chart.yaml b/charts/outlook-addin/Chart.yaml index 02c87c184a1..6367ea14dfa 100644 --- a/charts/outlook-addin/Chart.yaml +++ b/charts/outlook-addin/Chart.yaml @@ -1,4 +1,4 @@ apiVersion: v1 name: outlook-addin -version: 4.35.0 +version: 4.38.0 description: Helm chart for outlook addin for Wire From 2c53363a600c67a61ab5bacdd6b457e6e6bdfc2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Ivankovi=C4=87?= <33936733+Veki301@users.noreply.github.com> Date: Thu, 31 Aug 2023 11:28:17 +0200 Subject: [PATCH 107/225] feat: add SUPPORT_URL env var refactor: docs consistency --- charts/outlook-addin/README.md | 4 ++-- charts/outlook-addin/templates/deployment.yaml | 4 +++- charts/outlook-addin/values.yaml | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/charts/outlook-addin/README.md b/charts/outlook-addin/README.md index 6fe0d2263d1..18c6e40048e 100644 --- a/charts/outlook-addin/README.md +++ b/charts/outlook-addin/README.md @@ -129,7 +129,7 @@ curl -s -X POST localhost:8080/i/oauth/clients \ -H "Content-Type: application/json" \ -d '{ "application_name":"Wire Microsoft Outlook Calendar Add-in", - "redirect_url":"https://outlook.your_domain.com/callback.html" + "redirect_url":"https://outlook.example.com/callback.html" }' ``` @@ -187,5 +187,5 @@ d helm upgrade --install outlook-addin charts/outlook-addin --values values/outl ## Install Wire AddIn in Microsoft Outlook -After deploying `outlook-addin` you will be able to find `manifest.xml` file on https://outlook.your.domain.com/manifest.xml which you can use to install the addin in your outlook. You can find instructions and screenshots how to do it [here](https://github.com/tlebon/outlook-addin/blob/staging/README.md#how-to-install-the-add-in-in-ms-outlook). +After deploying `outlook-addin` you will be able to find `manifest.xml` file on https://outlook.example.com/manifest.xml which you can use to install the addin in your outlook. You can find instructions and screenshots how to do it [here](https://github.com/tlebon/outlook-addin/blob/staging/README.md#how-to-install-the-add-in-in-ms-outlook). NOTE: Links in the outlined documents are hardcoded for a testing/prod environment, any reference to zinfra.io or wire.com in it should be treated as example.com. diff --git a/charts/outlook-addin/templates/deployment.yaml b/charts/outlook-addin/templates/deployment.yaml index 37600f7ad55..4009fd0ffc5 100644 --- a/charts/outlook-addin/templates/deployment.yaml +++ b/charts/outlook-addin/templates/deployment.yaml @@ -17,7 +17,7 @@ spec: spec: containers: - name: {{ include "outlook.fullname" . }} - image: { { .Values.containerImage } } + image: {{ .Values.containerImage }} ports: - name: http containerPort: 80 @@ -30,6 +30,8 @@ spec: value: "{{ .Values.wireApiBaseUrl }}" - name: WIRE_AUTHORIZATION_ENDPOINT value: "{{ .Values.wireAuthorizationEndpoint }}" + - name: SUPPORT_URL + value: "{{ .Values.supportUrl }}" livenessProbe: httpGet: path: / diff --git a/charts/outlook-addin/values.yaml b/charts/outlook-addin/values.yaml index 621b65eea41..66da50a43ce 100644 --- a/charts/outlook-addin/values.yaml +++ b/charts/outlook-addin/values.yaml @@ -5,6 +5,7 @@ config: #host: "outlook.example.com" #wireApiBaseUrl: "https://nginz-https.example.com" #wireAuthorizationEndpoint: "https://webapp.example.com/auth" +#supportUrl: "" #tls: # issuerRef: # name: letsencrypt-http01 From c96a82698760412662ccbcf34d51877094b07a02 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 31 Aug 2023 15:12:13 +0200 Subject: [PATCH 108/225] Multi-ingress guest links (#3546) --- charts/galley/templates/configmap.yaml | 9 ++ charts/galley/values.yaml | 14 +++ .../src/developer/reference/config-options.md | 111 +++++++++--------- integration/test/API/Galley.hs | 40 +++++++ integration/test/Test/Conversation.hs | 67 +++++++++++ integration/test/Testlib/HTTP.hs | 12 +- integration/test/Testlib/JSON.hs | 3 + libs/wire-api/src/Wire/API/Routes/Public.hs | 5 +- .../API/Routes/Public/Galley/Conversation.hs | 3 + services/cargohold/src/CargoHold/Options.hs | 2 +- services/galley/src/Galley/API/Update.hs | 49 +++++--- services/galley/src/Galley/App.hs | 14 ++- services/galley/src/Galley/Cassandra/Code.hs | 12 +- .../galley/src/Galley/Effects/CodeStore.hs | 2 +- services/galley/src/Galley/Env.hs | 5 +- services/galley/src/Galley/Options.hs | 17 ++- 16 files changed, 273 insertions(+), 92 deletions(-) diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 10c22fafeb6..e5afcb5f428 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -59,7 +59,16 @@ data: {{- if .settings.exposeInvitationURLsTeamAllowlist }} exposeInvitationURLsTeamAllowlist: {{ .settings.exposeInvitationURLsTeamAllowlist }} {{- end }} + {{- if .settings.conversationCodeURI }} conversationCodeURI: {{ .settings.conversationCodeURI | quote }} + {{- else if .settings.multiIngress }} + multiIngress: {{- toYaml .settings.multiIngress | nindent 8 }} + {{- else }} + {{ fail "Either settings.conversationCodeURI or settings.multiIngress have to be set"}} + {{- end }} + {{- if (and .settings.conversationCodeURI .settings.multiIngress) }} + {{ fail "settings.conversationCodeURI and settings.multiIngress are mutually exclusive" }} + {{- end }} federationDomain: {{ .settings.federationDomain }} {{- if $.Values.secrets.mlsPrivateKeys }} mlsPrivateKeyPaths: diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 875864142ff..5b328a3e5b1 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -34,6 +34,20 @@ config: exposeInvitationURLsTeamAllowlist: [] maxConvSize: 500 intraListing: true + # Either `conversationCodeURI` or `multiIngress` must be set + # + # `conversationCodeURI` is the URI prefix for conversation invitation links + # It should be of form https://{ACCOUNT_PAGES}/conversation-join/ + conversationCodeURI: null + # + # `multiIngress` is a `Z-Host` depended setting of conversationCodeURI. + # Use this only if you want to expose the instance on mutliple ingresses. + # If set it must a map from `Z-Host` to URI prefix + # Example: + # multiIngress: + # example.com: https://accounts.example.com/conversation-join/ + # example.net: https://accounts.example.net/conversation-join/ + multiIngress: null # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: # brig, cannon, cargohold, galley, gundeck, proxy, spar. # disabledAPIVersions: [ v3 ] diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 3c681740f7b..3d9dc14c4f4 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -7,8 +7,6 @@ Fragment. This page is about the yaml files that determine the configuration of the Wire backend services. -## Settings in galley - ### MLS private key paths Note: This developer documentation. Documentation for site operators can be found here: {ref}`mls-message-layer-security` @@ -657,39 +655,71 @@ The default setting is that no API version is disabled. ## Settings in cargohold -### (Fake) AWS - AWS S3 (or an alternative provider / service) is used to upload and download assets. The Haddock of [`CargoHold.Options.AWSOpts`](https://github.com/wireapp/wire-server/blob/develop/services/cargohold/src/CargoHold/Options.hs#L64) provides a lot of useful information. -#### Multi-Ingress setup + +## Multi-Ingress setup In a multi-ingress setup the backend is reachable via several domains, each handled by a separate Kubernetes ingress. This is useful to obfuscate the relationship of clients to each other, as an attacker on TCP/IP-level could only see domains and IPs that do not obviously relate to each other. +Each of these backend domains represents a virtual backend. N.B. these backend +domains are *DNS domains* only, not to be confused of the "backend domain" term used for federation (see {ref}`configure-federation`). In single-ingress setups the backend DNS domain and federation backend domain is usually be the same, but this is not true for multi-ingress setups. + + +For a multi-ingress setup multiple services need to be configured: +### Nginz + +nginz sets [CORS +headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). To generate +them for multiple domains (usually, *nginz* works with only one root domain) +these need to be defined with `nginx_conf.additional_external_env_domains`. + +E.g. + +```yaml +nginx_conf: + additional_external_env_domains: + - red.example.com + - green.example.org + - blue.example.net +``` + +### Cannon + +*cannon* sets [CORS +headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) for direct API +accesses by clients. To generate them for multiple domains (usually, *cannon* +works with only one root domain) these need to be defined with +`nginx_conf.additional_external_env_domains`. + +E.g. + +```yaml +nginx_conf: + additional_external_env_domains: + - red.example.com + - green.example.org + - blue.example.net +``` + +### Cargohold -In case of a fake AWS S3 service its identity needs to be obfuscated by making -it accessible via several domains, too. Thus, there isn't one -`s3DownloadEndpoint`, but one per domain at which the backend is reachable. Each -of these backend domains represents a virtual backend. N.B. these backend -domains are *DNS domains*. Do not confuse them with the federation domain! The -latter is just an identifier, and may or may not be equal to the backend's DNS -domain. Backend DNS domain(s) and federation domain are usually set equal by -convention. But, this is not true for multi-ingress setups! The backend domain of a download request is defined by its `Z-Host` header which -is set by `nginz`. (Multi-ingress handlling only applies to download requests as +is set by `nginz`. Multi-ingress handling only applies to download requests as these are implemented by redirects to the S3 assets host for local assets. -Uploads are handled by cargohold directly itself.) +Uploads are handled by cargohold directly itself. + -The config `aws.multiIngress` is a map from backend domain (`Z-Host` header -value) to a S3 download endpoint. The `Z-Host` header is set by `nginz` to the +For a multi-ingress setup `aws.multiIngress` needs to be configured as a map from backend domain (`Z-Host` header value) to a S3 download endpoint. The `Z-Host` header is set by `nginz` to the value of the incoming requests `Host` header. If there's no config map entry for a provided `Z-Host` in a download request for a local asset, then an error is -returned. +returned. When configured the configuration of `s3DownloadEndpoint` is ignored. This example shows a setup with fake backends *red*, *green* and *blue*: @@ -724,45 +754,14 @@ Link to diagram: https://mermaid.live/edit#pako:eNrdVbFu2zAQ_ZUDJ7ewDdhtUkBDgBRB0CHIYCNL4eVEnmWiMk8lKbttkH8vJbsW5dCOUXSqBkHiPT6-e3yinoVkRSITEC5H32syku40FhbXCwP7C6VnC1hqSQNL6l1XeWRPwBuKqxk8OXKwpRyrahxGxvQD11VJY8mvSHPOB4UlMknSrtonbcfStBVar6Wu0HjQJgCdGwUNKfaonMGMax8WeH9acIq5FXKOuwVE7BcqN4U2v9IlibbgFZcqXZ5_ABeMxYK6uiXpwRb5YHp1NYTJ9FN7ixw3jW6ri5UHXva28rZ5BsVbUzIqB-gc-WgTD9DRzU3Pz7v9FChZYnk8L4KGiW23Gdyz3aJVQW7IoYvQbT3gDq2_wsIIbpWCr6MvHF5WhIpsL2p6g6HFhHePvdajFR6Yv0Fd7ZTDquF9mj3AMoR2t0zHcZg1CiJj92akdGP-OLBJ9JpDFOa73YGNxnRAFZ3Te9rxey5L3gZHdmueMrsLyBnHDwpScerGQr_9dn1tzfFeR_2k2MioRFIn15MhTD82Sb0-ndT4fPjM-emcdsDItf23eVlSW_D_ltXYv0uzenTknU_rOd_fzOsfy_9xYvtN_21ixVCsya5Rq_D3fG6KC-FXtKaFyMKjoiXWpV-IhXkJUKw9z38aKTJvaxqKulKBff-jFdkSS0cvvwHKl250 --> -## Settings in cannon +### Galley -### Multi-Ingress setup - -*cannon* sets [CORS -headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) for direct API -accesses by clients. To generate them for multiple domains (usually, *cannon* -works with only one root domain) these need to be defined with -`nginx_conf.additional_external_env_domains`. +For conversation invite links to be correct in a multi-ingress setup `settings.multiIngress` needs to be configured as map from `Z-Host` to the conversation URI prefix. This setting is a `Z-Host` depended version of `settings.conversationCodeURI`. In fact `settings.multiIngress` and `settings.conversationCodeURI` are mutually exclusive. -E.g. +Example: ```yaml -nginx_conf: - additional_external_env_domains: - - red.example.com - - green.example.org - - blue.example.net -``` - -This setting has a dual in the *nginz* configuration. - -## Settings in nginz - -### Multi-Ingress setup - -nginz sets [CORS -headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). To generate -them for multiple domains (usually, *nginz* works with only one root domain) -these need to be defined with `nginx_conf.additional_external_env_domains`. - -E.g. - -```yaml -nginx_conf: - additional_external_env_domains: - - red.example.com - - green.example.org - - blue.example.net -``` - -This setting has a dual in the *cannon* configuration. +multiIngress: + red.example.com: https://accounts.red.example.com/conversation-join/ + green.example.com: https://accounts.green.example.net/conversation-join/ +``` \ No newline at end of file diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 967786f86c0..bb3aa2ac0d5 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -42,6 +42,13 @@ defProteus = defMLS :: CreateConv defMLS = defProteus {protocol = "mls"} +allowGuests :: CreateConv -> CreateConv +allowGuests cc = + cc + { access = Just ["code"], + accessRole = Just ["team_member", "guest"] + } + instance MakesValue CreateConv where make cc = do quids <- for (cc.qualifiedUsers) objQidObject @@ -223,3 +230,36 @@ removeMember remover qcnv removed = do (removedDomain, removedId) <- objQid removed req <- baseRequest remover Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "members", removedDomain, removedId]) submit "DELETE" req + +postConversationCode :: + (HasCallStack, MakesValue user, MakesValue conv) => + user -> + conv -> + Maybe String -> + Maybe String -> + App Response +postConversationCode user conv mbpassword mbZHost = do + convId <- objId conv + req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convId, "code"]) + submit + "POST" + ( req + & addJSONObject ["password" .= pw | pw <- maybeToList mbpassword] + & maybe id zHost mbZHost + ) + +getConversationCode :: + (HasCallStack, MakesValue user, MakesValue conv) => + user -> + conv -> + Maybe String -> + App Response +getConversationCode user conv mbZHost = do + convId <- objId conv + req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convId, "code"]) + submit + "GET" + ( req + & addQueryParams [("cnv", convId)] + & maybe id zHost mbZHost + ) diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index ef1ef920c38..f956480dc85 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -11,6 +11,7 @@ import API.Gundeck (getNotifications) import Control.Applicative import Control.Concurrent (threadDelay) import Data.Aeson qualified as Aeson +import Data.Text qualified as T import GHC.Stack import SetupHelpers import Testlib.One2One (generateRemoteAndConvIdWithDomain) @@ -462,3 +463,69 @@ testGetOneOnOneConvInStatusSentFromRemote = do filter ((==) d2ConvId) qConvIds `shouldMatch` [d2ConvId] resp <- getConversation d1User d2ConvId resp.status `shouldMatchInt` 200 + +testMultiIngressGuestLinks :: HasCallStack => App () +testMultiIngressGuestLinks = do + do + configuredURI <- readServiceConfig Galley & (%. "settings.conversationCodeURI") & asText + + (user, _) <- createTeam OwnDomain + conv <- postConversation user (allowGuests defProteus) >>= getJSON 201 + + bindResponse (postConversationCode user conv Nothing Nothing) $ \resp -> do + res <- getJSON 201 resp + res %. "type" `shouldMatch` "conversation.code-update" + guestLink <- res %. "data.uri" & asText + assertBool "guestlink incorrect" $ configuredURI `T.isPrefixOf` guestLink + + bindResponse (getConversationCode user conv Nothing) $ \resp -> do + res <- getJSON 200 resp + guestLink <- res %. "uri" & asText + assertBool "guestlink incorrect" $ configuredURI `T.isPrefixOf` guestLink + + bindResponse (getConversationCode user conv (Just "red.example.com")) $ \resp -> do + res <- getJSON 200 resp + guestLink <- res %. "uri" & asText + assertBool "guestlink incorrect" $ configuredURI `T.isPrefixOf` guestLink + + withModifiedBackend + ( def + { galleyCfg = \conf -> + conf + & setField "settings.conversationCodeURI" Null + & setField + "settings.multiIngress" + ( object + [ "red.example.com" .= "https://red.example.com", + "blue.example.com" .= "https://blue.example.com" + ] + ) + } + ) + $ \domain -> do + (user, _) <- createTeam domain + conv <- postConversation user (allowGuests defProteus) >>= getJSON 201 + + bindResponse (postConversationCode user conv Nothing (Just "red.example.com")) $ \resp -> do + res <- getJSON 201 resp + res %. "type" `shouldMatch` "conversation.code-update" + guestLink <- res %. "data.uri" & asText + assertBool "guestlink incorrect" $ (fromString "https://red.example.com") `T.isPrefixOf` guestLink + + bindResponse (getConversationCode user conv (Just "red.example.com")) $ \resp -> do + res <- getJSON 200 resp + guestLink <- res %. "uri" & asText + assertBool "guestlink incorrect" $ (fromString "https://red.example.com") `T.isPrefixOf` guestLink + + bindResponse (getConversationCode user conv (Just "blue.example.com")) $ \resp -> do + res <- getJSON 200 resp + guestLink <- res %. "uri" & asText + assertBool "guestlink incorrect" $ (fromString "https://blue.example.com") `T.isPrefixOf` guestLink + + bindResponse (getConversationCode user conv Nothing) $ \resp -> do + res <- getJSON 403 resp + res %. "label" `shouldMatch` "access-denied" + + bindResponse (getConversationCode user conv (Just "unknown.example.com")) $ \resp -> do + res <- getJSON 403 resp + res %. "label" `shouldMatch` "access-denied" diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index 1aa0b80ca75..670fbdc679e 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -78,12 +78,6 @@ addQueryParams :: [(String, String)] -> HTTP.Request -> HTTP.Request addQueryParams params req = HTTP.setQueryString (map (\(k, v) -> (cs k, Just (cs v))) params) req -zType :: String -> HTTP.Request -> HTTP.Request -zType = addHeader "Z-Type" - -zHost :: String -> HTTP.Request -> HTTP.Request -zHost = addHeader "Z-Host" - contentTypeJSON :: HTTP.Request -> HTTP.Request contentTypeJSON = addHeader "Content-Type" "application/json" @@ -156,6 +150,12 @@ zConnection = addHeader "Z-Connection" zClient :: String -> HTTP.Request -> HTTP.Request zClient = addHeader "Z-Client" +zType :: String -> HTTP.Request -> HTTP.Request +zType = addHeader "Z-Type" + +zHost :: String -> HTTP.Request -> HTTP.Request +zHost = addHeader "Z-Host" + submit :: String -> HTTP.Request -> App Response submit method req0 = do let req = req0 {HTTP.method = T.encodeUtf8 (T.pack method)} diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index a5c932d8741..984a536ebe7 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -73,6 +73,9 @@ asString x = (String s) -> pure (T.unpack s) v -> assertFailureWithJSON x ("String" `typeWasExpectedButGot` v) +asText :: HasCallStack => MakesValue a => a -> App T.Text +asText = (fmap T.pack) . asString + asStringM :: HasCallStack => MakesValue a => a -> App (Maybe String) asStringM x = make x >>= \case diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index d6b30283c94..a9d5ab6646b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -33,6 +33,7 @@ module Wire.API.Routes.Public ZAccess, DescriptionOAuthScope, ZHostOpt, + ZHostValue, ) where @@ -217,8 +218,10 @@ type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] -- | Optional @Z-Host@ header (added by @nginz@) data ZHostOpt +type ZHostValue = Text + type ZOptHostHeader = - Header' '[Servant.Optional, Strict] "Z-Host" Text + Header' '[Servant.Optional, Strict] "Z-Host" ZHostValue instance HasSwagger api => HasSwagger (ZHostOpt :> api) where toSwagger _ = toSwagger (Proxy @api) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index ce778e7d62e..9b0b1250937 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -675,6 +675,7 @@ type ConversationAPI = :> CanThrow 'GuestLinksDisabled :> CanThrow 'CreateConversationCodeConflict :> ZUser + :> ZHostOpt :> ZOptConn :> "conversations" :> Capture' '[Description "Conversation ID"] "cnv" ConvId @@ -693,6 +694,7 @@ type ConversationAPI = :> CanThrow 'GuestLinksDisabled :> CanThrow 'CreateConversationCodeConflict :> ZUser + :> ZHostOpt :> ZOptConn :> "conversations" :> Capture' '[Description "Conversation ID"] "cnv" ConvId @@ -737,6 +739,7 @@ type ConversationAPI = :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'GuestLinksDisabled + :> ZHostOpt :> ZLocalUser :> "conversations" :> Capture' '[Description "Conversation ID"] "cnv" ConvId diff --git a/services/cargohold/src/CargoHold/Options.hs b/services/cargohold/src/CargoHold/Options.hs index 4f99c8300ad..aa515729a1f 100644 --- a/services/cargohold/src/CargoHold/Options.hs +++ b/services/cargohold/src/CargoHold/Options.hs @@ -104,7 +104,7 @@ data AWSOpts = AWSOpts -- -- This logic is: If the @Z-Host@ header is provided and found in this map, -- the map's values is taken as s3 download endpoint to redirect to; - -- otherwise, `_awsS3DownloadEndpoint` is used. This option is only useful + -- otherwise a 404 is retuned. This option is only useful -- in the context of multi-ingress setups where one backend / deployment is -- reachable under several domains. _multiIngress :: !(Maybe (Map String AWSEndpoint)) diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index eb3253d10a3..595a89d8858 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -79,6 +79,7 @@ import Data.Id import Data.Json.Util import Data.List1 import Data.Map.Strict qualified as Map +import Data.Misc (HttpsUrl) import Data.Qualified import Data.Set qualified as Set import Data.Singletons @@ -135,6 +136,7 @@ import Wire.API.Federation.Error import Wire.API.Message import Wire.API.Password (mkSafePassword) import Wire.API.Provider.Service (ServiceRef) +import Wire.API.Routes.Public (ZHostValue) import Wire.API.Routes.Public.Galley.Messaging import Wire.API.Routes.Public.Util (UpdateResult (..)) import Wire.API.ServantProto (RawProto (..)) @@ -497,11 +499,12 @@ addCodeUnqualifiedWithReqBody :: Member TeamFeatureStore r ) => UserId -> + Maybe Text -> Maybe ConnId -> ConvId -> CreateConversationCodeRequest -> Sem r AddCodeResult -addCodeUnqualifiedWithReqBody usr mZcon cnv req = addCodeUnqualified (Just req) usr mZcon cnv +addCodeUnqualifiedWithReqBody usr mbZHost mZcon cnv req = addCodeUnqualified (Just req) usr mbZHost mZcon cnv addCodeUnqualified :: forall r. @@ -521,13 +524,14 @@ addCodeUnqualified :: ) => Maybe CreateConversationCodeRequest -> UserId -> + Maybe ZHostValue -> Maybe ConnId -> ConvId -> Sem r AddCodeResult -addCodeUnqualified mReq usr mZcon cnv = do +addCodeUnqualified mReq usr mbZHost mZcon cnv = do lusr <- qualifyLocal usr lcnv <- qualifyLocal cnv - addCode lusr mZcon lcnv mReq + addCode lusr mbZHost mZcon lcnv mReq addCode :: forall r. @@ -545,37 +549,34 @@ addCode :: Member (Embed IO) r ) => Local UserId -> + Maybe ZHostValue -> Maybe ConnId -> Local ConvId -> Maybe CreateConversationCodeRequest -> Sem r AddCodeResult -addCode lusr mZcon lcnv mReq = do +addCode lusr mbZHost mZcon lcnv mReq = do conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound Query.ensureGuestLinksEnabled (Data.convTeam conv) Query.ensureConvAdmin (Data.convLocalMembers conv) (tUnqualified lusr) ensureAccess conv CodeAccess ensureGuestsOrNonTeamMembersAllowed conv - let (bots, users) = localBotsAndUsers $ Data.convLocalMembers conv + convUri <- getConversationCodeURI mbZHost key <- E.makeKey (tUnqualified lcnv) - mCode <- E.getCode key ReusableCode - case mCode of + E.getCode key ReusableCode >>= \case Nothing -> do code <- E.generateCode (tUnqualified lcnv) ReusableCode (Timeout 3600 * 24 * 365) -- one year FUTUREWORK: configurable - mPw <- forM ((.password) =<< mReq) mkSafePassword + mPw <- for (mReq >>= (.password)) mkSafePassword E.createCode code mPw now <- input - conversationCode <- createCode (isJust mPw) code - let event = Event (tUntagged lcnv) Nothing (tUntagged lusr) now (EdConvCodeUpdate conversationCode) + let event = Event (tUntagged lcnv) Nothing (tUntagged lusr) now (EdConvCodeUpdate (mkConversationCodeInfo (isJust mPw) (codeKey code) (codeValue code) convUri)) + let (bots, users) = localBotsAndUsers $ Data.convLocalMembers conv pushConversationEvent mZcon event (qualifyAs lusr (map lmId users)) bots pure $ CodeAdded event + -- In case conversation already has a code this case covers the allowed no-ops Just (code, mPw) -> do when (isJust mPw || isJust (mReq >>= (.password))) $ throwS @'CreateConversationCodeConflict - conversationCode <- createCode (isJust mPw) code - pure $ CodeAlreadyExisted conversationCode + pure $ CodeAlreadyExisted (mkConversationCodeInfo (isJust mPw) (codeKey code) (codeValue code) convUri) where - createCode :: Bool -> Code -> Sem r ConversationCodeInfo - createCode hasPw code = do - mkConversationCodeInfo hasPw (codeKey code) (codeValue code) <$> E.getConversationCodeURI ensureGuestsOrNonTeamMembersAllowed :: Data.Conversation -> Sem r () ensureGuestsOrNonTeamMembersAllowed conv = unless @@ -639,10 +640,11 @@ getCode :: Member (Input Opts) r, Member TeamFeatureStore r ) => + Maybe ZHostValue -> Local UserId -> ConvId -> Sem r ConversationCodeInfo -getCode lusr cnv = do +getCode mbZHost lusr cnv = do conv <- E.getConversation cnv >>= noteS @'ConvNotFound Query.ensureGuestLinksEnabled (Data.convTeam conv) @@ -650,7 +652,8 @@ getCode lusr cnv = do ensureConvMember (Data.convLocalMembers conv) (tUnqualified lusr) key <- E.makeKey cnv (c, mPw) <- E.getCode key ReusableCode >>= noteS @'CodeNotFound - mkConversationCodeInfo (isJust mPw) (codeKey c) (codeValue c) <$> E.getConversationCodeURI + convUri <- getConversationCodeURI mbZHost + pure $ mkConversationCodeInfo (isJust mPw) (codeKey c) (codeValue c) convUri checkReusableCode :: forall r. @@ -1618,3 +1621,15 @@ rmBot lusr zcon b = do ensureConvMember :: (Member (ErrorS 'ConvNotFound) r) => [LocalMember] -> UserId -> Sem r () ensureConvMember users usr = unless (usr `isMember` users) $ throwS @'ConvNotFound + +getConversationCodeURI :: + ( Member (ErrorS 'ConvAccessDenied) r, + Member CodeStore r + ) => + Maybe ZHostValue -> + Sem r HttpsUrl +getConversationCodeURI mbZHost = do + mbURI <- E.getConversationCodeURI mbZHost + case mbURI of + Just uri -> pure uri + Nothing -> throwS @'ConvAccessDenied diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 55c1dfab8df..2163f9a8a50 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -21,8 +21,8 @@ module Galley.App ( -- * Environment Env, reqId, - monitor, options, + monitor, applog, manager, federator, @@ -53,6 +53,7 @@ import Control.Lens hiding ((.=)) import Data.Default (def) import Data.List.NonEmpty qualified as NE import Data.Metrics.Middleware +import Data.Misc import Data.Qualified import Data.Range import Data.Text (unpack) @@ -128,7 +129,7 @@ type GalleyEffects0 = type GalleyEffects = Append GalleyEffects1 GalleyEffects0 -- Define some invariants for the options used -validateOptions :: Opts -> IO () +validateOptions :: Opts -> IO (Either HttpsUrl (Map Text HttpsUrl)) validateOptions o = do let settings' = view settings o optFanoutLimit = fromIntegral . fromRange $ currentFanoutLimit o @@ -140,19 +141,26 @@ validateOptions o = do (Nothing, Just _) -> error "RabbitMQ config is specified and federator is not, please specify both or none" (Just _, Nothing) -> error "Federator is specified and RabbitMQ config is not, please specify both or none" _ -> pure () + let errMsg = "Either conversationCodeURI or multiIngress needs to be set." + case (settings' ^. conversationCodeURI, settings' ^. multiIngress) of + (Nothing, Nothing) -> error errMsg + (Nothing, Just mi) -> pure (Right mi) + (Just uri, Nothing) -> pure (Left uri) + (Just _, Just _) -> error errMsg createEnv :: Metrics -> Opts -> Logger -> IO Env createEnv m o l = do cass <- initCassandra o l mgr <- initHttpManager o h2mgr <- initHttp2Manager - validateOptions o + codeURIcfg <- validateOptions o Env def m o l mgr h2mgr (o ^. O.federator) (o ^. O.brig) cass <$> Q.new 16000 <*> initExtEnv <*> maybe (pure Nothing) (fmap Just . Aws.mkEnv l mgr) (o ^. journal) <*> loadAllMLSKeys (fold (o ^. settings . mlsPrivateKeyPaths)) <*> traverse (mkRabbitMqChannelMVar l) (o ^. rabbitmq) + <*> pure codeURIcfg initCassandra :: Opts -> Logger -> IO ClientState initCassandra o l = do diff --git a/services/galley/src/Galley/Cassandra/Code.hs b/services/galley/src/Galley/Cassandra/Code.hs index 9206425afe3..f5b2770b38a 100644 --- a/services/galley/src/Galley/Cassandra/Code.hs +++ b/services/galley/src/Galley/Cassandra/Code.hs @@ -23,13 +23,13 @@ where import Cassandra import Control.Lens import Data.Code +import Data.Map qualified as Map import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store import Galley.Data.Types import Galley.Data.Types qualified as Code import Galley.Effects.CodeStore (CodeStore (..)) import Galley.Env -import Galley.Options import Imports import Polysemy import Polysemy.Input @@ -48,8 +48,14 @@ interpretCodeStoreToCassandra = interpret $ \case DeleteCode k s -> embedClient $ deleteCode k s MakeKey cid -> Code.mkKey cid GenerateCode cid s t -> Code.generate cid s t - GetConversationCodeURI -> - view (options . settings . conversationCodeURI) <$> input + GetConversationCodeURI mbHost -> do + env <- input + case env ^. convCodeURI of + Left uri -> pure (Just uri) + Right map' -> + case mbHost of + Just host -> pure (Map.lookup host map') + Nothing -> pure Nothing -- | Insert a conversation code insertCode :: Code -> Maybe Password -> Client () diff --git a/services/galley/src/Galley/Effects/CodeStore.hs b/services/galley/src/Galley/Effects/CodeStore.hs index 88b31b0dfc1..15d71162f3b 100644 --- a/services/galley/src/Galley/Effects/CodeStore.hs +++ b/services/galley/src/Galley/Effects/CodeStore.hs @@ -53,6 +53,6 @@ data CodeStore m a where DeleteCode :: Key -> Scope -> CodeStore m () MakeKey :: ConvId -> CodeStore m Key GenerateCode :: ConvId -> Scope -> Timeout -> CodeStore m Code - GetConversationCodeURI :: CodeStore m HttpsUrl + GetConversationCodeURI :: Maybe Text -> CodeStore m (Maybe HttpsUrl) makeSem ''CodeStore diff --git a/services/galley/src/Galley/Env.hs b/services/galley/src/Galley/Env.hs index 32e45f2aa73..2bdb38c27ff 100644 --- a/services/galley/src/Galley/Env.hs +++ b/services/galley/src/Galley/Env.hs @@ -25,7 +25,7 @@ import Control.Lens hiding ((.=)) import Data.ByteString.Conversion (toByteString') import Data.Id import Data.Metrics.Middleware -import Data.Misc (Fingerprint, Rsa) +import Data.Misc (Fingerprint, HttpsUrl, Rsa) import Data.Range import Galley.Aws qualified as Aws import Galley.Options @@ -63,7 +63,8 @@ data Env = Env _extEnv :: ExtEnv, _aEnv :: Maybe Aws.Env, _mlsKeys :: SignaturePurpose -> MLSKeys, - _rabbitmqChannel :: Maybe (MVar Q.Channel) + _rabbitmqChannel :: Maybe (MVar Q.Channel), + _convCodeURI :: Either HttpsUrl (Map Text HttpsUrl) } -- | Environment specific to the communication with external diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index ab34df7f996..3ec6ba07668 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -22,8 +22,8 @@ module Galley.Options httpPoolSize, maxTeamSize, maxFanoutSize, - exposeInvitationURLsTeamAllowlist, maxConvSize, + exposeInvitationURLsTeamAllowlist, intraListing, disabledAPIVersions, conversationCodeURI, @@ -32,6 +32,7 @@ module Galley.Options federationDomain, mlsPrivateKeyPaths, featureFlags, + multiIngress, defConcurrentDeletionEvents, defDeleteConvThrottleMillis, defFanoutLimit, @@ -90,7 +91,19 @@ data Settings = Settings -- | Whether to call Brig for device listing _intraListing :: !Bool, -- | URI prefix for conversations with access mode @code@ - _conversationCodeURI :: !HttpsUrl, + _conversationCodeURI :: !(Maybe HttpsUrl), + -- | Map from @Z-Host@ header to URI prefix for conversations with access mode @code@ + -- + -- If setMultiIngress is set then the URI prefix for guest links is looked + -- up in this config setting using the @Z-Host@ header value as a key. If + -- the lookup fails then no guest link can be created via the API. + -- + -- This option is only useful in the context of multi-ingress setups where + -- one backend / deployment is is reachable under several domains. + -- + -- multiIngress and conversationCodeURI are mutually exclusive. One of + -- both options need to be configured. + _multiIngress :: Maybe (Map Text HttpsUrl), -- | Throttling: limits to concurrent deletion events _concurrentDeletionEvents :: !(Maybe Int), -- | Throttling: delay between sending events upon team deletion From d1d85b24b680292e65e96088a52474b64aa32757 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 4 Sep 2023 10:21:18 +0200 Subject: [PATCH 109/225] Check validity of notification IDs (#3550) * Check validity of notification IDs * Add CHANGELOG entry * fixup! Add CHANGELOG entry * fixup! fixup! Add CHANGELOG entry --- changelog.d/5-internal/notification-500 | 1 + integration/test/SetupHelpers.hs | 7 +++++-- integration/test/Test/Notifications.hs | 19 +++++++++++++++++++ libs/wire-api/src/Wire/API/Notification.hs | 9 +++++++++ services/gundeck/src/Gundeck/Monad.hs | 6 +++--- services/gundeck/src/Gundeck/Notification.hs | 11 +++++++++++ 6 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 changelog.d/5-internal/notification-500 diff --git a/changelog.d/5-internal/notification-500 b/changelog.d/5-internal/notification-500 new file mode 100644 index 00000000000..7af7198c513 --- /dev/null +++ b/changelog.d/5-internal/notification-500 @@ -0,0 +1 @@ +Check validity of notification IDs in the notification API diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 8851e258900..d5e324ea41c 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -10,6 +10,7 @@ import Data.Aeson.Types qualified as Aeson import Data.Default import Data.Function import Data.List qualified as List +import Data.UUID.V1 (nextUUID) import Data.UUID.V4 (nextRandom) import GHC.Stack import Testlib.Prelude @@ -83,8 +84,10 @@ resetFedConns owndom = do Internal.deleteFedConn' owndom `mapM_` rdoms randomId :: HasCallStack => App String -randomId = do - liftIO (show <$> nextRandom) +randomId = liftIO (show <$> nextRandom) + +randomUUIDv1 :: HasCallStack => App String +randomUUIDv1 = liftIO (show . fromJust <$> nextUUID) randomUserId :: (HasCallStack, MakesValue domain) => domain -> App Value randomUserId domain = do diff --git a/integration/test/Test/Notifications.hs b/integration/test/Test/Notifications.hs index 5e6de9f3a59..ee17b0da485 100644 --- a/integration/test/Test/Notifications.hs +++ b/integration/test/Test/Notifications.hs @@ -61,3 +61,22 @@ testLastNotification = do lastNotif <- getLastNotification user "c" >>= getJSON 200 lastNotif %. "payload" `shouldMatch` [object ["client" .= "c"]] + +testInvalidNotification :: HasCallStack => App () +testInvalidNotification = do + user <- randomUserId OwnDomain + let client = "deadbeef" + + -- test uuid v4 as "since" + do + notifId <- randomId + void $ + getNotifications user client def {since = Just notifId} + >>= getJSON 400 + + -- test arbitrary uuid v1 as "since" + do + notifId <- randomUUIDv1 + void $ + getNotifications user client def {since = Just notifId} + >>= getJSON 404 diff --git a/libs/wire-api/src/Wire/API/Notification.hs b/libs/wire-api/src/Wire/API/Notification.hs index 3e808c02ef2..b404e05db61 100644 --- a/libs/wire-api/src/Wire/API/Notification.hs +++ b/libs/wire-api/src/Wire/API/Notification.hs @@ -20,6 +20,7 @@ module Wire.API.Notification ( NotificationId, + isValidNotificationId, RawNotificationId (..), Event, @@ -41,6 +42,7 @@ import Control.Lens (makeLenses, (.~)) import Control.Lens.Operators ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson.Types qualified as Aeson +import Data.Bits import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Id import Data.Json.Util @@ -50,6 +52,7 @@ import Data.Schema import Data.Swagger (ToParamSchema (..)) import Data.Swagger qualified as S import Data.Time.Clock (UTCTime) +import Data.UUID qualified as UUID import Imports import Servant import Wire.API.Routes.MultiVerb @@ -80,6 +83,12 @@ eventSchema = mkSchema sdoc Aeson.parseJSON (Just . Aeson.toJSON) ) ] +isValidNotificationId :: NotificationId -> Bool +isValidNotificationId (Id uuid) = + -- check that the version bits are set to 1 + case UUID.toWords uuid of + (_, w, _, _) -> (w `shiftR` 12) .&. 0xf == 1 + -------------------------------------------------------------------------------- -- QueuedNotification diff --git a/services/gundeck/src/Gundeck/Monad.hs b/services/gundeck/src/Gundeck/Monad.hs index 4ef03216436..f3a3a9512b3 100644 --- a/services/gundeck/src/Gundeck/Monad.hs +++ b/services/gundeck/src/Gundeck/Monad.hs @@ -45,9 +45,9 @@ where import Bilge hiding (Request, header, options, statusCode) import Bilge.RPC import Cassandra -import Control.Error hiding (err) +import Control.Error import Control.Exception (throwIO) -import Control.Lens hiding ((.=)) +import Control.Lens import Control.Monad.Catch hiding (tryJust) import Data.Aeson (FromJSON) import Data.Default (def) @@ -61,7 +61,7 @@ import Network.Wai import Network.Wai.Utilities import System.Logger qualified as Log import System.Logger qualified as Logger -import System.Logger.Class hiding (Error, info) +import System.Logger.Class import UnliftIO (async) -- | TODO: 'Client' already has an 'Env'. Why do we need two? How does this even work? We should diff --git a/services/gundeck/src/Gundeck/Notification.hs b/services/gundeck/src/Gundeck/Notification.hs index 5f41a7ba5cf..615ed79c29d 100644 --- a/services/gundeck/src/Gundeck/Notification.hs +++ b/services/gundeck/src/Gundeck/Notification.hs @@ -25,6 +25,8 @@ import Bilge.IO hiding (options) import Bilge.Request import Bilge.Response import Control.Lens (view) +import Control.Monad.Catch +import Control.Monad.Except import Data.ByteString.Conversion import Data.Id import Data.Misc (Milliseconds (..)) @@ -35,10 +37,13 @@ import Gundeck.Monad import Gundeck.Notification.Data qualified as Data import Gundeck.Options hiding (host, port) import Imports hiding (getLast) +import Network.HTTP.Types hiding (statusCode) +import Network.Wai.Utilities.Error import System.Logger.Class import System.Logger.Class qualified as Log import Util.Options hiding (host, port) import Wire.API.Internal.Notification +import Wire.API.Notification data PaginateResult = PaginateResult { paginateResultGap :: Bool, @@ -47,6 +52,7 @@ data PaginateResult = PaginateResult paginate :: UserId -> Maybe NotificationId -> Maybe ClientId -> Range 100 10000 Int32 -> Gundeck PaginateResult paginate uid since mclt size = do + traverse_ validateNotificationId since for_ mclt $ \clt -> updateActivity uid clt time <- posixTime @@ -60,6 +66,11 @@ paginate uid since mclt size = do (Just (millisToUTC time)) millisToUTC = posixSecondsToUTCTime . fromIntegral . (`div` 1000) . ms + validateNotificationId :: NotificationId -> Gundeck () + validateNotificationId n = + unless (isValidNotificationId n) $ + throwM (mkError status400 "bad-request" "Invalid Notification ID") + -- | Update last_active property of the given client by making a request to brig. updateActivity :: UserId -> ClientId -> Gundeck () updateActivity uid clt = do From 3653d56766032ad80951d3c412f24c3e491d449e Mon Sep 17 00:00:00 2001 From: Owen Harvey Date: Mon, 4 Sep 2023 18:26:04 +1000 Subject: [PATCH 110/225] WPB-633 Servantify Brig/Provider.Service API (#3554) * WPB-1214: Servantify Brig/Provider.Service API - Moving the routes over to servant, and removing the old routing code. - Adding new instances to types that needed them for servant. * WPB-663: Removing a redundant TODO comment, adding changelog --- changelog.d/5-internal/WPB-663 | 14 ++ libs/types-common/src/Data/Range.hs | 3 + .../wire-api/src/Wire/API/Provider/Service.hs | 115 +++++++++++- .../src/Wire/API/Provider/Service/Tag.hs | 67 ++++++- .../src/Wire/API/Routes/Public/Brig.hs | 149 ++++++++++++++++ services/brig/src/Brig/API/Public.hs | 88 ++++++++++ services/brig/src/Brig/Provider/API.hs | 165 ++---------------- services/brig/src/Brig/Team/API.hs | 24 +++ 8 files changed, 476 insertions(+), 149 deletions(-) create mode 100644 changelog.d/5-internal/WPB-663 diff --git a/changelog.d/5-internal/WPB-663 b/changelog.d/5-internal/WPB-663 new file mode 100644 index 00000000000..303cf529f7b --- /dev/null +++ b/changelog.d/5-internal/WPB-663 @@ -0,0 +1,14 @@ +Migrating the following routes to the Servant API form. + +POST /provider/services +GET /provider/services +GET /provider/services/:sid +PUT /provider/services/:sid +PUT /provider/services/:sid/connection +DELETE /provider/services/:sid +GET /providers/:pid/services +GET /providers/:pid/services/:sid +GET /services +GET /services/tags +GET /teams/:tid/services/whitelisted +POST /teams/:tid/services/whitelist \ No newline at end of file diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index e4c5be14781..8b3d3a9cb11 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -152,6 +152,9 @@ numRangedSchemaDocModifier n m = S.schema %~ ((S.minimum_ ?~ fromIntegral n) . ( instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d [a] where rangedSchemaDocModifier _ = listRangedSchemaDocModifier +-- Sets are effectively lists, so we can reuse that code. +instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d (Set a) where rangedSchemaDocModifier _ = listRangedSchemaDocModifier + instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d Text where rangedSchemaDocModifier _ = stringRangedSchemaDocModifier instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d String where rangedSchemaDocModifier _ = stringRangedSchemaDocModifier diff --git a/libs/wire-api/src/Wire/API/Provider/Service.hs b/libs/wire-api/src/Wire/API/Provider/Service.hs index 28a1e5609a1..9b59decbcec 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service.hs @@ -47,6 +47,7 @@ module Wire.API.Provider.Service -- * UpdateServiceWhitelist UpdateServiceWhitelist (..), + UpdateServiceWhitelistResp (..), ) where @@ -63,7 +64,8 @@ import Data.List1 (List1) import Data.Misc (HttpsUrl (..), PlainTextPassword6) import Data.PEM (PEM, pemParseBS, pemWriteLBS) import Data.Proxy -import Data.Range (Range) +import Data.Range (Range, fromRange, rangedSchema) +import Data.SOP import Data.Schema import Data.Swagger qualified as S import Data.Text qualified as Text @@ -71,6 +73,7 @@ import Data.Text.Ascii import Data.Text.Encoding qualified as Text import Imports import Wire.API.Provider.Service.Tag (ServiceTag (..)) +import Wire.API.Routes.MultiVerb import Wire.API.User.Profile (Asset, Name) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) @@ -205,6 +208,22 @@ data Service = Service } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform Service) + deriving (S.ToSchema) via (Schema Service) + +instance ToSchema Service where + schema = + object "Service" $ + Service + <$> serviceId .= field "id" schema + <*> serviceName .= field "name" schema + <*> serviceSummary .= field "summary" schema + <*> serviceDescr .= field "description" schema + <*> serviceUrl .= field "base_url" schema + <*> serviceTokens .= field "auth_tokens" schema + <*> serviceKeys .= field "public_keys" schema + <*> serviceAssets .= field "assets" (array schema) + <*> serviceTags .= field "tags" (set schema) + <*> serviceEnabled .= field "enabled" schema instance ToJSON Service where toJSON s = @@ -265,6 +284,7 @@ data ServiceProfile = ServiceProfile } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ServiceProfile) + deriving (S.ToSchema) via (Schema ServiceProfile) instance ToJSON ServiceProfile where toJSON s = @@ -291,6 +311,19 @@ instance FromJSON ServiceProfile where <*> o A..: "tags" <*> o A..: "enabled" +instance ToSchema ServiceProfile where + schema = + object "ServiceProfile" $ + ServiceProfile + <$> serviceProfileId .= field "id" schema + <*> serviceProfileProvider .= field "provider" schema + <*> serviceProfileName .= field "name" schema + <*> serviceProfileSummary .= field "summary" schema + <*> serviceProfileDescr .= field "description" schema + <*> serviceProfileAssets .= field "assets" (array schema) + <*> serviceProfileTags .= field "tags" (set schema) + <*> serviceProfileEnabled .= field "enabled" schema + -------------------------------------------------------------------------------- -- ServiceProfilePage @@ -300,6 +333,7 @@ data ServiceProfilePage = ServiceProfilePage } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ServiceProfilePage) + deriving (S.ToSchema) via (Schema ServiceProfilePage) instance ToJSON ServiceProfilePage where toJSON p = @@ -314,6 +348,13 @@ instance FromJSON ServiceProfilePage where <$> o A..: "has_more" <*> o A..: "services" +instance ToSchema ServiceProfilePage where + schema = + object "ServiceProfile" $ + ServiceProfilePage + <$> serviceProfilePageHasMore .= field "has_more" schema + <*> serviceProfilePageResults .= field "services" (array schema) + -------------------------------------------------------------------------------- -- NewService @@ -330,6 +371,20 @@ data NewService = NewService } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewService) + deriving (S.ToSchema) via (Schema NewService) + +instance ToSchema NewService where + schema = + object "NewService" $ + NewService + <$> newServiceName .= field "name" schema + <*> newServiceSummary .= field "summary" schema + <*> newServiceDescr .= field "description" schema + <*> newServiceUrl .= field "base_url" schema + <*> newServiceKey .= field "public_key" schema + <*> newServiceToken .= maybe_ (optField "auth_token" schema) + <*> newServiceAssets .= field "assets" (array schema) + <*> newServiceTags .= field "tags" (fromRange .= rangedSchema (set schema)) instance ToJSON NewService where toJSON s = @@ -366,6 +421,14 @@ data NewServiceResponse = NewServiceResponse } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewServiceResponse) + deriving (S.ToSchema) via (Schema NewServiceResponse) + +instance ToSchema NewServiceResponse where + schema = + object "NewServiceResponse" $ + NewServiceResponse + <$> rsNewServiceId .= field "id" schema + <*> rsNewServiceToken .= maybe_ (optField "auth_token" schema) instance ToJSON NewServiceResponse where toJSON r = @@ -393,6 +456,17 @@ data UpdateService = UpdateService } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UpdateService) + deriving (S.ToSchema) via (Schema UpdateService) + +instance ToSchema UpdateService where + schema = + object "UpdateService" $ + UpdateService + <$> updateServiceName .= maybe_ (optField "name" schema) + <*> updateServiceSummary .= maybe_ (optField "summary" schema) + <*> updateServiceDescr .= maybe_ (optField "description" schema) + <*> updateServiceAssets .= maybe_ (optField "assets" $ array schema) + <*> updateServiceTags .= maybe_ (optField "tags" (fromRange .= rangedSchema (set schema))) instance ToJSON UpdateService where toJSON u = @@ -427,6 +501,17 @@ data UpdateServiceConn = UpdateServiceConn } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UpdateServiceConn) + deriving (S.ToSchema) via (Schema UpdateServiceConn) + +instance ToSchema UpdateServiceConn where + schema = + object "UpdateServiceConn" $ + UpdateServiceConn + <$> updateServiceConnPassword .= field "password" schema + <*> updateServiceConnUrl .= maybe_ (optField "base_url" schema) + <*> updateServiceConnKeys .= maybe_ (optField "public_keys" (fromRange .= rangedSchema (array schema))) + <*> updateServiceConnTokens .= maybe_ (optField "auth_tokens" (fromRange .= rangedSchema (array schema))) + <*> updateServiceConnEnabled .= maybe_ (optField "enabled" schema) mkUpdateServiceConn :: PlainTextPassword6 -> UpdateServiceConn mkUpdateServiceConn pw = UpdateServiceConn pw Nothing Nothing Nothing Nothing @@ -458,6 +543,13 @@ newtype DeleteService = DeleteService {deleteServicePassword :: PlainTextPassword6} deriving stock (Eq, Show) deriving newtype (Arbitrary) + deriving (S.ToSchema) via (Schema DeleteService) + +instance ToSchema DeleteService where + schema = + object "DeleteService" $ + DeleteService + <$> deleteServicePassword .= field "password" schema instance ToJSON DeleteService where toJSON d = @@ -479,6 +571,15 @@ data UpdateServiceWhitelist = UpdateServiceWhitelist } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UpdateServiceWhitelist) + deriving (S.ToSchema) via (Schema UpdateServiceWhitelist) + +instance ToSchema UpdateServiceWhitelist where + schema = + object "UpdateServiceWhitelist" $ + UpdateServiceWhitelist + <$> updateServiceWhitelistProvider .= field "provider" schema + <*> updateServiceWhitelistService .= field "id" schema + <*> updateServiceWhitelistStatus .= field "whitelisted" schema instance ToJSON UpdateServiceWhitelist where toJSON u = @@ -494,3 +595,15 @@ instance FromJSON UpdateServiceWhitelist where <$> o A..: "provider" <*> o A..: "id" <*> o A..: "whitelisted" + +data UpdateServiceWhitelistResp + = UpdateServiceWhitelistRespChanged + | UpdateServiceWhitelistRespUnchanged + +-- basically the same as the instance for CheckBlacklistResponse +instance AsUnion '[RespondEmpty 200 "UpdateServiceWhitelistRespChanged", RespondEmpty 204 "UpdateServiceWhitelistRespUnchanged"] UpdateServiceWhitelistResp where + toUnion UpdateServiceWhitelistRespChanged = Z (I ()) + toUnion UpdateServiceWhitelistRespUnchanged = S (Z (I ())) + fromUnion (Z (I ())) = UpdateServiceWhitelistRespChanged + fromUnion (S (Z (I ()))) = UpdateServiceWhitelistRespUnchanged + fromUnion (S (S x)) = case x of {} diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index 522c519ff87..55a804d4cb0 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -1,4 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# OPTIONS_GHC -Wno-orphans #-} -- This file is part of the Wire Server implementation. -- @@ -39,18 +40,30 @@ module Wire.API.Provider.Service.Tag ) where +import Control.Lens (Prism', prism) import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON)) +import Data.Aeson qualified as A import Data.Aeson qualified as JSON +import Data.Attoparsec.ByteString (IResult (..), parse) +import Data.ByteString (toStrict) import Data.ByteString.Builder qualified as BB import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Conversion -import Data.Range (Range, fromRange) +import Data.Range (Range, fromRange, rangedSchema) import Data.Range qualified as Range +import Data.Schema import Data.Set qualified as Set +import Data.Swagger (ParamSchema (_paramSchemaEnum, _paramSchemaType), SwaggerType (SwaggerString), ToParamSchema (toParamSchema)) +import Data.Swagger qualified as S +import Data.Text qualified as T +import Data.Text.Encoding (decodeUtf8) +import Data.Text.Encoding qualified as T import Data.Text.Encoding qualified as Text import Data.Type.Ord import GHC.TypeLits (KnownNat, Nat) import Imports +import Web.HttpApiData (FromHttpApiData (parseUrlPiece), ToHttpApiData, toQueryParam) +import Web.Internal.HttpApiData (toUrlPiece) import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) -------------------------------------------------------------------------------- @@ -59,6 +72,13 @@ import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) newtype ServiceTagList = ServiceTagList [ServiceTag] deriving stock (Eq, Ord, Show) deriving newtype (FromJSON, ToJSON, Arbitrary) + deriving (S.ToSchema) via (Schema ServiceTagList) + +_ServiceTagList :: Prism' ServiceTagList [ServiceTag] +_ServiceTagList = prism ServiceTagList (\(ServiceTagList l) -> pure l) + +instance ToSchema ServiceTagList where + schema = named "ServiceTagList" $ tag _ServiceTagList $ array schema -- | A fixed enumeration of tags for services. data ServiceTag @@ -95,6 +115,7 @@ data ServiceTag | WeatherTag deriving stock (Eq, Show, Ord, Enum, Bounded, Generic) deriving (Arbitrary) via (GenericUniform ServiceTag) + deriving (S.ToSchema) via (Schema ServiceTag) instance FromByteString ServiceTag where parser = @@ -173,6 +194,12 @@ instance FromJSON ServiceTag where JSON.withText "ServiceTag" $ either fail pure . runParser parser . Text.encodeUtf8 +instance ToSchema ServiceTag where + schema = enum @Text "" . mconcat $ (\a -> element (decodeUtf8 $ toStrict $ toByteString a) a) <$> [minBound ..] + +instance ToHttpApiData ServiceTag where + toUrlPiece = cs . toByteString' + -------------------------------------------------------------------------------- -- Bounded ServiceTag Queries @@ -181,6 +208,19 @@ newtype QueryAnyTags (m :: Nat) (n :: Nat) = QueryAnyTags {queryAnyTagsRange :: Range m n (Set (QueryAllTags m n))} deriving stock (Eq, Show, Ord) +instance (m <= n) => ToParamSchema (QueryAnyTags m n) where + toParamSchema _ = + mempty + { _paramSchemaType = Just SwaggerString, + _paramSchemaEnum = Just (A.String . toQueryParam <$> [(minBound :: ServiceTag) ..]) + } + +instance (KnownNat n, KnownNat m, m <= n) => ToSchema (QueryAnyTags m n) where + schema = + let sch :: ValueSchema NamedSwaggerDoc (Range m n (Set (QueryAllTags m n))) + sch = fromRange .= rangedSchema (named "QueryAnyTags" $ set schema) + in queryAnyTagsRange .= (QueryAnyTags <$> sch) + instance (KnownNat m, KnownNat n, m <= n) => Arbitrary (QueryAnyTags m n) where arbitrary = QueryAnyTags <$> arbitrary @@ -207,11 +247,31 @@ instance (KnownNat n, KnownNat m, m <= n) => FromByteString (QueryAnyTags m n) w rs <- either fail pure (Range.checkedEither (Set.fromList ts)) pure $! QueryAnyTags rs +runPartial :: IsString i => Bool -> IResult i b -> Either Text b +runPartial alreadyRun result = case result of + Fail _ _ e -> Left $ T.pack e + Partial f -> + if alreadyRun + then Left "A partial parse returned another partial parse." + else runPartial True $ f "" + Done _ r -> pure r + +instance (KnownNat n, KnownNat m, m <= n) => FromHttpApiData (QueryAnyTags m n) where + parseUrlPiece t = do + txt <- parseUrlPiece t + runPartial False $ parse parser $ T.encodeUtf8 txt + -- | Bounded logical conjunction of 'm' to 'n' 'ServiceTag's to match. newtype QueryAllTags (m :: Nat) (n :: Nat) = QueryAllTags {queryAllTagsRange :: Range m n (Set ServiceTag)} deriving stock (Eq, Show, Ord) +instance (KnownNat n, KnownNat m, m <= n) => ToSchema (QueryAllTags m n) where + schema = + let sch :: ValueSchema NamedSwaggerDoc (Range m n (Set ServiceTag)) + sch = fromRange .= rangedSchema (named "QueryAllTags" $ set schema) + in queryAllTagsRange .= (QueryAllTags <$> sch) + instance (KnownNat m, KnownNat n, m <= n) => Arbitrary (QueryAllTags m n) where arbitrary = QueryAllTags <$> arbitrary @@ -236,6 +296,11 @@ instance (KnownNat m, KnownNat n, m <= n) => FromByteString (QueryAllTags m n) w rs <- either fail pure (Range.checkedEither (Set.fromList ts)) pure $! QueryAllTags rs +instance (KnownNat n, KnownNat m, m <= n) => FromHttpApiData (QueryAllTags m n) where + parseUrlPiece t = do + txt <- parseUrlPiece t + runPartial False $ parse parser $ T.encodeUtf8 txt + -------------------------------------------------------------------------------- -- ServiceTag Matchers diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index d83004bb54b..592fc7a8f5f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1,4 +1,6 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} +-- Required for `instance MimeRender PlainText ()` +{-# OPTIONS_GHC -Wno-orphans #-} -- This file is part of the Wire Server implementation. -- @@ -50,6 +52,12 @@ import Wire.API.MLS.Servant import Wire.API.MakesFederatedCall import Wire.API.OAuth import Wire.API.Properties + ( PropertyKey, + PropertyKeysAndValues, + RawPropertyValue, + ) +import Wire.API.Provider.Service qualified as Public +import Wire.API.Provider.Service.Tag qualified as Public import Wire.API.Routes.API import Wire.API.Routes.Bearer import Wire.API.Routes.Cookies @@ -94,6 +102,8 @@ type BrigAPI = :<|> SystemSettingsAPI :<|> OAuthAPI :<|> BotAPI + :<|> ProviderAPI + :<|> ServicesAPI data BrigAPITag @@ -286,6 +296,112 @@ type UserAPI = (Respond 200 "Protocols supported by the user" (Set BaseProtocolTag)) ) +type ProviderAPI = + Named + "post-provider-services" + ( Summary "" + :> Description "" + :> ZProvider + :> "provider" + :> "services" + :> ReqBody '[JSON] Public.NewService + :> MultiVerb1 'POST '[JSON] (Respond 201 "" Public.NewServiceResponse) + ) + :<|> Named + "get-provider-services" + ( Summary "" + :> Description "" + :> ZProvider + :> "provider" + :> "services" + :> Get '[JSON] [Public.Service] + ) + :<|> Named + "get-provider-services-by-service-id" + ( Summary "" + :> Description "" + :> ZProvider + :> "provider" + :> "services" + :> Capture "service-id" ServiceId + :> Get '[JSON] Public.Service + ) + :<|> Named + "put-provider-services-by-service-id" + ( Summary "" + :> Description "" + :> ZProvider + :> "provider" + :> "services" + :> Capture "service-id" ServiceId + :> ReqBody '[JSON] Public.UpdateService + :> Put '[PlainText] () + ) + :<|> Named + "put-provider-services-connection-by-service-id" + ( Summary "" + :> Description "" + :> ZProvider + :> "provider" + :> "services" + :> Capture "service-id" ServiceId + :> "connection" + :> ReqBody '[JSON] Public.UpdateServiceConn + :> Put '[PlainText] () + ) + :<|> Named + "delete-provider-services-by-service-id" + ( Summary "" + :> Description "" + :> ZProvider + :> "provider" + :> "services" + :> Capture "service-id" ServiceId + :> ReqBody '[JSON] Public.DeleteService + :> MultiVerb1 'DELETE '[PlainText] (RespondEmpty 202 "") + ) + :<|> Named + "get-provider-services-by-provider-id" + ( Summary "" + :> Description "" + :> ZAccess + :> "providers" + :> Capture "provider-id" ProviderId + :> "services" + :> Get '[JSON] [Public.ServiceProfile] + ) + :<|> Named + "get-provider-services-by-provider-id-and-service-id" + ( Summary "" + :> Description "" + :> ZAccess + :> "providers" + :> Capture "provider-id" ProviderId + :> "services" + :> Capture "service-id" ServiceId + :> Get '[JSON] Public.ServiceProfile + ) + +type ServicesAPI = + Named + "get-services" + ( Summary "" + :> Description "" + :> ZAccess + :> "services" + :> QueryParam "tags" (Public.QueryAnyTags 1 3) + :> QueryParam "start" Text + :> QueryParam "size" (Range 10 100 Int32) -- Default to 20 + :> Get '[JSON] Public.ServiceProfilePage + ) + :<|> Named + "get-services-tags" + ( Summary "" + :> Description "" + :> ZAccess + :> Get '[JSON] Public.ServiceTagList + ) + type SelfAPI = Named "get-self" @@ -1526,6 +1642,39 @@ type TeamsAPI = '[JSON] (Respond 200 "Number of team members" TeamSize) ) + :<|> Named + "get-whitelisted-services-by-team-id" + ( Summary "" + :> Description "" + :> ZAccess + :> "teams" + :> Capture "team-id" TeamId + :> "services" + :> "whitelisted" + :> QueryParam "prefix" (Range 1 128 Text) + -- Default to True + :> QueryParam "filter_disabled" Bool + -- Default to 20 + :> QueryParam "size" (Range 10 100 Int32) + :> Get '[JSON] Public.ServiceProfilePage + ) + :<|> Named + "post-team-whitelist-by-team-id" + ( Summary "" + :> Description "" + :> ZAccess + :> ZConn + :> "teams" + :> Capture "team-id" TeamId + :> "services" + :> "whitelist" + :> ReqBody '[JSON] Public.UpdateServiceWhitelist + :> MultiVerb 'POST '[PlainText] '[RespondEmpty 200 "UpdateServiceWhitelistRespChanged", RespondEmpty 204 "UpdateServiceWhitelistRespUnchanged"] Public.UpdateServiceWhitelistResp + ) + +-- Plaintext doesn't ship a renderer for (), so we have an orphan for it +instance MimeRender PlainText () where + mimeRender _ () = "" type SystemSettingsAPI = Named diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 46665c7122f..fedbc25d5d3 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -90,6 +90,7 @@ import Data.Misc (IpAddr (..)) import Data.Nonce (Nonce, randomNonce) import Data.Qualified import Data.Range +import Data.Schema () import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Ascii qualified as Ascii @@ -114,6 +115,8 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Federation.API import Wire.API.Federation.Error import Wire.API.Properties qualified as Public +import Wire.API.Provider.Service qualified as Public +import Wire.API.Provider.Service.Tag qualified as Public import Wire.API.Routes.API import Wire.API.Routes.Internal.Brig qualified as BrigInternalAPI import Wire.API.Routes.Internal.Cannon qualified as CannonInternalAPI @@ -288,7 +291,23 @@ servantSitemap = :<|> systemSettingsAPI :<|> oauthAPI :<|> botAPI + :<|> providerAPI + :<|> servicesAPI where + providerAPI :: ServerT ProviderAPI (Handler r) + providerAPI = + Named @"post-provider-services" addServiceH + :<|> Named @"get-provider-services" listServicesH + :<|> Named @"get-provider-services-by-service-id" getServiceH + :<|> Named @"put-provider-services-by-service-id" updateServiceH + :<|> Named @"put-provider-services-connection-by-service-id" updateServiceConnH + :<|> Named @"delete-provider-services-by-service-id" deleteServiceH + :<|> Named @"get-provider-services-by-provider-id" listServiceProfilesH + :<|> Named @"get-provider-services-by-provider-id-and-service-id" getServiceProfileH + servicesAPI :: ServerT ServicesAPI (Handler r) + servicesAPI = + Named @"get-services" searchServiceProfilesH + :<|> Named @"get-services-tags" getServiceTagListH userAPI :: ServerT UserAPI (Handler r) userAPI = Named @"get-user-unqualified" (callsFed (exposeAnnotations getUserUnqualifiedH)) @@ -1121,6 +1140,75 @@ updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do teamMember <- MaybeT $ lift $ liftSem $ GalleyProvider.getTeamMember zuserId teamId pure $ teamMember `hasPermission` ChangeTeamMemberProfiles +-- ProviderAPI +addServiceH :: + Member GalleyProvider r => + ProviderId -> + Public.NewService -> + (Handler r) Public.NewServiceResponse +addServiceH pid req = do + Provider.guardSecondFactorDisabled Nothing + Provider.addService pid req + +listServicesH :: Member GalleyProvider r => ProviderId -> (Handler r) [Public.Service] +listServicesH pid = do + Provider.guardSecondFactorDisabled Nothing + Provider.listServices pid + +getServiceH :: Member GalleyProvider r => ProviderId -> ServiceId -> (Handler r) Public.Service +getServiceH pid sid = do + Provider.guardSecondFactorDisabled Nothing + Provider.getService pid sid + +updateServiceH :: Member GalleyProvider r => ProviderId -> ServiceId -> Public.UpdateService -> (Handler r) () +updateServiceH pid sid req = do + Provider.guardSecondFactorDisabled Nothing + void $ Provider.updateService pid sid req + +updateServiceConnH :: Member GalleyProvider r => ProviderId -> ServiceId -> Public.UpdateServiceConn -> (Handler r) () +updateServiceConnH pid sid req = do + Provider.guardSecondFactorDisabled Nothing + void $ Provider.updateServiceConn pid sid req + +-- TODO: Send informational email to provider. + +-- | Member GalleyProvider r => The endpoint that is called to delete a service. +-- +-- Since deleting a service can be costly, it just marks the service as +-- disabled and then creates an event that will, when processed, actually +-- delete the service. See 'finishDeleteService'. +deleteServiceH :: Member GalleyProvider r => ProviderId -> ServiceId -> Public.DeleteService -> (Handler r) () +deleteServiceH pid sid req = do + Provider.guardSecondFactorDisabled Nothing + void $ Provider.deleteService pid sid req + +listServiceProfilesH :: Member GalleyProvider r => UserId -> ProviderId -> (Handler r) [Public.ServiceProfile] +listServiceProfilesH _ pid = do + Provider.guardSecondFactorDisabled Nothing + Provider.listServiceProfiles pid + +getServiceProfileH :: Member GalleyProvider r => UserId -> ProviderId -> ServiceId -> (Handler r) Public.ServiceProfile +getServiceProfileH _ pid sid = do + Provider.guardSecondFactorDisabled Nothing + Provider.getServiceProfile pid sid + +-- ServicesAPI +searchServiceProfilesH :: + Member GalleyProvider r => + UserId -> + Maybe (Public.QueryAnyTags 1 3) -> + Maybe Text -> + Maybe (Range 10 100 Int32) -> -- Default to 20 + (Handler r) Public.ServiceProfilePage +searchServiceProfilesH _ qt start size = do + Provider.guardSecondFactorDisabled Nothing + Provider.searchServiceProfiles qt start $ fromMaybe (unsafeRange 20) size + +getServiceTagListH :: Member GalleyProvider r => UserId -> (Handler r) Public.ServiceTagList +getServiceTagListH _ = do + Provider.guardSecondFactorDisabled Nothing + Provider.getServiceTagList () + -- activation activate :: diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index a836861f8f5..46cc9b38045 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -23,6 +23,23 @@ module Brig.Provider.API -- * Event handlers finishDeleteService, + + -- * Extras + guardSecondFactorDisabled, + addService, + listServices, + getService, + updateService, + updateServiceConn, + deleteService, + listServiceProfiles, + getServiceProfile, + searchServiceProfilesH, + searchServiceProfiles, + getServiceTagList, + searchTeamServiceProfiles, + updateServiceWhitelist, + UpdateServiceWhitelistResp (..), ) where @@ -81,7 +98,7 @@ import GHC.TypeNats import Imports import Network.HTTP.Types.Status import Network.Wai (Response) -import Network.Wai.Predicate (accept, def, opt, query) +import Network.Wai.Predicate (accept, query) import Network.Wai.Routing import Network.Wai.Utilities.Error ((!>>)) import Network.Wai.Utilities.Error qualified as Wai @@ -206,46 +223,11 @@ routesPublic = do .&> zauth ZAuthProvider .&> zauthProviderId - post "/provider/services" (continue addServiceH) $ - accept "application" "json" - .&> zauth ZAuthProvider - .&> zauthProviderId - .&. jsonRequest @Public.NewService - - get "/provider/services" (continue listServicesH) $ - accept "application" "json" - .&> zauth ZAuthProvider - .&> zauthProviderId - - get "/provider/services/:sid" (continue getServiceH) $ - accept "application" "json" - .&> zauth ZAuthProvider - .&> zauthProviderId - .&. capture "sid" - - put "/provider/services/:sid" (continue updateServiceH) $ - zauth ZAuthProvider - .&> zauthProviderId - .&. capture "sid" - .&. jsonRequest @Public.UpdateService - - put "/provider/services/:sid/connection" (continue updateServiceConnH) $ - zauth ZAuthProvider - .&> zauthProviderId - .&. capture "sid" - .&. jsonRequest @Public.UpdateServiceConn - -- TODO -- post "/provider/services/:sid/token" (continue genServiceTokenH) $ -- accept "application" "json" -- .&. zauthProvider - delete "/provider/services/:sid" (continue deleteServiceH) $ - zauth ZAuthProvider - .&> zauthProviderId - .&. capture "sid" - .&. jsonRequest @Public.DeleteService - -- User API ---------------------------------------------------------------- get "/providers/:pid" (continue getProviderProfileH) $ @@ -253,44 +235,6 @@ routesPublic = do .&> zauth ZAuthAccess .&> capture "pid" - get "/providers/:pid/services" (continue listServiceProfilesH) $ - accept "application" "json" - .&> zauth ZAuthAccess - .&> capture "pid" - - get "/providers/:pid/services/:sid" (continue getServiceProfileH) $ - accept "application" "json" - .&> zauth ZAuthAccess - .&> capture "pid" - .&. capture "sid" - - get "/services" (continue searchServiceProfilesH) $ - accept "application" "json" - .&> zauth ZAuthAccess - .&> opt (query "tags") - .&. opt (query "start") - .&. def (unsafeRange 20) (query "size") - - get "/services/tags" (continue getServiceTagListH) $ - accept "application" "json" - .&> zauth ZAuthAccess - - get "/teams/:tid/services/whitelisted" (continue searchTeamServiceProfilesH) $ - accept "application" "json" - .&> zauthUserId - .&. capture "tid" - .&. opt (query "prefix") - .&. def True (query "filter_disabled") - .&. def (unsafeRange 20) (query "size") - - post "/teams/:tid/services/whitelist" (continue updateServiceWhitelistH) $ - accept "application" "json" - .&> zauth ZAuthAccess - .&> zauthUserId - .&. zauthConnId - .&. capture "tid" - .&. jsonRequest @Public.UpdateServiceWhitelist - routesInternal :: Member GalleyProvider r => Routes a (Handler r) () routesInternal = do get "/i/provider/activation-code" (continue getActivationCodeH) $ @@ -519,11 +463,6 @@ updateAccountPassword pid upd = do throwStd newPasswordMustDiffer wrapClientE $ DB.updateAccountPassword pid (newPassword upd) -addServiceH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.NewService -> (Handler r) Response -addServiceH (pid ::: req) = do - guardSecondFactorDisabled Nothing - setStatus status201 . json <$> (addService pid =<< parseJsonBody req) - addService :: ProviderId -> Public.NewService -> (Handler r) Public.NewServiceResponse addService pid new = do _ <- wrapClientE (DB.lookupAccount pid) >>= maybeInvalidProvider @@ -540,28 +479,13 @@ addService pid new = do let rstoken = maybe (Just token) (const Nothing) (newServiceToken new) pure $ Public.NewServiceResponse sid rstoken -listServicesH :: Member GalleyProvider r => ProviderId -> (Handler r) Response -listServicesH pid = do - guardSecondFactorDisabled Nothing - json <$> listServices pid - listServices :: ProviderId -> (Handler r) [Public.Service] listServices = wrapClientE . DB.listServices -getServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId -> (Handler r) Response -getServiceH (pid ::: sid) = do - guardSecondFactorDisabled Nothing - json <$> getService pid sid - getService :: ProviderId -> ServiceId -> (Handler r) Public.Service getService pid sid = wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound -updateServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateService -> (Handler r) Response -updateServiceH (pid ::: sid ::: req) = do - guardSecondFactorDisabled Nothing - empty <$ (updateService pid sid =<< parseJsonBody req) - updateService :: ProviderId -> ServiceId -> Public.UpdateService -> (Handler r) () updateService pid sid upd = do _ <- wrapClientE (DB.lookupAccount pid) >>= maybeInvalidProvider @@ -590,11 +514,6 @@ updateService pid sid upd = do tagsChange (serviceEnabled svc) -updateServiceConnH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateServiceConn -> (Handler r) Response -updateServiceConnH (pid ::: sid ::: req) = do - guardSecondFactorDisabled Nothing - empty <$ (updateServiceConn pid sid =<< parseJsonBody req) - updateServiceConn :: ProviderId -> ServiceId -> Public.UpdateServiceConn -> (Handler r) () updateServiceConn pid sid upd = do pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials @@ -629,18 +548,6 @@ updateServiceConn pid sid upd = do then DB.deleteServiceIndexes pid sid name tags else DB.insertServiceIndexes pid sid name tags --- TODO: Send informational email to provider. - --- | Member GalleyProvider r => The endpoint that is called to delete a service. --- --- Since deleting a service can be costly, it just marks the service as --- disabled and then creates an event that will, when processed, actually --- delete the service. See 'finishDeleteService'. -deleteServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.DeleteService -> (Handler r) Response -deleteServiceH (pid ::: sid ::: req) = do - guardSecondFactorDisabled Nothing - setStatus status202 empty <$ (deleteService pid sid =<< parseJsonBody req) - -- | The endpoint that is called to delete a service. -- -- Since deleting a service can be costly, it just marks the service as @@ -734,19 +641,9 @@ getProviderProfile :: ProviderId -> (Handler r) Public.ProviderProfile getProviderProfile pid = wrapClientE (DB.lookupAccountProfile pid) >>= maybeProviderNotFound -listServiceProfilesH :: Member GalleyProvider r => ProviderId -> (Handler r) Response -listServiceProfilesH pid = do - guardSecondFactorDisabled Nothing - json <$> listServiceProfiles pid - listServiceProfiles :: ProviderId -> (Handler r) [Public.ServiceProfile] listServiceProfiles = wrapClientE . DB.listServiceProfiles -getServiceProfileH :: Member GalleyProvider r => ProviderId ::: ServiceId -> (Handler r) Response -getServiceProfileH (pid ::: sid) = do - guardSecondFactorDisabled Nothing - json <$> getServiceProfile pid sid - getServiceProfile :: ProviderId -> ServiceId -> (Handler r) Public.ServiceProfile getServiceProfile pid sid = wrapClientE (DB.lookupServiceProfile pid sid) >>= maybeServiceNotFound @@ -769,14 +666,6 @@ searchServiceProfiles (Just tags) start size = do searchServiceProfiles Nothing Nothing _ = do throwStd $ badRequest "At least `tags` or `start` must be provided." -searchTeamServiceProfilesH :: - Member GalleyProvider r => - UserId ::: TeamId ::: Maybe (Range 1 128 Text) ::: Bool ::: Range 10 100 Int32 -> - (Handler r) Response -searchTeamServiceProfilesH (uid ::: tid ::: prefix ::: filterDisabled ::: size) = do - guardSecondFactorDisabled (Just uid) - json <$> searchTeamServiceProfiles uid tid prefix filterDisabled size - -- NB: unlike 'searchServiceProfiles', we don't filter by service provider here searchTeamServiceProfiles :: UserId -> @@ -795,29 +684,11 @@ searchTeamServiceProfiles uid tid prefix filterDisabled size = do -- Get search results wrapClientE $ DB.paginateServiceWhitelist tid prefix filterDisabled (fromRange size) -getServiceTagListH :: Member GalleyProvider r => () -> (Handler r) Response -getServiceTagListH () = do - guardSecondFactorDisabled Nothing - json <$> getServiceTagList () - getServiceTagList :: () -> Monad m => m Public.ServiceTagList getServiceTagList () = pure (Public.ServiceTagList allTags) where allTags = [(minBound :: Public.ServiceTag) ..] -updateServiceWhitelistH :: Member GalleyProvider r => UserId ::: ConnId ::: TeamId ::: JsonRequest Public.UpdateServiceWhitelist -> (Handler r) Response -updateServiceWhitelistH (uid ::: con ::: tid ::: req) = do - guardSecondFactorDisabled (Just uid) - resp <- updateServiceWhitelist uid con tid =<< parseJsonBody req - let status = case resp of - UpdateServiceWhitelistRespChanged -> status200 - UpdateServiceWhitelistRespUnchanged -> status204 - pure $ setStatus status empty - -data UpdateServiceWhitelistResp - = UpdateServiceWhitelistRespChanged - | UpdateServiceWhitelistRespUnchanged - updateServiceWhitelist :: Member GalleyProvider r => UserId -> ConnId -> TeamId -> Public.UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp updateServiceWhitelist uid con tid upd = do let pid = updateServiceWhitelistProvider upd diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 18a60a9e797..bcfcb310958 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -37,6 +37,7 @@ import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Email qualified as Email import Brig.Options (setMaxTeamSize, setTeamInvitationTimeout) import Brig.Phone qualified as Phone +import Brig.Provider.API (guardSecondFactorDisabled, searchTeamServiceProfiles, updateServiceWhitelist) import Brig.Team.DB qualified as DB import Brig.Team.Email import Brig.Team.Types (ShowOrHideInvitationUrl (..)) @@ -64,6 +65,7 @@ import System.Logger.Class qualified as Log import Util.Logging (logFunction, logTeam) import Wire.API.Error import Wire.API.Error.Brig qualified as E +import Wire.API.Provider.Service (ServiceProfilePage, UpdateServiceWhitelist, UpdateServiceWhitelistResp (..)) import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Named import Wire.API.Routes.Public.Brig @@ -91,6 +93,8 @@ servantAPI = :<|> Named @"get-team-invitation-info" getInvitationByCode :<|> Named @"head-team-invitations" headInvitationByEmail :<|> Named @"get-team-size" teamSizePublic + :<|> Named @"get-whitelisted-services-by-team-id" searchTeamServiceProfilesH + :<|> Named @"post-team-whitelist-by-team-id" updateServiceWhitelistH routesInternal :: ( Member BlacklistStore r, @@ -124,6 +128,26 @@ routesInternal = do accept "application" "json" .&. jsonRequest @NewUserScimInvitation +searchTeamServiceProfilesH :: + Member GalleyProvider r => + UserId -> + TeamId -> + Maybe (Range 1 128 Text) -> + Maybe Bool -> + Maybe (Range 10 100 Int32) -> + (Handler r) ServiceProfilePage +searchTeamServiceProfilesH uid tid prefix filterDisabled' size' = do + guardSecondFactorDisabled (Just uid) + searchTeamServiceProfiles uid tid prefix filterDisabled size + where + size = fromMaybe (unsafeRange 20) size' + filterDisabled = fromMaybe True filterDisabled' + +updateServiceWhitelistH :: Member GalleyProvider r => UserId -> ConnId -> TeamId -> UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp +updateServiceWhitelistH uid con tid req = do + guardSecondFactorDisabled (Just uid) + updateServiceWhitelist uid con tid req + teamSizePublic :: Member GalleyProvider r => UserId -> TeamId -> (Handler r) TeamSize teamSizePublic uid tid = do ensurePermissions uid tid [AddTeamMember] -- limit this to team admins to reduce risk of involuntary DOS attacks From 37743035c49e07c107fb5f9edc28b18be707b54c Mon Sep 17 00:00:00 2001 From: fisx Date: Mon, 4 Sep 2023 12:56:07 +0200 Subject: [PATCH 111/225] Fix ES migration script. (#3558) --- .../WPB-4425-fix-es-migration-script | 1 + services/brig/src/Brig/Index/Migrations.hs | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-4425-fix-es-migration-script diff --git a/changelog.d/3-bug-fixes/WPB-4425-fix-es-migration-script b/changelog.d/3-bug-fixes/WPB-4425-fix-es-migration-script new file mode 100644 index 00000000000..66cbb384787 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-4425-fix-es-migration-script @@ -0,0 +1 @@ +Fix ES migration script. diff --git a/services/brig/src/Brig/Index/Migrations.hs b/services/brig/src/Brig/Index/Migrations.hs index c6101df53a7..27d7559fed2 100644 --- a/services/brig/src/Brig/Index/Migrations.hs +++ b/services/brig/src/Brig/Index/Migrations.hs @@ -120,21 +120,21 @@ failIfIndexAbsent targetIndex = -- | Runs only the migrations which need to run runMigration :: MigrationVersion -> MigrationActionT IO () -runMigration ver = do - vmax <- latestMigrationVersion - if ver > vmax +runMigration expectedVersion = do + foundVersion <- latestMigrationVersion + if expectedVersion > foundVersion then do Log.info $ Log.msg (Log.val "Migration necessary.") - . Log.field "expectedVersion" vmax - . Log.field "foundVersion" ver + . Log.field "expectedVersion" expectedVersion + . Log.field "foundVersion" foundVersion Search.reindexAllIfSameOrNewer - persistVersion ver + persistVersion expectedVersion else do Log.info $ Log.msg (Log.val "No migration necessary.") - . Log.field "expectedVersion" vmax - . Log.field "foundVersion" ver + . Log.field "expectedVersion" expectedVersion + . Log.field "foundVersion" foundVersion persistVersion :: (MonadThrow m, MonadIO m) => MigrationVersion -> MigrationActionT m () persistVersion v = @@ -148,6 +148,7 @@ persistVersion v = . Log.field "migrationVersion" v else throwM $ PersistVersionFailed v $ show persistResponse +-- | Which version is the table space currently running on? latestMigrationVersion :: (MonadThrow m, MonadIO m) => MigrationActionT m MigrationVersion latestMigrationVersion = do resp <- ES.parseEsResponse =<< ES.searchByIndex indexName (ES.mkSearch Nothing Nothing) From c29cb196c52e6b4e2d5cc5606b511e1409479eec Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 5 Sep 2023 10:20:27 +0200 Subject: [PATCH 112/225] Revert "WPB-633 Servantify Brig/Provider.Service API (#3554)" This reverts commit 3653d56766032ad80951d3c412f24c3e491d449e. --- changelog.d/5-internal/WPB-663 | 14 -- libs/types-common/src/Data/Range.hs | 3 - .../wire-api/src/Wire/API/Provider/Service.hs | 115 +----------- .../src/Wire/API/Provider/Service/Tag.hs | 67 +------ .../src/Wire/API/Routes/Public/Brig.hs | 149 ---------------- services/brig/src/Brig/API/Public.hs | 88 ---------- services/brig/src/Brig/Provider/API.hs | 165 ++++++++++++++++-- services/brig/src/Brig/Team/API.hs | 24 --- 8 files changed, 149 insertions(+), 476 deletions(-) delete mode 100644 changelog.d/5-internal/WPB-663 diff --git a/changelog.d/5-internal/WPB-663 b/changelog.d/5-internal/WPB-663 deleted file mode 100644 index 303cf529f7b..00000000000 --- a/changelog.d/5-internal/WPB-663 +++ /dev/null @@ -1,14 +0,0 @@ -Migrating the following routes to the Servant API form. - -POST /provider/services -GET /provider/services -GET /provider/services/:sid -PUT /provider/services/:sid -PUT /provider/services/:sid/connection -DELETE /provider/services/:sid -GET /providers/:pid/services -GET /providers/:pid/services/:sid -GET /services -GET /services/tags -GET /teams/:tid/services/whitelisted -POST /teams/:tid/services/whitelist \ No newline at end of file diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index 8b3d3a9cb11..e4c5be14781 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -152,9 +152,6 @@ numRangedSchemaDocModifier n m = S.schema %~ ((S.minimum_ ?~ fromIntegral n) . ( instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d [a] where rangedSchemaDocModifier _ = listRangedSchemaDocModifier --- Sets are effectively lists, so we can reuse that code. -instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d (Set a) where rangedSchemaDocModifier _ = listRangedSchemaDocModifier - instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d Text where rangedSchemaDocModifier _ = stringRangedSchemaDocModifier instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d String where rangedSchemaDocModifier _ = stringRangedSchemaDocModifier diff --git a/libs/wire-api/src/Wire/API/Provider/Service.hs b/libs/wire-api/src/Wire/API/Provider/Service.hs index 9b59decbcec..28a1e5609a1 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service.hs @@ -47,7 +47,6 @@ module Wire.API.Provider.Service -- * UpdateServiceWhitelist UpdateServiceWhitelist (..), - UpdateServiceWhitelistResp (..), ) where @@ -64,8 +63,7 @@ import Data.List1 (List1) import Data.Misc (HttpsUrl (..), PlainTextPassword6) import Data.PEM (PEM, pemParseBS, pemWriteLBS) import Data.Proxy -import Data.Range (Range, fromRange, rangedSchema) -import Data.SOP +import Data.Range (Range) import Data.Schema import Data.Swagger qualified as S import Data.Text qualified as Text @@ -73,7 +71,6 @@ import Data.Text.Ascii import Data.Text.Encoding qualified as Text import Imports import Wire.API.Provider.Service.Tag (ServiceTag (..)) -import Wire.API.Routes.MultiVerb import Wire.API.User.Profile (Asset, Name) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) @@ -208,22 +205,6 @@ data Service = Service } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform Service) - deriving (S.ToSchema) via (Schema Service) - -instance ToSchema Service where - schema = - object "Service" $ - Service - <$> serviceId .= field "id" schema - <*> serviceName .= field "name" schema - <*> serviceSummary .= field "summary" schema - <*> serviceDescr .= field "description" schema - <*> serviceUrl .= field "base_url" schema - <*> serviceTokens .= field "auth_tokens" schema - <*> serviceKeys .= field "public_keys" schema - <*> serviceAssets .= field "assets" (array schema) - <*> serviceTags .= field "tags" (set schema) - <*> serviceEnabled .= field "enabled" schema instance ToJSON Service where toJSON s = @@ -284,7 +265,6 @@ data ServiceProfile = ServiceProfile } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ServiceProfile) - deriving (S.ToSchema) via (Schema ServiceProfile) instance ToJSON ServiceProfile where toJSON s = @@ -311,19 +291,6 @@ instance FromJSON ServiceProfile where <*> o A..: "tags" <*> o A..: "enabled" -instance ToSchema ServiceProfile where - schema = - object "ServiceProfile" $ - ServiceProfile - <$> serviceProfileId .= field "id" schema - <*> serviceProfileProvider .= field "provider" schema - <*> serviceProfileName .= field "name" schema - <*> serviceProfileSummary .= field "summary" schema - <*> serviceProfileDescr .= field "description" schema - <*> serviceProfileAssets .= field "assets" (array schema) - <*> serviceProfileTags .= field "tags" (set schema) - <*> serviceProfileEnabled .= field "enabled" schema - -------------------------------------------------------------------------------- -- ServiceProfilePage @@ -333,7 +300,6 @@ data ServiceProfilePage = ServiceProfilePage } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ServiceProfilePage) - deriving (S.ToSchema) via (Schema ServiceProfilePage) instance ToJSON ServiceProfilePage where toJSON p = @@ -348,13 +314,6 @@ instance FromJSON ServiceProfilePage where <$> o A..: "has_more" <*> o A..: "services" -instance ToSchema ServiceProfilePage where - schema = - object "ServiceProfile" $ - ServiceProfilePage - <$> serviceProfilePageHasMore .= field "has_more" schema - <*> serviceProfilePageResults .= field "services" (array schema) - -------------------------------------------------------------------------------- -- NewService @@ -371,20 +330,6 @@ data NewService = NewService } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewService) - deriving (S.ToSchema) via (Schema NewService) - -instance ToSchema NewService where - schema = - object "NewService" $ - NewService - <$> newServiceName .= field "name" schema - <*> newServiceSummary .= field "summary" schema - <*> newServiceDescr .= field "description" schema - <*> newServiceUrl .= field "base_url" schema - <*> newServiceKey .= field "public_key" schema - <*> newServiceToken .= maybe_ (optField "auth_token" schema) - <*> newServiceAssets .= field "assets" (array schema) - <*> newServiceTags .= field "tags" (fromRange .= rangedSchema (set schema)) instance ToJSON NewService where toJSON s = @@ -421,14 +366,6 @@ data NewServiceResponse = NewServiceResponse } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewServiceResponse) - deriving (S.ToSchema) via (Schema NewServiceResponse) - -instance ToSchema NewServiceResponse where - schema = - object "NewServiceResponse" $ - NewServiceResponse - <$> rsNewServiceId .= field "id" schema - <*> rsNewServiceToken .= maybe_ (optField "auth_token" schema) instance ToJSON NewServiceResponse where toJSON r = @@ -456,17 +393,6 @@ data UpdateService = UpdateService } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UpdateService) - deriving (S.ToSchema) via (Schema UpdateService) - -instance ToSchema UpdateService where - schema = - object "UpdateService" $ - UpdateService - <$> updateServiceName .= maybe_ (optField "name" schema) - <*> updateServiceSummary .= maybe_ (optField "summary" schema) - <*> updateServiceDescr .= maybe_ (optField "description" schema) - <*> updateServiceAssets .= maybe_ (optField "assets" $ array schema) - <*> updateServiceTags .= maybe_ (optField "tags" (fromRange .= rangedSchema (set schema))) instance ToJSON UpdateService where toJSON u = @@ -501,17 +427,6 @@ data UpdateServiceConn = UpdateServiceConn } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UpdateServiceConn) - deriving (S.ToSchema) via (Schema UpdateServiceConn) - -instance ToSchema UpdateServiceConn where - schema = - object "UpdateServiceConn" $ - UpdateServiceConn - <$> updateServiceConnPassword .= field "password" schema - <*> updateServiceConnUrl .= maybe_ (optField "base_url" schema) - <*> updateServiceConnKeys .= maybe_ (optField "public_keys" (fromRange .= rangedSchema (array schema))) - <*> updateServiceConnTokens .= maybe_ (optField "auth_tokens" (fromRange .= rangedSchema (array schema))) - <*> updateServiceConnEnabled .= maybe_ (optField "enabled" schema) mkUpdateServiceConn :: PlainTextPassword6 -> UpdateServiceConn mkUpdateServiceConn pw = UpdateServiceConn pw Nothing Nothing Nothing Nothing @@ -543,13 +458,6 @@ newtype DeleteService = DeleteService {deleteServicePassword :: PlainTextPassword6} deriving stock (Eq, Show) deriving newtype (Arbitrary) - deriving (S.ToSchema) via (Schema DeleteService) - -instance ToSchema DeleteService where - schema = - object "DeleteService" $ - DeleteService - <$> deleteServicePassword .= field "password" schema instance ToJSON DeleteService where toJSON d = @@ -571,15 +479,6 @@ data UpdateServiceWhitelist = UpdateServiceWhitelist } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UpdateServiceWhitelist) - deriving (S.ToSchema) via (Schema UpdateServiceWhitelist) - -instance ToSchema UpdateServiceWhitelist where - schema = - object "UpdateServiceWhitelist" $ - UpdateServiceWhitelist - <$> updateServiceWhitelistProvider .= field "provider" schema - <*> updateServiceWhitelistService .= field "id" schema - <*> updateServiceWhitelistStatus .= field "whitelisted" schema instance ToJSON UpdateServiceWhitelist where toJSON u = @@ -595,15 +494,3 @@ instance FromJSON UpdateServiceWhitelist where <$> o A..: "provider" <*> o A..: "id" <*> o A..: "whitelisted" - -data UpdateServiceWhitelistResp - = UpdateServiceWhitelistRespChanged - | UpdateServiceWhitelistRespUnchanged - --- basically the same as the instance for CheckBlacklistResponse -instance AsUnion '[RespondEmpty 200 "UpdateServiceWhitelistRespChanged", RespondEmpty 204 "UpdateServiceWhitelistRespUnchanged"] UpdateServiceWhitelistResp where - toUnion UpdateServiceWhitelistRespChanged = Z (I ()) - toUnion UpdateServiceWhitelistRespUnchanged = S (Z (I ())) - fromUnion (Z (I ())) = UpdateServiceWhitelistRespChanged - fromUnion (S (Z (I ()))) = UpdateServiceWhitelistRespUnchanged - fromUnion (S (S x)) = case x of {} diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index 55a804d4cb0..522c519ff87 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -1,5 +1,4 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# OPTIONS_GHC -Wno-orphans #-} -- This file is part of the Wire Server implementation. -- @@ -40,30 +39,18 @@ module Wire.API.Provider.Service.Tag ) where -import Control.Lens (Prism', prism) import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON)) -import Data.Aeson qualified as A import Data.Aeson qualified as JSON -import Data.Attoparsec.ByteString (IResult (..), parse) -import Data.ByteString (toStrict) import Data.ByteString.Builder qualified as BB import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Conversion -import Data.Range (Range, fromRange, rangedSchema) +import Data.Range (Range, fromRange) import Data.Range qualified as Range -import Data.Schema import Data.Set qualified as Set -import Data.Swagger (ParamSchema (_paramSchemaEnum, _paramSchemaType), SwaggerType (SwaggerString), ToParamSchema (toParamSchema)) -import Data.Swagger qualified as S -import Data.Text qualified as T -import Data.Text.Encoding (decodeUtf8) -import Data.Text.Encoding qualified as T import Data.Text.Encoding qualified as Text import Data.Type.Ord import GHC.TypeLits (KnownNat, Nat) import Imports -import Web.HttpApiData (FromHttpApiData (parseUrlPiece), ToHttpApiData, toQueryParam) -import Web.Internal.HttpApiData (toUrlPiece) import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) -------------------------------------------------------------------------------- @@ -72,13 +59,6 @@ import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) newtype ServiceTagList = ServiceTagList [ServiceTag] deriving stock (Eq, Ord, Show) deriving newtype (FromJSON, ToJSON, Arbitrary) - deriving (S.ToSchema) via (Schema ServiceTagList) - -_ServiceTagList :: Prism' ServiceTagList [ServiceTag] -_ServiceTagList = prism ServiceTagList (\(ServiceTagList l) -> pure l) - -instance ToSchema ServiceTagList where - schema = named "ServiceTagList" $ tag _ServiceTagList $ array schema -- | A fixed enumeration of tags for services. data ServiceTag @@ -115,7 +95,6 @@ data ServiceTag | WeatherTag deriving stock (Eq, Show, Ord, Enum, Bounded, Generic) deriving (Arbitrary) via (GenericUniform ServiceTag) - deriving (S.ToSchema) via (Schema ServiceTag) instance FromByteString ServiceTag where parser = @@ -194,12 +173,6 @@ instance FromJSON ServiceTag where JSON.withText "ServiceTag" $ either fail pure . runParser parser . Text.encodeUtf8 -instance ToSchema ServiceTag where - schema = enum @Text "" . mconcat $ (\a -> element (decodeUtf8 $ toStrict $ toByteString a) a) <$> [minBound ..] - -instance ToHttpApiData ServiceTag where - toUrlPiece = cs . toByteString' - -------------------------------------------------------------------------------- -- Bounded ServiceTag Queries @@ -208,19 +181,6 @@ newtype QueryAnyTags (m :: Nat) (n :: Nat) = QueryAnyTags {queryAnyTagsRange :: Range m n (Set (QueryAllTags m n))} deriving stock (Eq, Show, Ord) -instance (m <= n) => ToParamSchema (QueryAnyTags m n) where - toParamSchema _ = - mempty - { _paramSchemaType = Just SwaggerString, - _paramSchemaEnum = Just (A.String . toQueryParam <$> [(minBound :: ServiceTag) ..]) - } - -instance (KnownNat n, KnownNat m, m <= n) => ToSchema (QueryAnyTags m n) where - schema = - let sch :: ValueSchema NamedSwaggerDoc (Range m n (Set (QueryAllTags m n))) - sch = fromRange .= rangedSchema (named "QueryAnyTags" $ set schema) - in queryAnyTagsRange .= (QueryAnyTags <$> sch) - instance (KnownNat m, KnownNat n, m <= n) => Arbitrary (QueryAnyTags m n) where arbitrary = QueryAnyTags <$> arbitrary @@ -247,31 +207,11 @@ instance (KnownNat n, KnownNat m, m <= n) => FromByteString (QueryAnyTags m n) w rs <- either fail pure (Range.checkedEither (Set.fromList ts)) pure $! QueryAnyTags rs -runPartial :: IsString i => Bool -> IResult i b -> Either Text b -runPartial alreadyRun result = case result of - Fail _ _ e -> Left $ T.pack e - Partial f -> - if alreadyRun - then Left "A partial parse returned another partial parse." - else runPartial True $ f "" - Done _ r -> pure r - -instance (KnownNat n, KnownNat m, m <= n) => FromHttpApiData (QueryAnyTags m n) where - parseUrlPiece t = do - txt <- parseUrlPiece t - runPartial False $ parse parser $ T.encodeUtf8 txt - -- | Bounded logical conjunction of 'm' to 'n' 'ServiceTag's to match. newtype QueryAllTags (m :: Nat) (n :: Nat) = QueryAllTags {queryAllTagsRange :: Range m n (Set ServiceTag)} deriving stock (Eq, Show, Ord) -instance (KnownNat n, KnownNat m, m <= n) => ToSchema (QueryAllTags m n) where - schema = - let sch :: ValueSchema NamedSwaggerDoc (Range m n (Set ServiceTag)) - sch = fromRange .= rangedSchema (named "QueryAllTags" $ set schema) - in queryAllTagsRange .= (QueryAllTags <$> sch) - instance (KnownNat m, KnownNat n, m <= n) => Arbitrary (QueryAllTags m n) where arbitrary = QueryAllTags <$> arbitrary @@ -296,11 +236,6 @@ instance (KnownNat m, KnownNat n, m <= n) => FromByteString (QueryAllTags m n) w rs <- either fail pure (Range.checkedEither (Set.fromList ts)) pure $! QueryAllTags rs -instance (KnownNat n, KnownNat m, m <= n) => FromHttpApiData (QueryAllTags m n) where - parseUrlPiece t = do - txt <- parseUrlPiece t - runPartial False $ parse parser $ T.encodeUtf8 txt - -------------------------------------------------------------------------------- -- ServiceTag Matchers diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 592fc7a8f5f..d83004bb54b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1,6 +1,4 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} --- Required for `instance MimeRender PlainText ()` -{-# OPTIONS_GHC -Wno-orphans #-} -- This file is part of the Wire Server implementation. -- @@ -52,12 +50,6 @@ import Wire.API.MLS.Servant import Wire.API.MakesFederatedCall import Wire.API.OAuth import Wire.API.Properties - ( PropertyKey, - PropertyKeysAndValues, - RawPropertyValue, - ) -import Wire.API.Provider.Service qualified as Public -import Wire.API.Provider.Service.Tag qualified as Public import Wire.API.Routes.API import Wire.API.Routes.Bearer import Wire.API.Routes.Cookies @@ -102,8 +94,6 @@ type BrigAPI = :<|> SystemSettingsAPI :<|> OAuthAPI :<|> BotAPI - :<|> ProviderAPI - :<|> ServicesAPI data BrigAPITag @@ -296,112 +286,6 @@ type UserAPI = (Respond 200 "Protocols supported by the user" (Set BaseProtocolTag)) ) -type ProviderAPI = - Named - "post-provider-services" - ( Summary "" - :> Description "" - :> ZProvider - :> "provider" - :> "services" - :> ReqBody '[JSON] Public.NewService - :> MultiVerb1 'POST '[JSON] (Respond 201 "" Public.NewServiceResponse) - ) - :<|> Named - "get-provider-services" - ( Summary "" - :> Description "" - :> ZProvider - :> "provider" - :> "services" - :> Get '[JSON] [Public.Service] - ) - :<|> Named - "get-provider-services-by-service-id" - ( Summary "" - :> Description "" - :> ZProvider - :> "provider" - :> "services" - :> Capture "service-id" ServiceId - :> Get '[JSON] Public.Service - ) - :<|> Named - "put-provider-services-by-service-id" - ( Summary "" - :> Description "" - :> ZProvider - :> "provider" - :> "services" - :> Capture "service-id" ServiceId - :> ReqBody '[JSON] Public.UpdateService - :> Put '[PlainText] () - ) - :<|> Named - "put-provider-services-connection-by-service-id" - ( Summary "" - :> Description "" - :> ZProvider - :> "provider" - :> "services" - :> Capture "service-id" ServiceId - :> "connection" - :> ReqBody '[JSON] Public.UpdateServiceConn - :> Put '[PlainText] () - ) - :<|> Named - "delete-provider-services-by-service-id" - ( Summary "" - :> Description "" - :> ZProvider - :> "provider" - :> "services" - :> Capture "service-id" ServiceId - :> ReqBody '[JSON] Public.DeleteService - :> MultiVerb1 'DELETE '[PlainText] (RespondEmpty 202 "") - ) - :<|> Named - "get-provider-services-by-provider-id" - ( Summary "" - :> Description "" - :> ZAccess - :> "providers" - :> Capture "provider-id" ProviderId - :> "services" - :> Get '[JSON] [Public.ServiceProfile] - ) - :<|> Named - "get-provider-services-by-provider-id-and-service-id" - ( Summary "" - :> Description "" - :> ZAccess - :> "providers" - :> Capture "provider-id" ProviderId - :> "services" - :> Capture "service-id" ServiceId - :> Get '[JSON] Public.ServiceProfile - ) - -type ServicesAPI = - Named - "get-services" - ( Summary "" - :> Description "" - :> ZAccess - :> "services" - :> QueryParam "tags" (Public.QueryAnyTags 1 3) - :> QueryParam "start" Text - :> QueryParam "size" (Range 10 100 Int32) -- Default to 20 - :> Get '[JSON] Public.ServiceProfilePage - ) - :<|> Named - "get-services-tags" - ( Summary "" - :> Description "" - :> ZAccess - :> Get '[JSON] Public.ServiceTagList - ) - type SelfAPI = Named "get-self" @@ -1642,39 +1526,6 @@ type TeamsAPI = '[JSON] (Respond 200 "Number of team members" TeamSize) ) - :<|> Named - "get-whitelisted-services-by-team-id" - ( Summary "" - :> Description "" - :> ZAccess - :> "teams" - :> Capture "team-id" TeamId - :> "services" - :> "whitelisted" - :> QueryParam "prefix" (Range 1 128 Text) - -- Default to True - :> QueryParam "filter_disabled" Bool - -- Default to 20 - :> QueryParam "size" (Range 10 100 Int32) - :> Get '[JSON] Public.ServiceProfilePage - ) - :<|> Named - "post-team-whitelist-by-team-id" - ( Summary "" - :> Description "" - :> ZAccess - :> ZConn - :> "teams" - :> Capture "team-id" TeamId - :> "services" - :> "whitelist" - :> ReqBody '[JSON] Public.UpdateServiceWhitelist - :> MultiVerb 'POST '[PlainText] '[RespondEmpty 200 "UpdateServiceWhitelistRespChanged", RespondEmpty 204 "UpdateServiceWhitelistRespUnchanged"] Public.UpdateServiceWhitelistResp - ) - --- Plaintext doesn't ship a renderer for (), so we have an orphan for it -instance MimeRender PlainText () where - mimeRender _ () = "" type SystemSettingsAPI = Named diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index fedbc25d5d3..46665c7122f 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -90,7 +90,6 @@ import Data.Misc (IpAddr (..)) import Data.Nonce (Nonce, randomNonce) import Data.Qualified import Data.Range -import Data.Schema () import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Ascii qualified as Ascii @@ -115,8 +114,6 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Federation.API import Wire.API.Federation.Error import Wire.API.Properties qualified as Public -import Wire.API.Provider.Service qualified as Public -import Wire.API.Provider.Service.Tag qualified as Public import Wire.API.Routes.API import Wire.API.Routes.Internal.Brig qualified as BrigInternalAPI import Wire.API.Routes.Internal.Cannon qualified as CannonInternalAPI @@ -291,23 +288,7 @@ servantSitemap = :<|> systemSettingsAPI :<|> oauthAPI :<|> botAPI - :<|> providerAPI - :<|> servicesAPI where - providerAPI :: ServerT ProviderAPI (Handler r) - providerAPI = - Named @"post-provider-services" addServiceH - :<|> Named @"get-provider-services" listServicesH - :<|> Named @"get-provider-services-by-service-id" getServiceH - :<|> Named @"put-provider-services-by-service-id" updateServiceH - :<|> Named @"put-provider-services-connection-by-service-id" updateServiceConnH - :<|> Named @"delete-provider-services-by-service-id" deleteServiceH - :<|> Named @"get-provider-services-by-provider-id" listServiceProfilesH - :<|> Named @"get-provider-services-by-provider-id-and-service-id" getServiceProfileH - servicesAPI :: ServerT ServicesAPI (Handler r) - servicesAPI = - Named @"get-services" searchServiceProfilesH - :<|> Named @"get-services-tags" getServiceTagListH userAPI :: ServerT UserAPI (Handler r) userAPI = Named @"get-user-unqualified" (callsFed (exposeAnnotations getUserUnqualifiedH)) @@ -1140,75 +1121,6 @@ updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do teamMember <- MaybeT $ lift $ liftSem $ GalleyProvider.getTeamMember zuserId teamId pure $ teamMember `hasPermission` ChangeTeamMemberProfiles --- ProviderAPI -addServiceH :: - Member GalleyProvider r => - ProviderId -> - Public.NewService -> - (Handler r) Public.NewServiceResponse -addServiceH pid req = do - Provider.guardSecondFactorDisabled Nothing - Provider.addService pid req - -listServicesH :: Member GalleyProvider r => ProviderId -> (Handler r) [Public.Service] -listServicesH pid = do - Provider.guardSecondFactorDisabled Nothing - Provider.listServices pid - -getServiceH :: Member GalleyProvider r => ProviderId -> ServiceId -> (Handler r) Public.Service -getServiceH pid sid = do - Provider.guardSecondFactorDisabled Nothing - Provider.getService pid sid - -updateServiceH :: Member GalleyProvider r => ProviderId -> ServiceId -> Public.UpdateService -> (Handler r) () -updateServiceH pid sid req = do - Provider.guardSecondFactorDisabled Nothing - void $ Provider.updateService pid sid req - -updateServiceConnH :: Member GalleyProvider r => ProviderId -> ServiceId -> Public.UpdateServiceConn -> (Handler r) () -updateServiceConnH pid sid req = do - Provider.guardSecondFactorDisabled Nothing - void $ Provider.updateServiceConn pid sid req - --- TODO: Send informational email to provider. - --- | Member GalleyProvider r => The endpoint that is called to delete a service. --- --- Since deleting a service can be costly, it just marks the service as --- disabled and then creates an event that will, when processed, actually --- delete the service. See 'finishDeleteService'. -deleteServiceH :: Member GalleyProvider r => ProviderId -> ServiceId -> Public.DeleteService -> (Handler r) () -deleteServiceH pid sid req = do - Provider.guardSecondFactorDisabled Nothing - void $ Provider.deleteService pid sid req - -listServiceProfilesH :: Member GalleyProvider r => UserId -> ProviderId -> (Handler r) [Public.ServiceProfile] -listServiceProfilesH _ pid = do - Provider.guardSecondFactorDisabled Nothing - Provider.listServiceProfiles pid - -getServiceProfileH :: Member GalleyProvider r => UserId -> ProviderId -> ServiceId -> (Handler r) Public.ServiceProfile -getServiceProfileH _ pid sid = do - Provider.guardSecondFactorDisabled Nothing - Provider.getServiceProfile pid sid - --- ServicesAPI -searchServiceProfilesH :: - Member GalleyProvider r => - UserId -> - Maybe (Public.QueryAnyTags 1 3) -> - Maybe Text -> - Maybe (Range 10 100 Int32) -> -- Default to 20 - (Handler r) Public.ServiceProfilePage -searchServiceProfilesH _ qt start size = do - Provider.guardSecondFactorDisabled Nothing - Provider.searchServiceProfiles qt start $ fromMaybe (unsafeRange 20) size - -getServiceTagListH :: Member GalleyProvider r => UserId -> (Handler r) Public.ServiceTagList -getServiceTagListH _ = do - Provider.guardSecondFactorDisabled Nothing - Provider.getServiceTagList () - -- activation activate :: diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 46cc9b38045..a836861f8f5 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -23,23 +23,6 @@ module Brig.Provider.API -- * Event handlers finishDeleteService, - - -- * Extras - guardSecondFactorDisabled, - addService, - listServices, - getService, - updateService, - updateServiceConn, - deleteService, - listServiceProfiles, - getServiceProfile, - searchServiceProfilesH, - searchServiceProfiles, - getServiceTagList, - searchTeamServiceProfiles, - updateServiceWhitelist, - UpdateServiceWhitelistResp (..), ) where @@ -98,7 +81,7 @@ import GHC.TypeNats import Imports import Network.HTTP.Types.Status import Network.Wai (Response) -import Network.Wai.Predicate (accept, query) +import Network.Wai.Predicate (accept, def, opt, query) import Network.Wai.Routing import Network.Wai.Utilities.Error ((!>>)) import Network.Wai.Utilities.Error qualified as Wai @@ -223,11 +206,46 @@ routesPublic = do .&> zauth ZAuthProvider .&> zauthProviderId + post "/provider/services" (continue addServiceH) $ + accept "application" "json" + .&> zauth ZAuthProvider + .&> zauthProviderId + .&. jsonRequest @Public.NewService + + get "/provider/services" (continue listServicesH) $ + accept "application" "json" + .&> zauth ZAuthProvider + .&> zauthProviderId + + get "/provider/services/:sid" (continue getServiceH) $ + accept "application" "json" + .&> zauth ZAuthProvider + .&> zauthProviderId + .&. capture "sid" + + put "/provider/services/:sid" (continue updateServiceH) $ + zauth ZAuthProvider + .&> zauthProviderId + .&. capture "sid" + .&. jsonRequest @Public.UpdateService + + put "/provider/services/:sid/connection" (continue updateServiceConnH) $ + zauth ZAuthProvider + .&> zauthProviderId + .&. capture "sid" + .&. jsonRequest @Public.UpdateServiceConn + -- TODO -- post "/provider/services/:sid/token" (continue genServiceTokenH) $ -- accept "application" "json" -- .&. zauthProvider + delete "/provider/services/:sid" (continue deleteServiceH) $ + zauth ZAuthProvider + .&> zauthProviderId + .&. capture "sid" + .&. jsonRequest @Public.DeleteService + -- User API ---------------------------------------------------------------- get "/providers/:pid" (continue getProviderProfileH) $ @@ -235,6 +253,44 @@ routesPublic = do .&> zauth ZAuthAccess .&> capture "pid" + get "/providers/:pid/services" (continue listServiceProfilesH) $ + accept "application" "json" + .&> zauth ZAuthAccess + .&> capture "pid" + + get "/providers/:pid/services/:sid" (continue getServiceProfileH) $ + accept "application" "json" + .&> zauth ZAuthAccess + .&> capture "pid" + .&. capture "sid" + + get "/services" (continue searchServiceProfilesH) $ + accept "application" "json" + .&> zauth ZAuthAccess + .&> opt (query "tags") + .&. opt (query "start") + .&. def (unsafeRange 20) (query "size") + + get "/services/tags" (continue getServiceTagListH) $ + accept "application" "json" + .&> zauth ZAuthAccess + + get "/teams/:tid/services/whitelisted" (continue searchTeamServiceProfilesH) $ + accept "application" "json" + .&> zauthUserId + .&. capture "tid" + .&. opt (query "prefix") + .&. def True (query "filter_disabled") + .&. def (unsafeRange 20) (query "size") + + post "/teams/:tid/services/whitelist" (continue updateServiceWhitelistH) $ + accept "application" "json" + .&> zauth ZAuthAccess + .&> zauthUserId + .&. zauthConnId + .&. capture "tid" + .&. jsonRequest @Public.UpdateServiceWhitelist + routesInternal :: Member GalleyProvider r => Routes a (Handler r) () routesInternal = do get "/i/provider/activation-code" (continue getActivationCodeH) $ @@ -463,6 +519,11 @@ updateAccountPassword pid upd = do throwStd newPasswordMustDiffer wrapClientE $ DB.updateAccountPassword pid (newPassword upd) +addServiceH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.NewService -> (Handler r) Response +addServiceH (pid ::: req) = do + guardSecondFactorDisabled Nothing + setStatus status201 . json <$> (addService pid =<< parseJsonBody req) + addService :: ProviderId -> Public.NewService -> (Handler r) Public.NewServiceResponse addService pid new = do _ <- wrapClientE (DB.lookupAccount pid) >>= maybeInvalidProvider @@ -479,13 +540,28 @@ addService pid new = do let rstoken = maybe (Just token) (const Nothing) (newServiceToken new) pure $ Public.NewServiceResponse sid rstoken +listServicesH :: Member GalleyProvider r => ProviderId -> (Handler r) Response +listServicesH pid = do + guardSecondFactorDisabled Nothing + json <$> listServices pid + listServices :: ProviderId -> (Handler r) [Public.Service] listServices = wrapClientE . DB.listServices +getServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId -> (Handler r) Response +getServiceH (pid ::: sid) = do + guardSecondFactorDisabled Nothing + json <$> getService pid sid + getService :: ProviderId -> ServiceId -> (Handler r) Public.Service getService pid sid = wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound +updateServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateService -> (Handler r) Response +updateServiceH (pid ::: sid ::: req) = do + guardSecondFactorDisabled Nothing + empty <$ (updateService pid sid =<< parseJsonBody req) + updateService :: ProviderId -> ServiceId -> Public.UpdateService -> (Handler r) () updateService pid sid upd = do _ <- wrapClientE (DB.lookupAccount pid) >>= maybeInvalidProvider @@ -514,6 +590,11 @@ updateService pid sid upd = do tagsChange (serviceEnabled svc) +updateServiceConnH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateServiceConn -> (Handler r) Response +updateServiceConnH (pid ::: sid ::: req) = do + guardSecondFactorDisabled Nothing + empty <$ (updateServiceConn pid sid =<< parseJsonBody req) + updateServiceConn :: ProviderId -> ServiceId -> Public.UpdateServiceConn -> (Handler r) () updateServiceConn pid sid upd = do pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials @@ -548,6 +629,18 @@ updateServiceConn pid sid upd = do then DB.deleteServiceIndexes pid sid name tags else DB.insertServiceIndexes pid sid name tags +-- TODO: Send informational email to provider. + +-- | Member GalleyProvider r => The endpoint that is called to delete a service. +-- +-- Since deleting a service can be costly, it just marks the service as +-- disabled and then creates an event that will, when processed, actually +-- delete the service. See 'finishDeleteService'. +deleteServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.DeleteService -> (Handler r) Response +deleteServiceH (pid ::: sid ::: req) = do + guardSecondFactorDisabled Nothing + setStatus status202 empty <$ (deleteService pid sid =<< parseJsonBody req) + -- | The endpoint that is called to delete a service. -- -- Since deleting a service can be costly, it just marks the service as @@ -641,9 +734,19 @@ getProviderProfile :: ProviderId -> (Handler r) Public.ProviderProfile getProviderProfile pid = wrapClientE (DB.lookupAccountProfile pid) >>= maybeProviderNotFound +listServiceProfilesH :: Member GalleyProvider r => ProviderId -> (Handler r) Response +listServiceProfilesH pid = do + guardSecondFactorDisabled Nothing + json <$> listServiceProfiles pid + listServiceProfiles :: ProviderId -> (Handler r) [Public.ServiceProfile] listServiceProfiles = wrapClientE . DB.listServiceProfiles +getServiceProfileH :: Member GalleyProvider r => ProviderId ::: ServiceId -> (Handler r) Response +getServiceProfileH (pid ::: sid) = do + guardSecondFactorDisabled Nothing + json <$> getServiceProfile pid sid + getServiceProfile :: ProviderId -> ServiceId -> (Handler r) Public.ServiceProfile getServiceProfile pid sid = wrapClientE (DB.lookupServiceProfile pid sid) >>= maybeServiceNotFound @@ -666,6 +769,14 @@ searchServiceProfiles (Just tags) start size = do searchServiceProfiles Nothing Nothing _ = do throwStd $ badRequest "At least `tags` or `start` must be provided." +searchTeamServiceProfilesH :: + Member GalleyProvider r => + UserId ::: TeamId ::: Maybe (Range 1 128 Text) ::: Bool ::: Range 10 100 Int32 -> + (Handler r) Response +searchTeamServiceProfilesH (uid ::: tid ::: prefix ::: filterDisabled ::: size) = do + guardSecondFactorDisabled (Just uid) + json <$> searchTeamServiceProfiles uid tid prefix filterDisabled size + -- NB: unlike 'searchServiceProfiles', we don't filter by service provider here searchTeamServiceProfiles :: UserId -> @@ -684,11 +795,29 @@ searchTeamServiceProfiles uid tid prefix filterDisabled size = do -- Get search results wrapClientE $ DB.paginateServiceWhitelist tid prefix filterDisabled (fromRange size) +getServiceTagListH :: Member GalleyProvider r => () -> (Handler r) Response +getServiceTagListH () = do + guardSecondFactorDisabled Nothing + json <$> getServiceTagList () + getServiceTagList :: () -> Monad m => m Public.ServiceTagList getServiceTagList () = pure (Public.ServiceTagList allTags) where allTags = [(minBound :: Public.ServiceTag) ..] +updateServiceWhitelistH :: Member GalleyProvider r => UserId ::: ConnId ::: TeamId ::: JsonRequest Public.UpdateServiceWhitelist -> (Handler r) Response +updateServiceWhitelistH (uid ::: con ::: tid ::: req) = do + guardSecondFactorDisabled (Just uid) + resp <- updateServiceWhitelist uid con tid =<< parseJsonBody req + let status = case resp of + UpdateServiceWhitelistRespChanged -> status200 + UpdateServiceWhitelistRespUnchanged -> status204 + pure $ setStatus status empty + +data UpdateServiceWhitelistResp + = UpdateServiceWhitelistRespChanged + | UpdateServiceWhitelistRespUnchanged + updateServiceWhitelist :: Member GalleyProvider r => UserId -> ConnId -> TeamId -> Public.UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp updateServiceWhitelist uid con tid upd = do let pid = updateServiceWhitelistProvider upd diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index bcfcb310958..18a60a9e797 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -37,7 +37,6 @@ import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Email qualified as Email import Brig.Options (setMaxTeamSize, setTeamInvitationTimeout) import Brig.Phone qualified as Phone -import Brig.Provider.API (guardSecondFactorDisabled, searchTeamServiceProfiles, updateServiceWhitelist) import Brig.Team.DB qualified as DB import Brig.Team.Email import Brig.Team.Types (ShowOrHideInvitationUrl (..)) @@ -65,7 +64,6 @@ import System.Logger.Class qualified as Log import Util.Logging (logFunction, logTeam) import Wire.API.Error import Wire.API.Error.Brig qualified as E -import Wire.API.Provider.Service (ServiceProfilePage, UpdateServiceWhitelist, UpdateServiceWhitelistResp (..)) import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Named import Wire.API.Routes.Public.Brig @@ -93,8 +91,6 @@ servantAPI = :<|> Named @"get-team-invitation-info" getInvitationByCode :<|> Named @"head-team-invitations" headInvitationByEmail :<|> Named @"get-team-size" teamSizePublic - :<|> Named @"get-whitelisted-services-by-team-id" searchTeamServiceProfilesH - :<|> Named @"post-team-whitelist-by-team-id" updateServiceWhitelistH routesInternal :: ( Member BlacklistStore r, @@ -128,26 +124,6 @@ routesInternal = do accept "application" "json" .&. jsonRequest @NewUserScimInvitation -searchTeamServiceProfilesH :: - Member GalleyProvider r => - UserId -> - TeamId -> - Maybe (Range 1 128 Text) -> - Maybe Bool -> - Maybe (Range 10 100 Int32) -> - (Handler r) ServiceProfilePage -searchTeamServiceProfilesH uid tid prefix filterDisabled' size' = do - guardSecondFactorDisabled (Just uid) - searchTeamServiceProfiles uid tid prefix filterDisabled size - where - size = fromMaybe (unsafeRange 20) size' - filterDisabled = fromMaybe True filterDisabled' - -updateServiceWhitelistH :: Member GalleyProvider r => UserId -> ConnId -> TeamId -> UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp -updateServiceWhitelistH uid con tid req = do - guardSecondFactorDisabled (Just uid) - updateServiceWhitelist uid con tid req - teamSizePublic :: Member GalleyProvider r => UserId -> TeamId -> (Handler r) TeamSize teamSizePublic uid tid = do ensurePermissions uid tid [AddTeamMember] -- limit this to team admins to reduce risk of involuntary DOS attacks From 10de29f53d7348a55c7a234da555403dd96c2fe0 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 5 Sep 2023 13:23:39 +0200 Subject: [PATCH 113/225] Integration tests: delete all rabbitmq queues during dynamic backends setup phase (#3523) --- charts/integration/templates/configmap.yaml | 6 + .../templates/integration-integration.yaml | 2 +- charts/integration/values.yaml | 1 + integration/default.nix | 2 + integration/integration.cabal | 2 +- integration/test/MLS/Util.hs | 1 - integration/test/Testlib/App.hs | 2 - integration/test/Testlib/Assertions.hs | 31 +++ integration/test/Testlib/Cannon.hs | 1 - integration/test/Testlib/Env.hs | 120 +-------- integration/test/Testlib/HTTP.hs | 1 - integration/test/Testlib/JSON.hs | 1 - integration/test/Testlib/ModService.hs | 2 - integration/test/Testlib/Ports.hs | 38 +-- integration/test/Testlib/Prelude.hs | 2 - integration/test/Testlib/ResourcePool.hs | 73 ++---- integration/test/Testlib/Run.hs | 8 +- integration/test/Testlib/Service.hs | 49 ---- integration/test/Testlib/Types.hs | 248 +++++++++++++++--- libs/extended/src/Network/RabbitMqAdmin.hs | 11 +- .../Wire/BackendNotificationPusherSpec.hs | 7 +- services/integration.yaml | 8 + 22 files changed, 338 insertions(+), 278 deletions(-) delete mode 100644 integration/test/Testlib/Service.hs diff --git a/charts/integration/templates/configmap.yaml b/charts/integration/templates/configmap.yaml index 990eec362bc..99a247203ae 100644 --- a/charts/integration/templates/configmap.yaml +++ b/charts/integration/templates/configmap.yaml @@ -57,6 +57,10 @@ data: originDomain: federation-test-helper.{{ .Release.Namespace }}.svc.cluster.local + rabbitmq: + host: rabbitmq + adminPort: 15672 + backendTwo: brig: @@ -115,3 +119,5 @@ data: domain: {{ $dynamicBackend.federatorExternalHostPrefix }}.{{ $.Release.Namespace }}.svc.cluster.local federatorExternalPort: {{ $dynamicBackend.federatorExternalPort }} {{- end }} + cassandra: +{{ toYaml .Values.config.cassandra | indent 6}} diff --git a/charts/integration/templates/integration-integration.yaml b/charts/integration/templates/integration-integration.yaml index 044d28b7410..f4ad967a975 100644 --- a/charts/integration/templates/integration-integration.yaml +++ b/charts/integration/templates/integration-integration.yaml @@ -111,7 +111,7 @@ spec: - | set -euo pipefail # FUTUREWORK: Do all of this in the integration test binary - integration-dynamic-backends-db-schemas.sh --host {{ .Values.config.cassandra.host }} --port 9042 --replication-factor {{ .Values.config.cassandra.replicationFactor }} + integration-dynamic-backends-db-schemas.sh --host {{ .Values.config.cassandra.host }} --port {{ .Values.config.cassandra.port }} --replication-factor {{ .Values.config.cassandra.replicationFactor }} integration-dynamic-backends-brig-index.sh --elasticsearch-server http://{{ .Values.config.elasticsearch.host }}:9200 integration-dynamic-backends-sqs.sh {{ .Values.config.sqsEndpointUrl }} integration-dynamic-backends-ses.sh {{ .Values.config.sesEndpointUrl }} diff --git a/charts/integration/values.yaml b/charts/integration/values.yaml index 96302f56baf..5b17cc91899 100644 --- a/charts/integration/values.yaml +++ b/charts/integration/values.yaml @@ -26,6 +26,7 @@ config: cassandra: host: cassandra-ephemeral + port: 9042 replicationFactor: 1 elasticsearch: diff --git a/integration/default.nix b/integration/default.nix index 3d0bd4907ee..1aa88b39fda 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -20,6 +20,7 @@ , directory , errors , exceptions +, extended , extra , filepath , gitignoreSource @@ -84,6 +85,7 @@ mkDerivation { directory errors exceptions + extended extra filepath hex diff --git a/integration/integration.cabal b/integration/integration.cabal index b00ed4317af..57ae450ee8f 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -128,7 +128,6 @@ library Testlib.ResourcePool Testlib.Run Testlib.RunServices - Testlib.Service Testlib.Types build-depends: @@ -148,6 +147,7 @@ library , directory , errors , exceptions + , extended , extra , filepath , hex diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 414195a0653..e7341e18dd1 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -34,7 +34,6 @@ import System.Posix.Files import System.Process import Testlib.App import Testlib.Assertions -import Testlib.Env import Testlib.HTTP import Testlib.JSON import Testlib.Prelude diff --git a/integration/test/Testlib/App.hs b/integration/test/Testlib/App.hs index b219f3da9e1..3ca68cac789 100644 --- a/integration/test/Testlib/App.hs +++ b/integration/test/Testlib/App.hs @@ -10,9 +10,7 @@ import Data.Yaml qualified as Yaml import GHC.Exception import GHC.Stack (HasCallStack) import System.FilePath -import Testlib.Env import Testlib.JSON -import Testlib.Service import Testlib.Types import Prelude diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index ef2b2c46eb0..299c1ae20e8 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -4,14 +4,21 @@ module Testlib.Assertions where import Control.Exception as E import Control.Monad.Reader +import Data.Aeson qualified as Aeson +import Data.Aeson.Encode.Pretty qualified as Aeson import Data.ByteString.Base64 qualified as B64 +import Data.ByteString.Lazy qualified as BS import Data.Char import Data.Foldable +import Data.Hex import Data.List import Data.Map qualified as Map import Data.Text qualified as Text import Data.Text.Encoding qualified as Text +import Data.Text.Lazy qualified as TL +import Data.Text.Lazy.Encoding qualified as TL import GHC.Stack as Stack +import Network.HTTP.Client qualified as HTTP import System.FilePath import Testlib.JSON import Testlib.Printing @@ -241,3 +248,27 @@ getLineNumber lineNo s = case drop (lineNo - 1) (lines s) of [] -> Nothing (l : _) -> pure l + +prettyResponse :: Response -> String +prettyResponse r = + unlines $ + concat + [ pure $ colored yellow "request: \n" <> showRequest r.request, + pure $ colored yellow "request headers: \n" <> showHeaders (HTTP.requestHeaders r.request), + case getRequestBody r.request of + Nothing -> [] + Just b -> + [ colored yellow "request body:", + Text.unpack . Text.decodeUtf8 $ case Aeson.decode (BS.fromStrict b) of + Just v -> BS.toStrict (Aeson.encodePretty (v :: Aeson.Value)) + Nothing -> hex b + ], + pure $ colored blue "response status: " <> show r.status, + pure $ colored blue "response body:", + pure $ + ( TL.unpack . TL.decodeUtf8 $ + case r.jsonBody of + Just b -> (Aeson.encodePretty b) + Nothing -> BS.fromStrict r.body + ) + ] diff --git a/integration/test/Testlib/Cannon.hs b/integration/test/Testlib/Cannon.hs index 162dd58fb0f..4fefd3dfe3f 100644 --- a/integration/test/Testlib/Cannon.hs +++ b/integration/test/Testlib/Cannon.hs @@ -68,7 +68,6 @@ import Testlib.Env import Testlib.HTTP import Testlib.JSON import Testlib.Printing -import Testlib.Service import Testlib.Types import Prelude diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 1390ceed213..5ddb4cfb29f 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -4,18 +4,12 @@ module Testlib.Env where import Control.Monad.Codensity import Control.Monad.IO.Class -import Data.Aeson hiding ((.=)) -import Data.ByteString (ByteString) import Data.Functor import Data.IORef -import Data.Map (Map) import Data.Map qualified as Map import Data.Set (Set) import Data.Set qualified as Set -import Data.String -import Data.Word import Data.Yaml qualified as Yaml -import GHC.Generics import Network.HTTP.Client qualified as HTTP import System.Exit import System.FilePath @@ -23,91 +17,9 @@ import System.IO import System.IO.Temp import Testlib.Prekeys import Testlib.ResourcePool -import Testlib.Service +import Testlib.Types import Prelude --- | Initialised once per test. -data Env = Env - { serviceMap :: Map String ServiceMap, - domain1 :: String, - domain2 :: String, - dynamicDomains :: [String], - defaultAPIVersion :: Int, - manager :: HTTP.Manager, - servicesCwdBase :: Maybe FilePath, - removalKeyPath :: FilePath, - prekeys :: IORef [(Int, String)], - lastPrekeys :: IORef [String], - mls :: IORef MLSState, - resourcePool :: ResourcePool BackendResource - } - --- | Initialised once per testsuite. -data GlobalEnv = GlobalEnv - { gServiceMap :: Map String ServiceMap, - gDomain1 :: String, - gDomain2 :: String, - gDynamicDomains :: [String], - gDefaultAPIVersion :: Int, - gManager :: HTTP.Manager, - gServicesCwdBase :: Maybe FilePath, - gRemovalKeyPath :: FilePath, - gBackendResourcePool :: ResourcePool BackendResource - } - -data IntegrationConfig = IntegrationConfig - { backendOne :: BackendConfig, - backendTwo :: BackendConfig, - dynamicBackends :: Map String DynamicBackendConfig - } - deriving (Show, Generic) - -instance FromJSON IntegrationConfig where - parseJSON = - withObject "IntegrationConfig" $ \o -> - IntegrationConfig - <$> parseJSON (Object o) - <*> o .: "backendTwo" - <*> o .: "dynamicBackends" - -data ServiceMap = ServiceMap - { brig :: HostPort, - backgroundWorker :: HostPort, - cannon :: HostPort, - cargohold :: HostPort, - federatorInternal :: HostPort, - federatorExternal :: HostPort, - galley :: HostPort, - gundeck :: HostPort, - nginz :: HostPort, - spar :: HostPort, - proxy :: HostPort, - stern :: HostPort - } - deriving (Show, Generic) - -instance FromJSON ServiceMap - -data BackendConfig = BackendConfig - { beServiceMap :: ServiceMap, - originDomain :: String - } - deriving (Show, Generic) - -instance FromJSON BackendConfig where - parseJSON v = - BackendConfig - <$> parseJSON v - <*> withObject "BackendConfig" (\ob -> ob .: fromString "originDomain") v - -data HostPort = HostPort - { host :: String, - port :: Word16 - } - deriving (Show, Generic) - -instance FromJSON HostPort - serviceHostPort :: ServiceMap -> Service -> HostPort serviceHostPort m Brig = m.brig serviceHostPort m Galley = m.galley @@ -137,7 +49,10 @@ mkGlobalEnv cfgFile = do else Nothing manager <- HTTP.newManager HTTP.defaultManagerSettings - resourcePool <- createBackendResourcePool (Map.elems intConfig.dynamicBackends) + resourcePool <- + createBackendResourcePool + (Map.elems intConfig.dynamicBackends) + intConfig.rabbitmq pure GlobalEnv { gServiceMap = @@ -152,7 +67,8 @@ mkGlobalEnv cfgFile = do gManager = manager, gServicesCwdBase = devEnvProjectRoot <&> ( "services"), gRemovalKeyPath = error "Uninitialised removal key path", - gBackendResourcePool = resourcePool + gBackendResourcePool = resourcePool, + gRabbitMQConfig = intConfig.rabbitmq } mkEnv :: GlobalEnv -> Codensity IO Env @@ -174,7 +90,8 @@ mkEnv ge = do prekeys = pks, lastPrekeys = lpks, mls = mls, - resourcePool = ge.gBackendResourcePool + resourcePool = ge.gBackendResourcePool, + rabbitMQConfig = ge.gRabbitMQConfig } destroy :: IORef (Set BackendResource) -> BackendResource -> IO () @@ -189,18 +106,6 @@ create ioRef = Nothing -> error "No resources available" Just (r, s') -> (s', r) -data MLSState = MLSState - { baseDir :: FilePath, - members :: Set ClientIdentity, - -- | users expected to receive a welcome message after the next commit - newMembers :: Set ClientIdentity, - groupId :: Maybe String, - convId :: Maybe Value, - clientGroupState :: Map ClientIdentity ByteString, - epoch :: Word64 - } - deriving (Show) - mkMLSState :: Codensity IO MLSState mkMLSState = Codensity $ \k -> withSystemTempDirectory "mls" $ \tmp -> do @@ -214,10 +119,3 @@ mkMLSState = Codensity $ \k -> clientGroupState = mempty, epoch = 0 } - -data ClientIdentity = ClientIdentity - { domain :: String, - user :: String, - client :: String - } - deriving (Show, Eq, Ord) diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index 670fbdc679e..d07176f3278 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -25,7 +25,6 @@ import Network.URI (URI (..), URIAuth (..), parseURI) import Testlib.Assertions import Testlib.Env import Testlib.JSON -import Testlib.Service import Testlib.Types import Prelude diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index 984a536ebe7..2c52b1bb760 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -20,7 +20,6 @@ import Data.String import Data.Text qualified as T import Data.Vector ((!?)) import GHC.Stack -import Testlib.Env import Testlib.Types import Prelude diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 003fb9e2df3..b5e0b526004 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -42,12 +42,10 @@ import System.Posix (killProcess, signalProcess) import System.Process (CreateProcess (..), ProcessHandle, StdStream (..), createProcess, getPid, proc, terminateProcess, waitForProcess) import System.Timeout (timeout) import Testlib.App -import Testlib.Env import Testlib.HTTP import Testlib.JSON import Testlib.Printing import Testlib.ResourcePool -import Testlib.Service import Testlib.Types import Text.RawString.QQ import Prelude diff --git a/integration/test/Testlib/Ports.hs b/integration/test/Testlib/Ports.hs index 05f48483c2b..4ca16d06910 100644 --- a/integration/test/Testlib/Ports.hs +++ b/integration/test/Testlib/Ports.hs @@ -1,39 +1,39 @@ module Testlib.Ports where -import Testlib.Service qualified as Service +import Testlib.Types hiding (port) import Prelude data PortNamespace = NginzSSL | NginzHttp2 | FederatorExternal - | ServiceInternal Service.Service + | ServiceInternal Service -port :: Num a => PortNamespace -> Service.BackendName -> a +port :: Num a => PortNamespace -> BackendName -> a port NginzSSL bn = mkPort 8443 bn port NginzHttp2 bn = mkPort 8099 bn port FederatorExternal bn = mkPort 8098 bn -port (ServiceInternal Service.BackgroundWorker) bn = mkPort 8089 bn -port (ServiceInternal Service.Brig) bn = mkPort 8082 bn -port (ServiceInternal Service.Cannon) bn = mkPort 8083 bn -port (ServiceInternal Service.Cargohold) bn = mkPort 8084 bn -port (ServiceInternal Service.FederatorInternal) bn = mkPort 8097 bn -port (ServiceInternal Service.Galley) bn = mkPort 8085 bn -port (ServiceInternal Service.Gundeck) bn = mkPort 8086 bn -port (ServiceInternal Service.Nginz) bn = mkPort 8080 bn -port (ServiceInternal Service.Spar) bn = mkPort 8088 bn -port (ServiceInternal Service.Stern) bn = mkPort 8091 bn +port (ServiceInternal BackgroundWorker) bn = mkPort 8089 bn +port (ServiceInternal Brig) bn = mkPort 8082 bn +port (ServiceInternal Cannon) bn = mkPort 8083 bn +port (ServiceInternal Cargohold) bn = mkPort 8084 bn +port (ServiceInternal FederatorInternal) bn = mkPort 8097 bn +port (ServiceInternal Galley) bn = mkPort 8085 bn +port (ServiceInternal Gundeck) bn = mkPort 8086 bn +port (ServiceInternal Nginz) bn = mkPort 8080 bn +port (ServiceInternal Spar) bn = mkPort 8088 bn +port (ServiceInternal Stern) bn = mkPort 8091 bn portForDyn :: Num a => PortNamespace -> Int -> a -portForDyn ns i = port ns (Service.DynamicBackend i) +portForDyn ns i = port ns (DynamicBackend i) -mkPort :: Num a => Int -> Service.BackendName -> a +mkPort :: Num a => Int -> BackendName -> a mkPort basePort bn = let i = case bn of - Service.BackendA -> 0 - Service.BackendB -> 1 - (Service.DynamicBackend k) -> 1 + k + BackendA -> 0 + BackendB -> 1 + (DynamicBackend k) -> 1 + k in fromIntegral basePort + (fromIntegral i) * 1000 -internalServicePorts :: Num a => Service.BackendName -> Service.Service -> a +internalServicePorts :: Num a => BackendName -> Service -> a internalServicePorts backend service = port (ServiceInternal service) backend diff --git a/integration/test/Testlib/Prelude.hs b/integration/test/Testlib/Prelude.hs index ce21e7ac6f0..05a04f366a3 100644 --- a/integration/test/Testlib/Prelude.hs +++ b/integration/test/Testlib/Prelude.hs @@ -8,7 +8,6 @@ module Testlib.Prelude module Testlib.HTTP, module Testlib.JSON, module Testlib.PTest, - module Testlib.Service, module Data.Aeson, module Prelude, module Control.Applicative, @@ -121,7 +120,6 @@ import Testlib.HTTP import Testlib.JSON import Testlib.ModService import Testlib.PTest -import Testlib.Service import Testlib.Types import UnliftIO.Exception import Prelude diff --git a/integration/test/Testlib/ResourcePool.hs b/integration/test/Testlib/ResourcePool.hs index 06b05f17904..582e36e7529 100644 --- a/integration/test/Testlib/ResourcePool.hs +++ b/integration/test/Testlib/ResourcePool.hs @@ -14,28 +14,26 @@ import Control.Concurrent import Control.Monad.Catch import Control.Monad.Codensity import Control.Monad.IO.Class -import Data.Aeson +import Data.Foldable (for_) import Data.Function ((&)) import Data.Functor import Data.IORef import Data.Set qualified as Set import Data.String +import Data.Text qualified as T import Data.Tuple -import Data.Word -import GHC.Generics import GHC.Stack (HasCallStack) +import Network.AMQP.Extended +import Network.RabbitMqAdmin import System.IO import Testlib.Ports qualified as Ports -import Testlib.Service +import Testlib.Types import Prelude -data ResourcePool a = ResourcePool - { sem :: QSemN, - resources :: IORef (Set.Set a) - } - acquireResources :: forall m a. (Ord a, MonadIO m, MonadMask m, HasCallStack) => Int -> ResourcePool a -> Codensity m [a] -acquireResources n pool = Codensity $ \f -> bracket acquire release (f . Set.toList) +acquireResources n pool = Codensity $ \f -> bracket acquire release $ \s -> do + liftIO $ mapM_ pool.onAcquire s + f $ Set.toList s where release :: Set.Set a -> m () release s = @@ -48,50 +46,27 @@ acquireResources n pool = Codensity $ \f -> bracket acquire release (f . Set.toL waitQSemN pool.sem n atomicModifyIORef pool.resources $ swap . Set.splitAt n -createBackendResourcePool :: [DynamicBackendConfig] -> IO (ResourcePool BackendResource) -createBackendResourcePool dynConfs = +createBackendResourcePool :: [DynamicBackendConfig] -> RabbitMQConfig -> IO (ResourcePool BackendResource) +createBackendResourcePool dynConfs rabbitmq = let resources = backendResources dynConfs in ResourcePool <$> newQSemN (length dynConfs) <*> newIORef resources + <*> pure (deleteAllRabbitMQQueues rabbitmq) -data BackendResource = BackendResource - { berName :: BackendName, - berBrigKeyspace :: String, - berGalleyKeyspace :: String, - berSparKeyspace :: String, - berGundeckKeyspace :: String, - berElasticsearchIndex :: String, - berFederatorInternal :: Word16, - berFederatorExternal :: Word16, - berDomain :: String, - berAwsUserJournalQueue :: String, - berAwsPrekeyTable :: String, - berAwsS3Bucket :: String, - berAwsQueueName :: String, - berBrigInternalEvents :: String, - berEmailSMSSesQueue :: String, - berEmailSMSEmailSender :: String, - berGalleyJournal :: String, - berVHost :: String, - berNginzSslPort :: Word16, - berNginzHttp2Port :: Word16, - berInternalServicePorts :: forall a. Num a => Service -> a - } - -instance Eq BackendResource where - a == b = a.berName == b.berName - -instance Ord BackendResource where - a `compare` b = a.berName `compare` b.berName - -data DynamicBackendConfig = DynamicBackendConfig - { domain :: String, - federatorExternalPort :: Word16 - } - deriving (Show, Generic) - -instance FromJSON DynamicBackendConfig +deleteAllRabbitMQQueues :: RabbitMQConfig -> BackendResource -> IO () +deleteAllRabbitMQQueues rc resource = do + let opts = + RabbitMqAdminOpts + { host = rc.host, + port = 0, + adminPort = fromIntegral rc.adminPort, + vHost = T.pack resource.berVHost + } + client <- mkRabbitMqAdminClientEnv opts + queues <- listQueuesByVHost client (T.pack resource.berVHost) + for_ queues $ \queue -> + deleteQueue client (T.pack resource.berVHost) queue.name backendResources :: [DynamicBackendConfig] -> Set.Set BackendResource backendResources dynConfs = diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index ee6b76f531c..777ad6ebcc6 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -23,7 +23,6 @@ import Testlib.Env import Testlib.JSON import Testlib.Options import Testlib.Printing -import Testlib.Service import Testlib.Types import Text.Printf import UnliftIO.Async @@ -161,11 +160,8 @@ runTests tests cfg = do exitFailure doListTests :: [(String, String, String, x)] -> IO () -doListTests tests = for_ tests $ \(qname, desc, full, _) -> do - putStrLn $ qname <> " " <> colored gray desc - unless (null full) $ - putStr $ - colored gray (indent 2 full) +doListTests tests = for_ tests $ \(qname, _desc, _full, _) -> do + putStrLn qname -- like `main` but meant to run from a repl mainI :: [String] -> IO () diff --git a/integration/test/Testlib/Service.hs b/integration/test/Testlib/Service.hs deleted file mode 100644 index a921858051d..00000000000 --- a/integration/test/Testlib/Service.hs +++ /dev/null @@ -1,49 +0,0 @@ -module Testlib.Service where - -import Prelude - -data Service = Brig | Galley | Cannon | Gundeck | Cargohold | Nginz | Spar | BackgroundWorker | Stern | FederatorInternal - deriving - ( Show, - Eq, - Ord, - Enum, - Bounded - ) - -serviceName :: Service -> String -serviceName = \case - Brig -> "brig" - Galley -> "galley" - Cannon -> "cannon" - Gundeck -> "gundeck" - Cargohold -> "cargohold" - Nginz -> "nginz" - Spar -> "spar" - BackgroundWorker -> "backgroundWorker" - Stern -> "stern" - FederatorInternal -> "federator" - --- | Converts the service name to kebab-case. -configName :: Service -> String -configName = \case - Brig -> "brig" - Galley -> "galley" - Cannon -> "cannon" - Gundeck -> "gundeck" - Cargohold -> "cargohold" - Nginz -> "nginz" - Spar -> "spar" - BackgroundWorker -> "background-worker" - Stern -> "stern" - FederatorInternal -> "federator" - -data BackendName - = BackendA - | BackendB - | -- | The index of dynamic backends begin with 1 - DynamicBackend Int - deriving (Show, Eq, Ord) - -allServices :: [Service] -allServices = [minBound .. maxBound] diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index d6a2f590f1f..847b8eaa107 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -1,13 +1,16 @@ +{- +NOTE: Don't import any other Testlib modules here. Use this module to break dependency cycles. +-} module Testlib.Types where +import Control.Concurrent (QSemN) import Control.Exception as E import Control.Monad.Base import Control.Monad.Catch import Control.Monad.Reader import Control.Monad.Trans.Control -import Data.Aeson (Value) +import Data.Aeson import Data.Aeson qualified as Aeson -import Data.Aeson.Encode.Pretty qualified as Aeson import Data.ByteString (ByteString) import Data.ByteString qualified as BS import Data.ByteString.Char8 qualified as C8 @@ -15,23 +18,167 @@ import Data.ByteString.Lazy qualified as L import Data.CaseInsensitive qualified as CI import Data.Default import Data.Functor -import Data.Hex import Data.IORef import Data.List +import Data.Map import Data.Map qualified as Map +import Data.Set qualified as Set +import Data.String import Data.Text qualified as T import Data.Text.Encoding qualified as T +import Data.Word +import GHC.Generics (Generic) import GHC.Records import GHC.Stack import Network.HTTP.Client qualified as HTTP import Network.HTTP.Types qualified as HTTP import Network.URI -import Testlib.Env -import Testlib.Printing -import Testlib.Service import UnliftIO (MonadUnliftIO) import Prelude +data ResourcePool a = ResourcePool + { sem :: QSemN, + resources :: IORef (Set.Set a), + onAcquire :: a -> IO () + } + +data BackendResource = BackendResource + { berName :: BackendName, + berBrigKeyspace :: String, + berGalleyKeyspace :: String, + berSparKeyspace :: String, + berGundeckKeyspace :: String, + berElasticsearchIndex :: String, + berFederatorInternal :: Word16, + berFederatorExternal :: Word16, + berDomain :: String, + berAwsUserJournalQueue :: String, + berAwsPrekeyTable :: String, + berAwsS3Bucket :: String, + berAwsQueueName :: String, + berBrigInternalEvents :: String, + berEmailSMSSesQueue :: String, + berEmailSMSEmailSender :: String, + berGalleyJournal :: String, + berVHost :: String, + berNginzSslPort :: Word16, + berNginzHttp2Port :: Word16, + berInternalServicePorts :: forall a. Num a => Service -> a + } + +instance Eq BackendResource where + a == b = a.berName == b.berName + +instance Ord BackendResource where + a `compare` b = a.berName `compare` b.berName + +data DynamicBackendConfig = DynamicBackendConfig + { domain :: String, + federatorExternalPort :: Word16 + } + deriving (Show, Generic) + +instance FromJSON DynamicBackendConfig + +data RabbitMQConfig = RabbitMQConfig + { host :: String, + adminPort :: Word16 + } + deriving (Show) + +instance FromJSON RabbitMQConfig where + parseJSON = + withObject "RabbitMQConfig" $ \ob -> + RabbitMQConfig + <$> ob .: fromString "host" + <*> ob .: fromString "adminPort" + +-- | Initialised once per testsuite. +data GlobalEnv = GlobalEnv + { gServiceMap :: Map String ServiceMap, + gDomain1 :: String, + gDomain2 :: String, + gDynamicDomains :: [String], + gDefaultAPIVersion :: Int, + gManager :: HTTP.Manager, + gServicesCwdBase :: Maybe FilePath, + gRemovalKeyPath :: FilePath, + gBackendResourcePool :: ResourcePool BackendResource, + gRabbitMQConfig :: RabbitMQConfig + } + +data IntegrationConfig = IntegrationConfig + { backendOne :: BackendConfig, + backendTwo :: BackendConfig, + dynamicBackends :: Map String DynamicBackendConfig, + rabbitmq :: RabbitMQConfig + } + deriving (Show, Generic) + +instance FromJSON IntegrationConfig where + parseJSON = + withObject "IntegrationConfig" $ \o -> + IntegrationConfig + <$> parseJSON (Object o) + <*> o .: fromString "backendTwo" + <*> o .: fromString "dynamicBackends" + <*> o .: fromString "rabbitmq" + +data ServiceMap = ServiceMap + { brig :: HostPort, + backgroundWorker :: HostPort, + cannon :: HostPort, + cargohold :: HostPort, + federatorInternal :: HostPort, + federatorExternal :: HostPort, + galley :: HostPort, + gundeck :: HostPort, + nginz :: HostPort, + spar :: HostPort, + proxy :: HostPort, + stern :: HostPort + } + deriving (Show, Generic) + +instance FromJSON ServiceMap + +data BackendConfig = BackendConfig + { beServiceMap :: ServiceMap, + originDomain :: String + } + deriving (Show, Generic) + +instance FromJSON BackendConfig where + parseJSON v = + BackendConfig + <$> parseJSON v + <*> withObject "BackendConfig" (\ob -> ob .: fromString "originDomain") v + +data HostPort = HostPort + { host :: String, + port :: Word16 + } + deriving (Show, Generic) + +instance FromJSON HostPort + +-- | Initialised once per test. +data Env = Env + { serviceMap :: Map String ServiceMap, + domain1 :: String, + domain2 :: String, + dynamicDomains :: [String], + defaultAPIVersion :: Int, + manager :: HTTP.Manager, + servicesCwdBase :: Maybe FilePath, + removalKeyPath :: FilePath, + prekeys :: IORef [(Int, String)], + lastPrekeys :: IORef [String], + mls :: IORef MLSState, + resourcePool :: ResourcePool BackendResource, + rabbitMQConfig :: RabbitMQConfig + } + data Response = Response { jsonBody :: Maybe Aeson.Value, body :: ByteString, @@ -44,6 +191,25 @@ data Response = Response instance HasField "json" Response (App Aeson.Value) where getField response = maybe (assertFailure "Response has no json body") pure response.jsonBody +data ClientIdentity = ClientIdentity + { domain :: String, + user :: String, + client :: String + } + deriving (Show, Eq, Ord) + +data MLSState = MLSState + { baseDir :: FilePath, + members :: Set.Set ClientIdentity, + -- | users expected to receive a welcome message after the next commit + newMembers :: Set.Set ClientIdentity, + groupId :: Maybe String, + convId :: Maybe Value, + clientGroupState :: Map ClientIdentity ByteString, + epoch :: Word64 + } + deriving (Show) + showRequest :: HTTP.Request -> String showRequest r = T.unpack (T.decodeUtf8 (HTTP.method r)) @@ -62,30 +228,6 @@ getRequestBody req = case HTTP.requestBody req of HTTP.RequestBodyBS bs -> pure bs _ -> Nothing -prettyResponse :: Response -> String -prettyResponse r = - unlines $ - concat - [ pure $ colored yellow "request: \n" <> showRequest r.request, - pure $ colored yellow "request headers: \n" <> showHeaders (HTTP.requestHeaders r.request), - case getRequestBody r.request of - Nothing -> [] - Just b -> - [ colored yellow "request body:", - T.unpack . T.decodeUtf8 $ case Aeson.decode (L.fromStrict b) of - Just v -> L.toStrict (Aeson.encodePretty (v :: Aeson.Value)) - Nothing -> hex b - ], - pure $ colored blue "response status: " <> show r.status, - pure $ colored blue "response body:", - pure $ - ( T.unpack . T.decodeUtf8 $ - case r.jsonBody of - Just b -> L.toStrict (Aeson.encodePretty b) - Nothing -> r.body - ) - ] - data AssertionFailure = AssertionFailure { callstack :: CallStack, response :: Maybe Response, @@ -252,3 +394,49 @@ lookupConfigOverride overrides = \case BackgroundWorker -> overrides.backgroundWorkerCfg Stern -> overrides.sternCfg FederatorInternal -> overrides.federatorInternalCfg + +data Service = Brig | Galley | Cannon | Gundeck | Cargohold | Nginz | Spar | BackgroundWorker | Stern | FederatorInternal + deriving + ( Show, + Eq, + Ord, + Enum, + Bounded + ) + +serviceName :: Service -> String +serviceName = \case + Brig -> "brig" + Galley -> "galley" + Cannon -> "cannon" + Gundeck -> "gundeck" + Cargohold -> "cargohold" + Nginz -> "nginz" + Spar -> "spar" + BackgroundWorker -> "backgroundWorker" + Stern -> "stern" + FederatorInternal -> "federator" + +-- | Converts the service name to kebab-case. +configName :: Service -> String +configName = \case + Brig -> "brig" + Galley -> "galley" + Cannon -> "cannon" + Gundeck -> "gundeck" + Cargohold -> "cargohold" + Nginz -> "nginz" + Spar -> "spar" + BackgroundWorker -> "background-worker" + Stern -> "stern" + FederatorInternal -> "federator" + +data BackendName + = BackendA + | BackendB + | -- | The index of dynamic backends begin with 1 + DynamicBackend Int + deriving (Show, Eq, Ord) + +allServices :: [Service] +allServices = [minBound .. maxBound] diff --git a/libs/extended/src/Network/RabbitMqAdmin.hs b/libs/extended/src/Network/RabbitMqAdmin.hs index 3b65d0a5f31..68251f97f23 100644 --- a/libs/extended/src/Network/RabbitMqAdmin.hs +++ b/libs/extended/src/Network/RabbitMqAdmin.hs @@ -11,6 +11,8 @@ type RabbitMqBasicAuth = BasicAuth "RabbitMq Management" BasicAuthData type VHost = Text +type QueueName = Text + -- | Upstream Docs: -- https://rawcdn.githack.com/rabbitmq/rabbitmq-server/v3.12.0/deps/rabbitmq_management/priv/www/api/index.html data AdminAPI route = AdminAPI @@ -22,7 +24,14 @@ data AdminAPI route = AdminAPI :- "api" :> "queues" :> Capture "vhost" VHost - :> Get '[JSON] [Queue] + :> Get '[JSON] [Queue], + deleteQueue :: + route + :- "api" + :> "queues" + :> Capture "vhost" VHost + :> Capture "queue" QueueName + :> DeleteNoContent } deriving (Generic) diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index d7a275cf285..f38680c1d43 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -310,7 +310,8 @@ newMockRabbitMqAdmin isBroken queues = do mockApi :: MockRabbitMqAdmin -> AdminAPI (AsServerT Servant.Handler) mockApi mockAdmin = AdminAPI - { listQueuesByVHost = mockListQueuesByVHost mockAdmin + { listQueuesByVHost = mockListQueuesByVHost mockAdmin, + deleteQueue = mockListDeleteQueue mockAdmin } mockListQueuesByVHost :: MockRabbitMqAdmin -> Text -> Servant.Handler [Queue] @@ -320,6 +321,10 @@ mockListQueuesByVHost MockRabbitMqAdmin {..} vhost = do True -> throwError $ Servant.err500 False -> pure $ map (\n -> Queue n vhost) queues +mockListDeleteQueue :: MockRabbitMqAdmin -> Text -> Text -> Servant.Handler NoContent +mockListDeleteQueue _ _ _ = do + pure NoContent + mockRabbitMqAdminApp :: MockRabbitMqAdmin -> Application mockRabbitMqAdminApp mockAdmin = genericServe (mockApi mockAdmin) diff --git a/services/integration.yaml b/services/integration.yaml index 08f5fa48476..65543e45f10 100644 --- a/services/integration.yaml +++ b/services/integration.yaml @@ -134,3 +134,11 @@ dynamicBackends: dynamic-backend-3: domain: d3.example.com federatorExternalPort: 12098 + +rabbitmq: + host: localhost + adminPort: 15672 + +cassandra: + host: 127.0.0.1 + port: 9042 From e859176b04dee8d975d5a5ab54be2ccb5c7ab045 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 6 Sep 2023 09:16:11 +0200 Subject: [PATCH 114/225] [WPB-4406] federator improve logging (#3556) --- changelog.d/5-internal/WPB-4406 | 2 + integration/integration.cabal | 1 + integration/test/API/Federator.hs | 24 ++++++ integration/test/Test/Federator.hs | 55 ++++++++++---- .../src/Network/Wai/Utilities/Error.hs | 11 ++- .../src/Network/Wai/Utilities/Server.hs | 3 +- .../src/Wire/API/Federation/Client.hs | 3 +- .../src/Wire/API/Federation/Error.hs | 74 +++++++++++-------- services/federator/default.nix | 2 + services/federator/federator.cabal | 2 + services/federator/src/Federator/Env.hs | 9 ++- .../federator/src/Federator/ExternalServer.hs | 8 +- .../federator/src/Federator/InternalServer.hs | 19 ++++- services/federator/src/Federator/Metrics.hs | 55 ++++++++++++++ services/federator/src/Federator/Remote.hs | 26 +++++-- services/federator/src/Federator/Response.hs | 5 +- services/federator/src/Federator/Run.hs | 20 +++++ .../integration/Test/Federator/IngressSpec.hs | 4 +- .../unit/Test/Federator/ExternalServer.hs | 19 ++++- .../unit/Test/Federator/InternalServer.hs | 12 +++ .../test/unit/Test/Federator/Remote.hs | 6 +- .../test/unit/Test/Federator/Response.hs | 1 + 22 files changed, 290 insertions(+), 71 deletions(-) create mode 100644 changelog.d/5-internal/WPB-4406 create mode 100644 integration/test/API/Federator.hs create mode 100644 services/federator/src/Federator/Metrics.hs diff --git a/changelog.d/5-internal/WPB-4406 b/changelog.d/5-internal/WPB-4406 new file mode 100644 index 00000000000..5313ca41b5d --- /dev/null +++ b/changelog.d/5-internal/WPB-4406 @@ -0,0 +1,2 @@ +- Extending the information returned in errors for Federator. Paths and response bodies, if available, are included in error logs. +- Prometheus metrics for outgoing and incoming federation requests added. diff --git a/integration/integration.cabal b/integration/integration.cabal index 57ae450ee8f..0041bca3610 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -90,6 +90,7 @@ library API.BrigInternal API.Cargohold API.Common + API.Federator API.Galley API.GalleyInternal API.Gundeck diff --git a/integration/test/API/Federator.hs b/integration/test/API/Federator.hs new file mode 100644 index 00000000000..089b79c45c7 --- /dev/null +++ b/integration/test/API/Federator.hs @@ -0,0 +1,24 @@ +module API.Federator where + +import Data.Function +import GHC.Stack +import Network.HTTP.Client qualified as HTTP +import Testlib.Prelude + +getMetrics :: + (HasCallStack, MakesValue domain) => + domain -> + (ServiceMap -> HostPort) -> + App Response +getMetrics domain service = do + req <- rawBaseRequestF domain service "i/metrics" + submit "GET" req + +rawBaseRequestF :: (HasCallStack, MakesValue domain) => domain -> (ServiceMap -> HostPort) -> String -> App HTTP.Request +rawBaseRequestF domain getService path = do + domainV <- objDomain domain + serviceMap <- getServiceMap domainV + + liftIO . HTTP.parseRequest $ + let HostPort h p = getService serviceMap + in "http://" <> h <> ":" <> show p <> ("/" <> joinHttpPath (splitHttpPath path)) diff --git a/integration/test/Test/Federator.hs b/integration/test/Test/Federator.hs index 6e90308940c..99bdf155f28 100644 --- a/integration/test/Test/Federator.hs +++ b/integration/test/Test/Federator.hs @@ -1,32 +1,28 @@ +{-# LANGUAGE OverloadedStrings #-} + module Test.Federator where +import API.Brig +import API.Federator (getMetrics) +import Data.Attoparsec.Text import Data.ByteString qualified as BS import Data.String.Conversions -import Network.HTTP.Client qualified as HTTP +import Data.Text +import SetupHelpers (randomUser) import Testlib.Prelude runFederatorMetrics :: (ServiceMap -> HostPort) -> App () runFederatorMetrics getService = do - let req = submit "GET" =<< rawBaseRequestF OwnDomain getService "/i/metrics" - handleRes res = res <$ res.status `shouldMatchInt` 200 - first <- bindResponse req handleRes - second <- bindResponse req handleRes + let handleRes res = res <$ res.status `shouldMatchInt` 200 + first <- bindResponse (getMetrics OwnDomain getService) handleRes + second <- bindResponse (getMetrics OwnDomain getService) handleRes assertBool "Two metric requests should never match" $ first.body /= second.body assertBool "Second metric response should never be 0 length (the first might be)" $ BS.length second.body /= 0 assertBool "The seconds metric response should have text indicating that it is returning metrics" $ - BS.isInfixOf (cs expectedString) second.body + BS.isInfixOf expectedString second.body where expectedString = "# TYPE http_request_duration_seconds histogram" -rawBaseRequestF :: (HasCallStack, MakesValue domain) => domain -> (ServiceMap -> HostPort) -> String -> App HTTP.Request -rawBaseRequestF domain getService path = do - domainV <- objDomain domain - serviceMap <- getServiceMap domainV - - liftIO . HTTP.parseRequest $ - let HostPort h p = getService serviceMap - in "http://" <> h <> ":" <> show p <> ("/" <> joinHttpPath (splitHttpPath path)) - -- The metrics setup for both internal and external federator servers -- are the same, so we can simply run the same test for both. testFederatorMetricsInternal :: App () @@ -34,3 +30,32 @@ testFederatorMetricsInternal = runFederatorMetrics federatorInternal testFederatorMetricsExternal :: App () testFederatorMetricsExternal = runFederatorMetrics federatorExternal + +testFederatorNumRequestsMetrics :: HasCallStack => App () +testFederatorNumRequestsMetrics = do + u1 <- randomUser OwnDomain def + u2 <- randomUser OtherDomain def + incomingBefore <- getMetric parseIncomingRequestCount OtherDomain OwnDomain + outgoingBefore <- getMetric parseOutgoingRequestCount OwnDomain OtherDomain + bindResponse (searchContacts u1 (u2 %. "name") OtherDomain) $ \resp -> + resp.status `shouldMatchInt` 200 + incomingAfter <- getMetric parseIncomingRequestCount OtherDomain OwnDomain + outgoingAfter <- getMetric parseOutgoingRequestCount OwnDomain OtherDomain + assertBool "Incoming requests count should have increased by at least 2" $ incomingAfter >= incomingBefore + 2 + assertBool "Outgoing requests count should have increased by at least 2" $ outgoingAfter >= outgoingBefore + 2 + where + getMetric :: (Text -> Parser Integer) -> Domain -> Domain -> App Integer + getMetric p domain origin = do + m <- getMetrics domain federatorInternal + d <- cs <$> asString origin + pure $ fromRight 0 (parseOnly (p d) (cs m.body)) + + parseIncomingRequestCount :: Text -> Parser Integer + parseIncomingRequestCount d = + manyTill anyChar (string ("com_wire_federator_incoming_requests{origin_domain=\"" <> d <> "\"} ")) + *> decimal + + parseOutgoingRequestCount :: Text -> Parser Integer + parseOutgoingRequestCount d = + manyTill anyChar (string ("com_wire_federator_outgoing_requests{target_domain=\"" <> d <> "\"} ")) + *> decimal diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs index 01b7a4cee8f..6aeb602ede2 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs @@ -31,7 +31,7 @@ import Control.Error import Data.Aeson hiding (Error) import Data.Aeson.Types (Pair) import Data.Domain -import Data.Text.Lazy.Encoding (decodeUtf8) +import Data.Text.Lazy.Encoding (decodeUtf8, encodeUtf8) import Imports import Network.HTTP.Types @@ -50,16 +50,18 @@ instance Exception Error data ErrorData = FederationErrorData { federrDomain :: !Domain, - federrPath :: !Text + federrPath :: !Text, + federrResp :: !(Maybe LByteString) } deriving (Eq, Show, Typeable) instance ToJSON ErrorData where - toJSON (FederationErrorData d p) = + toJSON (FederationErrorData d p b) = object [ "type" .= ("federation" :: Text), "domain" .= d, - "path" .= p + "path" .= p, + "response" .= fmap decodeUtf8 b ] instance FromJSON ErrorData where @@ -67,6 +69,7 @@ instance FromJSON ErrorData where FederationErrorData <$> o .: "domain" <*> o .: "path" + <*> (fmap encodeUtf8 <$> (o .: "response")) -- | Assumes UTF-8 encoding. byteStringError :: Status -> LByteString -> LByteString -> Error diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs index a0eaa4d5886..05856b3974b 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs @@ -398,9 +398,10 @@ logErrorMsg (Wai.Error c l m md) = . maybe id logErrorData md . msg (val "\"" +++ m +++ val "\"") where - logErrorData (Wai.FederationErrorData d p) = + logErrorData (Wai.FederationErrorData d p b) = field "domain" (domainText d) . field "path" p + . field "response" (fromMaybe "" b) logErrorMsgWithRequest :: Maybe ByteString -> Wai.Error -> Msg -> Msg logErrorMsgWithRequest mr e = diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs index f2a220a2c3d..f9f0473306f 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs @@ -254,7 +254,8 @@ mkFailureResponse status domain path body { Wai.federrDomain = domain, Wai.federrPath = "/federation" - <> Text.decodeUtf8With Text.lenientDecode (LBS.toStrict path) + <> Text.decodeUtf8With Text.lenientDecode (LBS.toStrict path), + Wai.federrResp = pure body } } where 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 02fd6403a44..6d057ea3a68 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs @@ -83,6 +83,7 @@ module Wire.API.Federation.Error ) where +import Data.Domain (Domain (..)) import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Text.Lazy qualified as LT @@ -206,27 +207,35 @@ federationClientErrorToWai FederatorClientVersionMismatch = "internal-error" "Endpoint version mismatch in federation client" -federationRemoteHTTP2Error :: FederatorClientHTTP2Error -> Wai.Error -federationRemoteHTTP2Error FederatorClientNoStatusCode = - Wai.mkError - unexpectedFederationResponseStatus - "federation-http2-error" - "No status code in HTTP2 response" -federationRemoteHTTP2Error (FederatorClientHTTP2Exception e) = - Wai.mkError - unexpectedFederationResponseStatus - "federation-http2-error" - (LT.pack (displayException e)) -federationRemoteHTTP2Error (FederatorClientTLSException e) = - Wai.mkError - (HTTP.mkStatus 525 "SSL Handshake Failure") - "federation-tls-error" - (LT.pack (displayException e)) -federationRemoteHTTP2Error (FederatorClientConnectionError e) = - Wai.mkError - federatorConnectionRefusedStatus - "federation-connection-refused" - (LT.pack (displayException e)) +federationRemoteHTTP2Error :: Domain -> Text -> FederatorClientHTTP2Error -> Wai.Error +federationRemoteHTTP2Error domain path FederatorClientNoStatusCode = + let err = + Wai.mkError + unexpectedFederationResponseStatus + "federation-http2-error" + "No status code in HTTP2 response" + in err {Wai.errorData = pure $ Wai.FederationErrorData domain path Nothing} +federationRemoteHTTP2Error domain path (FederatorClientHTTP2Exception e) = + let err = + Wai.mkError + unexpectedFederationResponseStatus + "federation-http2-error" + (LT.pack (displayException e)) + in err {Wai.errorData = pure $ Wai.FederationErrorData domain path Nothing} +federationRemoteHTTP2Error domain path (FederatorClientTLSException e) = + let err = + Wai.mkError + (HTTP.mkStatus 525 "SSL Handshake Failure") + "federation-tls-error" + (LT.pack (displayException e)) + in err {Wai.errorData = pure $ Wai.FederationErrorData domain path Nothing} +federationRemoteHTTP2Error domain path (FederatorClientConnectionError e) = + let err = + Wai.mkError + federatorConnectionRefusedStatus + "federation-connection-refused" + (LT.pack (displayException e)) + in err {Wai.errorData = pure $ Wai.FederationErrorData domain path Nothing} federationClientHTTP2Error :: FederatorClientHTTP2Error -> Wai.Error federationClientHTTP2Error (FederatorClientConnectionError e) = @@ -240,14 +249,21 @@ federationClientHTTP2Error e = "federation-local-error" (LT.pack (displayException e)) -federationRemoteResponseError :: HTTP.Status -> Wai.Error -federationRemoteResponseError status = - Wai.mkError - unexpectedFederationResponseStatus - "federation-remote-error" - ( "A remote federator failed with status code " - <> LT.pack (show (HTTP.statusCode status)) - ) +federationRemoteResponseError :: Domain -> Text -> HTTP.Status -> LByteString -> Wai.Error +federationRemoteResponseError domain path status resp = + err + { Wai.errorData = pure $ Wai.FederationErrorData domain path $ pure resp + } + where + err = + Wai.mkError + unexpectedFederationResponseStatus + "federation-remote-error" + ( "A remote federator (" + <> LT.fromStrict domain._domainText + <> ") failed with status code " + <> LT.pack (show (HTTP.statusCode status)) + ) federationServantErrorToWai :: ClientError -> Wai.Error federationServantErrorToWai (DecodeFailure msg _) = federationInvalidBody msg diff --git a/services/federator/default.nix b/services/federator/default.nix index a6a88ed1d37..77517559a75 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -41,6 +41,7 @@ , pem , polysemy , polysemy-wire-zoo +, prometheus-client , QuickCheck , random , servant @@ -105,6 +106,7 @@ mkDerivation { pem polysemy polysemy-wire-zoo + prometheus-client servant servant-client-core servant-server diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index e3e28a30089..53895622a41 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -43,6 +43,7 @@ library Federator.ExternalServer Federator.Health Federator.InternalServer + Federator.Metrics Federator.MockServer Federator.Monitor Federator.Monitor.Internal @@ -134,6 +135,7 @@ library , pem , polysemy , polysemy-wire-zoo + , prometheus-client , servant , servant-client-core , servant-server diff --git a/services/federator/src/Federator/Env.hs b/services/federator/src/Federator/Env.hs index 52a581891a4..90e8b1c21cf 100644 --- a/services/federator/src/Federator/Env.hs +++ b/services/federator/src/Federator/Env.hs @@ -30,11 +30,17 @@ import Imports import Network.DNS.Resolver (Resolver) import Network.HTTP.Client qualified as HTTP import OpenSSL.Session (SSLContext) +import Prometheus import System.Logger.Class qualified as LC import Util.Options import Wire.API.Federation.Component import Wire.API.Routes.FederationDomainConfig (FederationDomainConfigs) +data FederatorMetrics = FederatorMetrics + { outgoingRequests :: Vector Text Counter, + incomingRequests :: Vector Text Counter + } + data Env = Env { _metrics :: Metrics, _applog :: LC.Logger, @@ -46,7 +52,8 @@ data Env = Env _externalPort :: Word16, _internalPort :: Word16, _httpManager :: HTTP.Manager, - _http2Manager :: IORef Http2Manager + _http2Manager :: IORef Http2Manager, + _federatorMetrics :: FederatorMetrics } makeLenses ''Env diff --git a/services/federator/src/Federator/ExternalServer.hs b/services/federator/src/Federator/ExternalServer.hs index 6ec2178efb7..733a838b8be 100644 --- a/services/federator/src/Federator/ExternalServer.hs +++ b/services/federator/src/Federator/ExternalServer.hs @@ -40,6 +40,7 @@ import Federator.Discovery import Federator.Env import Federator.Error.ServerError import Federator.Health qualified as Health +import Federator.Metrics import Federator.RPC import Federator.Response import Federator.Service @@ -103,7 +104,8 @@ server :: Member (Error ValidationError) r, Member (Error DiscoveryFailure) r, Member (Error ServerError) r, - Member (Input FederationDomainConfigs) r + Member (Input FederationDomainConfigs) r, + Member Metrics r ) => Manager -> Word16 -> @@ -125,7 +127,8 @@ callInward :: Member (Error ValidationError) r, Member (Error DiscoveryFailure) r, Member (Error ServerError) r, - Member (Input FederationDomainConfigs) r + Member (Input FederationDomainConfigs) r, + Member Metrics r ) => Component -> RPC -> @@ -134,6 +137,7 @@ callInward :: Wai.Request -> Sem r Wai.Response callInward component (RPC rpc) originDomain (CertHeader cert) wreq = do + incomingCounterIncr originDomain -- only POST is supported when (Wai.requestMethod wreq /= HTTP.methodPost) $ throw InvalidRoute diff --git a/services/federator/src/Federator/InternalServer.hs b/services/federator/src/Federator/InternalServer.hs index 5be50a2bde3..b9e3d903a36 100644 --- a/services/federator/src/Federator/InternalServer.hs +++ b/services/federator/src/Federator/InternalServer.hs @@ -29,6 +29,7 @@ import Data.Proxy import Federator.Env import Federator.Error.ServerError import Federator.Health qualified as Health +import Federator.Metrics (Metrics, outgoingCounterIncr) import Federator.RPC import Federator.Remote import Federator.Response @@ -44,8 +45,10 @@ import Servant.API import Servant.API.Extended.Endpath import Servant.Server (Tagged (..)) import Servant.Server.Generic +import System.Logger.Class qualified as Log import Wire.API.Federation.Component import Wire.API.Routes.FederationDomainConfig +import Wire.Sem.Logger (Logger, debug) data API mode = API { status :: @@ -75,7 +78,9 @@ server :: Member (Embed IO) r, Member (Error ValidationError) r, Member (Error ServerError) r, - Member (Input FederationDomainConfigs) r + Member (Input FederationDomainConfigs) r, + Member Metrics r, + Member (Logger (Log.Msg -> Log.Msg)) r ) => Manager -> Word16 -> @@ -93,7 +98,9 @@ callOutward :: Member (Embed IO) r, Member (Error ValidationError) r, Member (Error ServerError) r, - Member (Input FederationDomainConfigs) r + Member (Input FederationDomainConfigs) r, + Member Metrics r, + Member (Logger (Log.Msg -> Log.Msg)) r ) => Domain -> Component -> @@ -107,9 +114,15 @@ callOutward targetDomain component (RPC path) req = do -- No query parameters are allowed unless (BS.null . Wai.rawQueryString $ req) $ throw InvalidRoute - ensureCanFederateWith targetDomain + outgoingCounterIncr targetDomain body <- embed $ Wai.lazyRequestBody req + debug $ + Log.msg (Log.val "Federator outward call") + . Log.field "domain" targetDomain._domainText + . Log.field "component" (show component) + . Log.field "path" path + . Log.field "body" body resp <- discoverAndCall targetDomain diff --git a/services/federator/src/Federator/Metrics.hs b/services/federator/src/Federator/Metrics.hs new file mode 100644 index 00000000000..b2f01b6ebd1 --- /dev/null +++ b/services/federator/src/Federator/Metrics.hs @@ -0,0 +1,55 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Federator.Metrics + ( Metrics (..), + interpretMetrics, + outgoingCounterIncr, + incomingCounterIncr, + ) +where + +import Control.Lens (view) +import Data.Domain (Domain, domainText) +import Federator.Env +import Imports +import Polysemy +import Polysemy.Input (Input, inputs) +import Prometheus + +data Metrics m a where + OutgoingCounterIncr :: Domain -> Metrics m () + IncomingCounterIncr :: Domain -> Metrics m () + +makeSem ''Metrics + +interpretMetrics :: + ( Member (Input Env) r, + Member (Embed IO) r + ) => + Sem (Metrics ': r) a -> + Sem r a +interpretMetrics = interpret $ \case + OutgoingCounterIncr targetDomain -> do + m <- inputs (view federatorMetrics) + liftIO $ withLabel m.outgoingRequests (domainText targetDomain) incCounter + IncomingCounterIncr originDomain -> do + m <- inputs (view federatorMetrics) + liftIO $ withLabel m.incomingRequests (domainText originDomain) incCounter diff --git a/services/federator/src/Federator/Remote.hs b/services/federator/src/Federator/Remote.hs index e72682144b9..3741bad1bf9 100644 --- a/services/federator/src/Federator/Remote.hs +++ b/services/federator/src/Federator/Remote.hs @@ -32,6 +32,7 @@ import Data.Binary.Builder import Data.ByteString.Lazy qualified as LBS import Data.Domain import Data.Text qualified as Text +import Data.Text.Encoding (decodeUtf8) import Data.Text.Encoding qualified as Text import Data.Text.Encoding.Error qualified as Text import Federator.Discovery @@ -55,26 +56,33 @@ import Wire.Network.DNS.SRV data RemoteError = -- | This means that an error occurred while trying to make a request to a -- remote federator. - RemoteError SrvTarget FederatorClientHTTP2Error + RemoteError SrvTarget Text FederatorClientHTTP2Error | -- | This means that a request to a remote federator returned an error -- response. The error response could be due to an error in the remote -- federator itself, or in the services it proxied to. - RemoteErrorResponse SrvTarget HTTP.Status LByteString + RemoteErrorResponse SrvTarget Text HTTP.Status LByteString deriving (Show) instance AsWai RemoteError where - toWai (RemoteError _ e) = federationRemoteHTTP2Error e - toWai (RemoteErrorResponse _ status _) = - federationRemoteResponseError status + toWai (RemoteError target path e) = + let domain = Domain . decodeUtf8 $ target.srvTargetDomain + in federationRemoteHTTP2Error domain path e + toWai (RemoteErrorResponse target path status resp) = + let domain = Domain . decodeUtf8 $ target.srvTargetDomain + in federationRemoteResponseError domain path status resp - waiErrorDescription (RemoteError tgt e) = + waiErrorDescription (RemoteError tgt path e) = "Error while connecting to " <> displayTarget tgt + <> " on path " + <> path <> ": " <> Text.pack (displayException e) - waiErrorDescription (RemoteErrorResponse tgt status body) = + waiErrorDescription (RemoteErrorResponse tgt path status body) = "Federator at " <> displayTarget tgt + <> " on path " + <> path <> " failed with status code " <> Text.pack (show (HTTP.statusCode status)) <> ": " @@ -112,12 +120,13 @@ interpretRemote = interpret $ \case let path = LBS.toStrict . toLazyByteString $ HTTP.encodePathSegments ["federation", componentName component, rpc] + pathT = decodeUtf8 path -- filter out Host header, because the HTTP2 client adds it back headers' = filter ((/= "Host") . fst) headers req' = HTTP2.requestBuilder HTTP.methodPost path headers' body mgr <- input - resp <- mapError (RemoteError target) . (fromEither @FederatorClientHTTP2Error =<<) . embed $ + resp <- mapError (RemoteError target pathT) . (fromEither @FederatorClientHTTP2Error =<<) . embed $ Codensity $ \k -> E.catches (H2Manager.withHTTP2Request mgr (True, hostname, fromIntegral port) req' (consumeStreamingResponseWith $ k . Right)) @@ -132,6 +141,7 @@ interpretRemote = interpret $ \case throw $ RemoteErrorResponse target + pathT (responseStatusCode resp) (toLazyByteString bdy) pure resp diff --git a/services/federator/src/Federator/Response.hs b/services/federator/src/Federator/Response.hs index 8c447cbc2fe..e7089d9a6d5 100644 --- a/services/federator/src/Federator/Response.hs +++ b/services/federator/src/Federator/Response.hs @@ -34,6 +34,7 @@ import Federator.Discovery import Federator.Env import Federator.Error import Federator.Error.ServerError +import Federator.Metrics (Metrics, interpretMetrics) import Federator.Options import Federator.Remote import Federator.Service @@ -137,7 +138,8 @@ serveServant middleware server env port = genericServe server type AllEffects = - '[ Remote, + '[ Metrics, + Remote, DiscoverFederator, DNSLookup, -- needed by DiscoverFederator ServiceStreaming, @@ -175,6 +177,7 @@ runFederator env = . runDNSLookupWithResolver (view dnsResolver env) . runFederatorDiscovery . interpretRemote + . interpretMetrics streamingResponseToWai :: StreamingResponse -> Wai.Response streamingResponseToWai resp = diff --git a/services/federator/src/Federator/Run.hs b/services/federator/src/Federator/Run.hs index e3072294ec6..676d5e233c3 100644 --- a/services/federator/src/Federator/Run.hs +++ b/services/federator/src/Federator/Run.hs @@ -47,6 +47,7 @@ import Federator.Options as Opt import Imports import Network.DNS qualified as DNS import Network.HTTP.Client qualified as HTTP +import Prometheus import System.Logger qualified as Log import System.Logger.Extended qualified as LogExt import Util.Options @@ -104,8 +105,27 @@ newEnv o _dnsResolver _applog _domainConfigs = do _httpManager <- initHttpManager sslContext <- mkTLSSettingsOrThrow _runSettings _http2Manager <- newIORef =<< mkHttp2Manager sslContext + _federatorMetrics <- mkFederatorMetrics pure Env {..} +mkFederatorMetrics :: IO FederatorMetrics +mkFederatorMetrics = + FederatorMetrics + <$> register + ( vector "target_domain" $ + counter $ + Prometheus.Info + "com_wire_federator_outgoing_requests" + "Number of outgoing requests" + ) + <*> register + ( vector "origin_domain" $ + counter $ + Prometheus.Info + "com_wire_federator_incoming_requests" + "Number of incoming requests" + ) + closeEnv :: Env -> IO () closeEnv e = do Log.flush $ e ^. applog diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs index c5c13ea41af..a93a4fa78bf 100644 --- a/services/federator/test/integration/Test/Federator/IngressSpec.hs +++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs @@ -106,9 +106,9 @@ spec env = do (Aeson.fromEncoding (Aeson.toEncoding hdl)) liftToCodensity . embed $ case r of Right _ -> expectationFailure "Expected client certificate error, got response" - Left (RemoteError _ _) -> + Left (RemoteError {}) -> expectationFailure "Expected client certificate error, got remote error" - Left (RemoteErrorResponse _ status _) -> status `shouldBe` HTTP.status400 + Left (RemoteErrorResponse _ _ status _) -> status `shouldBe` HTTP.status400 -- FUTUREWORK: ORMOLU_DISABLE -- @END diff --git a/services/federator/test/unit/Test/Federator/ExternalServer.hs b/services/federator/test/unit/Test/Federator/ExternalServer.hs index 2f95e96aa24..66961412305 100644 --- a/services/federator/test/unit/Test/Federator/ExternalServer.hs +++ b/services/federator/test/unit/Test/Federator/ExternalServer.hs @@ -27,6 +27,7 @@ import Data.Text.Encoding qualified as Text import Federator.Discovery import Federator.Error.ServerError (ServerError (..)) import Federator.ExternalServer +import Federator.Metrics import Federator.Options import Federator.Response import Federator.Service (Service (..), ServiceStreaming) @@ -70,6 +71,11 @@ tests = testMethod ] +interpretMetricsEmpty :: Sem (Metrics ': r) a -> Sem r a +interpretMetricsEmpty = interpret $ \case + OutgoingCounterIncr _ -> pure () + IncomingCounterIncr _ -> pure () + exampleRequest :: FilePath -> ByteString -> IO Wai.Request exampleRequest certFile path = do cert <- BS.readFile certFile @@ -113,8 +119,15 @@ requestBrigSuccess = "test/resources/unit/localhost.example.com.pem" "/federation/brig/get-user-by-handle" Right cert <- decodeCertificate <$> BS.readFile "test/resources/unit/localhost.example.com.pem" + + let assertMetrics :: Member (Embed IO) r => Sem (Metrics ': r) a -> Sem r a + assertMetrics = interpret $ \case + OutgoingCounterIncr _ -> embed @IO $ assertFailure "Should not increment outgoing counter" + IncomingCounterIncr od -> embed @IO $ od @?= aValidDomain + (actualCalls, res) <- runM + . assertMetrics . runOutputList . mockService HTTP.ok200 . assertNoError @ValidationError @@ -142,6 +155,7 @@ requestBrigFailure = (actualCalls, res) <- runM + . interpretMetricsEmpty . runOutputList . mockService HTTP.notFound404 . assertNoError @ValidationError @@ -172,6 +186,7 @@ requestGalleySuccess = runM $ do (actualCalls, res) <- runOutputList + . interpretMetricsEmpty . mockService HTTP.ok200 . assertNoError @ValidationError . assertNoError @DiscoveryFailure @@ -318,7 +333,8 @@ testMethod = testInterpretter :: IORef [Call] -> Sem - '[ Input FederationDomainConfigs, + '[ Metrics, + Input FederationDomainConfigs, Input RunSettings, DiscoverFederator, Error DiscoveryFailure, @@ -341,6 +357,7 @@ testInterpretter serviceCallsRef = . mockDiscoveryTrivial . runInputConst noClientCertSettings . runInputConst scaffoldingFederationDomainConfigs + . interpretMetricsEmpty exampleDomain :: Text exampleDomain = "localhost.example.com" diff --git a/services/federator/test/unit/Test/Federator/InternalServer.hs b/services/federator/test/unit/Test/Federator/InternalServer.hs index 6d0e61cd393..9a433081f94 100644 --- a/services/federator/test/unit/Test/Federator/InternalServer.hs +++ b/services/federator/test/unit/Test/Federator/InternalServer.hs @@ -25,6 +25,7 @@ import Data.Default import Data.Domain import Federator.Error.ServerError import Federator.InternalServer (callOutward) +import Federator.Metrics import Federator.RPC import Federator.Remote import Federator.Validation @@ -86,6 +87,12 @@ federatedRequestSuccess = responseHttpVersion = HTTP.http20, responseBody = source ["\"bar\""] } + + let assertMetrics :: Member (Embed IO) r => Sem (Metrics ': r) a -> Sem r a + assertMetrics = interpret $ \case + OutgoingCounterIncr td -> embed @IO $ td @?= targetDomain + IncomingCounterIncr _ -> embed @IO $ assertFailure "Should not increment incoming counter" + res <- runM . interpretCall @@ -94,6 +101,7 @@ federatedRequestSuccess = . discardTinyLogs . runInputConst settings . runInputConst (FederationDomainConfigs AllowDynamic [FederationDomainConfig (Domain "target.example.com") FullSearch] 10) + . assertMetrics $ callOutward targetDomain Brig (RPC "get-user-by-handle") request Wai.responseStatus res @?= HTTP.status200 body <- Wai.lazyResponseBody res @@ -126,6 +134,9 @@ federatedRequestFailureAllowList = responseHttpVersion = HTTP.http20, responseBody = source ["\"bar\""] } + let interpretMetricsEmpty = interpret $ \case + OutgoingCounterIncr _ -> pure () + IncomingCounterIncr _ -> pure () eith <- runM @@ -136,6 +147,7 @@ federatedRequestFailureAllowList = . discardTinyLogs . runInputConst settings . runInputConst (FederationDomainConfigs AllowDynamic [FederationDomainConfig (Domain "hello.world") FullSearch] 10) + . interpretMetricsEmpty $ callOutward targetDomain Brig (RPC "get-user-by-handle") request eith @?= Left (FederationDenied targetDomain) diff --git a/services/federator/test/unit/Test/Federator/Remote.hs b/services/federator/test/unit/Test/Federator/Remote.hs index f9a1f1161ed..af13e26f1d9 100644 --- a/services/federator/test/unit/Test/Federator/Remote.hs +++ b/services/federator/test/unit/Test/Federator/Remote.hs @@ -142,14 +142,14 @@ testValidatesCertificateWrongHostname = withMockServer certForWrongDomain $ \port -> do tlsSettings <- mkTLSSettingsOrThrow settings runCodensity (mkTestCall tlsSettings "localhost" port) $ \case - Left (RemoteError _ (FederatorClientTLSException _)) -> pure () + Left (RemoteError _ _ (FederatorClientTLSException _)) -> pure () Left x -> assertFailure $ "Expected TLS failure, got: " <> show x Right _ -> assertFailure "Expected connection with the server to fail", testCase "when the server's certificate does not have the server key usage flag" $ withMockServer certWithoutServerKeyUsage $ \port -> do tlsSettings <- mkTLSSettingsOrThrow settings runCodensity (mkTestCall tlsSettings "localhost" port) $ \case - Left (RemoteError _ (FederatorClientTLSException _)) -> pure () + Left (RemoteError _ _ (FederatorClientTLSException _)) -> pure () Left x -> assertFailure $ "Expected TLS failure, got: " <> show x Right _ -> assertFailure "Expected connection with the server to fail" ] @@ -160,7 +160,7 @@ testConnectionError :: TestTree testConnectionError = testCase "connection failures are reported correctly" $ do tlsSettings <- mkTLSSettingsOrThrow settings runCodensity (mkTestCall tlsSettings "localhost" 1) $ \case - Left (RemoteError _ (FederatorClientConnectionError _)) -> pure () + Left (RemoteError _ _ (FederatorClientConnectionError _)) -> pure () Left x -> assertFailure $ "Expected connection error, got: " <> show x Right _ -> assertFailure "Expected connection with the server to fail" diff --git a/services/federator/test/unit/Test/Federator/Response.hs b/services/federator/test/unit/Test/Federator/Response.hs index 8bc559cd9a6..dcc0fec008c 100644 --- a/services/federator/test/unit/Test/Federator/Response.hs +++ b/services/federator/test/unit/Test/Federator/Response.hs @@ -95,6 +95,7 @@ testRemoteError = $ throw ( RemoteError (SrvTarget "example.com" 7777) + "" FederatorClientNoStatusCode ) body <- Wai.lazyResponseBody resp From c5a3f5c0625840e9108b221eb90d08c408d68d68 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 6 Sep 2023 11:09:59 +0200 Subject: [PATCH 115/225] Makefile: Avoid executing the hint (#3564) Backticks execute the command even when they are in quotes. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d013fde4041..b2706f460a2 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,7 @@ clean-hint: @echo -e "\n\n\n>>> PSA: if you get errors that are hard to explain," @echo -e ">>> try 'git submodule update --init --recursive' and 'make full-clean' and run your command again." @echo -e ">>> see https://github.com/wireapp/wire-server/blob/develop/docs/developer/building.md#linker-errors-while-compiling" - @echo -e ">>> to never have to remember submodules again, try `git config --global submodule.recurse true`" + @echo -e ">>> to never have to remember submodules again, try 'git config --global submodule.recurse true'" @echo -e "\n\n\n" .PHONY: cabal.project.local From 75b1afdfcf41b4b4e7342a30948510b41a7932ae Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 6 Sep 2023 15:47:06 +0200 Subject: [PATCH 116/225] Finalise v4 (#3545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove MLS endpoints from the API They will be reintroduced when merging the mls branch. These endpoints are not currently functional on develop, so removing them from here will reduce the amount of conflicts. * Finalise v4 * Add CHANGELOG entry * Add pregenerated swagger for v4 * Delete MLS tests in brig * Remove more MLS endpoints from v4 * Set default API version to 5 in integration tests * Update the documentation on API versioning --------- Co-authored-by: Marko Dimjašević --- changelog.d/1-api-changes/finalise-v4 | 1 + .../src/developer/developer/api-versioning.md | 2 +- integration/test/Testlib/Env.hs | 2 +- libs/metrics-wai/src/Data/Metrics/Servant.hs | 3 + libs/wire-api/src/Wire/API/Routes/Named.hs | 9 + .../src/Wire/API/Routes/Public/Brig.hs | 52 +- .../API/Routes/Public/Galley/Conversation.hs | 40 - .../Wire/API/Routes/Public/Galley/Feature.hs | 8 +- .../src/Wire/API/Routes/Public/Galley/MLS.hs | 153 +- libs/wire-api/src/Wire/API/Routes/Version.hs | 1 + services/brig/brig.cabal | 3 + services/brig/docs/swagger-v4.json | 21796 ++++++++++++++++ services/brig/src/Brig/API/Public.hs | 25 +- .../brig/test/integration/API/Federation.hs | 13 - .../brig/test/integration/API/Internal.hs | 158 +- services/brig/test/integration/API/MLS.hs | 213 +- .../test/integration/Federation/End2end.hs | 31 - .../src/Galley/API/Public/Conversation.hs | 3 - services/galley/src/Galley/API/Public/MLS.hs | 10 +- 19 files changed, 21830 insertions(+), 693 deletions(-) create mode 100644 changelog.d/1-api-changes/finalise-v4 create mode 100644 services/brig/docs/swagger-v4.json diff --git a/changelog.d/1-api-changes/finalise-v4 b/changelog.d/1-api-changes/finalise-v4 new file mode 100644 index 00000000000..41a18ee0ca5 --- /dev/null +++ b/changelog.d/1-api-changes/finalise-v4 @@ -0,0 +1 @@ +Remove MLS endpoints from API v4 and finalise it diff --git a/docs/src/developer/developer/api-versioning.md b/docs/src/developer/developer/api-versioning.md index 3cf36134547..29ebb8dee32 100644 --- a/docs/src/developer/developer/api-versioning.md +++ b/docs/src/developer/developer/api-versioning.md @@ -110,7 +110,7 @@ are several steps to make apart from deciding what endpoint changes are part of the version: - In `wire-api` extend the `Version` type with a new version by appending the - new version to the end, e.g., by adding `V4`. + new version to the end, e.g., by adding `V6`. - In the same `Version` module update the `developmentVersions` value to list only the new version, - Consider updating the `backendApiVersion` value in Stern, which is diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 5ddb4cfb29f..b531a43bf54 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -63,7 +63,7 @@ mkGlobalEnv cfgFile = do gDomain1 = intConfig.backendOne.originDomain, gDomain2 = intConfig.backendTwo.originDomain, gDynamicDomains = (.domain) <$> Map.elems intConfig.dynamicBackends, - gDefaultAPIVersion = 4, + gDefaultAPIVersion = 5, gManager = manager, gServicesCwdBase = devEnvProjectRoot <&> ( "services"), gRemovalKeyPath = error "Uninitialised removal key path", diff --git a/libs/metrics-wai/src/Data/Metrics/Servant.hs b/libs/metrics-wai/src/Data/Metrics/Servant.hs index 184609ca2c8..372cdc95055 100644 --- a/libs/metrics-wai/src/Data/Metrics/Servant.hs +++ b/libs/metrics-wai/src/Data/Metrics/Servant.hs @@ -154,5 +154,8 @@ instance where getRoutes = getRoutes @route <> getRoutes @routes +instance RoutesToPaths EmptyAPI where + getRoutes = mempty + instance RoutesToPaths Raw where getRoutes = [] diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index 804e3161ea2..79136daebfa 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -134,3 +134,12 @@ namedClient :: (HasEndpoint api endpoint name, HasClient m endpoint) => Client m endpoint namedClient = clientIn (Proxy @endpoint) (Proxy @m) + +--------------------------------------------- +-- Utility to add a combinator to a Named API + +type family x ::> api + +type instance + x ::> (Named name api) = + Named name (x :> api) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index d83004bb54b..48e8c36fca4 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -45,8 +45,6 @@ import Wire.API.Connection hiding (MissingLegalholdConsent) import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Error.Empty -import Wire.API.MLS.KeyPackage -import Wire.API.MLS.Servant import Wire.API.MakesFederatedCall import Wire.API.OAuth import Wire.API.Properties @@ -85,7 +83,6 @@ type BrigAPI = :<|> UserClientAPI :<|> ConnectionAPI :<|> PropertiesAPI - :<|> MLSAPI :<|> UserHandleAPI :<|> SearchAPI :<|> AuthAPI @@ -276,6 +273,7 @@ type UserAPI = :<|> Named "get-supported-protocols" ( Summary "Get a user's supported protocols" + :> From 'V5 :> ZLocalUser :> "users" :> QualifiedCaptureUserId "uid" @@ -418,6 +416,7 @@ type SelfAPI = :<|> Named "change-supported-protocols" ( Summary "Change your supported protocols" + :> From 'V5 :> ZLocalUser :> ZConn :> "self" @@ -1100,6 +1099,8 @@ type ConnectionAPI = :> Get '[Servant.JSON] (SearchResult Contact) ) +-- Properties API ----------------------------------------------------- + type PropertiesAPI = LiftNamed ( ZUser @@ -1160,49 +1161,6 @@ type PropertiesAPI = :> Get '[JSON] PropertyKeysAndValues ) --- Properties API ----------------------------------------------------- - -type MLSKeyPackageAPI = - "key-packages" - :> ( Named - "mls-key-packages-upload" - ( "self" - :> Summary "Upload a fresh batch of key packages" - :> Description "The request body should be a json object containing a list of base64-encoded key packages." - :> ZLocalUser - :> CanThrow 'MLSProtocolError - :> CanThrow 'MLSIdentityMismatch - :> CaptureClientId "client" - :> ReqBody '[JSON] KeyPackageUpload - :> MultiVerb 'POST '[JSON, MLS] '[RespondEmpty 201 "Key packages uploaded"] () - ) - :<|> Named - "mls-key-packages-claim" - ( "claim" - :> Summary "Claim one key package for each client of the given user" - :> MakesFederatedCall 'Brig "claim-key-packages" - :> ZLocalUser - :> QualifiedCaptureUserId "user" - :> QueryParam' - [ Optional, - Strict, - Description "Do not claim a key package for the given own client" - ] - "skip_own" - ClientId - :> MultiVerb1 'POST '[JSON] (Respond 200 "Claimed key packages" KeyPackageBundle) - ) - :<|> Named - "mls-key-packages-count" - ( "self" - :> ZLocalUser - :> CaptureClientId "client" - :> "count" - :> Summary "Return the number of unused key packages for the given client" - :> MultiVerb1 'GET '[JSON] (Respond 200 "Number of key packages" KeyPackageCount) - ) - ) - -- Search API ----------------------------------------------------- type SearchAPI = @@ -1264,8 +1222,6 @@ type SearchAPI = (SearchResult TeamContact) ) -type MLSAPI = LiftNamed ("mls" :> MLSKeyPackageAPI) - type AuthAPI = Named "access" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 9b0b1250937..58b42a4e4a1 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -32,8 +32,6 @@ import Wire.API.Conversation.Typing import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation -import Wire.API.MLS.PublicGroupState -import Wire.API.MLS.Servant import Wire.API.MakesFederatedCall import Wire.API.OAuth import Wire.API.Routes.MultiVerb @@ -196,27 +194,6 @@ type ConversationAPI = :> "roles" :> Get '[Servant.JSON] ConversationRolesList ) - :<|> Named - "get-group-info" - ( Summary "Get MLS group information" - :> From 'V4 - :> MakesFederatedCall 'Galley "query-group-info" - :> CanThrow 'ConvNotFound - :> CanThrow 'MLSMissingGroupInfo - :> CanThrow 'MLSNotEnabled - :> ZLocalUser - :> "conversations" - :> QualifiedCapture "cnv" ConvId - :> "groupinfo" - :> MultiVerb1 - 'GET - '[MLS] - ( Respond - 200 - "The group information" - OpaquePublicGroupState - ) - ) :<|> Named "list-conversation-ids-unqualified" ( Summary "[deprecated] Get all local conversation IDs." @@ -460,23 +437,6 @@ type ConversationAPI = :> "self" :> ConversationVerb ) - :<|> Named - "get-mls-self-conversation" - ( Summary "Get the user's MLS self-conversation" - :> From 'V4 - :> ZLocalUser - :> "conversations" - :> "mls-self" - :> CanThrow 'MLSNotEnabled - :> MultiVerb1 - 'GET - '[JSON] - ( Respond - 200 - "The MLS self-conversation" - Conversation - ) - ) -- This endpoint can lead to the following events being sent: -- - ConvCreate event to members -- TODO: add note: "On 201, the conversation ID is the `Location` header" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index 1a7a5d11965..eb8e6cd3075 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -79,16 +79,16 @@ type FeatureAPI = :<|> FeatureStatusPut '[] '() GuestLinksConfig :<|> FeatureStatusGet SndFactorPasswordChallengeConfig :<|> FeatureStatusPut '[] '() SndFactorPasswordChallengeConfig - :<|> FeatureStatusGet MLSConfig - :<|> FeatureStatusPut '[] '() MLSConfig + :<|> From 'V5 ::> FeatureStatusGet MLSConfig + :<|> From 'V5 ::> FeatureStatusPut '[] '() MLSConfig :<|> FeatureStatusGet ExposeInvitationURLsToTeamAdminConfig :<|> FeatureStatusPut '[] '() ExposeInvitationURLsToTeamAdminConfig :<|> FeatureStatusGet SearchVisibilityInboundConfig :<|> FeatureStatusPut '[] '() SearchVisibilityInboundConfig :<|> FeatureStatusGet OutlookCalIntegrationConfig :<|> FeatureStatusPut '[] '() OutlookCalIntegrationConfig - :<|> FeatureStatusGet MlsE2EIdConfig - :<|> FeatureStatusPut '[] '() MlsE2EIdConfig + :<|> From 'V5 ::> FeatureStatusGet MlsE2EIdConfig + :<|> From 'V5 ::> FeatureStatusPut '[] '() MlsE2EIdConfig :<|> AllFeatureConfigsUserGet :<|> AllFeatureConfigsTeamGet :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" LegalholdConfig diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index f026628c4e6..0ae7d3feb10 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -17,155 +17,6 @@ module Wire.API.Routes.Public.Galley.MLS where -import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () -import Wire.API.Error -import Wire.API.Error.Galley -import Wire.API.Event.Conversation -import Wire.API.MLS.CommitBundle -import Wire.API.MLS.Keys -import Wire.API.MLS.Message -import Wire.API.MLS.Serialisation -import Wire.API.MLS.Servant -import Wire.API.MLS.Welcome -import Wire.API.MakesFederatedCall -import Wire.API.Routes.MultiVerb -import Wire.API.Routes.Named -import Wire.API.Routes.Public -import Wire.API.Routes.Version +import Servant -type MLSMessagingAPI = - Named - "mls-welcome-message" - ( Summary "Post an MLS welcome message" - :> Until 'V3 - :> MakesFederatedCall 'Galley "mls-welcome" - :> CanThrow 'MLSKeyPackageRefNotFound - :> CanThrow 'MLSNotEnabled - :> "welcome" - :> ZLocalUser - :> ZConn - :> ReqBody '[MLS] (RawMLS Welcome) - :> MultiVerb1 'POST '[JSON] (RespondEmpty 201 "Welcome message sent") - ) - :<|> Named - "mls-message-v1" - ( Summary "Post an MLS message" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "send-mls-message" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Brig "get-mls-clients" - :> Until 'V2 - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvMemberNotFound - :> CanThrow 'ConvNotFound - :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'MLSClientMismatch - :> CanThrow 'MLSCommitMissingReferences - :> CanThrow 'MLSKeyPackageRefNotFound - :> CanThrow 'MLSNotEnabled - :> CanThrow 'MLSProposalNotFound - :> CanThrow 'MLSProtocolErrorTag - :> CanThrow 'MLSSelfRemovalNotAllowed - :> CanThrow 'MLSStaleMessage - :> CanThrow 'MLSUnsupportedMessage - :> CanThrow 'MLSUnsupportedProposal - :> CanThrow 'MLSClientSenderUserMismatch - :> CanThrow 'MLSGroupConversationMismatch - :> CanThrow 'MLSMissingSenderClient - :> CanThrow 'MissingLegalholdConsent - :> CanThrow MLSProposalFailure - :> CanThrow NonFederatingBackends - :> CanThrow UnreachableBackends - :> "messages" - :> ZLocalUser - :> ZOptClient - :> ZConn - :> ReqBody '[MLS] (RawMLS SomeMessage) - :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" [Event]) - ) - :<|> Named - "mls-message" - ( Summary "Post an MLS message" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "send-mls-message" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Brig "get-mls-clients" - :> From 'V2 - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvMemberNotFound - :> CanThrow 'ConvNotFound - :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'MLSClientMismatch - :> CanThrow 'MLSCommitMissingReferences - :> CanThrow 'MLSKeyPackageRefNotFound - :> CanThrow 'MLSNotEnabled - :> CanThrow 'MLSProposalNotFound - :> CanThrow 'MLSProtocolErrorTag - :> CanThrow 'MLSSelfRemovalNotAllowed - :> CanThrow 'MLSStaleMessage - :> CanThrow 'MLSUnsupportedMessage - :> CanThrow 'MLSUnsupportedProposal - :> CanThrow 'MLSClientSenderUserMismatch - :> CanThrow 'MLSGroupConversationMismatch - :> CanThrow 'MLSMissingSenderClient - :> CanThrow 'MissingLegalholdConsent - :> CanThrow MLSProposalFailure - :> CanThrow NonFederatingBackends - :> CanThrow UnreachableBackends - :> "messages" - :> ZLocalUser - :> ZOptClient - :> ZConn - :> ReqBody '[MLS] (RawMLS SomeMessage) - :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" MLSMessageSendingStatus) - ) - :<|> Named - "mls-commit-bundle" - ( Summary "Post a MLS CommitBundle" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "mls-welcome" - :> MakesFederatedCall 'Galley "send-mls-commit-bundle" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Brig "get-mls-clients" - :> From 'V4 - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvMemberNotFound - :> CanThrow 'ConvNotFound - :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'MLSClientMismatch - :> CanThrow 'MLSCommitMissingReferences - :> CanThrow 'MLSKeyPackageRefNotFound - :> CanThrow 'MLSNotEnabled - :> CanThrow 'MLSProposalNotFound - :> CanThrow 'MLSProtocolErrorTag - :> CanThrow 'MLSSelfRemovalNotAllowed - :> CanThrow 'MLSStaleMessage - :> CanThrow 'MLSUnsupportedMessage - :> CanThrow 'MLSUnsupportedProposal - :> CanThrow 'MLSClientSenderUserMismatch - :> CanThrow 'MLSGroupConversationMismatch - :> CanThrow 'MLSMissingSenderClient - :> CanThrow 'MLSWelcomeMismatch - :> CanThrow 'MissingLegalholdConsent - :> CanThrow MLSProposalFailure - :> CanThrow NonFederatingBackends - :> CanThrow UnreachableBackends - :> "commit-bundles" - :> ZLocalUser - :> ZOptClient - :> ZConn - :> ReqBody '[CommitBundleMimeType] CommitBundle - :> MultiVerb1 'POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus) - ) - :<|> Named - "mls-public-keys" - ( Summary "Get public keys used by the backend to sign external proposals" - :> From 'V4 - :> CanThrow 'MLSNotEnabled - :> "public-keys" - :> ZLocalUser - :> MultiVerb1 'GET '[JSON] (Respond 200 "Public keys" MLSPublicKeys) - ) - -type MLSAPI = LiftNamed ("mls" :> MLSMessagingAPI) +type MLSAPI = EmptyAPI diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index 826da8873a5..ea8bfc26f8b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -194,6 +194,7 @@ isDevelopmentVersion V0 = False isDevelopmentVersion V1 = False isDevelopmentVersion V2 = False isDevelopmentVersion V3 = False +isDevelopmentVersion V4 = False isDevelopmentVersion _ = True developmentVersions :: [Version] diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 998ff4edf7b..b5024f2e869 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -12,6 +12,9 @@ build-type: Simple extra-source-files: docs/swagger-v0.json docs/swagger-v1.json + docs/swagger-v2.json + docs/swagger-v3.json + docs/swagger-v4.json docs/swagger.md library diff --git a/services/brig/docs/swagger-v4.json b/services/brig/docs/swagger-v4.json new file mode 100644 index 00000000000..8362974c3e9 --- /dev/null +++ b/services/brig/docs/swagger-v4.json @@ -0,0 +1,21796 @@ +{ + "definitions": { + "": { + "description": "Username to use for authenticating against the given TURN servers", + "type": "string" + }, + "ASCII": { + "description": "Stable conversation identifier", + "maxLength": 20, + "minLength": 20, + "type": "string" + }, + "Access": { + "description": "How users can join conversations", + "enum": [ + "private", + "invite", + "link", + "code" + ], + "type": "string" + }, + "AccessRole": { + "description": "Which users/services can join conversations. This replaces legacy access roles and allows a more fine grained configuration of access roles, and in particular a separation of guest and services access.\n\nThis field is optional. If it is not present, the default will be `[team_member, non_team_member, service]`. Please note that an empty list is not allowed when creating a new conversation.", + "enum": [ + "team_member", + "non_team_member", + "guest", + "service" + ], + "type": "string" + }, + "AccessRoleLegacy": { + "description": "Deprecated, please use access_role_v2", + "enum": [ + "private", + "team", + "activated", + "non_activated" + ], + "type": "string" + }, + "AccessToken": { + "properties": { + "access_token": { + "description": "The opaque access token string", + "type": "string" + }, + "expires_in": { + "description": "The number of seconds this token is valid", + "type": "integer" + }, + "token_type": { + "$ref": "#/definitions/TokenType" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "access_token", + "token_type", + "expires_in" + ], + "type": "object" + }, + "AccessTokenType": { + "enum": [ + "DPoP" + ], + "type": "string" + }, + "Action": { + "enum": [ + "add_conversation_member", + "remove_conversation_member", + "modify_conversation_name", + "modify_conversation_message_timer", + "modify_conversation_receipt_mode", + "modify_conversation_access", + "modify_other_conversation_member", + "leave_conversation", + "delete_conversation" + ], + "type": "string" + }, + "Activate": { + "description": "Data for an activation request.", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "dryrun": { + "description": "At least one of key, email, or phone has to be present while key takes precedence over email, and email takes precedence over phone. Whether to perform a dryrun, i.e. to only check whether activation would succeed. Dry-runs never issue access cookies or tokens on success but failures still count towards the maximum failure count.", + "type": "boolean" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "required": [ + "code", + "dryrun" + ], + "type": "object" + }, + "ActivationResponse": { + "description": "Response body of a successful activation request", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "first": { + "description": "Whether this is the first successful activation (i.e. account activation).", + "type": "boolean" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "sso_id": { + "$ref": "#/definitions/UserSSOId" + } + }, + "type": "object" + }, + "AddBot": { + "properties": { + "locale": { + "$ref": "#/definitions/Locale" + }, + "provider": { + "$ref": "#/definitions/UUID" + }, + "service": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "provider", + "service" + ], + "type": "object" + }, + "AddBotResponse": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "client": { + "$ref": "#/definitions/ClientId" + }, + "event": { + "$ref": "#/definitions/Event" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "client", + "name", + "accent_id", + "assets", + "event" + ], + "type": "object" + }, + "AllFeatureConfigs": { + "properties": { + "appLock": { + "$ref": "#/definitions/AppLockConfig.WithStatus" + }, + "classifiedDomains": { + "$ref": "#/definitions/ClassifiedDomainsConfig.WithStatus" + }, + "conferenceCalling": { + "$ref": "#/definitions/ConferenceCallingConfig.WithStatus" + }, + "conversationGuestLinks": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + }, + "digitalSignatures": { + "$ref": "#/definitions/DigitalSignaturesConfig.WithStatus" + }, + "exposeInvitationURLsToTeamAdmin": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + }, + "fileSharing": { + "$ref": "#/definitions/FileSharingConfig.WithStatus" + }, + "legalhold": { + "$ref": "#/definitions/LegalholdConfig.WithStatus" + }, + "mls": { + "$ref": "#/definitions/MLSConfig.WithStatus" + }, + "mlsE2EId": { + "$ref": "#/definitions/MlsE2EIdConfig.WithStatus" + }, + "outlookCalIntegration": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatus" + }, + "searchVisibility": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatus" + }, + "searchVisibilityInbound": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatus" + }, + "selfDeletingMessages": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatus" + }, + "sndFactorPasswordChallenge": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatus" + }, + "sso": { + "$ref": "#/definitions/SSOConfig.WithStatus" + }, + "validateSAMLemails": { + "$ref": "#/definitions/ValidateSAMLEmailsConfig.WithStatus" + } + }, + "required": [ + "legalhold", + "sso", + "searchVisibility", + "searchVisibilityInbound", + "validateSAMLemails", + "digitalSignatures", + "appLock", + "fileSharing", + "classifiedDomains", + "conferenceCalling", + "selfDeletingMessages", + "conversationGuestLinks", + "sndFactorPasswordChallenge", + "mls", + "exposeInvitationURLsToTeamAdmin", + "outlookCalIntegration", + "mlsE2EId" + ], + "type": "object" + }, + "Alpha": { + "enum": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "UYI", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBA", + "XBB", + "XBC", + "XBD", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "XSU", + "XTS", + "XUA", + "XXX", + "YER", + "ZAR", + "ZMW", + "ZWL" + ], + "type": "string" + }, + "AppLockConfig": { + "properties": { + "enforceAppLock": { + "type": "boolean" + }, + "inactivityTimeoutSecs": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforceAppLock", + "inactivityTimeoutSecs" + ], + "type": "object" + }, + "AppLockConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/AppLockConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "AppLockConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/definitions/AppLockConfig" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "ApproveLegalHoldForUserRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "Asset": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "expires": { + "$ref": "#/definitions/UTCTime" + }, + "key": { + "$ref": "#/definitions/AssetKey" + }, + "token": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "key", + "domain" + ], + "type": "object" + }, + "AssetKey": { + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", + "type": "string" + }, + "AssetSize": { + "enum": [ + "preview", + "complete" + ], + "type": "string" + }, + "AssetSource": {}, + "AssetType": { + "enum": [ + "image" + ], + "type": "string" + }, + "AuthnRequest": { + "properties": { + "iD": { + "$ref": "#/definitions/ID" + }, + "issueInstant": { + "$ref": "#/definitions/Time" + }, + "issuer": { + "type": "string" + }, + "nameIDPolicy": { + "$ref": "#/definitions/NameIdPolicy" + } + }, + "required": [ + "iD", + "issueInstant", + "issuer" + ], + "type": "object" + }, + "Base64ByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "BaseProtocol": { + "enum": [ + "proteus", + "mls" + ], + "type": "string" + }, + "BindingNewTeamUser": { + "properties": { + "currency": { + "$ref": "#/definitions/Alpha" + }, + "icon": { + "$ref": "#/definitions/Icon" + }, + "icon_key": { + "description": "team icon asset key", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "members": { + "description": "initial team member ids (between 1 and 127)" + }, + "name": { + "description": "team name", + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "icon" + ], + "type": "object" + }, + "Body": {}, + "BotUserView": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "handle": { + "$ref": "#/definitions/Handle" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "id", + "name", + "accent_id" + ], + "type": "object" + }, + "CheckHandles": { + "properties": { + "handles": { + "items": { + "type": "string" + }, + "maxItems": 50, + "minItems": 1, + "type": "array" + }, + "return": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "handles", + "return" + ], + "type": "object" + }, + "CipherSuiteTag": { + "description": "The cipher suite of the corresponding MLS group", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "ClassifiedDomainsConfig": { + "properties": { + "domains": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "domains" + ], + "type": "object" + }, + "ClassifiedDomainsConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/ClassifiedDomainsConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "Client": { + "properties": { + "capabilities": { + "$ref": "#/definitions/ClientCapabilityList" + }, + "class": { + "$ref": "#/definitions/ClientClass" + }, + "cookie": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/ClientId" + }, + "label": { + "type": "string" + }, + "last_active": { + "$ref": "#/definitions/UTCTime" + }, + "location": { + "$ref": "#/definitions/Location" + }, + "mls_public_keys": { + "$ref": "#/definitions/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "time": { + "$ref": "#/definitions/UTCTime" + }, + "type": { + "$ref": "#/definitions/ClientType" + } + }, + "required": [ + "id", + "type", + "time" + ], + "type": "object" + }, + "ClientCapability": { + "enum": [ + "legalhold-implicit-consent" + ], + "type": "string" + }, + "ClientCapabilityList": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/definitions/ClientCapability" + }, + "type": "array" + } + }, + "required": [ + "capabilities" + ], + "type": "object" + }, + "ClientClass": { + "enum": [ + "phone", + "tablet", + "desktop", + "legalhold" + ], + "type": "string" + }, + "ClientId": { + "type": "string" + }, + "ClientMismatch": { + "properties": { + "deleted": { + "$ref": "#/definitions/UserClients" + }, + "missing": { + "$ref": "#/definitions/UserClients" + }, + "redundant": { + "$ref": "#/definitions/UserClients" + }, + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted" + ], + "type": "object" + }, + "ClientPrekey": { + "properties": { + "client": { + "$ref": "#/definitions/ClientId" + }, + "prekey": { + "$ref": "#/definitions/Prekey" + } + }, + "required": [ + "client", + "prekey" + ], + "type": "object" + }, + "ClientType": { + "enum": [ + "temporary", + "permanent", + "legalhold" + ], + "type": "string" + }, + "CodeChallengeMethod": { + "description": "The method used to encode the code challenge. Only `S256` is supported.", + "enum": [ + "S256" + ], + "type": "string" + }, + "CompletePasswordReset": { + "description": "Data to complete a password reset", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "password": { + "description": "New password (6 - 1024 characters)", + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "required": [ + "code", + "password" + ], + "type": "object" + }, + "ConferenceCallingConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Connect": { + "properties": { + "email": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "qualified_recipient": { + "$ref": "#/definitions/Qualified_UserId" + }, + "recipient": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_recipient" + ], + "type": "object" + }, + "ConnectionUpdate": { + "properties": { + "status": { + "$ref": "#/definitions/Relation" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Connections_Page": { + "properties": { + "connections": { + "items": { + "$ref": "#/definitions/UserConnection" + }, + "type": "array" + }, + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/definitions/Connections_PagingState" + } + }, + "required": [ + "connections", + "has_more", + "paging_state" + ], + "type": "object" + }, + "Connections_PagingState": { + "type": "string" + }, + "Contact": { + "description": "Contact discovered through search", + "properties": { + "accent_id": { + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "type": "string" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_id", + "name" + ], + "type": "object" + }, + "ConvMembers": { + "description": "Users of a conversation", + "properties": { + "others": { + "description": "All other current users of this conversation", + "items": { + "$ref": "#/definitions/OtherMember" + }, + "type": "array" + }, + "self": { + "$ref": "#/definitions/Member" + } + }, + "required": [ + "self", + "others" + ], + "type": "object" + }, + "ConvTeamInfo": { + "description": "Team information of this conversation", + "properties": { + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + }, + "teamid": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "teamid", + "managed" + ], + "type": "object" + }, + "ConvType": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer" + }, + "Conversation": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/definitions/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "creator": { + "$ref": "#/definitions/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "group_id": { + "$ref": "#/definitions/GroupId" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/definitions/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "name": { + "type": "string" + }, + "protocol": { + "$ref": "#/definitions/Protocol" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/definitions/UUID" + }, + "type": { + "$ref": "#/definitions/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "creator", + "access", + "members", + "group_id", + "epoch", + "cipher_suite" + ], + "type": "object" + }, + "ConversationAccessDatav2": { + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/definitions/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access" + ], + "type": "object" + }, + "ConversationAccessDatav3": { + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access", + "access_role" + ], + "type": "object" + }, + "ConversationCode": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "uri": { + "$ref": "#/definitions/HttpsUrl" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "ConversationCodeInfo": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "uri": { + "$ref": "#/definitions/HttpsUrl" + } + }, + "required": [ + "key", + "code", + "has_password" + ], + "type": "object" + }, + "ConversationCoverView": { + "description": "Limited view of Conversation.", + "properties": { + "has_password": { + "type": "boolean" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "has_password" + ], + "type": "object" + }, + "ConversationIds_Page": { + "properties": { + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/definitions/ConversationIds_PagingState" + }, + "qualified_conversations": { + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "qualified_conversations", + "has_more", + "paging_state" + ], + "type": "object" + }, + "ConversationIds_PagingState": { + "type": "string" + }, + "ConversationMessageTimerUpdate": { + "description": "Contains conversation properties to update", + "properties": { + "message_timer": { + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "type": "object" + }, + "ConversationReceiptModeUpdate": { + "description": "Contains conversation receipt mode to update to. Receipt mode tells clients whether certain types of receipts should be sent in the given conversation or not. How this value is interpreted is up to clients.", + "properties": { + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "receipt_mode" + ], + "type": "object" + }, + "ConversationRename": { + "properties": { + "name": { + "description": "The new conversation name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "ConversationRole": { + "properties": { + "actions": { + "description": "The set of actions allowed for this role", + "items": { + "$ref": "#/definitions/Action" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/definitions/RoleName" + } + } + }, + "ConversationRolesList": { + "properties": { + "conversation_roles": { + "items": { + "$ref": "#/definitions/ConversationRole" + }, + "type": "array" + } + }, + "required": [ + "conversation_roles" + ], + "type": "object" + }, + "ConversationsResponse": { + "description": "Response object for getting metadata of a list of conversations", + "properties": { + "failed": { + "description": "The server failed to fetch these conversations, most likely due to network issues while contacting a remote server", + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/definitions/Conversation" + }, + "type": "array" + }, + "not_found": { + "description": "These conversations either don't exist or are deleted.", + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "found", + "not_found", + "failed" + ], + "type": "object" + }, + "Cookie": { + "properties": { + "created": { + "$ref": "#/definitions/UTCTime" + }, + "expires": { + "$ref": "#/definitions/UTCTime" + }, + "id": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "label": { + "type": "string" + }, + "successor": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": { + "$ref": "#/definitions/CookieType" + } + }, + "required": [ + "id", + "type", + "created", + "expires" + ], + "type": "object" + }, + "CookieList": { + "description": "List of cookie information", + "properties": { + "cookies": { + "items": { + "$ref": "#/definitions/Cookie" + }, + "type": "array" + } + }, + "required": [ + "cookies" + ], + "type": "object" + }, + "CookieType": { + "enum": [ + "session", + "persistent" + ], + "type": "string" + }, + "CreateConversationCodeRequest": { + "description": "Request body for creating a conversation code", + "properties": { + "password": { + "description": "Password for accessing the conversation via guest link. Set to null or omit for no password.", + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "type": "object" + }, + "CreateGroupConversation": { + "description": "A created group-conversation object extended with a list of failed-to-add users", + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "creator": { + "$ref": "#/definitions/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "failed_to_add": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "group_id": { + "$ref": "#/definitions/GroupId" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/definitions/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "name": { + "type": "string" + }, + "protocol": { + "$ref": "#/definitions/Protocol" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/definitions/UUID" + }, + "type": { + "$ref": "#/definitions/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "creator", + "access", + "access_role", + "members", + "group_id", + "epoch", + "cipher_suite", + "failed_to_add" + ], + "type": "object" + }, + "CreateOAuthAuthorizationCodeRequest": { + "properties": { + "client_id": { + "$ref": "#/definitions/UUID" + }, + "code_challenge": { + "$ref": "#/definitions/OAuthCodeChallenge" + }, + "code_challenge_method": { + "$ref": "#/definitions/CodeChallengeMethod" + }, + "redirect_uri": { + "$ref": "#/definitions/RedirectUrl" + }, + "response_type": { + "$ref": "#/definitions/OAuthResponseType" + }, + "scope": { + "description": "The scopes which are requested to get authorization for, separated by a space", + "type": "string" + }, + "state": { + "description": "An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery", + "type": "string" + } + }, + "required": [ + "client_id", + "scope", + "response_type", + "redirect_uri", + "state", + "code_challenge_method", + "code_challenge" + ], + "type": "object" + }, + "CreateScimToken": { + "properties": { + "description": { + "type": "string" + }, + "password": { + "type": "string" + }, + "verification_code": { + "type": "string" + } + }, + "required": [ + "description" + ], + "type": "object" + }, + "CreateScimTokenResponse": { + "properties": { + "info": { + "$ref": "#/definitions/ScimTokenInfo" + }, + "token": { + "description": "Authentication token", + "type": "string" + } + }, + "required": [ + "token", + "info" + ], + "type": "object" + }, + "CustomBackend": { + "description": "Description of a custom backend", + "properties": { + "config_json_url": { + "$ref": "#/definitions/HttpsUrl" + }, + "webapp_welcome_url": { + "$ref": "#/definitions/HttpsUrl" + } + }, + "required": [ + "config_json_url", + "webapp_welcome_url" + ], + "type": "object" + }, + "DPoPAccessToken": { + "type": "string" + }, + "DPoPAccessTokenResponse": { + "properties": { + "expires_in": { + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "token": { + "$ref": "#/definitions/DPoPAccessToken" + }, + "type": { + "$ref": "#/definitions/AccessTokenType" + } + }, + "required": [ + "token", + "type", + "expires_in" + ], + "type": "object" + }, + "DeleteClient": { + "properties": { + "password": { + "description": "The password of the authenticated user for verification. The password is not required for deleting temporary clients.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeleteUser": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeletionCodeTimeout": { + "properties": { + "expires_in": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "expires_in" + ], + "type": "object" + }, + "DeprecatedMatchingResult": { + "properties": { + "auto-connects": { + "items": {}, + "type": "array" + }, + "results": { + "items": {}, + "type": "array" + } + }, + "required": [ + "results", + "auto-connects" + ], + "type": "object" + }, + "DigitalSignaturesConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "DisableLegalHoldForUserRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "Domain": { + "example": "example.com", + "type": "string" + }, + "Either": { + "maxProperties": 1, + "minProperties": 1, + "properties": { + "Left": { + "$ref": "#/definitions/OAuthAccessTokenRequest" + }, + "Right": { + "$ref": "#/definitions/OAuthRefreshAccessTokenRequest" + } + }, + "type": "object" + }, + "Email": { + "type": "string" + }, + "EmailUpdate": { + "properties": { + "email": { + "$ref": "#/definitions/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "Event": { + "properties": { + "conversation": { + "$ref": "#/definitions/UUID" + }, + "data": { + "description": "Encrypted message of a conversation", + "example": "ZXhhbXBsZQo=", + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/definitions/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "code": { + "$ref": "#/definitions/ASCII" + }, + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "creator": { + "$ref": "#/definitions/UUID" + }, + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "email": { + "type": "string" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "group_id": { + "$ref": "#/definitions/GroupId" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/definitions/ConvMembers" + }, + "message": { + "type": "string" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "name": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "protocol": { + "$ref": "#/definitions/Protocol" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "qualified_recipient": { + "$ref": "#/definitions/Qualified_UserId" + }, + "qualified_target": { + "$ref": "#/definitions/Qualified_UserId" + }, + "qualified_user_ids": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "recipient": { + "$ref": "#/definitions/ClientId" + }, + "sender": { + "$ref": "#/definitions/ClientId" + }, + "status": { + "$ref": "#/definitions/TypingStatus" + }, + "target": { + "$ref": "#/definitions/UUID" + }, + "team": { + "$ref": "#/definitions/UUID" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + }, + "type": { + "$ref": "#/definitions/ConvType" + }, + "uri": { + "$ref": "#/definitions/HttpsUrl" + }, + "user_ids": { + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/definitions/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users", + "qualified_user_ids", + "user_ids", + "qualified_target", + "name", + "access", + "key", + "code", + "has_password", + "qualified_id", + "type", + "creator", + "members", + "group_id", + "epoch", + "cipher_suite", + "qualified_recipient", + "receipt_mode", + "sender", + "recipient", + "text", + "status" + ], + "type": "object" + }, + "from": { + "$ref": "#/definitions/UUID" + }, + "qualified_conversation": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "qualified_from": { + "$ref": "#/definitions/Qualified_UserId" + }, + "subconv": { + "type": "string" + }, + "time": { + "$ref": "#/definitions/UTCTime" + }, + "type": { + "$ref": "#/definitions/EventType" + } + }, + "required": [ + "type", + "data", + "qualified_conversation", + "qualified_from", + "time" + ], + "type": "object" + }, + "EventType": { + "enum": [ + "conversation.member-join", + "conversation.member-leave", + "conversation.member-update", + "conversation.rename", + "conversation.access-update", + "conversation.receipt-mode-update", + "conversation.message-timer-update", + "conversation.code-update", + "conversation.code-delete", + "conversation.create", + "conversation.delete", + "conversation.connect-request", + "conversation.typing", + "conversation.otr-message-add", + "conversation.mls-message-add", + "conversation.mls-welcome" + ], + "type": "string" + }, + "ExposeInvitationURLsToTeamAdminConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "ExposeInvitationURLsToTeamAdminConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "FeatureStatus": { + "enum": [ + "enabled", + "disabled" + ], + "type": "string" + }, + "FederatedUserSearchPolicy": { + "description": "Search policy that was applied when searching for users", + "enum": [ + "no_search", + "exact_handle_search", + "full_search" + ], + "type": "string" + }, + "FileSharingConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "FileSharingConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Fingerprint": { + "example": "ioy3GeIjgQRsobf2EKGO3O8mq/FofFxHRqy0T4ERIZ8=", + "type": "string" + }, + "FormRedirect": { + "properties": { + "uri": { + "type": "string" + }, + "xml": { + "$ref": "#/definitions/AuthnRequest" + } + }, + "type": "object" + }, + "GetPaginated_Connections": { + "description": "A request to list some or all of a user's Connections, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/definitions/Connections_PagingState" + }, + "size": { + "description": "optional, must be <= 500, defaults to 100.", + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GetPaginated_ConversationIds": { + "description": "A request to list some or all of a user's ConversationIds, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/definitions/ConversationIds_PagingState" + }, + "size": { + "description": "optional, must be <= 1000, defaults to 1000.", + "format": "int32", + "maximum": 1000, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GroupId": { + "description": "An MLS group identifier (at most 256 bytes long)", + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "GuestLinksConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "GuestLinksConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Handle": { + "type": "string" + }, + "HandleUpdate": { + "properties": { + "handle": { + "type": "string" + } + }, + "required": [ + "handle" + ], + "type": "object" + }, + "HttpsUrl": { + "description": "Full URI (containing key/code) to join a conversation", + "example": "https://example.com", + "type": "string" + }, + "ID": { + "properties": { + "iD": { + "$ref": "#/definitions/XmlText" + } + }, + "required": [ + "iD" + ], + "type": "object" + }, + "Icon": { + "type": "string" + }, + "Id": { + "properties": { + "id": { + "$ref": "#/definitions/ClientId" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "IdPConfig": { + "properties": { + "extraInfo": { + "$ref": "#/definitions/WireIdP" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "metadata": { + "$ref": "#/definitions/IdPMetadata" + } + }, + "required": [ + "id", + "metadata", + "extraInfo" + ], + "type": "object" + }, + "IdPList": { + "properties": { + "providers": { + "items": { + "$ref": "#/definitions/IdPConfig" + }, + "type": "array" + } + }, + "required": [ + "providers" + ], + "type": "object" + }, + "IdPMetadata": { + "properties": { + "certAuthnResponse": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "issuer": { + "type": "string" + }, + "requestURI": { + "type": "string" + } + }, + "required": [ + "issuer", + "requestURI", + "certAuthnResponse" + ], + "type": "object" + }, + "IdPMetadataInfo": { + "maxProperties": 1, + "minProperties": 1, + "properties": { + "value": { + "type": "string" + } + }, + "type": "object" + }, + "Invitation": { + "description": "An invitation to join a team on Wire", + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters)", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "role": { + "$ref": "#/definitions/Role" + }, + "team": { + "$ref": "#/definitions/UUID" + }, + "url": { + "$ref": "#/definitions/URIRef Absolute" + } + }, + "required": [ + "team", + "id", + "created_at", + "email" + ], + "type": "object" + }, + "InvitationList": { + "description": "A list of sent team invitations.", + "properties": { + "has_more": { + "description": "Indicator that the server has more invitations than returned.", + "type": "boolean" + }, + "invitations": { + "items": { + "$ref": "#/definitions/Invitation" + }, + "type": "array" + } + }, + "required": [ + "invitations", + "has_more" + ], + "type": "object" + }, + "InvitationRequest": { + "description": "A request to join a team on Wire.", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters).", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "role": { + "$ref": "#/definitions/Role" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "InviteQualified": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "qualified_users": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "JoinConversationByCode": { + "description": "Request body for joining a conversation by code", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "uri": { + "$ref": "#/definitions/HttpsUrl" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "LHServiceStatus": { + "enum": [ + "configured", + "not_configured", + "disabled" + ], + "type": "string" + }, + "LegalholdConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LegalholdConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "LimitedQualifiedUserIdList": { + "properties": { + "qualified_users": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "ListConversations": { + "description": "A request to list some of a user's conversations, including remote ones. Maximum 1000 qualified conversation IDs", + "properties": { + "qualified_ids": { + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_ids" + ], + "type": "object" + }, + "ListType": { + "description": "true if 'members' doesn't contain all team members", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "ListUsersById": { + "properties": { + "failed": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "minItems": 1, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/definitions/UserProfile" + }, + "type": "array" + } + }, + "required": [ + "found" + ], + "type": "object" + }, + "ListUsersQuery": { + "description": "exactly one of qualified_ids or qualified_handles must be provided.", + "example": { + "qualified_ids": [ + { + "domain": "example.com", + "id": "00000000-0000-0000-0000-000000000000" + } + ] + }, + "properties": { + "qualified_handles": { + "items": { + "$ref": "#/definitions/Qualified_Handle" + }, + "type": "array" + }, + "qualified_ids": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + } + }, + "type": "object" + }, + "Locale": { + "type": "string" + }, + "LocaleUpdate": { + "properties": { + "locale": { + "$ref": "#/definitions/Locale" + } + }, + "required": [ + "locale" + ], + "type": "object" + }, + "Location": { + "properties": { + "lat": { + "format": "double", + "type": "number" + }, + "lon": { + "format": "double", + "type": "number" + } + }, + "required": [ + "lat", + "lon" + ], + "type": "object" + }, + "LockStatus": { + "enum": [ + "locked", + "unlocked" + ], + "type": "string" + }, + "Login": { + "properties": { + "code": { + "$ref": "#/definitions/LoginCode" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "handle": { + "$ref": "#/definitions/Handle" + }, + "label": { + "description": "This label can be used to delete all cookies matching it (cf. /cookies/remove)", + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "verification_code": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "password", + "phone", + "code" + ], + "type": "object" + }, + "LoginCode": { + "type": "string" + }, + "LoginCodeTimeout": { + "description": "A response for a successfully sent login code", + "properties": { + "expires_in": { + "description": "Number of seconds before the login code expires", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "expires_in" + ], + "type": "object" + }, + "MLSConfig": { + "properties": { + "allowedCipherSuites": { + "items": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "type": "array" + }, + "defaultCipherSuite": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "defaultProtocol": { + "$ref": "#/definitions/Protocol" + }, + "protocolToggleUsers": { + "description": "allowlist of users that may change protocols", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + } + }, + "required": [ + "protocolToggleUsers", + "defaultProtocol", + "allowedCipherSuites", + "defaultCipherSuite" + ], + "type": "object" + }, + "MLSConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/MLSConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MLSPublicKeys": { + "additionalProperties": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "description": "Mapping from signature scheme (tags) to public key data", + "example": { + "ed25519": "ZXhhbXBsZQo=" + }, + "type": "object" + }, + "ManagedBy": { + "enum": [ + "wire", + "scim" + ], + "type": "string" + }, + "Member": { + "description": "The user ID of the requestor", + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "status": {}, + "status_ref": {}, + "status_time": {} + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "MemberUpdate": { + "properties": { + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "type": "object" + }, + "MemberUpdateData": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_target": { + "$ref": "#/definitions/Qualified_UserId" + }, + "target": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_target" + ], + "type": "object" + }, + "MessageSendingStatus": { + "description": "The Proteus message sending status. It has these fields:\n- `time`: Time of sending message.\n- `missing`: Clients that the message /should/ have been encrypted for, but wasn't.\n- `redundant`: Clients that the message /should not/ have been encrypted for, but was.\n- `deleted`: Clients that were deleted.\n- `failed_to_send`: When message sending fails for some clients but succeeds for others, e.g., because a remote backend is unreachable, this field will contain the list of clients for which the message sending failed. This list should be empty when message sending is not even tried, like when some clients are missing.", + "properties": { + "deleted": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "failed_to_confirm_clients": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "failed_to_send": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "missing": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "redundant": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted", + "failed_to_send", + "failed_to_confirm_clients" + ], + "type": "object" + }, + "MlsE2EIdConfig": { + "properties": { + "acmeDiscoveryUrl": { + "$ref": "#/definitions/HttpsUrl" + }, + "verificationExpiration": { + "description": "When a client first tries to fetch or renew a certificate, they may need to login to an identity provider (IdP) depending on their IdP domain authentication policy. The user may have a grace period during which they can “snooze” this login. The duration of this grace period (in seconds) is set in the `verificationDuration` parameter, which is enforced separately by each client. After the grace period has expired, the client will not allow the user to use the application until they have logged to refresh the certificate. The default value is 1 day (86400s). The client enrolls using the Automatic Certificate Management Environment (ACME) protocol. The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: `https://acme.example.com/acme/provisioner1/discovery`.", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "verificationExpiration" + ], + "type": "object" + }, + "MlsE2EIdConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/MlsE2EIdConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "NameIDFormat": { + "enum": [ + "NameIDFUnspecified", + "NameIDFEmail", + "NameIDFX509", + "NameIDFWindows", + "NameIDFKerberos", + "NameIDFEntity", + "NameIDFPersistent", + "NameIDFTransient" + ], + "type": "string" + }, + "NameIdPolicy": { + "properties": { + "allowCreate": { + "type": "boolean" + }, + "format": { + "$ref": "#/definitions/NameIDFormat" + }, + "spNameQualifier": { + "$ref": "#/definitions/XmlText" + } + }, + "required": [ + "format", + "allowCreate" + ], + "type": "object" + }, + "NewAssetToken": { + "properties": { + "token": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "NewClient": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/definitions/ClientCapability" + }, + "type": "array" + }, + "class": { + "$ref": "#/definitions/ClientClass" + }, + "cookie": { + "description": "The cookie label, i.e. the label used when logging in.", + "type": "string" + }, + "label": { + "type": "string" + }, + "lastkey": { + "$ref": "#/definitions/Prekey" + }, + "mls_public_keys": { + "$ref": "#/definitions/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "password": { + "description": "The password of the authenticated user for verification. Note: Required for registration of the 2nd, 3rd, ... client.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "prekeys": { + "description": "Prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/definitions/Prekey" + }, + "type": "array" + }, + "type": { + "$ref": "#/definitions/ClientType" + }, + "verification_code": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "prekeys", + "lastkey", + "type" + ], + "type": "object" + }, + "NewConv": { + "description": "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'", + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "message_timer": { + "description": "Per-conversation message timer", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "protocol": { + "$ref": "#/definitions/Protocol" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/definitions/ConvTeamInfo" + }, + "users": { + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + } + }, + "type": "object" + }, + "NewLegalHoldService": { + "properties": { + "auth_token": { + "$ref": "#/definitions/ASCII" + }, + "base_url": { + "$ref": "#/definitions/HttpsUrl" + }, + "public_key": { + "$ref": "#/definitions/ServiceKeyPEM" + } + }, + "required": [ + "base_url", + "public_key", + "auth_token" + ], + "type": "object" + }, + "NewPasswordReset": { + "description": "Data to initiate a password reset", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "type": "object" + }, + "NewTeamMember": { + "description": "Required data when creating new team members", + "properties": { + "member": { + "description": "the team member to add (the legalhold_status field must be null or missing!)", + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "permissions": { + "$ref": "#/definitions/Permissions" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + } + }, + "required": [ + "member" + ], + "type": "object" + }, + "NewUser": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "email_code": { + "$ref": "#/definitions/ASCII" + }, + "expires_in": { + "maximum": 604800, + "minimum": 1, + "type": "integer" + }, + "invitation_code": { + "$ref": "#/definitions/ASCII" + }, + "label": { + "type": "string" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "managed_by": { + "$ref": "#/definitions/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "phone_code": { + "$ref": "#/definitions/ASCII" + }, + "picture": { + "$ref": "#/definitions/Pict" + }, + "sso_id": { + "$ref": "#/definitions/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/definitions/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/definitions/BindingNewTeamUser" + }, + "team_code": { + "$ref": "#/definitions/ASCII" + }, + "team_id": { + "$ref": "#/definitions/UUID" + }, + "uuid": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "OAuthAccessTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/definitions/UUID" + }, + "code": { + "$ref": "#/definitions/OAuthAuthorizationCode" + }, + "code_verifier": { + "description": "The code verifier to complete the code challenge", + "maxLength": 128, + "minLength": 43, + "type": "string" + }, + "grant_type": { + "$ref": "#/definitions/OAuthGrantType" + }, + "redirect_uri": { + "$ref": "#/definitions/RedirectUrl" + } + }, + "required": [ + "grant_type", + "client_id", + "code_verifier", + "code", + "redirect_uri" + ], + "type": "object" + }, + "OAuthAccessTokenResponse": { + "properties": { + "access_token": { + "description": "The access token, which has a relatively short lifetime", + "type": "string" + }, + "expires_in": { + "description": "The lifetime of the access token in seconds", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "refresh_token": { + "description": "The refresh token, which has a relatively long lifetime, and can be used to obtain a new access token", + "type": "string" + }, + "token_type": { + "$ref": "#/definitions/OAuthAccessTokenType" + } + }, + "required": [ + "access_token", + "token_type", + "expires_in", + "refresh_token" + ], + "type": "object" + }, + "OAuthAccessTokenType": { + "description": "The type of the access token. Currently only `Bearer` is supported.", + "enum": [ + "Bearer" + ], + "type": "string" + }, + "OAuthApplication": { + "properties": { + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "description": "The OAuth client's name", + "maxLength": 256, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "OAuthAuthorizationCode": { + "description": "The authorization code", + "type": "string" + }, + "OAuthClient": { + "properties": { + "application_name": { + "maxLength": 256, + "minLength": 6, + "type": "string" + }, + "client_id": { + "$ref": "#/definitions/UUID" + }, + "redirect_url": { + "$ref": "#/definitions/RedirectUrl" + } + }, + "required": [ + "client_id", + "application_name", + "redirect_url" + ], + "type": "object" + }, + "OAuthCodeChallenge": { + "description": "Generated by the client from the code verifier (unpadded base64url-encoded SHA256 hash of the code verifier)", + "type": "string" + }, + "OAuthGrantType": { + "description": "Indicates which authorization flow to use. Use `authorization_code` for authorization code flow.", + "enum": [ + "authorization_code", + "refresh_token" + ], + "type": "string" + }, + "OAuthRefreshAccessTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/definitions/UUID" + }, + "grant_type": { + "$ref": "#/definitions/OAuthGrantType" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "grant_type", + "client_id", + "refresh_token" + ], + "type": "object" + }, + "OAuthResponseType": { + "description": "Indicates which authorization flow to use. Use `code` for authorization code flow.", + "enum": [ + "code" + ], + "type": "string" + }, + "OAuthRevokeRefreshTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/definitions/UUID" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "client_id", + "refresh_token" + ], + "type": "object" + }, + "Object": {}, + "OtherMember": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "status": { + "description": "deprecated", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "OtherMemberUpdate": { + "description": "Update user properties of other members relative to a conversation", + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + } + }, + "type": "object" + }, + "OtrMessage": { + "description": "Encrypted message of a conversation", + "properties": { + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "recipient": { + "$ref": "#/definitions/ClientId" + }, + "sender": { + "$ref": "#/definitions/ClientId" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + } + }, + "required": [ + "sender", + "recipient", + "text" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "PagingState": { + "description": "Paging state that should be supplied to retrieve the next page of results", + "type": "string" + }, + "PasswordChange": { + "description": "Data to change a password. The old password is required if a password already exists.", + "properties": { + "new_password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "old_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "new_password" + ], + "type": "object" + }, + "PasswordReset": { + "description": "Data to complete a password reset", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "password": { + "description": "New password (6 - 1024 characters)", + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "required": [ + "code", + "password" + ], + "type": "object" + }, + "Permissions": { + "description": "This is just a complicated way of representing a team role. self and copy always have to contain the same integer, and only the following integers are allowed: 1025 (partner), 1587 (member), 5951 (admin), 8191 (owner). Unit tests of the galley-types package in wire-server contain an authoritative list.", + "properties": { + "copy": { + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "self": { + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "self", + "copy" + ], + "type": "object" + }, + "PhoneNumber": { + "description": "Phone number of the invitee, in the E.164 format", + "type": "string" + }, + "PhoneUpdate": { + "properties": { + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "required": [ + "phone" + ], + "type": "object" + }, + "Pict": { + "items": {}, + "maxItems": 10, + "minItems": 0, + "type": "array" + }, + "Prekey": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "PrekeyBundle": { + "properties": { + "clients": { + "items": { + "$ref": "#/definitions/ClientPrekey" + }, + "type": "array" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "clients" + ], + "type": "object" + }, + "Priority": { + "enum": [ + "low", + "high" + ], + "type": "string" + }, + "PropertyKeysAndValues": { + "type": "object" + }, + "PropertyValue": { + "description": "An arbitrary JSON value for a property" + }, + "Protocol": { + "enum": [ + "proteus", + "mls" + ], + "type": "string" + }, + "PubClient": { + "properties": { + "class": { + "$ref": "#/definitions/ClientClass" + }, + "id": { + "$ref": "#/definitions/ClientId" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "PushToken": { + "description": "Native Push Token", + "properties": { + "app": { + "description": "Application", + "type": "string" + }, + "client": { + "$ref": "#/definitions/ClientId" + }, + "token": { + "description": "Access Token", + "type": "string" + }, + "transport": { + "$ref": "#/definitions/Transport" + } + }, + "required": [ + "transport", + "app", + "token", + "client" + ], + "type": "object" + }, + "PushTokenList": { + "description": "List of Native Push Tokens", + "properties": { + "tokens": { + "description": "Push tokens", + "items": { + "$ref": "#/definitions/PushToken" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "QualifiedNewOtrMessage": { + "description": "This object can only be parsed from Protobuf.\nThe specification for the protobuf types is here: \nhttps://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." + }, + "QualifiedUserClientPrekeyMapV4": { + "properties": { + "failed_to_list": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "qualified_user_client_prekeys": { + "additionalProperties": { + "$ref": "#/definitions/UserClientPrekeyMap" + }, + "type": "object" + } + }, + "required": [ + "qualified_user_client_prekeys" + ], + "type": "object" + }, + "QualifiedUserClients": { + "additionalProperties": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/ClientId" + }, + "type": "array" + }, + "type": "object" + }, + "description": "Map of Domain to UserClients", + "example": { + "domain1.example.com": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + } + }, + "type": "object" + }, + "QualifiedUserIdList": { + "properties": { + "qualified_user_ids": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "user_ids": { + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + } + }, + "required": [ + "qualified_user_ids", + "user_ids" + ], + "type": "object" + }, + "QualifiedUserMap_Set_PubClient": { + "additionalProperties": { + "$ref": "#/definitions/UserMap_Set_PubClient" + }, + "description": "Map of Domain to (UserMap (Set_PubClient)).", + "example": { + "domain1.example.com": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + { + "class": "legalhold", + "id": "d0" + } + ] + } + }, + "type": "object" + }, + "Qualified_ConvId": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "id": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "Qualified_Handle": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "handle": { + "$ref": "#/definitions/Handle" + } + }, + "required": [ + "domain", + "handle" + ], + "type": "object" + }, + "Qualified_UserId": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "id": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "QueuedNotification": { + "description": "A single notification", + "properties": { + "id": { + "$ref": "#/definitions/UUID" + }, + "payload": { + "description": "List of events", + "items": { + "$ref": "#/definitions/Object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "id", + "payload" + ], + "type": "object" + }, + "QueuedNotificationList": { + "description": "Zero or more notifications", + "properties": { + "has_more": { + "description": "Whether there are still more notifications.", + "type": "boolean" + }, + "notifications": { + "description": "Notifications", + "items": { + "$ref": "#/definitions/QueuedNotification" + }, + "type": "array" + }, + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "notifications" + ], + "type": "object" + }, + "RTCConfiguration": { + "description": "A subset of the WebRTC 'RTCConfiguration' dictionary", + "properties": { + "ice_servers": { + "description": "Array of 'RTCIceServer' objects", + "items": { + "$ref": "#/definitions/RTCIceServer" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers": { + "description": "Array of 'SFTServer' objects (optional)", + "items": { + "$ref": "#/definitions/SftServer" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers_all": { + "description": "Array of all SFT servers", + "items": { + "$ref": "#/definitions/SftServer" + }, + "type": "array" + }, + "ttl": { + "description": "Number of seconds after which the configuration should be refreshed (advisory)", + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "ice_servers", + "ttl" + ], + "type": "object" + }, + "RTCIceServer": { + "description": "A subset of the WebRTC 'RTCIceServer' object", + "properties": { + "credential": { + "$ref": "#/definitions/ASCII" + }, + "urls": { + "description": "Array of TURN server addresses of the form 'turn::'", + "items": { + "$ref": "#/definitions/TurnURI" + }, + "minItems": 1, + "type": "array" + }, + "username": { + "$ref": "#/definitions/" + } + }, + "required": [ + "urls", + "username", + "credential" + ], + "type": "object" + }, + "RedirectUrl": { + "description": "The URL must match the URL that was used to generate the authorization code.", + "type": "string" + }, + "Relation": { + "enum": [ + "accepted", + "blocked", + "pending", + "ignored", + "sent", + "cancelled", + "missing-legalhold-consent" + ], + "type": "string" + }, + "RemoveBotResponse": { + "properties": { + "event": { + "$ref": "#/definitions/Event" + } + }, + "required": [ + "event" + ], + "type": "object" + }, + "RemoveCookies": { + "description": "Data required to remove cookies", + "properties": { + "ids": { + "description": "A list of cookie IDs to revoke", + "items": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": "array" + }, + "labels": { + "description": "A list of cookie labels for which to revoke the cookies", + "items": { + "type": "string" + }, + "type": "array" + }, + "password": { + "description": "The user's password", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "RemoveLegalHoldSettingsRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "RichField": { + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "type": "object" + }, + "RichInfoAssocList": { + "description": "json object with case-insensitive fields.", + "properties": { + "fields": { + "items": { + "$ref": "#/definitions/RichField" + }, + "type": "array" + }, + "version": { + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "version", + "fields" + ], + "type": "object" + }, + "Role": { + "description": "Role of the invited user", + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "RoleName": { + "description": "Role name, between 2 and 128 chars, 'wire_' prefix is reserved for roles designed by Wire (i.e., no custom roles can have the same prefix)", + "type": "string" + }, + "SSOConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "ScimTokenInfo": { + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "description": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "idp": { + "$ref": "#/definitions/UUID" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "team", + "id", + "created_at", + "description" + ], + "type": "object" + }, + "ScimTokenList": { + "properties": { + "tokens": { + "items": { + "$ref": "#/definitions/ScimTokenInfo" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "SearchResult": { + "properties": { + "documents": { + "description": "List of contacts found", + "items": { + "$ref": "#/definitions/TeamContact" + }, + "type": "array" + }, + "found": { + "description": "Total number of hits", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "has_more": { + "description": "Indicates whether there are more results to be fetched", + "type": "boolean" + }, + "paging_state": { + "$ref": "#/definitions/PagingState" + }, + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "search_policy": { + "$ref": "#/definitions/FederatedUserSearchPolicy" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "found", + "returned", + "took", + "documents", + "search_policy" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig": { + "properties": { + "enforcedTimeoutSeconds": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforcedTimeoutSeconds" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/SelfDeletingMessagesConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/definitions/SelfDeletingMessagesConfig" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "SendActivationCode": { + "description": "Data for requesting an email or phone activation code to be sent. One of 'email' or 'phone' must be present.", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "voice_call": { + "description": "Request the code with a call instead (default is SMS).", + "type": "boolean" + } + }, + "type": "object" + }, + "SendLoginCode": { + "description": "Payload for requesting a login code to be sent", + "properties": { + "force": { + "type": "boolean" + }, + "phone": { + "description": "E.164 phone number to send the code to", + "type": "string" + }, + "voice_call": { + "description": "Request the code with a call instead (default is SMS)", + "type": "boolean" + } + }, + "required": [ + "phone" + ], + "type": "object" + }, + "SendVerificationCode": { + "properties": { + "action": { + "$ref": "#/definitions/VerificationAction" + }, + "email": { + "$ref": "#/definitions/Email" + } + }, + "required": [ + "action", + "email" + ], + "type": "object" + }, + "ServiceKeyPEM": { + "example": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0\nG06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH\nWvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV\nVPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS\nbUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8\n7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la\nnQIDAQAB\n-----END PUBLIC KEY-----\n", + "type": "string" + }, + "ServiceRef": { + "properties": { + "id": { + "$ref": "#/definitions/UUID" + }, + "provider": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "id", + "provider" + ], + "type": "object" + }, + "SftServer": { + "description": "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers", + "properties": { + "urls": { + "description": "Array containing exactly one SFT server address of the form 'https://:'", + "items": { + "$ref": "#/definitions/HttpsUrl" + }, + "type": "array" + } + }, + "required": [ + "urls" + ], + "type": "object" + }, + "SimpleMember": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "SimpleMembers": { + "properties": { + "user_ids": { + "description": "deprecated", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/definitions/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Sso": { + "properties": { + "issuer": { + "type": "string" + }, + "nameid": { + "type": "string" + } + }, + "required": [ + "issuer", + "nameid" + ], + "type": "object" + }, + "SsoSettings": { + "properties": { + "default_sso_code": { + "$ref": "#/definitions/UUID" + } + }, + "type": "object" + }, + "SystemSettings": { + "properties": { + "setEnableMls": { + "description": "Whether MLS is enabled or not", + "type": "boolean" + }, + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation", + "setEnableMls" + ], + "type": "object" + }, + "SystemSettingsPublic": { + "properties": { + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation" + ], + "type": "object" + }, + "Team": { + "description": "`binding` is deprecated, and should be ignored. The non-binding teams API is not used (and will not be supported from API version V4 onwards), and `binding` will always be `true`.", + "properties": { + "binding": { + "$ref": "#/definitions/TeamBinding" + }, + "creator": { + "$ref": "#/definitions/UUID" + }, + "icon": { + "$ref": "#/definitions/Icon" + }, + "icon_key": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "type": "string" + }, + "splash_screen": { + "$ref": "#/definitions/Icon" + } + }, + "required": [ + "id", + "creator", + "name", + "icon" + ], + "type": "object" + }, + "TeamBinding": { + "description": "Deprecated, please ignore.", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "TeamContact": { + "properties": { + "accent_id": { + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "email_unvalidated": { + "$ref": "#/definitions/Email" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "managed_by": { + "$ref": "#/definitions/ManagedBy" + }, + "name": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/Role" + }, + "saml_idp": { + "type": "string" + }, + "scim_external_id": { + "type": "string" + }, + "sso": { + "$ref": "#/definitions/Sso" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "TeamConversation": { + "description": "Team conversation data", + "properties": { + "conversation": { + "$ref": "#/definitions/UUID" + }, + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + } + }, + "required": [ + "conversation", + "managed" + ], + "type": "object" + }, + "TeamConversationList": { + "description": "Team conversation list", + "properties": { + "conversations": { + "items": { + "$ref": "#/definitions/TeamConversation" + }, + "type": "array" + } + }, + "required": [ + "conversations" + ], + "type": "object" + }, + "TeamDeleteData": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/definitions/ASCII" + } + }, + "type": "object" + }, + "TeamMember": { + "description": "team member data", + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "legalhold_status": { + "$ref": "#/definitions/UserLegalHoldStatus" + }, + "permissions": { + "$ref": "#/definitions/Permissions" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user" + ], + "type": "object" + }, + "TeamMemberDeleteData": { + "description": "Data for a team member deletion request in case of binding teams.", + "properties": { + "password": { + "description": "The account password to authorise the deletion.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "TeamMemberList": { + "description": "list of team member", + "properties": { + "hasMore": { + "$ref": "#/definitions/ListType" + }, + "members": { + "description": "the array of team members", + "items": { + "$ref": "#/definitions/TeamMember" + }, + "type": "array" + } + }, + "required": [ + "members", + "hasMore" + ], + "type": "object" + }, + "TeamMembersPage": { + "properties": { + "hasMore": { + "type": "boolean" + }, + "members": { + "items": { + "$ref": "#/definitions/TeamMember" + }, + "type": "array" + }, + "pagingState": { + "$ref": "#/definitions/TeamMembers_PagingState" + } + }, + "required": [ + "members", + "hasMore", + "pagingState" + ], + "type": "object" + }, + "TeamMembers_PagingState": { + "type": "string" + }, + "TeamSearchVisibility": { + "description": "value of visibility", + "enum": [ + "standard", + "no-name-outside-team" + ], + "type": "string" + }, + "TeamSearchVisibilityView": { + "description": "Search visibility value for the team", + "properties": { + "search_visibility": { + "$ref": "#/definitions/TeamSearchVisibility" + } + }, + "required": [ + "search_visibility" + ], + "type": "object" + }, + "TeamSize": { + "description": "A simple object with a total number of team members.", + "properties": { + "teamSize": { + "description": "Team size.", + "exclusiveMinimum": false, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "teamSize" + ], + "type": "object" + }, + "TeamUpdateData": { + "properties": { + "icon": { + "$ref": "#/definitions/Icon" + }, + "icon_key": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "splash_screen": { + "$ref": "#/definitions/Icon" + } + }, + "type": "object" + }, + "Time": { + "properties": { + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "TokenType": { + "enum": [ + "Bearer" + ], + "type": "string" + }, + "Transport": { + "description": "Transport", + "enum": [ + "GCM", + "APNS", + "APNS_SANDBOX", + "APNS_VOIP", + "APNS_VOIP_SANDBOX" + ], + "type": "string" + }, + "TurnURI": { + "type": "string" + }, + "TypingData": { + "properties": { + "status": { + "$ref": "#/definitions/TypingStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "TypingStatus": { + "enum": [ + "started", + "stopped" + ], + "type": "string" + }, + "URIRef Absolute": { + "description": "URL of the invitation link to be sent to the invitee", + "type": "string" + }, + "UTCTime": { + "example": "2021-05-12T10:52:02.671Z", + "format": "yyyy-mm-ddThh:MM:ss.qqq", + "type": "string" + }, + "UUID": { + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "Unnamed": { + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "permissions": { + "$ref": "#/definitions/Permissions" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + }, + "UpdateBotPrekeys": { + "properties": { + "prekeys": { + "items": { + "$ref": "#/definitions/Prekey" + }, + "type": "array" + } + }, + "required": [ + "prekeys" + ], + "type": "object" + }, + "UpdateClient": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/definitions/ClientCapability" + }, + "type": "array" + }, + "label": { + "description": "A new name for this client.", + "type": "string" + }, + "lastkey": { + "$ref": "#/definitions/Prekey" + }, + "mls_public_keys": { + "$ref": "#/definitions/MLSPublicKeys" + }, + "prekeys": { + "description": "New prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/definitions/Prekey" + }, + "type": "array" + } + }, + "type": "object" + }, + "User": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "expires_at": { + "$ref": "#/definitions/UTCTime" + }, + "handle": { + "$ref": "#/definitions/Handle" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "managed_by": { + "$ref": "#/definitions/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "picture": { + "$ref": "#/definitions/Pict" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "sso_id": { + "$ref": "#/definitions/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/definitions/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "id", + "qualified_id", + "name", + "accent_id", + "locale" + ], + "type": "object" + }, + "UserAsset": { + "properties": { + "key": { + "$ref": "#/definitions/AssetKey" + }, + "size": { + "$ref": "#/definitions/AssetSize" + }, + "type": { + "$ref": "#/definitions/AssetType" + } + }, + "required": [ + "key", + "type" + ], + "type": "object" + }, + "UserClientMap": { + "additionalProperties": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "object" + }, + "UserClientPrekeyMap": { + "additionalProperties": { + "additionalProperties": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "type": "object" + }, + "example": { + "000600d0-000b-9c1a-000d-a4130002c221": { + "44901fb0712e588f": { + "id": 1, + "key": "pQABAQECoQBYIOjl7hw0D8YRNq..." + } + } + }, + "type": "object" + }, + "UserClients": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/ClientId" + }, + "type": "array" + }, + "description": "Map of user id to list of client ids.", + "example": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + }, + "type": "object" + }, + "UserConnection": { + "properties": { + "conversation": { + "$ref": "#/definitions/UUID" + }, + "from": { + "$ref": "#/definitions/UUID" + }, + "last_update": { + "$ref": "#/definitions/UTCTime" + }, + "qualified_conversation": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "qualified_to": { + "$ref": "#/definitions/Qualified_UserId" + }, + "status": { + "$ref": "#/definitions/Relation" + }, + "to": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "from", + "qualified_to", + "status", + "last_update" + ], + "type": "object" + }, + "UserIdList": { + "properties": { + "user_ids": { + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + } + }, + "required": [ + "user_ids" + ], + "type": "object" + }, + "UserLegalHoldStatus": { + "description": "states whether a user is under legal hold, or whether legal hold is pending approval.", + "enum": [ + "enabled", + "pending", + "disabled", + "no_consent" + ], + "type": "string" + }, + "UserLegalHoldStatusResponse": { + "properties": { + "client": { + "$ref": "#/definitions/Id" + }, + "last_prekey": { + "$ref": "#/definitions/Prekey" + }, + "status": { + "$ref": "#/definitions/UserLegalHoldStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "UserMap_Set_PubClient": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/PubClient" + }, + "type": "array", + "uniqueItems": true + }, + "description": "Map of UserId to (Set PubClient)", + "example": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + { + "class": "legalhold", + "id": "d0" + } + ] + }, + "type": "object" + }, + "UserProfile": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "expires_at": { + "$ref": "#/definitions/UTCTime" + }, + "handle": { + "$ref": "#/definitions/Handle" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "legalhold_status": { + "$ref": "#/definitions/UserLegalHoldStatus" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/definitions/Pict" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "supported_protocols": { + "items": { + "$ref": "#/definitions/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "legalhold_status" + ], + "type": "object" + }, + "UserSSOId": { + "properties": { + "scim_external_id": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "tenant": { + "type": "string" + } + }, + "type": "object" + }, + "UserUpdate": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/definitions/Pict" + } + }, + "type": "object" + }, + "ValidateSAMLEmailsConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "VerificationAction": { + "enum": [ + "create_scim_token", + "login", + "delete_team" + ], + "type": "string" + }, + "VerifyDeleteUser": { + "description": "Data for verifying an account deletion.", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "key": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "VersionInfo": { + "example": { + "development": [ + 5 + ], + "domain": "example.com", + "federation": false, + "supported": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + }, + "properties": { + "development": { + "items": { + "$ref": "#/definitions/VersionNumber" + }, + "type": "array" + }, + "domain": { + "$ref": "#/definitions/Domain" + }, + "federation": { + "type": "boolean" + }, + "supported": { + "items": { + "$ref": "#/definitions/VersionNumber" + }, + "type": "array" + } + }, + "required": [ + "supported", + "development", + "federation", + "domain" + ], + "type": "object" + }, + "VersionNumber": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer" + }, + "ViewLegalHoldService": { + "properties": { + "settings": { + "$ref": "#/definitions/ViewLegalHoldServiceInfo" + }, + "status": { + "$ref": "#/definitions/LHServiceStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ViewLegalHoldServiceInfo": { + "properties": { + "auth_token": { + "$ref": "#/definitions/ASCII" + }, + "base_url": { + "$ref": "#/definitions/HttpsUrl" + }, + "fingerprint": { + "$ref": "#/definitions/Fingerprint" + }, + "public_key": { + "$ref": "#/definitions/ServiceKeyPEM" + }, + "team_id": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "team_id", + "base_url", + "fingerprint", + "auth_token", + "public_key" + ], + "type": "object" + }, + "WireIdP": { + "properties": { + "apiVersion": { + "$ref": "#/definitions/WireIdPAPIVersion" + }, + "handle": { + "type": "string" + }, + "oldIssuers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "replacedBy": { + "$ref": "#/definitions/UUID" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "team", + "oldIssuers", + "handle" + ], + "type": "object" + }, + "WireIdPAPIVersion": { + "enum": [ + "WireIdPAPIV1", + "WireIdPAPIV2" + ], + "type": "string" + }, + "XmlText": { + "properties": { + "fromXmlText": { + "type": "string" + } + }, + "required": [ + "fromXmlText" + ], + "type": "object" + }, + "new-otr-message": { + "properties": { + "data": { + "type": "string" + }, + "native_priority": { + "$ref": "#/definitions/Priority" + }, + "native_push": { + "type": "boolean" + }, + "recipients": { + "$ref": "#/definitions/UserClientMap" + }, + "report_missing": { + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + }, + "sender": { + "$ref": "#/definitions/ClientId" + }, + "transient": { + "type": "boolean" + } + }, + "required": [ + "sender", + "recipients" + ], + "type": "object" + } + }, + "info": { + "description": "## Authentication / Authorization\n\nThe end-points in this API support differing authorization protocols:\nsome are unauthenticated (`/api-version`, `/login`), some require\n[zauth](), and some support both [zauth]() and [oauth]().\n\nThe end-points that require zauth are labelled so in the description\nbelow. The end-points that support oauth as an alternative to zauth\nhave the required oauth scopes listed in the same description.\n\nFuther reading:\n- https://docs.wire.com/developer/reference/oauth.html\n- https://github.com/wireapp/wire-server/blob/develop/libs/wire-api/src/Wire/API/Routes/Public.hs (search for HasSwagger instances)\n- `curl https://staging-nginz-https.zinfra.io/v4/api/swagger.json | jq '.security, .securityDefinitions`\n\n### SSO Endpoints\n\n#### Overview\n\n`/sso/metadata` will be requested by the IdPs to learn how to talk to wire.\n\n`/sso/initiate-login`, `/sso/finalize-login` are for the SAML authentication handshake performed by a user in order to log into wire. They are not exactly standard in their details: they may return HTML or XML; redirect to error URLs instead of throwing errors, etc.\n\n`/identity-providers` end-points are for use in the team settings page when IdPs are registered. They talk json.\n\n\n#### Configuring IdPs\n\nIdPs usually allow you to copy the metadata into your clipboard. That should contain all the details you need to post the idp in your team under `/identity-providers`. (Team id is derived from the authorization credentials of the request.)\n\n##### okta.com\n\nOkta will ask you to provide two URLs when you set it up for talking to wireapp:\n\n1. The `Single sign on URL`. This is the end-point that accepts the user's credentials after successful authentication against the IdP. Choose `/sso/finalize-login` with schema and hostname of the wire server you are configuring.\n\n2. The `Audience URI`. You can find this in the metadata returned by the `/sso/metadata` end-point. It is the contents of the `md:OrganizationURL` element.\n\n##### centrify.com\n\nCentrify allows you to upload the metadata xml document that you get from the `/sso/metadata` end-point. You can also enter the metadata url and have centrify retrieve the xml, but to guarantee integrity of the setup, the metadata should be copied from the team settings page and pasted into the centrify setup page without any URL indirections.\n\n## Federation errors\n\nEndpoints involving federated calls to other domains can return some extra failure responses, common to all endpoints. Instead of listing them as possible responses for each endpoint, we document them here.\n\nFor errors that are more likely to be transient, we suggest clients to retry whatever request resulted in the error. Transient errors are indicated explicitly below.\n\n**Note**: when a failure occurs as a result of making a federated RPC to another backend, the error response contains the following extra fields:\n\n - `type`: \"federation\" (just the literal string in quotes, which can be used as an error type identifier when parsing errors)\n - `domain`: the target backend of the RPC that failed;\n - `path`: the path of the RPC that failed.\n\n### Domain errors\n\nErrors in this category result from trying to communicate with a backend that is considered non-existent or invalid. They can result from invalid user input or client issues, but they can also be a symptom of misconfiguration in one or multiple backends. These errors have a 4xx status code.\n\n - **Remote backend not found** (status: 422, label: `invalid-domain`): This backend attempted to contact a backend which does not exist or is not properly configured. For the most part, clients can consider this error equivalent to a domain not existing, although it should be noted that certain mistakes in the DNS configuration on a remote backend can lead to the backend not being recognized, and hence to this error. It is therefore not advisable to take any destructive action upon encountering this error, such as deleting remote users from conversations.\n - **Federation denied locally** (status: 400, label: `federation-denied`): This backend attempted an RPC to a non-whitelisted backend. Similar considerations as for the previous error apply.\n - **Federation not enabled** (status: 400, label: `federation-not-enabled`): Federation has not been configured for this backend. This will happen if a federation-aware client tries to talk to a backend for which federation is disabled, or if federation was disabled on the backend after reaching a federation-specific state (e.g. conversations with remote users). There is no way to cleanly recover from these errors at this point.\n\n### Local federation errors\n\nAn error in this category likely indicates an issue with the configuration of federation on the local backend. Possibly transient errors are indicated explicitly below. All these errors have a 500 status code.\n\n - **Federation unavailable** (status: 500, label: `federation-not-available`): Federation is configured for this backend, but the local federator cannot be reached. This can be transient, so clients should retry the request.\n - **Federation not implemented** (status: 500, label: `federation-not-implemented`): Federated behaviour for a certain endpoint is not yet implemented.\n - **Federator discovery failed** (status: 400, label: `discovery-failure`): A DNS error occurred during discovery of a remote backend. This can be transient, so clients should retry the request.\n - **Local federation error** (status: 500, label: `federation-local-error`): An error occurred in the communication between this backend and its local federator. These errors are most likely caused by bugs in the backend, and should be reported as such.\n\n### Remote federation errors\n\nErrors in this category are returned in case of communication issues between the local backend and a remote one, or if the remote side encountered an error while processing an RPC. Some errors in this category might be caused by incorrect client behaviour, wrong user input, or incorrect certificate configuration. Possibly transient errors are indicated explicitly. We use non-standard 5xx status codes for these errors.\n\n - **HTTP2 error** (status: 533, label: `federation-http2-error`): The current federator encountered an error when making an HTTP2 request to a remote one. Check the error message for more details.\n - **Connection refused** (status: 521, label: `federation-connection-refused`): The local federator could not connect to a remote one. This could be transient, so clients should retry the request.\n - **TLS failure**: (status: 525, label: `federation-tls-error`): An error occurred during the TLS handshake between the local federator and a remote one. This is most likely due to an issue with the certificate on the remote end.\n - **Remote federation error** (status: 533, label: `federation-remote-error`): The remote backend could not process a request coming from this backend. Check the error message for more details.\n - **Version negotiation error** (status: 533, label: `federation-version-error`): The remote backend returned invalid version information.\n\n### Backend compatibility errors\n\nAn error in this category will be returned when this backend makes an invalid or unsupported RPC to another backend. This can indicate some incompatibility between backends or a backend bug. These errors are unlikely to be transient, so retrying requests is *not* advised.\n\n - **Version mismatch** (status: 531, label: `federation-version-mismatch`): A remote backend is running an unsupported version of the federator.\n - **Invalid content type** (status: 533, label: `federation-invalid-content-type`): An RPC to another backend returned with an invalid content type.\n - **Unsupported content type** (status: 533, label: `federation-unsupported-content-type`): An RPC to another backend returned with an unsupported content type.\n", + "title": "Wire-Server API", + "version": "" + }, + "paths": { + "/access": { + "post": { + "description": " [internal route ID: \"access\"]\n\nYou can provide only a cookie or a cookie and token. Every other combination is invalid. Access tokens can be given as query parameter or authorisation header, with the latter being preferred.", + "parameters": [ + { + "in": "query", + "name": "client_id", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "Set-Cookie": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/AccessToken" + } + }, + "400": { + "description": "Invalid `client_id`" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Obtain an access tokens for a cookie" + } + }, + "/access/logout": { + "post": { + "description": " [internal route ID: \"logout\"]\n\nCalling this endpoint will effectively revoke the given cookie and subsequent calls to /access with the same cookie will result in a 403.", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Logout" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Log out in order to remove a cookie from the server" + } + }, + "/access/self/email": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"change-self-email\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/EmailUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "202": { + "description": "Update accepted and pending activation of the new email", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "204": { + "description": "No update, current and new email address are the same", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "400": { + "description": "Invalid e-mail address. (label: `invalid-email`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "blacklisted-phone", + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your email address" + } + }, + "/activate": { + "get": { + "description": " [internal route ID: \"get-activate\"]\n\nSee also 'POST /activate' which has a larger feature set.", + "parameters": [ + { + "description": "Activation key", + "in": "query", + "name": "key", + "required": true, + "type": "string" + }, + { + "description": "Activation code", + "in": "query", + "name": "code", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful.", + "schema": { + "$ref": "#/definitions/ActivationResponse" + } + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "description": "Invalid `code` or `key`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)", + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Activate (i.e. confirm) an email address or phone number." + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-activate\"]\n\nActivation only succeeds once and the number of failed attempts for a valid key is limited.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Activate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful.", + "schema": { + "$ref": "#/definitions/ActivationResponse" + } + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)", + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Activate (i.e. confirm) an email address or phone number." + } + }, + "/activate/send": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-activate-send\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SendActivationCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Activation code sent." + }, + "400": { + "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "description": "The given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)", + "schema": { + "example": { + "code": 403, + "label": "blacklisted-phone", + "message": "The given phone number has been blacklisted due to suspected abuse or a complaint" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "blacklisted-phone", + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "451": { + "description": "[Customer extension] the email domain example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department. (label: `domain-blocked-for-registration`)", + "schema": { + "example": { + "code": 451, + "label": "domain-blocked-for-registration", + "message": "[Customer extension] the email domain example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department." + }, + "properties": { + "code": { + "enum": [ + 451 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-blocked-for-registration" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Send (or resend) an email or phone activation code." + } + }, + "/api-version": { + "get": { + "description": " [internal route ID: \"get-version\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/VersionInfo" + } + } + } + } + }, + "/assets": { + "post": { + "consumes": [ + "multipart/mixed" + ], + "parameters": [ + { + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server.", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AssetSource" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Asset" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "description": "Asset too large (label: `client-error`)", + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Upload an asset" + } + }, + "/assets/{key_domain}/{key}": { + "delete": { + "description": "**Note**: only local assets can be deleted.", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "400": { + "description": "Invalid `key` or `key_domain`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": "**Note**: local assets result in a redirect, while remote assets are streamed directly.", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Asset returned directly with content type `application/octet-stream`" + }, + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + } + }, + "400": { + "description": "Invalid `asset_token` or `Asset-Token` or `key` or `key_domain`" + }, + "404": { + "description": "Asset not found (label: `not-found`)\n\nAsset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Download an asset", + "x-wire-makes-federated-call-to": [ + [ + "cargohold", + "get-asset" + ], + [ + "cargohold", + "stream-asset" + ] + ] + } + }, + "/assets/{key}/token": { + "delete": { + "description": "**Note**: deleting the token makes the asset public.", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset token deleted" + }, + "400": { + "description": "Invalid `key`" + } + }, + "summary": "Delete an asset token" + }, + "post": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/NewAssetToken" + } + }, + "400": { + "description": "Invalid `key`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Renew an asset token" + } + }, + "/await": { + "get": { + "description": " [internal route ID: \"await-notifications\"]\n\n", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "type": "string" + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "400": { + "description": "Invalid `client`" + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Establish websocket connection" + } + }, + "/bot/assets": { + "post": { + "consumes": [ + "multipart/mixed" + ], + "parameters": [ + { + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server.", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AssetSource" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Asset" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "description": "Asset too large (label: `client-error`)", + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Upload an asset" + } + }, + "/bot/assets/{key}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "400": { + "description": "Invalid `key`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete an asset" + }, + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + } + }, + "400": { + "description": "Invalid `asset_token` or `Asset-Token` or `key`" + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Download an asset" + } + }, + "/bot/client": { + "get": { + "description": " [internal route ID: \"bot-get-client\"]\n\n", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Client found", + "schema": { + "$ref": "#/definitions/Client" + } + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Client not found (label: `client-not-found`)\n\nClient not found (label: `client-not-found`)", + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get client for bot" + } + }, + "/bot/client/prekeys": { + "get": { + "description": " [internal route ID: \"bot-list-prekeys\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "List prekeys for bot" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"bot-update-prekeys\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateBotPrekeys" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Client not found (label: `client-not-found`)", + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update prekeys for bot" + } + }, + "/bot/messages": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-bot-message-unqualified\"]\n\n", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/new-otr-message" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + }, + "400": { + "description": "Invalid `body` or `report_missing` or `ignore_missing`" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + } + }, + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-message-sent" + ], + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/bot/self": { + "delete": { + "description": " [internal route ID: \"bot-delete-self\"]\n\n", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "" + }, + "403": { + "description": "The targeted user is not a bot. (label: `invalid-bot`)\n\nAccess denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-bot", + "message": "The targeted user is not a bot." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-bot", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete self" + }, + "get": { + "description": " [internal route ID: \"bot-get-self\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/UserProfile" + } + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "User not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get self" + } + }, + "/bot/users": { + "get": { + "description": " [internal route ID: \"bot-list-users\"]\n\n", + "parameters": [ + { + "in": "query", + "name": "ids", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "$ref": "#/definitions/BotUserView" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `ids`" + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "List users" + } + }, + "/bot/users/prekeys": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"bot-claim-users-prekeys\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UserClients" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/UserClientPrekeyMap" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nToo many clients (label: `too-many-clients`)\n\nAccess denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "too-many-clients", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Claim users prekeys" + } + }, + "/bot/users/{User ID}/clients": { + "get": { + "description": " [internal route ID: \"bot-get-user-clients\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "User ID", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "$ref": "#/definitions/PubClient" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `User ID`" + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get user clients" + } + }, + "/broadcast/otr/messages": { + "post": { + "consumes": [ + "application/json;charset=utf-8", + "application/x-protobuf" + ], + "description": " [internal route ID: \"post-otr-broadcast-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/new-otr-message" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + }, + "400": { + "description": "Invalid `body` or `report_missing` or `ignore_missing`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + } + }, + "summary": "Broadcast an encrypted message to all team members and all contacts (accepts JSON or Protobuf)" + } + }, + "/broadcast/proteus/messages": { + "post": { + "consumes": [ + "application/x-protobuf" + ], + "description": " [internal route ID: \"post-proteus-broadcast\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `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\nThe 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, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QualifiedNewOtrMessage" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + }, + "400": { + "description": "Invalid `body`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + } + }, + "summary": "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" + } + }, + "/calls/config": { + "get": { + "description": " [internal route ID: \"get-calls-config\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RTCConfiguration" + } + } + }, + "summary": "[deprecated] Retrieve TURN server addresses and credentials for IP addresses, scheme `turn` and transport `udp` only" + } + }, + "/calls/config/v2": { + "get": { + "description": " [internal route ID: \"get-calls-config-v2\"]\n\n", + "parameters": [ + { + "description": "Limit resulting list. Allowed values [1..10]", + "in": "query", + "maximum": 10, + "minimum": 1, + "name": "limit", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RTCConfiguration" + } + }, + "400": { + "description": "Invalid `limit`" + } + }, + "summary": "Retrieve all TURN server addresses and credentials. Clients are expected to do a DNS lookup to resolve the IP addresses of the given hostnames " + } + }, + "/clients": { + "get": { + "description": " [internal route ID: \"list-clients\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "$ref": "#/definitions/Client" + }, + "type": "array" + } + } + }, + "summary": "List the registered clients" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"add-client\"]\n\n", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewClient" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "201": { + "description": "", + "headers": { + "Location": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Client" + } + }, + "400": { + "description": "Invalid `body` or `X-Forwarded-For`\n\nMalformed prekeys uploaded (label: `bad-request`)" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nToo many clients (label: `too-many-clients`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "missing-auth", + "too-many-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Register a new client" + } + }, + "/clients/{cid}/access-token": { + "post": { + "description": " [internal route ID: \"create-access-token\"]\n\nCreate an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. The access token will be returned as JWT DPoP token in the `DPoP` header.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "cid", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "DPoP", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Access token created", + "headers": { + "Cache-Control": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/DPoPAccessTokenResponse" + } + }, + "400": { + "description": "Invalid `DPoP` or `cid`" + } + }, + "summary": "Create a JWT DPoP access token" + } + }, + "/clients/{client}": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"delete-client\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteClient" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Client deleted" + }, + "400": { + "description": "Invalid `body` or `client`" + } + }, + "summary": "Delete an existing client" + }, + "get": { + "description": " [internal route ID: \"get-client\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Client found", + "schema": { + "$ref": "#/definitions/Client" + } + }, + "400": { + "description": "Invalid `client`" + }, + "404": { + "description": "Client not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a registered client by ID" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-client\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateClient" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Client updated" + }, + "400": { + "description": "Invalid `body` or `client`\n\nMalformed prekeys uploaded (label: `bad-request`)" + } + }, + "summary": "Update a registered client" + } + }, + "/clients/{client}/capabilities": { + "get": { + "description": " [internal route ID: \"get-client-capabilities\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ClientCapabilityList" + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "Read back what the client has been posting about itself" + } + }, + "/clients/{client}/nonce": { + "get": { + "description": " [internal route ID: \"get-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "No Content", + "headers": { + "Cache-Control": { + "type": "string" + }, + "Replay-Nonce": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "Get a new nonce for a client CSR" + }, + "head": { + "description": " [internal route ID: \"head-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "No Content", + "headers": { + "Cache-Control": { + "type": "string" + }, + "Replay-Nonce": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "Get a new nonce for a client CSR" + } + }, + "/clients/{client}/prekeys": { + "get": { + "description": " [internal route ID: \"get-client-prekeys\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "List the remaining prekey IDs of a client" + } + }, + "/connections/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-connection\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Connection found", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + }, + "404": { + "description": "Connection not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get an existing connection to another user (local or remote)" + }, + "post": { + "description": " [internal route ID: \"create-connection\"]\n\nYou can have no more than 1000 connections in accepted or sent state", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Connection existed", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "201": { + "description": "Connection was created", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "description": "The user has no verified identity (email or phone number) (label: `no-identity`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified identity (email or phone number)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "connection-limit", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create a connection to another user", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "send-connection-action" + ] + ] + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-connection\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConnectionUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Connection updated", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "204": { + "description": "Connection unchanged" + }, + "400": { + "description": "Invalid `body` or `uid` or `uid_domain`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "description": "The user has no verified identity (email or phone number) (label: `no-identity`)\n\nInvalid status transition (label: `bad-conn-update`)\n\nUsers are not connected (label: `not-connected`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified identity (email or phone number)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "bad-conn-update", + "not-connected", + "connection-limit", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update a connection to another user", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "send-connection-action" + ] + ] + } + }, + "/conversations": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"create-group-conversation\"]\n\nThis returns 201 when a new conversation is created, and 200 when the conversation already existed", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewConv" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "201": { + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/CreateGroupConversation" + } + }, + "400": { + "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nAttempting to add group members outside MLS (label: `non-empty-member-list`)" + }, + "403": { + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nUsers are not connected (label: `not-connected`)\n\nThe client has to refresh their access token and provide their client ID (label: `mls-missing-sender-client`)\n\nConversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "operation-denied", + "no-team-member", + "not-connected", + "mls-missing-sender-client", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph", + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + }, + "533": { + "description": "Some domains are unreachable", + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "summary": "Create a new conversation", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "api-version" + ], + [ + "brig", + "get-not-fully-connected-backends" + ], + [ + "galley", + "on-conversation-created" + ], + [ + "galley", + "on-conversation-updated" + ] + ] + } + }, + "/conversations/code-check": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"code-check\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Valid" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Invalid conversation password (label: `invalid-conversation-password`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation-password", + "message": "Invalid conversation password" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Check validity of a conversation code.If the guest links team feature is disabled, this will fail with 404 CodeNotFound.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/join` which responds with 409 GuestLinksDisabled if guest links are disabled." + } + }, + "/conversations/join": { + "get": { + "description": " [internal route ID: \"get-conversation-by-reusable-code\"]\n\n", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "query", + "name": "code", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationCoverView" + } + }, + "400": { + "description": "Invalid `code` or `key`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get limited conversation information by key/code pair" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"join-conversation-by-code-unqualified\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/JoinConversationByCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation joined", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Maximum number of members per conversation reached (label: `too-many-members`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)", + "schema": { + "example": { + "code": 403, + "label": "too-many-members", + "message": "Maximum number of members per conversation reached" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-members", + "no-team-member", + "invalid-op", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Join a conversation using a reusable code.If the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled.", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ] + ] + } + }, + "/conversations/list": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"list-conversations\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ListConversations" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationsResponse" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Get conversation metadata for a list of conversation ids", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "get-conversations" + ] + ] + } + }, + "/conversations/list-ids": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"list-conversation-ids\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GetPaginated_ConversationIds" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationIds_Page" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Get all conversation IDs." + } + }, + "/conversations/one2one": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"create-one-to-one-conversation\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewConv" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "201": { + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nBoth users must be members of the same binding team (label: `non-binding-team-members`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "operation-denied", + "not-connected", + "no-team-member", + "non-binding-team-members", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)\n\nNot a member of a binding team (label: `non-binding-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "non-binding-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "533": { + "description": "Some domains are unreachable", + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "summary": "Create a 1:1 conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-created" + ] + ] + } + }, + "/conversations/self": { + "post": { + "description": " [internal route ID: \"create-self-conversation\"]\n\n", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "201": { + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + } + }, + "summary": "Create a self-conversation" + } + }, + "/conversations/{Conversation ID}/bots": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"add-bot\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "Conversation ID", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AddBot" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "", + "schema": { + "$ref": "#/definitions/AddBotResponse" + } + }, + "400": { + "description": "Invalid `body` or `Conversation ID`" + }, + "403": { + "description": "The desired service is currently disabled. (label: `service-disabled`)\n\nMaximum number of members per conversation reached. (label: `too-many-members`)\n\nThe operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "service-disabled", + "message": "The desired service is currently disabled." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "service-disabled", + "too-many-members", + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Add bot" + } + }, + "/conversations/{Conversation ID}/bots/{Bot ID}": { + "delete": { + "description": " [internal route ID: \"remove-bot\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "Conversation ID", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "Bot ID", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "User found", + "schema": { + "$ref": "#/definitions/RemoveBotResponse" + } + }, + "204": { + "description": "" + }, + "400": { + "description": "Invalid `Bot ID` or `Conversation ID`" + }, + "403": { + "description": "The operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation", + "message": "The operation is not allowed in this conversation." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove bot" + } + }, + "/conversations/{cnv_domain}/{cnv}": { + "get": { + "description": " [internal route ID: \"get-conversation\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "400": { + "description": "Invalid `cnv` or `cnv_domain`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a conversation by ID", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "get-conversations" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/access": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-access\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationAccessDatav3" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Access updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Access unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid target access (label: `invalid-op`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing modify_conversation_access) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid target access" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update access modes for a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/members": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"add-members-to-conversation\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/InviteQualified" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "not-connected", + "no-team-member", + "access-denied", + "too-many-members", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph", + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + }, + "533": { + "description": "Some domains are unreachable", + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "summary": "Add qualified members to an existing conversation.", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}": { + "delete": { + "description": " [internal route ID: \"remove-member\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "type": "string" + }, + { + "description": "Target User ID", + "format": "uuid", + "in": "path", + "name": "usr", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Member removed", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "No change" + }, + "400": { + "description": "Invalid `usr` or `usr_domain` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove a member from a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "leave-conversation" + ], + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-other-member\"]\n\n**Note**: at least one field has to be provided.", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "type": "string" + }, + { + "description": "Target User ID", + "format": "uuid", + "in": "path", + "name": "usr", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OtherMemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Membership updated" + }, + "400": { + "description": "Invalid `body` or `usr` or `usr_domain` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation-member", + "message": "Conversation member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation-member", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update membership of the specified user", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/message-timer": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-message-timer\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationMessageTimerUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Message timer updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Message timer unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update the message timer for a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/name": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-name\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationRename" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Name unchanged", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Name updated" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update conversation name", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/proteus/messages": { + "post": { + "consumes": [ + "application/x-protobuf" + ], + "description": " [internal route ID: \"post-proteus-message\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `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\nThe 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, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QualifiedNewOtrMessage" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + } + }, + "summary": "Post an encrypted message to a conversation (accepts only Protobuf)", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ], + [ + "galley", + "on-message-sent" + ], + [ + "galley", + "send-message" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/receipt-mode": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-receipt-mode\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationReceiptModeUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Receipt mode updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Receipt mode unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update receipt mode for a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "update-conversation" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/self": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-self\"]\n\n**Note**: at least one field has to be provided.", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Update successful" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update self membership properties" + } + }, + "/conversations/{cnv_domain}/{cnv}/typing": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"member-typing-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TypingData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification sent" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Sending typing notifications", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "update-typing-indicator" + ], + [ + "galley", + "on-typing-indicator-updated" + ] + ] + } + }, + "/conversations/{cnv}": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-name-deprecated\"]\n\nUse `/conversations/:domain/:conv/name` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationRename" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Name updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Name unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update conversation name (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/conversations/{cnv}/code": { + "delete": { + "description": " [internal route ID: \"remove-code-unqualified\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation code deleted.", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete conversation code" + }, + "get": { + "description": " [internal route ID: \"get-code\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation Code", + "schema": { + "$ref": "#/definitions/ConversationCodeInfo" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get existing conversation code" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"create-conversation-code-unqualified\"]\n\n\nOAuth scope: `write:conversations_code`", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateConversationCodeRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation code already exists.", + "schema": { + "$ref": "#/definitions/ConversationCodeInfo" + } + }, + "201": { + "description": "Conversation code created.", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "Conversation code already exists with a different password setting than the requested one. (label: `create-conv-code-conflict`)\n\nThe guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "create-conv-code-conflict", + "message": "Conversation code already exists with a different password setting than the requested one." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "create-conv-code-conflict", + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create or recreate a conversation code" + } + }, + "/conversations/{cnv}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: \"get-conversation-guest-links-status\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get the status of the guest links feature for a conversation that potentially has been created by someone from another team." + } + }, + "/conversations/{cnv}/join": { + "post": { + "description": " [internal route ID: \"join-conversation-by-id-unqualified\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation joined", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Maximum number of members per conversation reached (label: `too-many-members`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "too-many-members", + "message": "Maximum number of members per conversation reached" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-members", + "no-team-member", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Join a conversation by its ID (if link access enabled)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ] + ] + } + }, + "/conversations/{cnv}/members/{usr}": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-other-member-unqualified\"]\n\nUse `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "description": "Target User ID", + "format": "uuid", + "in": "path", + "name": "usr", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OtherMemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Membership updated" + }, + "400": { + "description": "Invalid `body` or `usr` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation-member", + "message": "Conversation member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation-member", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update membership of the specified user (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/conversations/{cnv}/message-timer": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-message-timer-unqualified\"]\n\nUse `/conversations/:domain/:cnv/message-timer` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationMessageTimerUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Message timer updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Message timer unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update the message timer for a conversation (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/conversations/{cnv}/name": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-name-unqualified\"]\n\nUse `/conversations/:domain/:conv/name` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationRename" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Name updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Name unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update conversation name (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/conversations/{cnv}/otr/messages": { + "post": { + "consumes": [ + "application/json;charset=utf-8", + "application/x-protobuf" + ], + "description": " [internal route ID: \"post-otr-message-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "query", + "name": "ignore_missing", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/new-otr-message" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + }, + "400": { + "description": "Invalid `body` or `report_missing` or `ignore_missing` or `cnv`" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + } + }, + "summary": "Post an encrypted message to a conversation (accepts JSON or Protobuf)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-message-sent" + ], + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/conversations/{cnv}/receipt-mode": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-receipt-mode-unqualified\"]\n\nUse `PUT /conversations/:domain/:cnv/receipt-mode` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationReceiptModeUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Receipt mode updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Receipt mode unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update receipt mode for a conversation (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "update-conversation" + ] + ] + } + }, + "/conversations/{cnv}/roles": { + "get": { + "description": " [internal route ID: \"get-conversation-roles\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationRolesList" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get existing roles available for the given conversation" + } + }, + "/conversations/{cnv}/self": { + "get": { + "description": " [internal route ID: \"get-conversation-self-unqualified\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Member" + } + }, + "400": { + "description": "Invalid `cnv`" + } + }, + "summary": "Get self membership properties (deprecated)" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-self-unqualified\"]\n\nUse `/conversations/:domain/:conv/self` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Update successful" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update self membership properties (deprecated)" + } + }, + "/cookies": { + "get": { + "description": " [internal route ID: \"list-cookies\"]\n\n", + "parameters": [ + { + "description": "Filter by label (comma-separated list)", + "in": "query", + "name": "labels", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of cookies", + "schema": { + "$ref": "#/definitions/CookieList" + } + }, + "400": { + "description": "Invalid `labels`" + } + }, + "summary": "Retrieve the list of cookies currently stored for the user" + } + }, + "/cookies/remove": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"remove-cookies\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RemoveCookies" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Cookies revoked" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Revoke stored cookies" + } + }, + "/custom-backend/by-domain/{domain}": { + "get": { + "description": " [internal route ID: \"get-custom-backend-by-domain\"]\n\n", + "parameters": [ + { + "description": "URL-encoded email domain", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/CustomBackend" + } + }, + "400": { + "description": "Invalid `domain`" + }, + "404": { + "description": "Custom backend not found (label: `custom-backend-not-found`)", + "schema": { + "example": { + "code": 404, + "label": "custom-backend-not-found", + "message": "Custom backend not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "custom-backend-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Shows information about custom backends related to a given email domain" + } + }, + "/delete": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"verify-delete\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/VerifyDeleteUser" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Invalid verification code (label: `invalid-code`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Verify account deletion with a code." + } + }, + "/feature-configs": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-user\"]\n\nGets feature configs for a user. If the user is a member of a team and has the required permissions, this will return the team's feature configs.If the user is not a member of a team, this will return the personal feature configs (the server defaults).\nOAuth scope: `read:feature_configs`", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AllFeatureConfigs" + } + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Gets feature configs for a user" + } + }, + "/identity-providers": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPList" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/IdPMetadataInfo" + } + }, + { + "format": "uuid", + "in": "query", + "name": "replaces", + "required": false, + "type": "string" + }, + { + "default": "v2", + "enum": [ + "v1", + "v2" + ], + "in": "query", + "name": "api_version", + "required": false, + "type": "string" + }, + { + "in": "query", + "maxLength": 1, + "minLength": 32, + "name": "handle", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "201": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPConfig" + } + }, + "400": { + "description": "Invalid `handle` or `api_version` or `replaces` or `body`" + } + } + } + }, + "/identity-providers/{id}": { + "delete": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + }, + { + "in": "query", + "name": "purge", + "required": false, + "type": "boolean" + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "Invalid `purge` or `id`" + } + } + }, + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPConfig" + } + }, + "400": { + "description": "Invalid `id`" + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/IdPMetadataInfo" + } + }, + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + }, + { + "in": "query", + "maxLength": 1, + "minLength": 32, + "name": "handle", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPConfig" + } + }, + "400": { + "description": "Invalid `handle` or `id` or `body`" + } + } + } + }, + "/identity-providers/{id}/raw": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/xml" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid `id`" + } + } + } + }, + "/list-connections": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"list-connections\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GetPaginated_Connections" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Connections_Page" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "List the connections to other users, including remote users" + } + }, + "/list-users": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"list-users-by-ids-or-handles\"]\n\nThe 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ListUsersQuery" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ListUsersById" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "List users", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/login": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"login\"]\n\nLogins are throttled at the server's discretion", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Login" + } + }, + { + "description": "Request a persistent cookie instead of a session cookie", + "in": "query", + "name": "persist", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "Set-Cookie": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/AccessToken" + } + }, + "400": { + "description": "Invalid `persist` or `body`" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nAccount pending activation (label: `pending-activation`)\n\nAccount suspended (label: `suspended`)\n\nAuthentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "pending-activation", + "suspended", + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Authenticate a user to obtain a cookie and first access token" + } + }, + "/login/send": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"send-login-code\"]\n\nThis operation generates and sends a login code via sms for phone login. A login code can be used only once and times out after 10 minutes. Only one login code may be pending at a time. For 2nd factor authentication login with email and password, use the `/verification-code/send` endpoint.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SendLoginCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/LoginCodeTimeout" + } + }, + "400": { + "description": "Invalid mobile phone number (label: `invalid-phone`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "The operation is not permitted because the user has a password set (label: `password-exists`)", + "schema": { + "example": { + "code": 403, + "label": "password-exists", + "message": "The operation is not permitted because the user has a password set" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Send a login code to a verified phone number" + } + }, + "/notifications": { + "get": { + "description": " [internal route ID: \"get-notifications\"]\n\n", + "parameters": [ + { + "description": "Only return notifications more recent than this", + "format": "uuid", + "in": "query", + "name": "since", + "required": false, + "type": "string" + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "type": "string" + }, + { + "description": "Maximum number of notifications to return", + "format": "int32", + "in": "query", + "maximum": 10000, + "minimum": 100, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification list", + "schema": { + "$ref": "#/definitions/QueuedNotificationList" + } + }, + "400": { + "description": "Invalid `size` or `client` or `since`" + }, + "404": { + "description": "Some notifications not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Fetch notifications" + } + }, + "/notifications/last": { + "get": { + "description": " [internal route ID: \"get-last-notification\"]\n\n", + "parameters": [ + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification found", + "schema": { + "$ref": "#/definitions/QueuedNotification" + } + }, + "400": { + "description": "Invalid `client`" + }, + "404": { + "description": "Some notifications not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Fetch the last notification" + } + }, + "/notifications/{id}": { + "get": { + "description": " [internal route ID: \"get-notification-by-id\"]\n\n", + "parameters": [ + { + "description": "Notification ID", + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification found", + "schema": { + "$ref": "#/definitions/QueuedNotification" + } + }, + "400": { + "description": "Invalid `client` or `id`" + }, + "404": { + "description": "Some notifications not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Fetch a notification by ID" + } + }, + "/oauth/applications": { + "get": { + "description": " [internal route ID: \"get-oauth-applications\"]\n\nGet all OAuth applications with active account access for a user.", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OAuth applications found", + "schema": { + "items": { + "$ref": "#/definitions/OAuthApplication" + }, + "type": "array" + } + } + }, + "summary": "Get OAuth applications with account access" + } + }, + "/oauth/applications/{OAuthClientId}": { + "delete": { + "description": " [internal route ID: \"revoke-oauth-account-access\"]\n\n", + "parameters": [ + { + "description": "The ID of the OAuth client", + "format": "uuid", + "in": "path", + "name": "OAuthClientId", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "OAuth application access revoked" + }, + "400": { + "description": "Invalid `OAuthClientId`" + } + }, + "summary": "Revoke account access from an OAuth application" + } + }, + "/oauth/authorization/codes": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"create-oauth-auth-code\"]\n\nCurrently only supports the 'code' response type, which corresponds to the authorization code flow.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateOAuthAuthorizationCodeRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request\n\nThe redirect URL does not match the one registered with the client (label: `redirect-url-miss-match`) or `body`", + "headers": { + "Location": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "headers": { + "Location": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Location": { + "type": "string" + } + } + } + }, + "summary": "Create an OAuth authorization code" + } + }, + "/oauth/clients/{OAuthClientId}": { + "get": { + "description": " [internal route ID: \"get-oauth-client\"]\n\n", + "parameters": [ + { + "description": "The ID of the OAuth client", + "format": "uuid", + "in": "path", + "name": "OAuthClientId", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OAuth client found", + "schema": { + "$ref": "#/definitions/OAuthClient" + } + }, + "400": { + "description": "Invalid `OAuthClientId`" + }, + "403": { + "description": "OAuth is disabled (label: `forbidden`)", + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "OAuth is disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "OAuth client not found (label: `not-found`)\n\nOAuth client not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get OAuth client information" + } + }, + "/oauth/revoke": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"revoke-oauth-refresh-token\"]\n\nRevoke an access token.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OAuthRevokeRefreshTokenRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Invalid refresh token (label: `forbidden`)", + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "Invalid refresh token" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "OAuth client not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "Internal error while handling JWT token (label: `jwt-error`)", + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Revoke an OAuth refresh token" + } + }, + "/oauth/token": { + "post": { + "consumes": [ + "application/x-www-form-urlencoded" + ], + "description": " [internal route ID: \"create-oauth-access-token\"]\n\nObtain a new access token from an authorization code or a refresh token.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Either" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/OAuthAccessTokenResponse" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Invalid grant (label: `invalid_grant`)\n\nInvalid client credentials (label: `forbidden`)\n\nInvalid grant type (label: `forbidden`)\n\nInvalid refresh token (label: `forbidden`)\n\nOAuth is disabled (label: `forbidden`)", + "schema": { + "example": { + "code": 403, + "label": "invalid_grant", + "message": "Invalid grant" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid_grant", + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "OAuth client not found (label: `not-found`)\n\nOAuth authorization code not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "Internal error while handling JWT token (label: `jwt-error`)", + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create an OAuth access token" + } + }, + "/onboarding/v3": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"onboarding\"]\n\nDEPRECATED: the feature has been turned off, the end-point does nothing and always returns '{\"results\":[],\"auto-connects\":[]}'.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Body" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/DeprecatedMatchingResult" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Upload contacts and invoke matching." + } + }, + "/password-reset": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-password-reset\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewPasswordReset" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Password reset code created and sent by email." + }, + "400": { + "description": "Invalid `body`\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + }, + "409": { + "description": "A password reset is already in progress. (label: `code-exists`)", + "schema": { + "example": { + "code": 409, + "label": "code-exists", + "message": "A password reset is already in progress." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Initiate a password reset." + } + }, + "/password-reset/complete": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-password-reset-complete\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CompletePasswordReset" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + } + }, + "summary": "Complete a password reset." + } + }, + "/password-reset/{key}": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-password-reset-key-deprecated\"]\n\nDEPRECATED: Use 'POST /password-reset/complete'.", + "parameters": [ + { + "description": "An opaque key for a pending password reset.", + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PasswordReset" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { + "description": "Invalid `body` or `key`\n\nInvalid password reset code. (label: `invalid-code`)\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + }, + "409": { + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)\n\nA password reset is already in progress. (label: `code-exists`)", + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ", + "code-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Complete a password reset." + } + }, + "/properties": { + "delete": { + "description": " [internal route ID: \"clear-properties\"]\n\n", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Properties cleared" + } + }, + "summary": "Clear all properties" + }, + "get": { + "description": " [internal route ID: \"list-property-keys\"]\n\n", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of property keys", + "schema": { + "items": { + "$ref": "#/definitions/ASCII" + }, + "type": "array" + } + } + }, + "summary": "List all property keys" + } + }, + "/properties-values": { + "get": { + "description": " [internal route ID: \"list-properties\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PropertyKeysAndValues" + } + } + }, + "summary": "List all properties with key and value" + } + }, + "/properties/{key}": { + "delete": { + "description": " [internal route ID: \"delete-property\"]\n\n", + "parameters": [ + { + "format": "printable", + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Property deleted" + }, + "400": { + "description": "Invalid `key`" + } + }, + "summary": "Delete a property" + }, + "get": { + "description": " [internal route ID: \"get-property\"]\n\n", + "parameters": [ + { + "format": "printable", + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "The property value", + "schema": { + "$ref": "#/definitions/PropertyValue" + } + }, + "400": { + "description": "Invalid `key`" + }, + "404": { + "description": "Property not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a property value" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"set-property\"]\n\n", + "parameters": [ + { + "format": "printable", + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PropertyValue" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Property set" + }, + "400": { + "description": "Invalid `body` or `key`" + } + }, + "summary": "Set a user property" + } + }, + "/provider/assets": { + "post": { + "consumes": [ + "multipart/mixed" + ], + "parameters": [ + { + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server.", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AssetSource" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Asset" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "description": "Asset too large (label: `client-error`)", + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Upload an asset" + } + }, + "/provider/assets/{key}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "400": { + "description": "Invalid `key`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete an asset" + }, + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + } + }, + "400": { + "description": "Invalid `asset_token` or `Asset-Token` or `key`" + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Download an asset" + } + }, + "/proxy/giphy/v1/gifs": {}, + "/proxy/googlemaps/api/staticmap": {}, + "/proxy/googlemaps/maps/api/geocode": {}, + "/proxy/youtube/v3": {}, + "/push/tokens": { + "get": { + "description": " [internal route ID: \"get-push-tokens\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PushTokenList" + } + } + }, + "summary": "List the user's registered push tokens" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"register-push-token\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PushToken" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Push token registered", + "headers": { + "Location": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/PushToken" + } + }, + "400": { + "description": "Invalid `body`" + }, + "404": { + "description": "App does not exist (label: `app-not-found`)\n\nInvalid push token (label: `invalid-token`)", + "schema": { + "example": { + "code": 404, + "label": "app-not-found", + "message": "App does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "app-not-found", + "invalid-token" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "413": { + "description": "Too many concurrent calls to SNS; is SNS down? (label: `sns-thread-budget-reached`)\n\nPush token length must be < 8192 for GCM or 400 for APNS (label: `token-too-long`)\n\nTried to add token to endpoint resulting in metadata length > 2048 (label: `metadata-too-long`)", + "schema": { + "example": { + "code": 413, + "label": "sns-thread-budget-reached", + "message": "Too many concurrent calls to SNS; is SNS down?" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "sns-thread-budget-reached", + "token-too-long", + "metadata-too-long" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Register a native push token" + } + }, + "/push/tokens/{pid}": { + "delete": { + "description": " [internal route ID: \"delete-push-token\"]\n\n", + "parameters": [ + { + "description": "The push token to delete", + "in": "path", + "name": "pid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "Push token unregistered" + }, + "400": { + "description": "Invalid `pid`" + }, + "404": { + "description": "Push token not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Push token not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Unregister a native push token" + } + }, + "/register": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"register\"]\n\nIf the environment where the registration takes place is private and a registered email address or phone number is not whitelisted, a 403 error is returned.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewUser" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "User created and pending activation", + "headers": { + "Location": { + "description": "UserId", + "format": "uuid", + "type": "string" + }, + "Set-Cookie": { + "description": "Cookie", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/User" + } + }, + "400": { + "description": "Invalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)\n\nInvalid mobile phone number (label: `invalid-phone`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email", + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "Unauthorized e-mail address or phone number. (label: `unauthorized`)\n\nUsing an invitation code requires registering the given email and/or phone. (label: `missing-identity`)\n\nThe given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nToo many members in this team. (label: `too-many-team-members`)\n\nThis instance does not allow creation of personal users or teams. (label: `user-creation-restricted`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address or phone number." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-phone", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "User does not exist (label: `invalid-code`)\n\nInvalid activation code (label: `invalid-code`)", + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "User does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Register a new user." + } + }, + "/scim/auth-tokens": { + "delete": { + "parameters": [ + { + "format": "uuid", + "in": "query", + "name": "id", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "Invalid `id`" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + } + }, + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ScimTokenList" + } + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + } + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateScimToken" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/CreateScimTokenResponse" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + } + } + }, + "/search/contacts": { + "get": { + "description": " [internal route ID: \"search-contacts\"]\n\n", + "parameters": [ + { + "description": "Search query", + "in": "query", + "name": "q", + "required": true, + "type": "string" + }, + { + "description": "Searched domain. Note: This is optional only for backwards compatibility, future versions will mandate this.", + "in": "query", + "name": "domain", + "required": false, + "type": "string" + }, + { + "description": "Number of results to return (min: 1, max: 500, default 15)", + "format": "int32", + "in": "query", + "maximum": 500, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchResult" + } + }, + "400": { + "description": "Invalid `size` or `domain` or `q`" + } + }, + "summary": "Search for users", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-users-by-ids" + ], + [ + "brig", + "search-users" + ] + ] + } + }, + "/self": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"delete-self\"]\n\nif the account has a verified identity, a verification code is sent and needs to be confirmed to authorise the deletion. if the account has no verified identity but a password, it must be provided. if password is correct, or if neither a verified identity nor a password exists, account deletion is scheduled immediately.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteUser" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "202": { + "description": "Deletion is pending verification with a code.", + "schema": { + "$ref": "#/definitions/DeletionCodeTimeout" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "description": "Team owners are not allowed to delete themselves; ask a fellow owner (label: `no-self-delete-for-team-owner`)\n\nA verification code for account deletion is still pending (label: `pending-delete`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)", + "schema": { + "example": { + "code": 403, + "label": "no-self-delete-for-team-owner", + "message": "Team owners are not allowed to delete themselves; ask a fellow owner" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-self-delete-for-team-owner", + "pending-delete", + "missing-auth", + "invalid-credentials", + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Initiate account deletion." + }, + "get": { + "description": " [internal route ID: \"get-self\"]\n\n\nOAuth scope: `read:self`", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/User" + } + } + }, + "summary": "Get your own profile" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"put-self\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UserUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "User updated" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Updating name is not allowed, because it is managed by SCIM (label: `managed-by-scim`)", + "schema": { + "example": { + "code": 403, + "label": "managed-by-scim", + "message": "Updating name is not allowed, because it is managed by SCIM" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "managed-by-scim" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "User not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update your profile." + } + }, + "/self/email": { + "delete": { + "description": " [internal route ID: \"remove-email\"]\n\nYour email address can only be removed if you also have a phone number.", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Identity Removed" + }, + "403": { + "description": "The last user identity (email or phone number) cannot be removed. (label: `last-identity`)\n\nThe user has no password. (label: `no-password`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)", + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity (email or phone number) cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-password", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove your email address." + } + }, + "/self/handle": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"change-handle\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/HandleUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Handle Changed" + }, + "400": { + "description": "The given handle is invalid (label: `invalid-handle`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-handle", + "message": "The given handle is invalid" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-handle" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "The user has no verified identity (email or phone number) (label: `no-identity`)\n\nUpdating handle is not allowed, because it is managed by SCIM (label: `managed-by-scim`)", + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified identity (email or phone number)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "managed-by-scim" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given handle is already taken (label: `handle-exists`)", + "schema": { + "example": { + "code": 409, + "label": "handle-exists", + "message": "The given handle is already taken" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "handle-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your handle." + } + }, + "/self/locale": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"change-locale\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LocaleUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Local Changed" + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Change your locale." + } + }, + "/self/password": { + "head": { + "description": " [internal route ID: \"check-password-exists\"]\n\n", + "responses": { + "200": { + "description": "Password is set" + }, + "404": { + "description": "Password is not set" + } + }, + "summary": "Check that your password is set." + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"change-password\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PasswordChange" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Password Changed" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "For password change, new and old password must be different. (label: `password-must-differ`)", + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password change, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your password." + } + }, + "/self/phone": { + "delete": { + "description": " [internal route ID: \"remove-phone\"]\n\nYour phone number can only be removed if you also have an email address and a password.", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Identity Removed" + }, + "403": { + "description": "The last user identity (email or phone number) cannot be removed. (label: `last-identity`)\n\nThe user has no password. (label: `no-password`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)", + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity (email or phone number) cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-password", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove your phone number." + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"change-phone\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PhoneUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "202": { + "description": "Phone updated" + }, + "400": { + "description": "Invalid mobile phone number (label: `invalid-phone`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "The given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)", + "schema": { + "example": { + "code": 403, + "label": "blacklisted-phone", + "message": "The given phone number has been blacklisted due to suspected abuse or a complaint" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "blacklisted-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your phone number." + } + }, + "/sso/finalize-login": { + "post": { + "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "produces": [ + "text/plain;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + } + } + } + }, + "/sso/finalize-login/{team}": { + "post": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "team", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/plain;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid `team`" + } + } + } + }, + "/sso/initiate-login/{idp}": { + "get": { + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "idp", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/html" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/FormRedirect" + } + }, + "400": { + "description": "Invalid `idp` or `error_redirect` or `success_redirect`" + } + } + }, + "head": { + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "idp", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/plain;charset=utf-8" + ], + "responses": { + "200": { + "description": "" + }, + "400": { + "description": "Invalid `idp` or `error_redirect` or `success_redirect`" + } + } + } + }, + "/sso/metadata": { + "get": { + "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "produces": [ + "application/xml" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + } + } + } + }, + "/sso/metadata/{team}": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "team", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/xml" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid `team`" + } + } + } + }, + "/sso/settings": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SsoSettings" + } + } + } + } + }, + "/system/settings": { + "get": { + "description": " [internal route ID: \"get-system-settings\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SystemSettings" + } + } + }, + "summary": "Returns a curated set of system configuration settings for authorized users." + } + }, + "/system/settings/unauthorized": { + "get": { + "description": " [internal route ID: \"get-system-settings-unauthorized\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SystemSettingsPublic" + } + } + }, + "summary": "Returns a curated set of system configuration settings." + } + }, + "/teams/invitations/by-email": { + "head": { + "description": " [internal route ID: \"head-team-invitations\"]\n\n", + "parameters": [ + { + "description": "Email address", + "in": "query", + "name": "email", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Pending invitation exists." + }, + "400": { + "description": "Invalid `email`" + }, + "404": { + "description": "No pending invitations exists. (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "No pending invitations exists." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "Multiple conflicting invitations to different teams exists. (label: `conflicting-invitations`)", + "schema": { + "example": { + "code": 409, + "label": "conflicting-invitations", + "message": "Multiple conflicting invitations to different teams exists." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "conflicting-invitations" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Check if there is an invitation pending given an email address." + } + }, + "/teams/invitations/info": { + "get": { + "description": " [internal route ID: \"get-team-invitation-info\"]\n\n", + "parameters": [ + { + "description": "Invitation code", + "in": "query", + "name": "code", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Invitation info", + "schema": { + "$ref": "#/definitions/Invitation" + } + }, + "400": { + "description": "Invalid `code`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get invitation info given a code." + } + }, + "/teams/notifications": { + "get": { + "description": " [internal route ID: \"get-team-notifications\"]\n\nThis is a work-around for scalability issues with gundeck user event fan-out. It does not track all team-wide events, but only `member-join`.\nNote that `/teams/notifications` behaves differently from `/notifications`:\n- If there is a gap between the notification id requested with `since` and the available data, team queues respond with 200 and the data that could be found. They do NOT respond with status 404, but valid data in the body.\n- The notification with the id given via `since` is included in the response if it exists. You should remove this and only use it to decide whether there was a gap between your last request and this one.\n- If the notification id does *not* exist, you get the more recent events from the queue (instead of all of them). This can be done because a notification id is a UUIDv1, which is essentially a time stamp.\n- There is no corresponding `/last` end-point to get only the most recent event. That end-point was only useful to avoid having to pull the entire queue. In team queues, if you have never requested the queue before and have no prior notification id, just pull with timestamp 'now'.", + "parameters": [ + { + "description": "Notification id to start with in the response (UUIDv1)", + "format": "uuid", + "in": "query", + "name": "since", + "required": false, + "type": "string" + }, + { + "description": "Maximum number of events to return (1..10000; default: 1000)", + "format": "int32", + "in": "query", + "maximum": 10000, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/QueuedNotificationList" + } + }, + "400": { + "description": "Invalid `size` or `since`\n\nCould not parse notification id (must be UUIDv1). (label: `invalid-notification-id`)" + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Read recently added team members from team queue" + } + }, + "/teams/{tid}": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"delete-team\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TeamDeleteData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "202": { + "description": "Team is scheduled for removal" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Verification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (missing DeleteTeam) (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Verification code required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "access-denied", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "503": { + "description": "The delete queue is full; no further delete requests can be processed at the moment (label: `queue-full`)", + "schema": { + "example": { + "code": 503, + "label": "queue-full", + "message": "The delete queue is full; no further delete requests can be processed at the moment" + }, + "properties": { + "code": { + "enum": [ + 503 + ], + "type": "integer" + }, + "label": { + "enum": [ + "queue-full" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete a team" + }, + "get": { + "description": " [internal route ID: \"get-team\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Team" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a team by ID" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-team\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TeamUpdateData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Team updated" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Insufficient permissions (missing SetTeamData) (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions (missing SetTeamData)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update team properties" + } + }, + "/teams/{tid}/conversations": { + "get": { + "description": " [internal route ID: \"get-team-conversations\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamConversationList" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get team conversations" + } + }, + "/teams/{tid}/conversations/roles": { + "get": { + "description": " [internal route ID: \"get-team-conversation-roles\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationRolesList" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get existing roles available for the given team" + } + }, + "/teams/{tid}/conversations/{cid}": { + "delete": { + "description": " [internal route ID: \"delete-team-conversation\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation deleted" + }, + "400": { + "description": "Invalid `cid` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing delete_conversation) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove a team conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + }, + "get": { + "description": " [internal route ID: \"get-team-conversation\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamConversation" + } + }, + "400": { + "description": "Invalid `cid` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get one team conversation" + } + }, + "/teams/{tid}/features": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-team\"]\n\nGets feature configs for a team. User must be a member of the team and have permission to view team features.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AllFeatureConfigs" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Gets feature configs for a team" + } + }, + "/teams/{tid}/features/appLock": { + "get": { + "description": " [internal route ID: (\"get\", AppLockConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AppLockConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for appLock" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", AppLockConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AppLockConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AppLockConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for appLock" + } + }, + "/teams/{tid}/features/classifiedDomains": { + "get": { + "description": " [internal route ID: (\"get\", ClassifiedDomainsConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ClassifiedDomainsConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for classifiedDomains" + } + }, + "/teams/{tid}/features/conferenceCalling": { + "get": { + "description": " [internal route ID: (\"get\", ConferenceCallingConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConferenceCallingConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for conferenceCalling" + } + }, + "/teams/{tid}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: (\"get\", GuestLinksConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for conversationGuestLinks" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", GuestLinksConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for conversationGuestLinks" + } + }, + "/teams/{tid}/features/digitalSignatures": { + "get": { + "description": " [internal route ID: (\"get\", DigitalSignaturesConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/DigitalSignaturesConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for digitalSignatures" + } + }, + "/teams/{tid}/features/exposeInvitationURLsToTeamAdmin": { + "get": { + "description": " [internal route ID: (\"get\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for exposeInvitationURLsToTeamAdmin" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for exposeInvitationURLsToTeamAdmin" + } + }, + "/teams/{tid}/features/fileSharing": { + "get": { + "description": " [internal route ID: (\"get\", FileSharingConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/FileSharingConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for fileSharing" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", FileSharingConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/FileSharingConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/FileSharingConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for fileSharing" + } + }, + "/teams/{tid}/features/legalhold": { + "get": { + "description": " [internal route ID: (\"get\", LegalholdConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/LegalholdConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for legalhold" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", LegalholdConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LegalholdConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/LegalholdConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nCannot enable legalhold on large teams (reason: for removing LH from team, we need to iterate over all members, which is only supported for teams with less than 2k members) (label: `too-large-team-for-legalhold`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "too-large-team-for-legalhold", + "code-authentication-required", + "code-authentication-failed", + "access-denied", + "action-denied", + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for legalhold", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/teams/{tid}/features/outlookCalIntegration": { + "get": { + "description": " [internal route ID: (\"get\", OutlookCalIntegrationConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for outlookCalIntegration" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", OutlookCalIntegrationConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for outlookCalIntegration" + } + }, + "/teams/{tid}/features/searchVisibility": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityAvailableConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for searchVisibility" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", SearchVisibilityAvailableConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for searchVisibility" + } + }, + "/teams/{tid}/features/searchVisibilityInbound": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityInboundConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for searchVisibilityInbound" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", SearchVisibilityInboundConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for searchVisibilityInbound" + } + }, + "/teams/{tid}/features/selfDeletingMessages": { + "get": { + "description": " [internal route ID: (\"get\", SelfDeletingMessagesConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for selfDeletingMessages" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", SelfDeletingMessagesConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for selfDeletingMessages" + } + }, + "/teams/{tid}/features/sndFactorPasswordChallenge": { + "get": { + "description": " [internal route ID: (\"get\", SndFactorPasswordChallengeConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for sndFactorPasswordChallenge" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", SndFactorPasswordChallengeConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for sndFactorPasswordChallenge" + } + }, + "/teams/{tid}/features/sso": { + "get": { + "description": " [internal route ID: (\"get\", SSOConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SSOConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for sso" + } + }, + "/teams/{tid}/features/validateSAMLemails": { + "get": { + "description": " [internal route ID: (\"get\", ValidateSAMLEmailsConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ValidateSAMLEmailsConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for validateSAMLemails" + } + }, + "/teams/{tid}/get-members-by-ids-using-post": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"get-team-members-by-ids\"]\n\nThe `has_more` field in the response body is always `false`.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Maximum results to be returned", + "format": "int32", + "in": "query", + "maximum": 2000, + "minimum": 1, + "name": "maxResults", + "required": false, + "type": "integer" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UserIdList" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamMemberList" + } + }, + "400": { + "description": "Invalid `body` or `maxResults` or `tid`\n\nCan only process 2000 user ids per request. (label: `too-many-uids`)" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get team members by user id list" + } + }, + "/teams/{tid}/invitations": { + "get": { + "description": " [internal route ID: \"get-team-invitations\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Invitation id to start from (ascending).", + "format": "uuid", + "in": "query", + "name": "start", + "required": false, + "type": "string" + }, + { + "description": "Number of results to return (default 100, max 500).", + "format": "int32", + "in": "query", + "maximum": 500, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of sent invitations", + "schema": { + "$ref": "#/definitions/InvitationList" + } + }, + "400": { + "description": "Invalid `size` or `start` or `tid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "List the sent team invitations" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"send-team-invitation\"]\n\nInvitations are sent by email. The maximum allowed number of pending team invitations is equal to the team size.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/InvitationRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Invitation was created and sent.", + "headers": { + "Location": { + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Invitation" + } + }, + "400": { + "description": "Invalid `body` or `tid`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)\n\nToo many team invitations for this team (label: `too-many-team-invitations`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)\n\nThis operation requires the user to have a verified email address. (label: `no-email`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions", + "too-many-team-invitations", + "blacklisted-email", + "no-identity", + "no-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create and send a new team invitation." + } + }, + "/teams/{tid}/invitations/{iid}": { + "delete": { + "description": " [internal route ID: \"delete-team-invitation\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "iid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Invitation deleted" + }, + "400": { + "description": "Invalid `iid` or `tid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete a pending team invitation by ID." + }, + "get": { + "description": " [internal route ID: \"get-team-invitation\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "iid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Invitation", + "schema": { + "$ref": "#/definitions/Invitation" + } + }, + "400": { + "description": "Invalid `iid` or `tid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Notification not found. (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Notification not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a pending team invitation by ID." + } + }, + "/teams/{tid}/legalhold/consent": { + "post": { + "description": " [internal route ID: \"consent-to-legal-hold\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Grant consent successful" + }, + "204": { + "description": "Consent already granted" + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Consent to legal hold", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/teams/{tid}/legalhold/settings": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"delete-legal-hold-settings\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to members with a legalhold client (via brig)\n- UserLegalHoldDisabled event to contacts of members with a legalhold client (via brig)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RemoveLegalHoldSettingsRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "Legal hold service settings deleted" + }, + "400": { + "description": "Invalid `body` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "invalid-op", + "action-denied", + "no-team-member", + "operation-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete legal hold service settings", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + }, + "get": { + "description": " [internal route ID: \"get-legal-hold-settings\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ViewLegalHoldService" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get legal hold service settings" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"create-legal-hold-settings\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewLegalHoldService" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Legal hold service settings created", + "schema": { + "$ref": "#/definitions/ViewLegalHoldService" + } + }, + "400": { + "description": "Invalid `body` or `tid`\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)\n\nlegal hold service pubkey is invalid (label: `legalhold-invalid-key`)" + }, + "403": { + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create legal hold service settings" + } + }, + "/teams/{tid}/legalhold/{uid}": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"disable-legal-hold-for-user\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to the user owning the client (via brig)\n- UserLegalHoldDisabled event to contacts of the user owning the client (via brig)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DisableLegalHoldForUserRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Disable legal hold successful" + }, + "204": { + "description": "Legal hold was not enabled" + }, + "400": { + "description": "Invalid `body` or `uid` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "action-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Disable legal hold for user", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + }, + "get": { + "description": " [internal route ID: \"get-legal-hold\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/UserLegalHoldStatusResponse" + } + }, + "400": { + "description": "Invalid `uid` or `tid`" + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get legal hold status" + }, + "post": { + "description": " [internal route ID: \"request-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- LegalHoldClientRequested event to contacts of the user the device is requested for, if they didn't already have a legalhold client (via brig)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Request device successful" + }, + "204": { + "description": "Request device already pending" + }, + "400": { + "description": "Invalid `uid` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)" + }, + "403": { + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "user has not given consent to using legal hold (label: `legalhold-no-consent`)\n\nlegal hold is already enabled for this user (label: `legalhold-already-enabled`)", + "schema": { + "example": { + "code": 409, + "label": "legalhold-no-consent", + "message": "user has not given consent to using legal hold" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-no-consent", + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "internal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)\n\nlegal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-illegal-op", + "message": "internal server error: inconsistent change of user's legalhold state" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-illegal-op", + "legalhold-internal" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Request legal hold device", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/teams/{tid}/legalhold/{uid}/approve": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"approve-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientAdded event to the user owning the client (via brig)\n- UserLegalHoldEnabled event to contacts of the user owning the client (via brig)\n- ClientRemoved event to the user, if removing old client due to max number (via brig)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ApproveLegalHoldForUserRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Legal hold approved" + }, + "400": { + "description": "Invalid `body` or `uid` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "no-team-member", + "action-denied", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow. (label: `legalhold-no-device-allocated`)", + "schema": { + "example": { + "code": 404, + "label": "legalhold-no-device-allocated", + "message": "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-no-device-allocated" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "legal hold is already enabled for this user (label: `legalhold-already-enabled`)", + "schema": { + "example": { + "code": 409, + "label": "legalhold-already-enabled", + "message": "legal hold is already enabled for this user" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "legal hold cannot be approved without being in a pending state (label: `legalhold-not-pending`)", + "schema": { + "example": { + "code": 412, + "label": "legalhold-not-pending", + "message": "legal hold cannot be approved without being in a pending state" + }, + "properties": { + "code": { + "enum": [ + 412 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-pending" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Approve legal hold device", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/teams/{tid}/members": { + "get": { + "description": " [internal route ID: \"get-team-members\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Maximum results to be returned", + "format": "int32", + "in": "query", + "maximum": 2000, + "minimum": 1, + "name": "maxResults", + "required": false, + "type": "integer" + }, + { + "description": "Optional, when not specified, the first page will be returned.Every returned page contains a `pagingState`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamMembersPage" + } + }, + "400": { + "description": "Invalid `pagingState` or `maxResults` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get team members" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-team-member\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewTeamMember" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nMaximum number of admins per team reached (label: `too-many-team-admins`)\n\nThe specified permissions are invalid (label: `invalid-permissions`)\n\nYou do not have permission to access this resource (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "too-many-team-admins", + "invalid-permissions", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)\n\nTeam not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update an existing team member" + } + }, + "/teams/{tid}/members/csv": { + "get": { + "description": " [internal route ID: \"get-team-members-csv\"]\n\nThe endpoint returns data in chunked transfer encoding. Internal server errors might result in a failed transfer instead of a 500 response.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/csv" + ], + "responses": { + "200": { + "description": "CSV of team members" + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "You do not have permission to access this resource (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "You do not have permission to access this resource" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get all members of the team as a CSV file" + } + }, + "/teams/{tid}/members/{uid}": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"delete-team-member\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TeamMemberDeleteData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "" + }, + "202": { + "description": "Team member scheduled for deletion" + }, + "400": { + "description": "Invalid `body` or `uid` or `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)\n\nTeam member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove an existing team member" + }, + "get": { + "description": " [internal route ID: \"get-team-member\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamMember" + } + }, + "400": { + "description": "Invalid `uid` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get single team member" + } + }, + "/teams/{tid}/search": { + "get": { + "description": " [internal route ID: \"browse-team\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Search expression", + "in": "query", + "name": "q", + "required": false, + "type": "string" + }, + { + "collectionFormat": null, + "description": "Role filter, eg. `member,partner`. Empty list means do not filter.", + "in": "query", + "items": { + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "name": "frole", + "required": false, + "type": "array" + }, + { + "description": "Can be one of name, handle, email, saml_idp, managed_by, role, created_at.", + "enum": [ + "name", + "handle", + "email", + "saml_idp", + "managed_by", + "role", + "created_at" + ], + "in": "query", + "name": "sortby", + "required": false, + "type": "string" + }, + { + "description": "Can be one of asc, desc.", + "enum": [ + "asc", + "desc" + ], + "in": "query", + "name": "sortorder", + "required": false, + "type": "string" + }, + { + "description": "Number of results to return (min: 1, max: 500, default: 15)", + "format": "int32", + "in": "query", + "maximum": 500, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + }, + { + "description": "Optional, when not specified, the first page will be returned. Every returned page contains a `paging_state`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Search results", + "schema": { + "$ref": "#/definitions/SearchResult" + } + }, + "400": { + "description": "Invalid `pagingState` or `size` or `sortorder` or `sortby` or `frole` or `q` or `tid`" + } + }, + "summary": "Browse team for members (requires add-user permission)" + } + }, + "/teams/{tid}/search-visibility": { + "get": { + "description": " [internal route ID: \"get-search-visibility\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamSearchVisibilityView" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Shows the value for search visibility" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"set-search-visibility\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TeamSearchVisibilityView" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "Search visibility set" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Custom search is not available for this team (label: `team-search-visibility-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "team-search-visibility-not-enabled", + "message": "Custom search is not available for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "team-search-visibility-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Sets the search visibility for the whole team" + } + }, + "/teams/{tid}/size": { + "get": { + "description": " [internal route ID: \"get-team-size\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Number of team members", + "schema": { + "$ref": "#/definitions/TeamSize" + } + }, + "400": { + "description": "Invalid `tid`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Returns the number of team members as an integer. Can be out of sync by roughly the `refresh_interval` of the ES index." + } + }, + "/users/handles": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"check-user-handles\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CheckHandles" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of free handles", + "schema": { + "items": { + "$ref": "#/definitions/Handle" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Check availability of user handles" + } + }, + "/users/handles/{handle}": { + "head": { + "description": " [internal route ID: \"check-user-handle\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "handle", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Handle is taken", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "400": { + "description": "Invalid `handle`\n\nThe given handle is invalid (label: `invalid-handle`)" + }, + "404": { + "description": "Handle not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Handle not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Check whether a user handle can be taken" + } + }, + "/users/list-clients": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"list-clients-bulk@v2\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LimitedQualifiedUserIdList" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "properties": { + "qualified_user_map": { + "$ref": "#/definitions/QualifiedUserMap_Set_PubClient" + } + }, + "type": "object" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "List all clients for a set of user ids", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/users/list-prekeys": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"get-multi-user-prekey-bundle-qualified\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QualifiedUserClients" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/QualifiedUserClientPrekeyMapV4" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Given a map of domain to (map of user IDs to client IDs) return a prekey for each one. You can't request information for more users than maximum conversation size.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "claim-multi-prekey-bundle" + ] + ] + } + }, + "/users/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-user-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "User found", + "schema": { + "$ref": "#/definitions/UserProfile" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + }, + "404": { + "description": "User not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a user by Domain and UserId", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/clients": { + "get": { + "description": " [internal route ID: \"get-user-clients-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "$ref": "#/definitions/PubClient" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + } + }, + "summary": "Get all of a user's clients", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/clients/{client}": { + "get": { + "description": " [internal route ID: \"get-user-client-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PubClient" + } + }, + "400": { + "description": "Invalid `client` or `uid` or `uid_domain`" + } + }, + "summary": "Get a specific client of a user", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/prekeys": { + "get": { + "description": " [internal route ID: \"get-users-prekey-bundle-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PrekeyBundle" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + } + }, + "summary": "Get a prekey for each client of a user.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "claim-prekey-bundle" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/prekeys/{client}": { + "get": { + "description": " [internal route ID: \"get-users-prekeys-client-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ClientPrekey" + } + }, + "400": { + "description": "Invalid `client` or `uid` or `uid_domain`" + } + }, + "summary": "Get a prekey for a specific client of a user.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "claim-prekey" + ] + ] + } + }, + "/users/{uid}/email": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-user-email\"]\n\nIf the user has a pending email validation, the validation email will be resent.", + "parameters": [ + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/EmailUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "400": { + "description": "Invalid `body` or `uid`" + } + }, + "summary": "Resend email address validation email." + } + }, + "/users/{uid}/rich-info": { + "get": { + "description": " [internal route ID: \"get-rich-info\"]\n\n", + "parameters": [ + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Rich info about the user", + "schema": { + "$ref": "#/definitions/RichInfoAssocList" + } + }, + "400": { + "description": "Invalid `uid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a user's rich info" + } + }, + "/verification-code/send": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"send-verification-code\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SendVerificationCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Verification code sent." + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Send a verification code to a given email address." + } + } + }, + "security": [ + { + "ZAuth": [] + } + ], + "securityDefinitions": { + "ZAuth": { + "description": "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'.", + "in": "header", + "name": "Authorization", + "type": "apiKey" + } + }, + "swagger": "2.0" +} diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 46665c7122f..3343b551185 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -31,7 +31,6 @@ import Brig.API.Client qualified as API import Brig.API.Connection qualified as API import Brig.API.Error import Brig.API.Handler -import Brig.API.MLS.KeyPackages import Brig.API.OAuth (oauthAPI) import Brig.API.Properties qualified as API import Brig.API.Public.Swagger @@ -185,26 +184,11 @@ versionedSwaggerDocsAPI (Just (VersionNumber V5)) = & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger.md") & cleanupSwagger -versionedSwaggerDocsAPI (Just (VersionNumber V4)) = - swaggerSchemaUIServer $ - ( serviceSwagger @VersionAPITag @'V4 - <> serviceSwagger @BrigAPITag @'V4 - <> serviceSwagger @GalleyAPITag @'V4 - <> serviceSwagger @SparAPITag @'V4 - <> serviceSwagger @CargoholdAPITag @'V4 - <> serviceSwagger @CannonAPITag @'V4 - <> serviceSwagger @GundeckAPITag @'V4 - <> serviceSwagger @ProxyAPITag @'V4 - <> serviceSwagger @OAuthAPITag @'V4 - <> serviceSwagger @BotAPITag @'V4 - ) - & S.info . S.title .~ "Wire-Server API" - & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger.md") - & cleanupSwagger versionedSwaggerDocsAPI (Just (VersionNumber V0)) = swaggerPregenUIServer $(pregenSwagger V0) versionedSwaggerDocsAPI (Just (VersionNumber V1)) = swaggerPregenUIServer $(pregenSwagger V1) versionedSwaggerDocsAPI (Just (VersionNumber V2)) = swaggerPregenUIServer $(pregenSwagger V2) versionedSwaggerDocsAPI (Just (VersionNumber V3)) = swaggerPregenUIServer $(pregenSwagger V3) +versionedSwaggerDocsAPI (Just (VersionNumber V4)) = swaggerPregenUIServer $(pregenSwagger V4) versionedSwaggerDocsAPI Nothing = allroutes (throwError listAllVersionsResp) where allroutes :: @@ -279,7 +263,6 @@ servantSitemap = :<|> userClientAPI :<|> connectionAPI :<|> propertiesAPI - :<|> mlsAPI :<|> userHandleAPI :<|> searchAPI :<|> authAPI @@ -386,12 +369,6 @@ servantSitemap = ) :<|> Named @"list-properties" listPropertyKeysAndValues - mlsAPI :: ServerT MLSAPI (Handler r) - mlsAPI = - Named @"mls-key-packages-upload" uploadKeyPackages - :<|> Named @"mls-key-packages-claim" (callsFed (exposeAnnotations claimKeyPackages)) - :<|> Named @"mls-key-packages-count" countKeyPackages - userHandleAPI :: ServerT UserHandleAPI (Handler r) userHandleAPI = Named @"check-user-handles" checkHandles diff --git a/services/brig/test/integration/API/Federation.hs b/services/brig/test/integration/API/Federation.hs index c58d022c901..0cc5eaf9be1 100644 --- a/services/brig/test/integration/API/Federation.hs +++ b/services/brig/test/integration/API/Federation.hs @@ -407,16 +407,3 @@ testAPIVersion :: Brig -> FedClient 'Brig -> Http () testAPIVersion _brig fedBrigClient = do vinfo <- runFedClient @"api-version" fedBrigClient (Domain "far-away.example.com") () liftIO $ vinfoSupported vinfo @?= toList supportedVersions - -testClaimKeyPackagesMLSDisabled :: HasCallStack => Opt.Opts -> Brig -> Http () -testClaimKeyPackagesMLSDisabled opts brig = do - alice <- fakeRemoteUser - bob <- userQualifiedId <$> randomUser brig - - mbundle <- - withSettingsOverrides (opts & Opt.optionSettings . Opt.enableMLS ?~ False) $ - runWaiTestFedClient (qDomain alice) $ - createWaiTestFedClient @"claim-key-packages" @'Brig $ - ClaimKeyPackageRequest (qUnqualified alice) (qUnqualified bob) - - liftIO $ mbundle @?= Nothing diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index 5e35b4caed8..c09719e9d70 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -24,8 +24,6 @@ module API.Internal where import API.Internal.Util -import API.MLS (createClient) -import API.MLS.Util import Bilge import Bilge.Assert import Brig.Data.Connection @@ -37,35 +35,27 @@ import Cassandra.Exec (x1) import Cassandra.Util import Control.Exception (ErrorCall (ErrorCall), throwIO) import Control.Lens ((^.), (^?!)) -import Data.Aeson (decode) import Data.Aeson.Lens qualified as Aeson import Data.Aeson.Types qualified as Aeson import Data.ByteString.Conversion (toByteString') -import Data.Default import Data.Domain import Data.Id import Data.Json.Util (toUTCTimeMillis) -import Data.Qualified (Qualified (qDomain, qUnqualified)) +import Data.Qualified import Data.Set qualified as Set import Data.Time import GHC.TypeLits (KnownSymbol) import Imports -import Servant.API (ToHttpApiData (toUrlPiece)) -import Test.QuickCheck (Arbitrary (arbitrary), generate) import Test.Tasty import Test.Tasty.HUnit -import UnliftIO (withSystemTempDirectory) import Util import Util.Options (Endpoint) import Wire.API.Connection qualified as Conn -import Wire.API.MLS.Credential -import Wire.API.MLS.KeyPackage import Wire.API.Routes.Internal.Brig import Wire.API.Team.Feature import Wire.API.Team.Feature qualified as ApiFt import Wire.API.Team.Member qualified as Team import Wire.API.User -import Wire.API.User.Client tests :: Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Gundeck -> Galley -> IO TestTree tests opts mgr db brig brigep gundeck galley = do @@ -77,15 +67,6 @@ tests opts mgr db brig brigep gundeck galley = do test mgr "suspend and unsuspend user" $ testSuspendUser db brig, test mgr "suspend non existing user and verify no db entry" $ testSuspendNonExistingUser db brig, - test mgr "mls/clients" $ testGetMlsClients brig, - testGroup - "mls/key-packages" - $ [ test mgr "fresh get" $ testKpcFreshGet brig, - test mgr "put,get" $ testKpcPutGet brig, - test mgr "get,get" $ testKpcGetGet brig, - test mgr "put,put" $ testKpcPutPut brig, - test mgr "add key package ref" $ testAddKeyPackageRef brig - ], test mgr "writetimeToInt64" $ testWritetimeRepresentation opts mgr db brig brigep galley, test mgr "delete-federation-remote-galley" $ testDeleteFederationRemoteGalley db brig ] @@ -300,143 +281,6 @@ testFeatureConferenceCallingByAccount (Opt.optSettings -> settings) mgr db brig check $ ApiFt.WithStatusNoLock ApiFt.FeatureStatusDisabled ApiFt.ConferenceCallingConfig ApiFt.FeatureTTLUnlimited check' -testGetMlsClients :: Brig -> Http () -testGetMlsClients brig = do - qusr <- userQualifiedId <$> randomUser brig - c <- createClient brig qusr 0 - (cs0 :: Set ClientInfo) <- - responseJsonError - =<< get - ( brig - . paths ["i", "mls", "clients", toByteString' (qUnqualified qusr)] - . queryItem "sig_scheme" "ed25519" - ) - liftIO $ toList cs0 @?= [ClientInfo c False] - - withSystemTempDirectory "mls" $ \tmp -> - uploadKeyPackages brig tmp def qusr c 2 - - (cs1 :: Set ClientInfo) <- - responseJsonError - =<< get - ( brig - . paths ["i", "mls", "clients", toByteString' (qUnqualified qusr)] - . queryItem "sig_scheme" "ed25519" - ) - liftIO $ toList cs1 @?= [ClientInfo c True] - -keyPackageCreate :: (HasCallStack) => Brig -> Http KeyPackageRef -keyPackageCreate brig = do - uid <- userQualifiedId <$> randomUser brig - clid <- createClient brig uid 0 - withSystemTempDirectory "mls" $ \tmp -> - uploadKeyPackages brig tmp def uid clid 2 - - uid2 <- userQualifiedId <$> randomUser brig - claimResp <- - post - ( brig - . paths - [ "mls", - "key-packages", - "claim", - toByteString' (qDomain uid), - toByteString' (qUnqualified uid) - ] - . zUser (qUnqualified uid2) - . contentJson - ) - liftIO $ - assertEqual "POST mls/key-packages/claim/:domain/:user failed" 200 (statusCode claimResp) - case responseBody claimResp >>= decode of - Nothing -> liftIO $ assertFailure "Claim response empty" - Just bundle -> case toList $ kpbEntries bundle of - [] -> liftIO $ assertFailure "Claim response held no bundles" - (h : _) -> pure $ kpbeRef h - -kpcPut :: (HasCallStack) => Brig -> KeyPackageRef -> Qualified ConvId -> Http () -kpcPut brig ref qConv = do - resp <- - put - ( brig - . paths ["i", "mls", "key-packages", toByteString' $ toUrlPiece ref, "conversation"] - . contentJson - . json qConv - ) - liftIO $ assertEqual "PUT i/mls/key-packages/:ref/conversation failed" 204 (statusCode resp) - -kpcGet :: (HasCallStack) => Brig -> KeyPackageRef -> Http (Maybe (Qualified ConvId)) -kpcGet brig ref = do - resp <- - get (brig . paths ["i", "mls", "key-packages", toByteString' $ toUrlPiece ref, "conversation"]) - liftIO $ case statusCode resp of - 404 -> pure Nothing - 200 -> pure $ responseBody resp >>= decode - _ -> assertFailure "GET i/mls/key-packages/:ref/conversation failed" - -testKpcFreshGet :: Brig -> Http () -testKpcFreshGet brig = do - ref <- keyPackageCreate brig - mqConv <- kpcGet brig ref - liftIO $ assertEqual "(fresh) Get ~= Nothing" Nothing mqConv - -testKpcPutGet :: Brig -> Http () -testKpcPutGet brig = do - ref <- keyPackageCreate brig - qConv <- liftIO $ generate arbitrary - kpcPut brig ref qConv - mqConv <- kpcGet brig ref - liftIO $ assertEqual "Put x; Get ~= x" (Just qConv) mqConv - -testKpcGetGet :: Brig -> Http () -testKpcGetGet brig = do - ref <- keyPackageCreate brig - liftIO (generate arbitrary) >>= kpcPut brig ref - mqConv1 <- kpcGet brig ref - mqConv2 <- kpcGet brig ref - liftIO $ assertEqual "Get; Get ~= Get" mqConv1 mqConv2 - -testKpcPutPut :: Brig -> Http () -testKpcPutPut brig = do - ref <- keyPackageCreate brig - qConv <- liftIO $ generate arbitrary - qConv2 <- liftIO $ generate arbitrary - kpcPut brig ref qConv - kpcPut brig ref qConv2 - mqConv <- kpcGet brig ref - liftIO $ assertEqual "Put x; Put y ~= Put y" (Just qConv2) mqConv - -testAddKeyPackageRef :: Brig -> Http () -testAddKeyPackageRef brig = do - ref <- keyPackageCreate brig - qcnv <- liftIO $ generate arbitrary - qusr <- liftIO $ generate arbitrary - c <- liftIO $ generate arbitrary - put - ( brig - . paths ["i", "mls", "key-packages", toByteString' $ toUrlPiece ref] - . json - NewKeyPackageRef - { nkprUserId = qusr, - nkprClientId = c, - nkprConversation = qcnv - } - ) - !!! const 201 === statusCode - ci <- - responseJsonError - =<< get (brig . paths ["i", "mls", "key-packages", toByteString' $ toUrlPiece ref]) - (Request -> Request) -> UserId -> m ResponseLBS getFeatureConfig galley uid = do get $ apiVersion "v1" . galley . paths ["feature-configs", featureNameBS @cfg] . zUser uid diff --git a/services/brig/test/integration/API/MLS.hs b/services/brig/test/integration/API/MLS.hs index 7fc652e8119..392d941bd0d 100644 --- a/services/brig/test/integration/API/MLS.hs +++ b/services/brig/test/integration/API/MLS.hs @@ -17,221 +17,10 @@ module API.MLS where -import API.MLS.Util import Bilge -import Bilge.Assert import Brig.Options -import Data.Aeson qualified as Aeson -import Data.ByteString.Conversion -import Data.Default -import Data.Id -import Data.Qualified -import Data.Set qualified as Set -import Data.Timeout -import Federation.Util -import Imports import Test.Tasty -import Test.Tasty.HUnit -import UnliftIO.Temporary import Util -import Web.HttpApiData -import Wire.API.MLS.Credential -import Wire.API.MLS.KeyPackage -import Wire.API.MLS.Serialisation -import Wire.API.User -import Wire.API.User.Client tests :: Manager -> Brig -> Opts -> TestTree -tests m b opts = - testGroup - "MLS" - [ test m "POST /mls/key-packages/self/:client" (testKeyPackageUpload b), - test m "POST /mls/key-packages/self/:client (no public keys)" (testKeyPackageUploadNoKey b), - test m "GET /mls/key-packages/self/:client/count" (testKeyPackageZeroCount b), - test m "GET /mls/key-packages/self/:client/count (expired package)" (testKeyPackageExpired b), - test m "GET /mls/key-packages/claim/local/:user" (testKeyPackageClaim b), - test m "GET /mls/key-packages/claim/local/:user - self claim" (testKeyPackageSelfClaim b), - test m "GET /mls/key-packages/claim/remote/:user" (testKeyPackageRemoteClaim opts b) - ] - -testKeyPackageUpload :: Brig -> Http () -testKeyPackageUpload brig = do - u <- userQualifiedId <$> randomUser brig - c <- createClient brig u 0 - withSystemTempDirectory "mls" $ \tmp -> - uploadKeyPackages brig tmp def u c 5 - - count <- getKeyPackageCount brig u c - liftIO $ count @?= 5 - -testKeyPackageUploadNoKey :: Brig -> Http () -testKeyPackageUploadNoKey brig = do - u <- userQualifiedId <$> randomUser brig - c <- createClient brig u 0 - withSystemTempDirectory "mls" $ \tmp -> - uploadKeyPackages brig tmp def {kiSetKey = DontSetKey} u c 5 - - count <- getKeyPackageCount brig u c - liftIO $ count @?= 0 - -testKeyPackageZeroCount :: Brig -> Http () -testKeyPackageZeroCount brig = do - u <- userQualifiedId <$> randomUser brig - c <- randomClient - count <- getKeyPackageCount brig u c - liftIO $ count @?= 0 - -testKeyPackageExpired :: Brig -> Http () -testKeyPackageExpired brig = do - u <- userQualifiedId <$> randomUser brig - let lifetime = 3 # Second - [c1, c2] <- for [(0, Just lifetime), (1, Nothing)] $ \(i, lt) -> do - c <- createClient brig u i - -- upload 1 key package for each client - withSystemTempDirectory "mls" $ \tmp -> - uploadKeyPackages brig tmp def {kiLifetime = lt} u c 1 - pure c - for_ [(c1, 1), (c2, 1)] $ \(cid, expectedCount) -> do - count <- getKeyPackageCount brig u cid - liftIO $ count @?= expectedCount - -- wait for c1's key package to expire - threadDelay (fromIntegral ((lifetime + 4 # Second) #> MicroSecond)) - - -- c1's key package has expired by now - for_ [(c1, 0), (c2, 1)] $ \(cid, expectedCount) -> do - count <- getKeyPackageCount brig u cid - liftIO $ count @?= expectedCount - -testKeyPackageClaim :: Brig -> Http () -testKeyPackageClaim brig = do - -- setup a user u with two clients c1 and c2 - u <- userQualifiedId <$> randomUser brig - [c1, c2] <- for [0, 1] $ \i -> do - c <- createClient brig u i - -- upload 3 key packages for each client - withSystemTempDirectory "mls" $ \tmp -> - uploadKeyPackages brig tmp def u c 3 - pure c - - -- claim packages for both clients of u - u' <- userQualifiedId <$> randomUser brig - bundle <- - responseJsonError - =<< post - ( brig - . paths ["mls", "key-packages", "claim", toByteString' (qDomain u), toByteString' (qUnqualified u)] - . zUser (qUnqualified u') - ) - (kpbeUser e, kpbeClient e)) (kpbEntries bundle) @?= Set.fromList [(u, c1), (u, c2)] - checkMapping brig u bundle - - -- check that we have one fewer key package now - for_ [c1, c2] $ \c -> do - count <- getKeyPackageCount brig u c - liftIO $ count @?= 2 - -testKeyPackageSelfClaim :: Brig -> Http () -testKeyPackageSelfClaim brig = do - -- setup a user u with two clients c1 and c2 - u <- userQualifiedId <$> randomUser brig - [c1, c2] <- for [0, 1] $ \i -> do - c <- createClient brig u i - -- upload 3 key packages for each client - withSystemTempDirectory "mls" $ \tmp -> - uploadKeyPackages brig tmp def u c 3 - pure c - - -- claim own packages but skip the first - do - bundle <- - responseJsonError - =<< post - ( brig - . paths ["mls", "key-packages", "claim", toByteString' (qDomain u), toByteString' (qUnqualified u)] - . queryItem "skip_own" (toByteString' c1) - . zUser (qUnqualified u) - ) - (kpbeUser e, kpbeClient e)) (kpbEntries bundle) @?= Set.fromList [(u, c2)] - - -- check that we still have all keypackages for client c1 - count <- getKeyPackageCount brig u c1 - liftIO $ count @?= 3 - - -- if another user sets skip_own, nothing is skipped - do - u' <- userQualifiedId <$> randomUser brig - bundle <- - responseJsonError - =<< post - ( brig - . paths ["mls", "key-packages", "claim", toByteString' (qDomain u), toByteString' (qUnqualified u)] - . queryItem "skip_own" (toByteString' c1) - . zUser (qUnqualified u') - ) - (kpbeUser e, kpbeClient e)) (kpbEntries bundle) @?= Set.fromList [(u, c1), (u, c2)] - - -- check package counts again - for_ [(c1, 2), (c2, 1)] $ \(c, n) -> do - count <- getKeyPackageCount brig u c - liftIO $ count @?= n - -testKeyPackageRemoteClaim :: Opts -> Brig -> Http () -testKeyPackageRemoteClaim opts brig = do - u <- fakeRemoteUser - - u' <- userQualifiedId <$> randomUser brig - - qcid <- mkClientIdentity u <$> randomClient - entries <- withSystemTempDirectory "mls" $ \tmp -> do - initStore tmp qcid - replicateM 2 $ do - (r, kp) <- generateKeyPackage tmp qcid Nothing - pure $ - KeyPackageBundleEntry - { kpbeUser = u, - kpbeClient = ciClient qcid, - kpbeRef = kp, - kpbeKeyPackage = KeyPackageData . rmRaw $ r - } - let mockBundle = KeyPackageBundle (Set.fromList entries) - (bundle :: KeyPackageBundle, _reqs) <- - liftIO . withTempMockFederator opts (Aeson.encode mockBundle) $ - responseJsonError - =<< post - ( brig - . paths ["mls", "key-packages", "claim", toByteString' (qDomain u), toByteString' (qUnqualified u)] - . zUser (qUnqualified u') - ) - Qualified UserId -> KeyPackageBundle -> Http () -checkMapping brig u bundle = - for_ (kpbEntries bundle) $ \e -> do - cid <- - responseJsonError - =<< get (brig . paths ["i", "mls", "key-packages", toHeader (kpbeRef e)]) - Qualified UserId -> Int -> Http ClientId -createClient brig u i = - fmap clientId $ - responseJsonError - =<< addClient - brig - (qUnqualified u) - (defNewClient PermanentClientType [somePrekeys !! i] (someLastPrekeys !! i)) - Brig -> Http () -claimRemoteKeyPackages brig1 brig2 = do - alice <- userQualifiedId <$> randomUser brig1 - - bob <- userQualifiedId <$> randomUser brig2 - bobClients <- for (take 3 someLastPrekeys) $ \lpk -> do - let new = defNewClient PermanentClientType [] lpk - fmap clientId $ responseJsonError =<< addClient brig2 (qUnqualified bob) new - - withSystemTempDirectory "mls" $ \tmp -> - for_ bobClients $ \c -> - uploadKeyPackages brig2 tmp def bob c 5 - - bundle <- - responseJsonError - =<< post - ( brig1 - . paths ["mls", "key-packages", "claim", toByteString' (qDomain bob), toByteString' (qUnqualified bob)] - . zUser (qUnqualified alice) - ) - (kpbeUser e, kpbeClient e)) (kpbEntries bundle) - @?= Set.fromList [(bob, c) | c <- bobClients] - testRemoteTypingIndicator :: Brig -> Brig -> Galley -> Galley -> Cannon -> Cannon -> Http () testRemoteTypingIndicator brig1 brig2 galley1 galley2 cannon1 cannon2 = do alice <- randomUser brig1 diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 6ea0853f8ba..ed901d4001f 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -18,7 +18,6 @@ module Galley.API.Public.Conversation where import Galley.API.Create -import Galley.API.MLS.GroupInfo import Galley.API.MLS.Types import Galley.API.Query import Galley.API.Update @@ -35,7 +34,6 @@ conversationAPI = <@> mkNamedAPI @"get-conversation@v2" (callsFed (exposeAnnotations getConversation)) <@> mkNamedAPI @"get-conversation" (callsFed (exposeAnnotations getConversation)) <@> mkNamedAPI @"get-conversation-roles" getConversationRoles - <@> mkNamedAPI @"get-group-info" (callsFed (exposeAnnotations getGroupInfo)) <@> mkNamedAPI @"list-conversation-ids-unqualified" conversationIdsPageFromUnqualified <@> mkNamedAPI @"list-conversation-ids-v2" (conversationIdsPageFromV2 DoNotListGlobalSelf) <@> mkNamedAPI @"list-conversation-ids" conversationIdsPageFrom @@ -49,7 +47,6 @@ conversationAPI = <@> mkNamedAPI @"create-group-conversation" (callsFed (exposeAnnotations createGroupConversation)) <@> mkNamedAPI @"create-self-conversation@v2" createProteusSelfConversation <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation - <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError <@> mkNamedAPI @"create-one-to-one-conversation@v2" (callsFed createOne2OneConversation) <@> mkNamedAPI @"create-one-to-one-conversation" (callsFed createOne2OneConversation) <@> mkNamedAPI @"add-members-to-conversation-unqualified" (callsFed addMembersUnqualified) diff --git a/services/galley/src/Galley/API/Public/MLS.hs b/services/galley/src/Galley/API/Public/MLS.hs index 73187b06da9..884a5e8b865 100644 --- a/services/galley/src/Galley/API/Public/MLS.hs +++ b/services/galley/src/Galley/API/Public/MLS.hs @@ -17,16 +17,10 @@ module Galley.API.Public.MLS where -import Galley.API.MLS import Galley.App -import Wire.API.Federation.API +import Servant import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.MLS mlsAPI :: API MLSAPI GalleyEffects -mlsAPI = - mkNamedAPI @"mls-welcome-message" (callsFed (exposeAnnotations postMLSWelcomeFromLocalUser)) - <@> mkNamedAPI @"mls-message-v1" (callsFed (exposeAnnotations postMLSMessageFromLocalUserV1)) - <@> mkNamedAPI @"mls-message" (callsFed (exposeAnnotations postMLSMessageFromLocalUser)) - <@> mkNamedAPI @"mls-commit-bundle" (callsFed (exposeAnnotations postMLSCommitBundleFromLocalUser)) - <@> mkNamedAPI @"mls-public-keys" getMLSPublicKeys +mlsAPI = mkAPI emptyServer From 6ba6cca273fe2a1cfb1aed85d688b12e7751802a Mon Sep 17 00:00:00 2001 From: fisx Date: Thu, 7 Sep 2023 13:59:22 +0200 Subject: [PATCH 117/225] Fix: SCIM user lookup after changing IdP issuer ID (#3473) --- .../src/Wire/API/User/IdentityProvider.hs | 7 + .../test-integration/Test/Spar/APISpec.hs | 126 ++++++++++++++---- services/spar/test-integration/Util/Core.hs | 7 + services/spar/test-integration/Util/Scim.hs | 6 + 4 files changed, 117 insertions(+), 29 deletions(-) diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs index e17db47e465..f45a6f991ea 100644 --- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs +++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs @@ -24,6 +24,7 @@ import Control.Lens (makeLenses, (.~), (?~)) import Control.Monad.Except import Data.Aeson import Data.Aeson.TH +import Data.Aeson.Types (parseMaybe) import Data.Attoparsec.ByteString qualified as AP import Data.Binary.Builder qualified as BSB import Data.ByteString.Conversion qualified as BSC @@ -169,6 +170,12 @@ instance ToJSON IdPMetadataInfo where toJSON (IdPMetadataValue _ x) = object ["value" .= SAML.encode x] +idPMetadataToInfo :: SAML.IdPMetadata -> IdPMetadataInfo +idPMetadataToInfo = + -- 'undefined' is fine because `instance toJSON IdPMetadataValue` ignores it. 'fromJust' is + -- ok as long as 'parseJSON . toJSON' always yields a value and not 'Nothing'. + fromJust . parseMaybe parseJSON . toJSON . IdPMetadataValue undefined + -- Swagger instances -- Same as WireIdP, check there for why this has different handling diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 2da4aea9ea1..8e10682c2a3 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -81,10 +81,11 @@ import Text.XML.DSig (SignPrivCreds, mkSignCredsWithCert) import qualified URI.ByteString as URI import URI.ByteString.QQ (uri) import Util.Core -import Util.Scim (filterBy, listUsers, registerScimToken) +import Util.Scim (createUser, filterBy, listUsers, randomScimUser, randomScimUserWithEmail, registerScimToken) import qualified Util.Scim as ScimT import Util.Types import qualified Web.Cookie as Cky +import qualified Web.Scim.Class.User as Scim import qualified Web.Scim.Schema.User as Scim import Wire.API.Team.Member (newTeamMemberDeleteData) import Wire.API.Team.Permission hiding (self) @@ -1054,34 +1055,99 @@ specCRUDIdentityProvider = do idp `shouldBe` idp' let prefix = " IdP - erase = - (idpId .~ (idp1 ^. idpId)) - . (idpMetadata . edIssuer .~ (idp1 ^. idpMetadata . edIssuer)) - . (idpExtraInfo . oldIssuers .~ (idp1 ^. idpExtraInfo . oldIssuers)) - . (idpExtraInfo . replacedBy .~ (idp1 ^. idpExtraInfo . replacedBy)) - . (idpExtraInfo . handle .~ (idp1 ^. idpExtraInfo . handle)) - erase idp1 `shouldBe` erase idp2 + + describe "replaces an existing idp" + $ forM_ + [ (h, u, e) + | h <- [False, True], -- are users scim provisioned or via team management invitations? + u <- [False, True], -- do we use update-by-put or update-by-post? (see below) + (h, u) /= (True, False), -- scim doesn't not work with more than one idp (https://wearezeta.atlassian.net/browse/WPB-689) + e <- [False, True], -- is the externalId an email address? (if not, it's a uuidv4, and the email address is stored in `emails`) + (u, u, e) /= (True, True, False) -- TODO: this combination fails, see https://github.com/wireapp/wire-server/pull/3563) + ] + $ \(haveScim, updateNotReplace, externalIdIsEmail) -> do + it ("creates new idp, setting old_issuer; sets replaced_by in old idp; scim user search still works " <> show (haveScim, updateNotReplace, externalIdIsEmail)) $ do + env <- ask + (owner1, teamid, idp1, (IdPMetadataValue _ idpmeta1, _privCreds)) <- registerTestIdPWithMeta + let idp1id = idp1 ^. idpId + + mbScimStuff :: Maybe (ScimToken, Scim.StoredUser SparTag, Scim.User SparTag) <- + if haveScim + then do + tok <- registerScimToken teamid (Just idp1id) + user <- + if externalIdIsEmail + then fst <$> randomScimUserWithEmail + else randomScimUser + scimStoredUser <- createUser tok user + pure $ Just (tok, scimStoredUser, user) + else pure Nothing + + let checkScimSearch :: + HasCallStack => + (ScimToken, Scim.StoredUser SparTag, Scim.User SparTag) -> + ReaderT TestEnv IO () + checkScimSearch (tok, target, searchKeys) = do + let Just externalId = Scim.externalId searchKeys + handle' = Scim.userName searchKeys + respId <- listUsers tok (Just (filterBy "externalId" externalId)) + respHandle <- listUsers tok (Just (filterBy "userName" handle')) + liftIO $ do + respId `shouldBe` [target] + respHandle `shouldBe` [target] + + checkScimSearch `mapM_` mbScimStuff + + issuer2 <- makeIssuer + idp2 <- do + let idpmeta2 = idpmeta1 & edIssuer .~ issuer2 + in call $ + -- There are two mechanisms for re-aligning your team when your IdP metadata + -- has changed: POST (create a new one, and mark it as replacing the old one), + -- and PUT (updating the existing IdP's metadata). The reason for having two + -- ways to do this has been lost in history, but we're testing both here. + -- + -- FUTUREWORK: deprecate POST! + if updateNotReplace + then callIdpUpdate' (env ^. teSpar) (Just owner1) (idp1 ^. SAML.idpId) (idPMetadataToInfo idpmeta2) + else callIdpCreateReplace (env ^. teWireIdPAPIVersion) (env ^. teSpar) (Just owner1) idpmeta2 (idp1 ^. SAML.idpId) + + idp1' <- call $ callIdpGet (env ^. teSpar) (Just owner1) (idp1 ^. SAML.idpId) + idp2' <- call $ callIdpGet (env ^. teSpar) (Just owner1) (idp2 ^. SAML.idpId) + liftIO $ do + let updateIdp1 = updateCurrentIssuer . updateOldIssuers + where + updateCurrentIssuer = idpMetadata . edIssuer .~ (idp2' ^. idpMetadata . edIssuer) + updateOldIssuers = idpExtraInfo . oldIssuers .~ [idp1 ^. idpMetadata . edIssuer] + replaceIdp1 = + idpExtraInfo . replacedBy .~ idp1' ^. idpExtraInfo . replacedBy + in idp1' `shouldBe` (idp1 & if updateNotReplace then updateIdp1 else replaceIdp1) + + idp2' `shouldBe` idp2 + idp1 ^. idpMetadata . SAML.edIssuer `shouldBe` (idpmeta1 ^. SAML.edIssuer) + idp2 ^. idpMetadata . SAML.edIssuer `shouldBe` issuer2 + + if updateNotReplace + then idp2 ^. idpId `shouldBe` idp1 ^. idpId + else idp2 ^. idpId `shouldNotBe` idp1 ^. idpId + + idp2 ^. idpExtraInfo . oldIssuers `shouldBe` [idpmeta1 ^. edIssuer] + idp1' ^. idpExtraInfo . replacedBy `shouldBe` if updateNotReplace then Nothing else Just (idp2 ^. idpId) + + -- erase everything that is supposed to be different between idp1, idp2, and make + -- sure the result is equal. + let erase :: IdP -> IdP + erase = + (idpId .~ (idp1 ^. idpId)) + . (idpMetadata . edIssuer .~ (idp1 ^. idpMetadata . edIssuer)) + . (idpExtraInfo . oldIssuers .~ (idp1 ^. idpExtraInfo . oldIssuers)) + . (idpExtraInfo . replacedBy .~ (idp1 ^. idpExtraInfo . replacedBy)) + . (idpExtraInfo . handle .~ (idp1 ^. idpExtraInfo . handle)) + in erase idp1 `shouldBe` erase idp2 + + checkScimSearch `mapM_` mbScimStuff + + describe "replaces an existing idp (cont.)" $ do it "users can still login on old idp as before" $ do env <- ask (owner1, _, idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta @@ -1100,6 +1166,7 @@ specCRUDIdentityProvider = do olduid `shouldBe` newuid (olduref ^. SAML.uidTenant) `shouldBe` issuer1 (newuref ^. SAML.uidTenant) `shouldBe` issuer1 + it "migrates old users to new idp on their next login on new idp; after that, login on old won't work any more" $ do env <- ask (owner1, _, idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta @@ -1120,6 +1187,7 @@ specCRUDIdentityProvider = do (olduref ^. SAML.uidTenant) `shouldBe` issuer1 (newuref ^. SAML.uidTenant) `shouldBe` issuer2 tryLoginFail privkey1 idp1 userSubject "cannont-provision-on-replaced-idp" + it "creates non-existent users on new idp" $ do env <- ask (owner1, _, idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 2a13b750abf..3298ca8047f 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -111,6 +111,7 @@ module Util.Core callIdpCreateReplace, callIdpCreateReplace', callIdpCreateWithHandle, + callIdpUpdate', callIdpUpdate, callIdpUpdateWithHandle, callIdpDelete, @@ -1164,6 +1165,12 @@ callIdpCreateReplace' apiversion sparreq_ muid metadata idpid = do . body (RequestBodyLBS . cs $ SAML.encode metadata) . header "Content-Type" "application/xml" +callIdpUpdate' :: (Monad m, MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> IdPId -> IdPMetadataInfo -> m IdP +callIdpUpdate' sparreq_ muid idpid metainfo = do + resp <- callIdpUpdate (sparreq_ . expect2xx) muid idpid metainfo + either (liftIO . throwIO . ErrorCall . show) pure $ + responseJsonEither @IdP resp + callIdpUpdate :: MonadHttp m => SparReq -> Maybe UserId -> IdPId -> IdPMetadataInfo -> m ResponseLBS callIdpUpdate sparreq_ muid idpid (IdPMetadataValue metadata _) = do put $ diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index ef714ed42e3..eeac183d19d 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -153,6 +153,12 @@ randomScimUserWithSubjectAndRichInfo richInfo = do subj ) +-- | Use the email address as externalId. +-- +-- FUTUREWORK: since https://wearezeta.atlassian.net/browse/SQSERVICES-157 is done, we also +-- support externalIds that are not emails, and storing email addresses in `emails` in the +-- scim schema. `randomScimUserWithEmail` is from a time where non-idp-authenticated users +-- could only be provisioned with email as externalId. we should probably rework all that. randomScimUserWithEmail :: MonadRandom m => m (Scim.User.User SparTag, Email) randomScimUserWithEmail = do suffix <- cs <$> replicateM 7 (getRandomR ('0', '9')) From b7213e3e30e8d538a7f434a7e97b739903a98481 Mon Sep 17 00:00:00 2001 From: Thomas Belin Date: Thu, 7 Sep 2023 15:49:09 +0200 Subject: [PATCH 118/225] doc: document webapp configuration for multi-ingress environments (#3569) --------- Co-authored-by: Sven Tennie --- .../src/developer/reference/config-options.md | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 3d9dc14c4f4..d92d461479b 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -764,4 +764,45 @@ Example: multiIngress: red.example.com: https://accounts.red.example.com/conversation-join/ green.example.com: https://accounts.green.example.net/conversation-join/ -``` \ No newline at end of file +``` + +### Webapp + +The webapp runs its own web server (a NodeJS server) to serve static files and the webapp config (based on environment variables). +In a multi-ingress configuration, a single webapp instance will be deployed and be accessible from multiple domains (say `webapp.red.example.com` and `webapp.green.example.com`). +When the webapp is loaded from one of those domains it first does a request to the web server to get the config (that will give it, for example, the backend endpoint that it should hit). + +Because of the single instance nature of the webapp, by default the configuration is static and the root url to the backend API can be set there (say `nginz-https.root.example.com`). +In order to completely hide this root domain to the webapp, an environment variable can be set to allow the webapp hostname to be used to generate the API endpoint, team settings links, account page links and CSP headers. + +The "hostname" is the result of the domain name minus the `webapp.` part of it. +So querying the webapp on `webapp.red.example.com` will resolve to `red.example.com`. + +To enable dynamic hostname replacement, first set this variable: + +``` +ENABLE_DYNAMIC_HOSTNAME="true" +``` + +Then, any other variable that will contain the string `[[hostname]]` will be replaced by the hostname of the running webapp. (eg. if a webapp is running on `webapp.red.example.com` then any occurrence of `[[hostname]]` in the config will be replaced by `red.example.com`). + +You may use the template variable `[[hostname]]` in any environment variable to not provide (reveal) actual domain names. + +For example: + +``` +APP_BASE: https://[[hostname]] +BACKEND_REST: https://nginz-https.[[hostname]] +BACKEND_WS: wss://nginz-ssl.[[hostname]] +CSP_EXTRA_CONNECT_SRC: https://*.[[hostname]], wss://*.[[hostname]] +CSP_EXTRA_DEFAULT_SRC: https://*.[[hostname]] +CSP_EXTRA_FONT_SRC: https://*.[[hostname]] +CSP_EXTRA_FRAME_SRC: https://*.[[hostname]] +CSP_EXTRA_IMG_SRC: https://*.[[hostname]] +CSP_EXTRA_MANIFEST_SRC: https://*.[[hostname]] +CSP_EXTRA_MEDIA_SRC: https://*.[[hostname]] +CSP_EXTRA_PREFETCH_SRC: https://*.[[hostname]] +CSP_EXTRA_SCRIPT_SRC: https://*.[[hostname]] +CSP_EXTRA_STYLE_SRC: https://*.[[hostname]] +CSP_EXTRA_WORKER_SRC: https://*.[[hostname]] +``` From bed7c08646e6865671df68c08dc699e944ae7c65 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 8 Sep 2023 14:12:29 +0200 Subject: [PATCH 119/225] [WPB-4361] upgrade jwt-tools (#3559) --- libs/jwt-tools/test/Spec.hs | 22 +++++++++++++------ nix/overlay.nix | 7 +++++- nix/pkgs/rusty_jwt_tools_ffi/default.nix | 6 ++--- nix/sources.json | 12 ++++++++++ services/brig/brig.cabal | 1 - services/brig/default.nix | 1 - .../brig/test/integration/API/User/Client.hs | 14 +++++++----- 7 files changed, 44 insertions(+), 19 deletions(-) diff --git a/libs/jwt-tools/test/Spec.hs b/libs/jwt-tools/test/Spec.hs index 7843d45dece..a2881bc5328 100644 --- a/libs/jwt-tools/test/Spec.hs +++ b/libs/jwt-tools/test/Spec.hs @@ -24,11 +24,19 @@ main :: IO () main = hspec $ do describe "generateDpopToken FFI when passing valid inputs" $ do it "should return an access token" $ do + -- FUTUREWORK(leif): fix this test, we need new valid test data, + -- this test exists mainly for debugging purposes + -- a functionality test is also coverd in the integration tests in services/brig/test/integration/API/User/Client.hs (`testCreateAccessToken`) + pending actual <- runExceptT $ generateDpopToken proof uid cid domain nonce uri method maxSkewSecs expires now pem print actual isRight actual `shouldBe` True describe "generateDpopToken FFI when passing a wrong nonce value" $ do it "should return BackendNonceMismatchError" $ do + -- FUTUREWORK(leif): fix this test, we need new valid test data, + -- this test exists mainly for debugging purposes + -- a functionality test is also coverd in the integration tests in services/brig/test/integration/API/User/Client.hs (`testCreateAccessToken`) + pending actual <- runExceptT $ generateDpopToken proof uid cid domain (Nonce "foobar") uri method maxSkewSecs expires now pem actual `shouldBe` Left BackendNonceMismatchError describe "toResult" $ do @@ -73,16 +81,16 @@ main = hspec $ do toResult Nothing Nothing `shouldBe` Left UnknownError where token = "" - proof = Proof "eyJhbGciOiJFZERTQSIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoidUhNR0paWllUbU9zOEdiaTdaRUJLT255TnJYYnJzNTI1dE1QQUZoYjBzbyJ9fQ.eyJpYXQiOjE2Nzg4MDUyNTgsImV4cCI6MjA4ODc3MzI1OCwibmJmIjoxNjc4ODA1MjU4LCJzdWIiOiJpbTp3aXJlYXBwPVpHSmlNRGRsT1RRM1pESTVOREU0TUdFM09UQmhOVGN6WkdWbU16VmtaRFUvN2M2MzExYTFjNDNjMmJhNkB3aXJlLmNvbSIsImp0aSI6ImQyOWFkYTQ2LTBjMzYtNGNiMS05OTVlLWFlMWNiYTY5M2IzNCIsIm5vbmNlIjoiYzB0RWNtOUNUME00TXpKU04zRjRkMEZIV0V4TGIxUm5aMDQ1U3psSFduTSIsImh0bSI6IlBPU1QiLCJodHUiOiJodHRwczovL3dpcmUuZXhhbXBsZS5jb20vY2xpZW50cy84OTYzMDI3MDY5ODc3MTAzNTI2L2FjY2Vzcy10b2tlbiIsImNoYWwiOiJaa3hVV25GWU1HbHFUVVpVU1hnNFdHdHBOa3h1WWpWU09XRnlVRU5hVGxnIn0.8p0lvdOPjJ8ogjjLP6QtOo216qD9ujP7y9vSOhdYb-O8ikmW09N00gjCf0iGT-ZkxBT-LfDE3eQx27tWQ3JPBQ" - uid = UserId "dbb07e94-7d29-4180-a790-a573def35dd5" - cid = ClientId 8963027069877103526 + proof = Proof "eyJhbGciOiJFZERTQSIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoidXE2c1hXcDdUM1E3YlNtUFd3eFNlRHJoUHFid1RfcTd4SFBQeGpGT0g5VSJ9fQ.eyJpYXQiOjE2OTQxMTc0MjgsImV4cCI6MTY5NDcyMjIyOCwibmJmIjoxNjk0MTE3NDIzLCJzdWIiOiJpbTp3aXJlYXBwPUlHOVl2enVXUUlLVWFSazEyRjVDSVEvOGUxODk2MjZlYWUwMTExZEBlbG5hLndpcmUubGluayIsImp0aSI6ImM0OGZmOTAyLTc5OGEtNDNjYi04YTk2LTE3NzM0NTgxNjIyMCIsIm5vbmNlIjoiR0FxNG5SajlSWVNzUnhoOVh1MWFtQSIsImh0bSI6IlBPU1QiLCJodHUiOiJodHRwczovL2VsbmEud2lyZS5saW5rL2NsaWVudHMvOGUxODk2MjZlYWUwMTExZC9hY2Nlc3MtdG9rZW4iLCJjaGFsIjoiMkxLbEFWMjR2VGtIMHlaaFdacEZrT01mSEE1d3lGQkgifQ.FW5i40CvndSSo3wQdA1DMUkGRmxk86cORAllwC2PCejVuk7TsdZuIKuJZFVa1VTJKWwNCPqPZ05Gsxxeh1DiDA" + uid = UserId "206f58bf-3b96-4082-9469-1935d85e4221" + cid = ClientId 10239098846720299293 domain = Domain "wire.com" - nonce = Nonce "c0tEcm9CT0M4MzJSN3F4d0FHWExLb1RnZ045SzlHWnM" - uri = Uri "https://wire.example.com/clients/8963027069877103526/access-token" + nonce = Nonce "GAq4nRj9RYSsRxh9Xu1amA" + uri = Uri "https://elna.wire.link/clients/10239098846720299293/access-token" method = POST maxSkewSecs = MaxSkewSecs 5 - now = NowEpoch 5435234232 - expires = ExpiryEpoch $ 2136351646 + now = NowEpoch 360 + expires = ExpiryEpoch 2136351646 pem = PemBundle $ "-----BEGIN PRIVATE KEY-----\n\ diff --git a/nix/overlay.nix b/nix/overlay.nix index 3bcd85b5a18..4d533dea9c8 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -49,15 +49,20 @@ let ''; }; + sources = import ./sources.nix; + pkgsCargo = import sources.nixpkgs-cargo {}; in self: super: { + cryptobox = self.callPackage ./pkgs/cryptobox { }; zauth = self.callPackage ./pkgs/zauth { }; mls-test-cli = self.callPackage ./pkgs/mls-test-cli { }; # Named like this so cabal2nix can find it - rusty_jwt_tools_ffi = self.callPackage ./pkgs/rusty_jwt_tools_ffi { }; + rusty_jwt_tools_ffi = self.callPackage ./pkgs/rusty_jwt_tools_ffi { + inherit (pkgsCargo) rustPlatform; + }; nginxModules = super.nginxModules // { zauth = { diff --git a/nix/pkgs/rusty_jwt_tools_ffi/default.nix b/nix/pkgs/rusty_jwt_tools_ffi/default.nix index 6fb4b58470e..1f0764c3b7a 100644 --- a/nix/pkgs/rusty_jwt_tools_ffi/default.nix +++ b/nix/pkgs/rusty_jwt_tools_ffi/default.nix @@ -7,12 +7,12 @@ }: let - version = "0.3.4"; + version = "0.5.0"; src = fetchFromGitHub { owner = "wireapp"; repo = "rusty-jwt-tools"; - rev = "fc4569c5b84d00a5cc8fc77b450714a5261cd3d9"; - sha256 = "sha256-cZffVKfH0FzA4Eo7YVxivT3JWTwz9uu1HWhPVlvbYqM="; + rev = "6704e08376bb49168133d8f4ce66155adeb6bfb0"; + sha256 = "sha256-ocmeFXjU3psCO+hpDuEAIzYIm4QzP+jHJR/V8yyw6Lw="; }; cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/ffi/Cargo.lock"); diff --git a/nix/sources.json b/nix/sources.json index 80e326af497..e1adedd2306 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -10,5 +10,17 @@ "type": "tarball", "url": "https://github.com/NixOS/nixpkgs/archive/402cc3633cc60dfc50378197305c984518b30773.tar.gz", "url_template": "https://github.com///archive/.tar.gz" + }, + "nixpkgs-cargo": { + "branch": "nixpkgs-unstable", + "description": "Nix Packages collection", + "homepage": "https://github.com/NixOS/nixpkgs", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "efd23a1c9ae8c574e2ca923c2b2dc336797f4cc4", + "sha256": "0pb1dgdgfsnsngw2ci807wln2jnlsha4zkm1y14x497qbw4izir3", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/efd23a1c9ae8c574e2ca923c2b2dc336797f4cc4.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index b5024f2e869..c7d3826c41b 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -536,7 +536,6 @@ executable brig-integration , attoparsec , base , base16-bytestring - , base64-bytestring , bilge , bloodhound , brig diff --git a/services/brig/default.nix b/services/brig/default.nix index e5859de9282..c6b3867fcaa 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -295,7 +295,6 @@ mkDerivation { attoparsec base base16-bytestring - base64-bytestring bilge bloodhound brig-types diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index 55510064b3b..bf0aeeea81b 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -40,7 +40,6 @@ import Data.Aeson hiding (json) import Data.Aeson qualified as A import Data.Aeson.KeyMap qualified as M import Data.Aeson.Lens -import Data.ByteString.Base64.URL qualified as B64 import Data.ByteString.Conversion import Data.Coerce (coerce) import Data.Default @@ -52,10 +51,10 @@ import Data.Nonce (isValidBase64UrlEncodedUUID) import Data.Qualified (Qualified (..)) import Data.Range (unsafeRange) import Data.Set qualified as Set -import Data.Text (replace) -import Data.Text.Ascii (AsciiChars (validate)) +import Data.Text.Ascii (AsciiChars (validate), encodeBase64UrlUnpadded, toText) import Data.Time (addUTCTime) import Data.Time.Clock.POSIX +import Data.UUID (toByteString) import Data.Vector qualified as Vec import Imports import Network.Wai.Utilities.Error qualified as Error @@ -1424,22 +1423,25 @@ testCreateAccessToken opts n brig = do let localDomain = opts ^. Opt.optionSettings & Opt.setFederationDomain u <- randomUser brig let uid = userId u + -- convert the user Id into 16 octets of binary and then base64url + let uidBS = Data.UUID.toByteString (toUUID uid) + let uidB64 = encodeBase64UrlUnpadded (cs uidBS) let email = fromMaybe (error "invalid email") $ userEmail u rs <- login n (defEmailLogin email) PersistentCookie (floor <$> getPOSIXTime) - let clientIdentity = cs $ "im:wireapp=" <> uidb64 <> "/" <> toByteString' cid <> "@" <> toByteString' localDomain + let clientIdentity = cs $ "im:wireapp=" <> cs (toText uidB64) <> "/" <> toByteString' cid <> "@" <> toByteString' localDomain let httpsUrl = cs $ "https://" <> toByteString' localDomain <> "/clients/" <> toByteString' cid <> "/access-token" + let expClaim = NumericDate (addUTCTime 10 now) let claimsSet' = emptyClaimsSet & claimIat ?~ NumericDate now - & claimExp ?~ NumericDate (addUTCTime 10 now) + & claimExp ?~ expClaim & claimNbf ?~ NumericDate now & claimSub ?~ fromMaybe (error "invalid sub claim") ((clientIdentity :: Text) ^? stringOrUri) & claimJti ?~ "6fc59e7f-b666-4ffc-b738-4f4760c884ca" From 2dfc268f233cc7b42a75243f2fd0071b81aef65b Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 8 Sep 2023 15:18:13 +0200 Subject: [PATCH 120/225] cassandra: Add column and table names in parsing error messages (#3555) --- nix/manual-overrides.nix | 8 ++++++-- nix/wire-server.nix | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 74c0da56158..31b6b7ce91a 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -1,11 +1,14 @@ -{ libsodium, protobuf, hlib, mls-test-cli }: +{ libsodium, protobuf, hlib, mls-test-cli, fetchpatch }: # FUTUREWORK: Figure out a way to detect if some of these packages are not # actually marked broken, so we can cleanup this file on every nixpkgs bump. hself: hsuper: { aeson = (hlib.doJailbreak hsuper.aeson_2_1_2_1); binary-parsers = hlib.markUnbroken (hlib.doJailbreak hsuper.binary-parsers); bytestring-arbitrary = hlib.markUnbroken (hlib.doJailbreak hsuper.bytestring-arbitrary); - cql = hlib.markUnbroken hsuper.cql; + cql = hlib.appendPatch (hlib.markUnbroken hsuper.cql) (fetchpatch { + url = "https://gitlab.com/twittner/cql/-/merge_requests/11.patch"; + sha256 = "sha256-qfcCRkKjSS1TEqPRVBU9Ox2DjsdGsYG/F3DrZ5JGoEI="; + }); hashtables = hsuper.hashtables_1_3; invertible = hlib.markUnbroken hsuper.invertible; lens-datetime = hlib.markUnbroken (hlib.doJailbreak hsuper.lens-datetime); @@ -32,6 +35,7 @@ hself: hsuper: { saml2-web-sso = hlib.dontCheck hsuper.saml2-web-sso; http2 = hlib.dontCheck hsuper.http2; + # Disable tests because they need network access to a running cassandra # # Explicitly enable haddock because cabal2nix disables it for packages with diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 3b5675ba5eb..ddaa3689533 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -131,7 +131,7 @@ let tests ]; manualOverrides = import ./manual-overrides.nix (with pkgs; { - inherit hlib libsodium protobuf mls-test-cli; + inherit hlib libsodium protobuf mls-test-cli fetchpatch; }); executables = hself: hsuper: From 2b7a0f60cd1e4cd80e2d9dafe6ed08b3cd0a1e1d Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Tue, 12 Sep 2023 02:15:17 +0200 Subject: [PATCH 121/225] s/CORS/CSP/ as mentionned by Sven in WPB-2912 --- docs/src/how-to/install/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/how-to/install/troubleshooting.md b/docs/src/how-to/install/troubleshooting.md index 7aa9f804791..de2d857e4be 100644 --- a/docs/src/how-to/install/troubleshooting.md +++ b/docs/src/how-to/install/troubleshooting.md @@ -1,6 +1,6 @@ # Troubleshooting during installation -## Problems with CORS on the web based applications (webapp, team-settings, account-pages) +## Problems with CSP on the web based applications (webapp, team-settings, account-pages) If you have installed wire-server, but the web application page in your browser has connection problems and throws errors in the console such as `"Refused to connect to 'https://assets.example.com' because it violates the following Content Security Policies"`, make sure to check that you have configured the `CSP_EXTRA_` environment variables. From 8e4fd6c084f149402a7dfaedf2d6c3c1f7fdd7c9 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Tue, 12 Sep 2023 02:45:39 +0200 Subject: [PATCH 122/225] Replace broken integrations with links see WPB-3599 --- .../developer/cassandra-interaction.md | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/docs/src/developer/developer/cassandra-interaction.md b/docs/src/developer/developer/cassandra-interaction.md index 4b59eda0547..08b20cae533 100644 --- a/docs/src/developer/developer/cassandra-interaction.md +++ b/docs/src/developer/developer/cassandra-interaction.md @@ -51,26 +51,13 @@ data you inserted, even if one replica of cassandra that holds that partition ke middle. The replication factor is specified when creating or migrating schemas, which is done in the -`cassandra-migrations` subchart of the `wire-server` chart: - -```{grepinclude} ../charts/cassandra-migrations/values.yaml host name and replication ---- -lines-after: 6 -language: yaml ---- -``` +`cassandra-migrations` subchart of the `wire-server` chart: [See link](https://github.com/wireapp/wire-server/blob/develop/charts/cassandra-migrations/values.yaml#L10) The number of cassandra nodes in use is specified on the infrastructure level (k8ssandra on kubernetes, or the inventory list when using ansible-cassandra) Quorum consistency (or, in our case, `LocalQuorum` consistency) is specified in our code. Random -example: - -```{grepinclude} ../services/brig/src/Brig/Data/User.hs userEmailUpdate \(params ---- -language: Haskell ---- -``` +example: [See link](https://github.com/wireapp/wire-server/blob/develop/services/brig/src/Brig/Data/User.hs#L637) Note that `Quorum` and `LocalQuorum` behave exactly the same in the context of a single datacentre (each datacentre can have multiple racks or availability zones). We switched from `Quorum` to From 56335a2d65ab674d3897221d599a376090d0cb36 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Wed, 13 Sep 2023 02:34:51 +0200 Subject: [PATCH 123/225] replace all instances of example.com with wire.example as per wpb-2621, in charts only --- charts/account-pages/values.yaml | 32 ++++++++--------- charts/aws-ingress/values.yaml | 24 ++++++------- charts/brig/values.yaml | 4 +-- charts/calling-test/values.yaml | 2 +- charts/cargohold/values.yaml | 4 +-- charts/coturn/values.yaml | 6 ++-- charts/fake-aws-ses/values.yaml | 2 +- charts/galley/values.yaml | 2 +- charts/inbucket/values.yaml | 2 +- .../openldap/templates/secret-newusers.yaml | 10 +++--- charts/sftd/README.md | 10 +++--- charts/sftd/values.yaml | 2 +- charts/team-settings/values.yaml | 34 +++++++++---------- charts/webapp/values.yaml | 34 +++++++++---------- 14 files changed, 84 insertions(+), 84 deletions(-) diff --git a/charts/account-pages/values.yaml b/charts/account-pages/values.yaml index ae22ad9fb44..14d7f59ca54 100644 --- a/charts/account-pages/values.yaml +++ b/charts/account-pages/values.yaml @@ -20,9 +20,9 @@ service: #config: # externalUrls: -# backendRest: nginz-https.example.com -# backendWebsocket: nginz-ssl.example.com -# appHost: account.example.com +# backendRest: nginz-https.wire.example +# backendWebsocket: nginz-ssl.wire.example +# appHost: account.wire.example # Some relevant environment options. For a comprehensive # list of available variables, please refer to: @@ -36,19 +36,19 @@ envVars: {} # FEATURE_ENABLE_DEBUG: "true" # You are likely to need at least following CSP headers # due to the fact that you are likely to do cross sub-domain requests -# i.e., from account.example.com to nginz-https.example.com -# CSP_EXTRA_CONNECT_SRC: "https://*.example.com, wss://*.example.com" -# CSP_EXTRA_IMG_SRC: "https://*.example.com" -# CSP_EXTRA_SCRIPT_SRC: "https://*.example.com" -# CSP_EXTRA_DEFAULT_SRC: "https://*.example.com" -# CSP_EXTRA_FONT_SRC: "https://*.example.com" -# CSP_EXTRA_FRAME_SRC: "https://*.example.com" -# CSP_EXTRA_MANIFEST_SRC: "https://*.example.com" -# CSP_EXTRA_OBJECT_SRC: "https://*.example.com" -# CSP_EXTRA_MEDIA_SRC: "https://*.example.com" -# CSP_EXTRA_PREFETCH_SRC: "https://*.example.com" -# CSP_EXTRA_STYLE_SRC: "https://*.example.com" -# CSP_EXTRA_WORKER_SRC: "https://*.example.com" +# i.e., from account.wire.example to nginz-https.wire.example +# CSP_EXTRA_CONNECT_SRC: "https://*.wire.example, wss://*.wire.example" +# CSP_EXTRA_IMG_SRC: "https://*.wire.example" +# CSP_EXTRA_SCRIPT_SRC: "https://*.wire.example" +# CSP_EXTRA_DEFAULT_SRC: "https://*.wire.example" +# CSP_EXTRA_FONT_SRC: "https://*.wire.example" +# CSP_EXTRA_FRAME_SRC: "https://*.wire.example" +# CSP_EXTRA_MANIFEST_SRC: "https://*.wire.example" +# CSP_EXTRA_OBJECT_SRC: "https://*.wire.example" +# CSP_EXTRA_MEDIA_SRC: "https://*.wire.example" +# CSP_EXTRA_PREFETCH_SRC: "https://*.wire.example" +# CSP_EXTRA_STYLE_SRC: "https://*.wire.example" +# CSP_EXTRA_WORKER_SRC: "https://*.wire.example" podSecurityContext: allowPrivilegeEscalation: false diff --git a/charts/aws-ingress/values.yaml b/charts/aws-ingress/values.yaml index 3373fc6cc8a..eff6b721e4f 100644 --- a/charts/aws-ingress/values.yaml +++ b/charts/aws-ingress/values.yaml @@ -9,26 +9,26 @@ ingress: webapp: https: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: webapp.example.com + hostname: webapp.wire.example ttl: 300 http: webappPort: 8080 nginz: https: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: nginz-https.example.com + hostname: nginz-https.wire.example ttl: 300 http: httpPort: 8080 wss: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: nginz-ssl.example.com + hostname: nginz-ssl.wire.example ttl: 300 ws: wsPort: 8081 @@ -36,9 +36,9 @@ ingress: enabled: false # set to true if you wish to use minio on AWS instead of using real S3 https: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: assets.example.com + hostname: assets.wire.example ttl: 300 http: s3Port: 9000 @@ -48,18 +48,18 @@ ingress: teamSettings: https: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: teams.example.com + hostname: teams.wire.example ttl: 300 http: teamSettingsPort: 8080 accountPages: https: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: account.example.com + hostname: account.wire.example ttl: 300 http: accountPagesPort: 8080 diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index 7f756955226..359101f77e1 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -87,7 +87,7 @@ config: # Details: https://github.com/wireapp/wire-server/blob/3d5684023c54fe580ab27c11d7dae8f19a29ddbc/services/brig/src/Brig/Options.hs#L465-L503 # setCustomerExtensions: # domainsBlockedForRegistration: - # - example.com + # - wire.example set2FACodeGenerationDelaySecs: 300 # 5 minutes setNonceTtlSecs: 300 # 5 minutes setDpopMaxSkewSecs: 1 @@ -122,7 +122,7 @@ turnStatic: turn: serversSource: files # files | dns - # baseDomain: turn.example.com # Must be configured if serversSource is dns + # baseDomain: turn.wire.example # Must be configured if serversSource is dns discoveryIntervalSeconds: 10 # Used only if serversSource is dns serviceAccount: diff --git a/charts/calling-test/values.yaml b/charts/calling-test/values.yaml index 4a8349841da..e6511f4a9c0 100644 --- a/charts/calling-test/values.yaml +++ b/charts/calling-test/values.yaml @@ -6,7 +6,7 @@ image: envVars: # note: this should be overridden in every deployment - BACKEND_HTTPS_URL: https://nginz-https.example.com + BACKEND_HTTPS_URL: https://nginz-https.wire.example # These name overrides are used also for routing. # Wire-server's nginz subchart will route /calling-test to this chart diff --git a/charts/cargohold/values.yaml b/charts/cargohold/values.yaml index 300e8b1472d..5dc4d282625 100644 --- a/charts/cargohold/values.yaml +++ b/charts/cargohold/values.yaml @@ -24,8 +24,8 @@ config: s3Bucket: assets # Multi-ingress configuration: # multiIngress: - # - nginz-https.red.example.com: assets.red.example.com - # - nginz-https.green.example.com: assets.green.example.com + # - nginz-https.red.wire.example: assets.red.wire.example + # - nginz-https.green.wire.example: assets.green.wire.example proxy: {} settings: maxTotalBytes: 5368709120 diff --git a/charts/coturn/values.yaml b/charts/coturn/values.yaml index 683cb2501d6..84934676739 100644 --- a/charts/coturn/values.yaml +++ b/charts/coturn/values.yaml @@ -78,11 +78,11 @@ federate: # # list of host/ip/cert common names / subject alt names, and optional issuer # # names to accept DTLS connections from. There can be multiple entries. # remoteWhitelist: - # - host: example.com + # - host: wire.example # issuer: Issuer Common Name - # - host: another.example.com + # - host: another.wire.example # issuer: "DigiCert SHA2 Extended Validation Server CA" - # - host: another-host-without-issuer.example.com + # - host: another-host-without-issuer.wire.example remoteWhitelist: [] metrics: diff --git a/charts/fake-aws-ses/values.yaml b/charts/fake-aws-ses/values.yaml index 3dcc068d8ba..8b82f73e00b 100644 --- a/charts/fake-aws-ses/values.yaml +++ b/charts/fake-aws-ses/values.yaml @@ -16,4 +16,4 @@ resources: ## The following needs to be provided (and consistent with the config in brig) #TODO: It would actually be useful if the deployment _fails_ if this is undefined -#sesSender: "sender@example.com" +#sesSender: "sender@wire.example" diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 5b328a3e5b1..b23806e5918 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -45,7 +45,7 @@ config: # If set it must a map from `Z-Host` to URI prefix # Example: # multiIngress: - # example.com: https://accounts.example.com/conversation-join/ + # wire.example: https://accounts.wire.example/conversation-join/ # example.net: https://accounts.example.net/conversation-join/ multiIngress: null # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: diff --git a/charts/inbucket/values.yaml b/charts/inbucket/values.yaml index 0e59481ac99..d438b634748 100644 --- a/charts/inbucket/values.yaml +++ b/charts/inbucket/values.yaml @@ -1,6 +1,6 @@ # Fully qualified domain name (FQDN) of the domain where to serve inbucket. # E.g. 'inbucket.my-test-env.wire.link' -host: "inbucket.example.com" +host: "inbucket.wire.example" config: ingressClass: "nginx" diff --git a/charts/openldap/templates/secret-newusers.yaml b/charts/openldap/templates/secret-newusers.yaml index 0397cb0af55..55157a0ba4f 100644 --- a/charts/openldap/templates/secret-newusers.yaml +++ b/charts/openldap/templates/secret-newusers.yaml @@ -20,7 +20,7 @@ stringData: objectClass: posixAccount objectClass: shadowAccount cn: john - uid: john@example.com + uid: john@wire.example uidNumber: 10001 gidNumber: 10001 homeDirectory: /home/john @@ -33,7 +33,7 @@ stringData: objectClass: posixAccount objectClass: shadowAccount cn: jane - uid: jane@example.com + uid: jane@wire.example uidNumber: 10002 gidNumber: 10002 homeDirectory: /home/jane @@ -46,7 +46,7 @@ stringData: objectClass: posixAccount objectClass: shadowAccount cn: me - uid: me@example.com + uid: me@wire.example uidNumber: 10003 gidNumber: 10003 homeDirectory: /home/me @@ -60,10 +60,10 @@ stringData: objectClass: shadowAccount objectClass: extensibleObject cn: usesemail - uid: usesemail@example.com + uid: usesemail@wire.example uidNumber: 10004 gidNumber: 10004 - email: uses@example.com + email: uses@wire.example homeDirectory: /home/me userPassword: notgonnatelleither loginShell: /bin/bash diff --git a/charts/sftd/README.md b/charts/sftd/README.md index 2d0fa74a076..2cdb05de31c 100644 --- a/charts/sftd/README.md +++ b/charts/sftd/README.md @@ -48,8 +48,8 @@ tags: sftd: true sftd: - host: sftd.example.com - allowOrigin: https://webapp.example.com + host: sftd.wire.example + allowOrigin: https://webapp.wire.example tls: # The https://cert-manager.io issuer to use to retrieve a certificate issuerRef: @@ -69,8 +69,8 @@ very slow. ``` helm install sftd wire/sftd \ - --set host=sftd.example.com \ - --set allowOrigin=https://webapp.example.com \ + --set host=sftd.wire.example \ + --set allowOrigin=https://webapp.wire.example \ --set-file tls.crt=/path/to/tls.crt \ --set-file tls.key=/path/to/tls.key ``` @@ -98,7 +98,7 @@ brig: # ... optSettings: # ... - setSftStaticUrl: https://sftd.example.com:443 + setSftStaticUrl: https://sftd.wire.example:443 ``` ## Routability diff --git a/charts/sftd/values.yaml b/charts/sftd/values.yaml index 3b65d81d677..063ae636726 100644 --- a/charts/sftd/values.yaml +++ b/charts/sftd/values.yaml @@ -60,7 +60,7 @@ tolerations: [] affinity: {} -# allowOrigin: https://webapp.example.com +# allowOrigin: https://webapp.wire.example # host: tls: {} # {key,crt} and issuerRef are mutally exclusive diff --git a/charts/team-settings/values.yaml b/charts/team-settings/values.yaml index fa4545c38b7..ac4d940714a 100644 --- a/charts/team-settings/values.yaml +++ b/charts/team-settings/values.yaml @@ -20,10 +20,10 @@ service: #config: # externalUrls: -# backendRest: nginz-https.example.com -# backendWebsocket: nginz-ssl.example.com -# backendDomain: example.com -# appHost: teams.example.com +# backendRest: nginz-https.wire.example +# backendWebsocket: nginz-ssl.wire.example +# backendDomain: wire.example +# appHost: teams.wire.example #secrets: # configJson: @@ -40,19 +40,19 @@ envVars: {} # FEATURE_ENABLE_DEBUG: "true" # You are likely to need at least following CSP headers # due to the fact that you are likely to do cross sub-domain requests -# i.e., from teams.example.com to nginz-https.example.com -# CSP_EXTRA_CONNECT_SRC: "https://*.example.com, wss://*.example.com" -# CSP_EXTRA_IMG_SRC: "https://*.example.com" -# CSP_EXTRA_SCRIPT_SRC: "https://*.example.com" -# CSP_EXTRA_DEFAULT_SRC: "https://*.example.com" -# CSP_EXTRA_FONT_SRC: "https://*.example.com" -# CSP_EXTRA_FRAME_SRC: "https://*.example.com" -# CSP_EXTRA_MANIFEST_SRC: "https://*.example.com" -# CSP_EXTRA_OBJECT_SRC: "https://*.example.com" -# CSP_EXTRA_MEDIA_SRC: "https://*.example.com" -# CSP_EXTRA_PREFETCH_SRC: "https://*.example.com" -# CSP_EXTRA_STYLE_SRC: "https://*.example.com" -# CSP_EXTRA_WORKER_SRC: "https://*.example.com" +# i.e., from teams.wire.example to nginz-https.wire.example +# CSP_EXTRA_CONNECT_SRC: "https://*.wire.example, wss://*.wire.example" +# CSP_EXTRA_IMG_SRC: "https://*.wire.example" +# CSP_EXTRA_SCRIPT_SRC: "https://*.wire.example" +# CSP_EXTRA_DEFAULT_SRC: "https://*.wire.example" +# CSP_EXTRA_FONT_SRC: "https://*.wire.example" +# CSP_EXTRA_FRAME_SRC: "https://*.wire.example" +# CSP_EXTRA_MANIFEST_SRC: "https://*.wire.example" +# CSP_EXTRA_OBJECT_SRC: "https://*.wire.example" +# CSP_EXTRA_MEDIA_SRC: "https://*.wire.example" +# CSP_EXTRA_PREFETCH_SRC: "https://*.wire.example" +# CSP_EXTRA_STYLE_SRC: "https://*.wire.example" +# CSP_EXTRA_WORKER_SRC: "https://*.wire.example" podSecurityContext: allowPrivilegeEscalation: false diff --git a/charts/webapp/values.yaml b/charts/webapp/values.yaml index 3235cbcbb37..45fc9d033a9 100644 --- a/charts/webapp/values.yaml +++ b/charts/webapp/values.yaml @@ -20,10 +20,10 @@ service: #config: # externalUrls: -# backendRest: nginz-https.example.com -# backendWebsocket: nginz-ssl.example.com -# backendDomain: example.com -# appHost: webapp.example.com +# backendRest: nginz-https.wire.example +# backendWebsocket: nginz-ssl.wire.example +# backendDomain: wire.example +# appHost: webapp.wire.example # Some relevant environment options. For a comprehensive # list of available variables, please refer to: @@ -37,19 +37,19 @@ envVars: {} # FEATURE_ENABLE_DEBUG: "true" # You are likely to need at least following CSP headers # due to the fact that you are likely to do cross sub-domain requests -# i.e., from webapp.example.com to nginz-https.example.com -# CSP_EXTRA_CONNECT_SRC: "https://*.example.com, wss://*.example.com" -# CSP_EXTRA_IMG_SRC: "https://*.example.com" -# CSP_EXTRA_SCRIPT_SRC: "https://*.example.com" -# CSP_EXTRA_DEFAULT_SRC: "https://*.example.com" -# CSP_EXTRA_FONT_SRC: "https://*.example.com" -# CSP_EXTRA_FRAME_SRC: "https://*.example.com" -# CSP_EXTRA_MANIFEST_SRC: "https://*.example.com" -# CSP_EXTRA_OBJECT_SRC: "https://*.example.com" -# CSP_EXTRA_MEDIA_SRC: "https://*.example.com" -# CSP_EXTRA_PREFETCH_SRC: "https://*.example.com" -# CSP_EXTRA_STYLE_SRC: "https://*.example.com" -# CSP_EXTRA_WORKER_SRC: "https://*.example.com" +# i.e., from webapp.wire.example to nginz-https.wire.example +# CSP_EXTRA_CONNECT_SRC: "https://*.wire.example, wss://*.wire.example" +# CSP_EXTRA_IMG_SRC: "https://*.wire.example" +# CSP_EXTRA_SCRIPT_SRC: "https://*.wire.example" +# CSP_EXTRA_DEFAULT_SRC: "https://*.wire.example" +# CSP_EXTRA_FONT_SRC: "https://*.wire.example" +# CSP_EXTRA_FRAME_SRC: "https://*.wire.example" +# CSP_EXTRA_MANIFEST_SRC: "https://*.wire.example" +# CSP_EXTRA_OBJECT_SRC: "https://*.wire.example" +# CSP_EXTRA_MEDIA_SRC: "https://*.wire.example" +# CSP_EXTRA_PREFETCH_SRC: "https://*.wire.example" +# CSP_EXTRA_STYLE_SRC: "https://*.wire.example" +# CSP_EXTRA_WORKER_SRC: "https://*.wire.example" podSecurityContext: allowPrivilegeEscalation: false From c0a06176b847b2438ffa72717d8142cf7fdb470b Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Wed, 13 Sep 2023 02:42:39 +0200 Subject: [PATCH 124/225] change back from wire.example to example.com as this was mistakenly commit to develop instead of to the proper branch --- charts/account-pages/values.yaml | 32 ++++++++--------- charts/aws-ingress/values.yaml | 24 ++++++------- charts/brig/values.yaml | 4 +-- charts/calling-test/values.yaml | 2 +- charts/cargohold/values.yaml | 4 +-- charts/coturn/values.yaml | 6 ++-- charts/fake-aws-ses/values.yaml | 2 +- charts/galley/values.yaml | 2 +- charts/inbucket/values.yaml | 2 +- .../openldap/templates/secret-newusers.yaml | 10 +++--- charts/sftd/README.md | 10 +++--- charts/sftd/values.yaml | 2 +- charts/team-settings/values.yaml | 34 +++++++++---------- charts/webapp/values.yaml | 34 +++++++++---------- 14 files changed, 84 insertions(+), 84 deletions(-) diff --git a/charts/account-pages/values.yaml b/charts/account-pages/values.yaml index 14d7f59ca54..ae22ad9fb44 100644 --- a/charts/account-pages/values.yaml +++ b/charts/account-pages/values.yaml @@ -20,9 +20,9 @@ service: #config: # externalUrls: -# backendRest: nginz-https.wire.example -# backendWebsocket: nginz-ssl.wire.example -# appHost: account.wire.example +# backendRest: nginz-https.example.com +# backendWebsocket: nginz-ssl.example.com +# appHost: account.example.com # Some relevant environment options. For a comprehensive # list of available variables, please refer to: @@ -36,19 +36,19 @@ envVars: {} # FEATURE_ENABLE_DEBUG: "true" # You are likely to need at least following CSP headers # due to the fact that you are likely to do cross sub-domain requests -# i.e., from account.wire.example to nginz-https.wire.example -# CSP_EXTRA_CONNECT_SRC: "https://*.wire.example, wss://*.wire.example" -# CSP_EXTRA_IMG_SRC: "https://*.wire.example" -# CSP_EXTRA_SCRIPT_SRC: "https://*.wire.example" -# CSP_EXTRA_DEFAULT_SRC: "https://*.wire.example" -# CSP_EXTRA_FONT_SRC: "https://*.wire.example" -# CSP_EXTRA_FRAME_SRC: "https://*.wire.example" -# CSP_EXTRA_MANIFEST_SRC: "https://*.wire.example" -# CSP_EXTRA_OBJECT_SRC: "https://*.wire.example" -# CSP_EXTRA_MEDIA_SRC: "https://*.wire.example" -# CSP_EXTRA_PREFETCH_SRC: "https://*.wire.example" -# CSP_EXTRA_STYLE_SRC: "https://*.wire.example" -# CSP_EXTRA_WORKER_SRC: "https://*.wire.example" +# i.e., from account.example.com to nginz-https.example.com +# CSP_EXTRA_CONNECT_SRC: "https://*.example.com, wss://*.example.com" +# CSP_EXTRA_IMG_SRC: "https://*.example.com" +# CSP_EXTRA_SCRIPT_SRC: "https://*.example.com" +# CSP_EXTRA_DEFAULT_SRC: "https://*.example.com" +# CSP_EXTRA_FONT_SRC: "https://*.example.com" +# CSP_EXTRA_FRAME_SRC: "https://*.example.com" +# CSP_EXTRA_MANIFEST_SRC: "https://*.example.com" +# CSP_EXTRA_OBJECT_SRC: "https://*.example.com" +# CSP_EXTRA_MEDIA_SRC: "https://*.example.com" +# CSP_EXTRA_PREFETCH_SRC: "https://*.example.com" +# CSP_EXTRA_STYLE_SRC: "https://*.example.com" +# CSP_EXTRA_WORKER_SRC: "https://*.example.com" podSecurityContext: allowPrivilegeEscalation: false diff --git a/charts/aws-ingress/values.yaml b/charts/aws-ingress/values.yaml index eff6b721e4f..3373fc6cc8a 100644 --- a/charts/aws-ingress/values.yaml +++ b/charts/aws-ingress/values.yaml @@ -9,26 +9,26 @@ ingress: webapp: https: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: webapp.wire.example + hostname: webapp.example.com ttl: 300 http: webappPort: 8080 nginz: https: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: nginz-https.wire.example + hostname: nginz-https.example.com ttl: 300 http: httpPort: 8080 wss: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: nginz-ssl.wire.example + hostname: nginz-ssl.example.com ttl: 300 ws: wsPort: 8081 @@ -36,9 +36,9 @@ ingress: enabled: false # set to true if you wish to use minio on AWS instead of using real S3 https: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: assets.wire.example + hostname: assets.example.com ttl: 300 http: s3Port: 9000 @@ -48,18 +48,18 @@ ingress: teamSettings: https: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: teams.wire.example + hostname: teams.example.com ttl: 300 http: teamSettingsPort: 8080 accountPages: https: externalPort: 443 - sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/wire.example + sslCert: arn:aws:iam::00000-accountnumber-00000:server-certificate/example.com sslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01 - hostname: account.wire.example + hostname: account.example.com ttl: 300 http: accountPagesPort: 8080 diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index 359101f77e1..7f756955226 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -87,7 +87,7 @@ config: # Details: https://github.com/wireapp/wire-server/blob/3d5684023c54fe580ab27c11d7dae8f19a29ddbc/services/brig/src/Brig/Options.hs#L465-L503 # setCustomerExtensions: # domainsBlockedForRegistration: - # - wire.example + # - example.com set2FACodeGenerationDelaySecs: 300 # 5 minutes setNonceTtlSecs: 300 # 5 minutes setDpopMaxSkewSecs: 1 @@ -122,7 +122,7 @@ turnStatic: turn: serversSource: files # files | dns - # baseDomain: turn.wire.example # Must be configured if serversSource is dns + # baseDomain: turn.example.com # Must be configured if serversSource is dns discoveryIntervalSeconds: 10 # Used only if serversSource is dns serviceAccount: diff --git a/charts/calling-test/values.yaml b/charts/calling-test/values.yaml index e6511f4a9c0..4a8349841da 100644 --- a/charts/calling-test/values.yaml +++ b/charts/calling-test/values.yaml @@ -6,7 +6,7 @@ image: envVars: # note: this should be overridden in every deployment - BACKEND_HTTPS_URL: https://nginz-https.wire.example + BACKEND_HTTPS_URL: https://nginz-https.example.com # These name overrides are used also for routing. # Wire-server's nginz subchart will route /calling-test to this chart diff --git a/charts/cargohold/values.yaml b/charts/cargohold/values.yaml index 5dc4d282625..300e8b1472d 100644 --- a/charts/cargohold/values.yaml +++ b/charts/cargohold/values.yaml @@ -24,8 +24,8 @@ config: s3Bucket: assets # Multi-ingress configuration: # multiIngress: - # - nginz-https.red.wire.example: assets.red.wire.example - # - nginz-https.green.wire.example: assets.green.wire.example + # - nginz-https.red.example.com: assets.red.example.com + # - nginz-https.green.example.com: assets.green.example.com proxy: {} settings: maxTotalBytes: 5368709120 diff --git a/charts/coturn/values.yaml b/charts/coturn/values.yaml index 84934676739..683cb2501d6 100644 --- a/charts/coturn/values.yaml +++ b/charts/coturn/values.yaml @@ -78,11 +78,11 @@ federate: # # list of host/ip/cert common names / subject alt names, and optional issuer # # names to accept DTLS connections from. There can be multiple entries. # remoteWhitelist: - # - host: wire.example + # - host: example.com # issuer: Issuer Common Name - # - host: another.wire.example + # - host: another.example.com # issuer: "DigiCert SHA2 Extended Validation Server CA" - # - host: another-host-without-issuer.wire.example + # - host: another-host-without-issuer.example.com remoteWhitelist: [] metrics: diff --git a/charts/fake-aws-ses/values.yaml b/charts/fake-aws-ses/values.yaml index 8b82f73e00b..3dcc068d8ba 100644 --- a/charts/fake-aws-ses/values.yaml +++ b/charts/fake-aws-ses/values.yaml @@ -16,4 +16,4 @@ resources: ## The following needs to be provided (and consistent with the config in brig) #TODO: It would actually be useful if the deployment _fails_ if this is undefined -#sesSender: "sender@wire.example" +#sesSender: "sender@example.com" diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index b23806e5918..5b328a3e5b1 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -45,7 +45,7 @@ config: # If set it must a map from `Z-Host` to URI prefix # Example: # multiIngress: - # wire.example: https://accounts.wire.example/conversation-join/ + # example.com: https://accounts.example.com/conversation-join/ # example.net: https://accounts.example.net/conversation-join/ multiIngress: null # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: diff --git a/charts/inbucket/values.yaml b/charts/inbucket/values.yaml index d438b634748..0e59481ac99 100644 --- a/charts/inbucket/values.yaml +++ b/charts/inbucket/values.yaml @@ -1,6 +1,6 @@ # Fully qualified domain name (FQDN) of the domain where to serve inbucket. # E.g. 'inbucket.my-test-env.wire.link' -host: "inbucket.wire.example" +host: "inbucket.example.com" config: ingressClass: "nginx" diff --git a/charts/openldap/templates/secret-newusers.yaml b/charts/openldap/templates/secret-newusers.yaml index 55157a0ba4f..0397cb0af55 100644 --- a/charts/openldap/templates/secret-newusers.yaml +++ b/charts/openldap/templates/secret-newusers.yaml @@ -20,7 +20,7 @@ stringData: objectClass: posixAccount objectClass: shadowAccount cn: john - uid: john@wire.example + uid: john@example.com uidNumber: 10001 gidNumber: 10001 homeDirectory: /home/john @@ -33,7 +33,7 @@ stringData: objectClass: posixAccount objectClass: shadowAccount cn: jane - uid: jane@wire.example + uid: jane@example.com uidNumber: 10002 gidNumber: 10002 homeDirectory: /home/jane @@ -46,7 +46,7 @@ stringData: objectClass: posixAccount objectClass: shadowAccount cn: me - uid: me@wire.example + uid: me@example.com uidNumber: 10003 gidNumber: 10003 homeDirectory: /home/me @@ -60,10 +60,10 @@ stringData: objectClass: shadowAccount objectClass: extensibleObject cn: usesemail - uid: usesemail@wire.example + uid: usesemail@example.com uidNumber: 10004 gidNumber: 10004 - email: uses@wire.example + email: uses@example.com homeDirectory: /home/me userPassword: notgonnatelleither loginShell: /bin/bash diff --git a/charts/sftd/README.md b/charts/sftd/README.md index 2cdb05de31c..2d0fa74a076 100644 --- a/charts/sftd/README.md +++ b/charts/sftd/README.md @@ -48,8 +48,8 @@ tags: sftd: true sftd: - host: sftd.wire.example - allowOrigin: https://webapp.wire.example + host: sftd.example.com + allowOrigin: https://webapp.example.com tls: # The https://cert-manager.io issuer to use to retrieve a certificate issuerRef: @@ -69,8 +69,8 @@ very slow. ``` helm install sftd wire/sftd \ - --set host=sftd.wire.example \ - --set allowOrigin=https://webapp.wire.example \ + --set host=sftd.example.com \ + --set allowOrigin=https://webapp.example.com \ --set-file tls.crt=/path/to/tls.crt \ --set-file tls.key=/path/to/tls.key ``` @@ -98,7 +98,7 @@ brig: # ... optSettings: # ... - setSftStaticUrl: https://sftd.wire.example:443 + setSftStaticUrl: https://sftd.example.com:443 ``` ## Routability diff --git a/charts/sftd/values.yaml b/charts/sftd/values.yaml index 063ae636726..3b65d81d677 100644 --- a/charts/sftd/values.yaml +++ b/charts/sftd/values.yaml @@ -60,7 +60,7 @@ tolerations: [] affinity: {} -# allowOrigin: https://webapp.wire.example +# allowOrigin: https://webapp.example.com # host: tls: {} # {key,crt} and issuerRef are mutally exclusive diff --git a/charts/team-settings/values.yaml b/charts/team-settings/values.yaml index ac4d940714a..fa4545c38b7 100644 --- a/charts/team-settings/values.yaml +++ b/charts/team-settings/values.yaml @@ -20,10 +20,10 @@ service: #config: # externalUrls: -# backendRest: nginz-https.wire.example -# backendWebsocket: nginz-ssl.wire.example -# backendDomain: wire.example -# appHost: teams.wire.example +# backendRest: nginz-https.example.com +# backendWebsocket: nginz-ssl.example.com +# backendDomain: example.com +# appHost: teams.example.com #secrets: # configJson: @@ -40,19 +40,19 @@ envVars: {} # FEATURE_ENABLE_DEBUG: "true" # You are likely to need at least following CSP headers # due to the fact that you are likely to do cross sub-domain requests -# i.e., from teams.wire.example to nginz-https.wire.example -# CSP_EXTRA_CONNECT_SRC: "https://*.wire.example, wss://*.wire.example" -# CSP_EXTRA_IMG_SRC: "https://*.wire.example" -# CSP_EXTRA_SCRIPT_SRC: "https://*.wire.example" -# CSP_EXTRA_DEFAULT_SRC: "https://*.wire.example" -# CSP_EXTRA_FONT_SRC: "https://*.wire.example" -# CSP_EXTRA_FRAME_SRC: "https://*.wire.example" -# CSP_EXTRA_MANIFEST_SRC: "https://*.wire.example" -# CSP_EXTRA_OBJECT_SRC: "https://*.wire.example" -# CSP_EXTRA_MEDIA_SRC: "https://*.wire.example" -# CSP_EXTRA_PREFETCH_SRC: "https://*.wire.example" -# CSP_EXTRA_STYLE_SRC: "https://*.wire.example" -# CSP_EXTRA_WORKER_SRC: "https://*.wire.example" +# i.e., from teams.example.com to nginz-https.example.com +# CSP_EXTRA_CONNECT_SRC: "https://*.example.com, wss://*.example.com" +# CSP_EXTRA_IMG_SRC: "https://*.example.com" +# CSP_EXTRA_SCRIPT_SRC: "https://*.example.com" +# CSP_EXTRA_DEFAULT_SRC: "https://*.example.com" +# CSP_EXTRA_FONT_SRC: "https://*.example.com" +# CSP_EXTRA_FRAME_SRC: "https://*.example.com" +# CSP_EXTRA_MANIFEST_SRC: "https://*.example.com" +# CSP_EXTRA_OBJECT_SRC: "https://*.example.com" +# CSP_EXTRA_MEDIA_SRC: "https://*.example.com" +# CSP_EXTRA_PREFETCH_SRC: "https://*.example.com" +# CSP_EXTRA_STYLE_SRC: "https://*.example.com" +# CSP_EXTRA_WORKER_SRC: "https://*.example.com" podSecurityContext: allowPrivilegeEscalation: false diff --git a/charts/webapp/values.yaml b/charts/webapp/values.yaml index 45fc9d033a9..3235cbcbb37 100644 --- a/charts/webapp/values.yaml +++ b/charts/webapp/values.yaml @@ -20,10 +20,10 @@ service: #config: # externalUrls: -# backendRest: nginz-https.wire.example -# backendWebsocket: nginz-ssl.wire.example -# backendDomain: wire.example -# appHost: webapp.wire.example +# backendRest: nginz-https.example.com +# backendWebsocket: nginz-ssl.example.com +# backendDomain: example.com +# appHost: webapp.example.com # Some relevant environment options. For a comprehensive # list of available variables, please refer to: @@ -37,19 +37,19 @@ envVars: {} # FEATURE_ENABLE_DEBUG: "true" # You are likely to need at least following CSP headers # due to the fact that you are likely to do cross sub-domain requests -# i.e., from webapp.wire.example to nginz-https.wire.example -# CSP_EXTRA_CONNECT_SRC: "https://*.wire.example, wss://*.wire.example" -# CSP_EXTRA_IMG_SRC: "https://*.wire.example" -# CSP_EXTRA_SCRIPT_SRC: "https://*.wire.example" -# CSP_EXTRA_DEFAULT_SRC: "https://*.wire.example" -# CSP_EXTRA_FONT_SRC: "https://*.wire.example" -# CSP_EXTRA_FRAME_SRC: "https://*.wire.example" -# CSP_EXTRA_MANIFEST_SRC: "https://*.wire.example" -# CSP_EXTRA_OBJECT_SRC: "https://*.wire.example" -# CSP_EXTRA_MEDIA_SRC: "https://*.wire.example" -# CSP_EXTRA_PREFETCH_SRC: "https://*.wire.example" -# CSP_EXTRA_STYLE_SRC: "https://*.wire.example" -# CSP_EXTRA_WORKER_SRC: "https://*.wire.example" +# i.e., from webapp.example.com to nginz-https.example.com +# CSP_EXTRA_CONNECT_SRC: "https://*.example.com, wss://*.example.com" +# CSP_EXTRA_IMG_SRC: "https://*.example.com" +# CSP_EXTRA_SCRIPT_SRC: "https://*.example.com" +# CSP_EXTRA_DEFAULT_SRC: "https://*.example.com" +# CSP_EXTRA_FONT_SRC: "https://*.example.com" +# CSP_EXTRA_FRAME_SRC: "https://*.example.com" +# CSP_EXTRA_MANIFEST_SRC: "https://*.example.com" +# CSP_EXTRA_OBJECT_SRC: "https://*.example.com" +# CSP_EXTRA_MEDIA_SRC: "https://*.example.com" +# CSP_EXTRA_PREFETCH_SRC: "https://*.example.com" +# CSP_EXTRA_STYLE_SRC: "https://*.example.com" +# CSP_EXTRA_WORKER_SRC: "https://*.example.com" podSecurityContext: allowPrivilegeEscalation: false From 9373c9aff937b25bfab7a79f7f80ee9e1e10e2de Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Wed, 13 Sep 2023 03:16:50 +0200 Subject: [PATCH 125/225] add documentation on creating a first user --- docs/src/how-to/administrate/users.md | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/src/how-to/administrate/users.md b/docs/src/how-to/administrate/users.md index b1ec7d1c694..90dd2cef3d1 100644 --- a/docs/src/how-to/administrate/users.md +++ b/docs/src/how-to/administrate/users.md @@ -588,3 +588,50 @@ Where: - `$EMAIL_CODE` is the validation code received by email after running the previous script/command - `$TEAM_CURRENCY` is the currency of the team - `$TEAM_NAME` is the name of the team + +## Workaround for deploying a first user. + +If you are running wire-server in an offline environment, where there is no mail server available, you might need to manually gather the email validation code for user creation. + +First do: + +```sh +curl --resolve nginz-https.MY_DOMAIN_NAME:31773:INGRESS_IP https://nginz-https.MY_DOMAIN_NAME:31773/register -XPOST -H"Content-Type: application/json" --data '{"email":"YOUR_USER_EMAIL", "name":"YOUR_USER_NAME"}' -k +``` + +Where: + +- `MY_DOMAIN_NAME` is the domain name of your wire-server instance. +- `INGRESS_IP` is the ingress IP address. +- `YOUR_USER_EMAIL` is the email address of the user you want to create. +- `YOUR_USER_NAME` is the display name of the user you want to create. + +This will result in an email being sent internally, but if you do not have an email server handling those, they simply stack up in the `demo-smtp` kubernetes pod. + +You need to get the name of that pod by running: + +```sh +kubectl get pod -lapp=demo-smtp +``` + +Once you have it, for example if it is `demo-smtp-5bb6449497-4w7c4`, you can run: + +```sh +d kubectl exec demo-smtp-5bb6449497-4w7c4 – sh -c 'grep -H -E "^[TX]" /var/spool/exim4/input/*-D' +``` + +This will display emails that have been sent, and you should be able to see the code from there, is is a 6 digit number, for example `607708`. + +From the email, you will also get the key, which will look something like `AoL0FrCGSnurvAJFUv3x5YkiA4hFbpVDlKpwGTzAuNU=`. + +Finally, you use that code to validate the user creation: + +```sh +curl --resolve nginz-https.MY_DOMAIN_NAME:31773:INGRESS_IP https://nginz-https.MY_DOMAIN_NAME:31773/activate -XPOST -H"Content-Type: application/json" --data '{"email":"YOUR_USER_EMAIL", "key":"AoL0FrCGSnurvAJFUv3x5YkiA4hFbpVDlKpwGTzAuNU=", "code": "607708"}' -k +``` + +Where: + +- `MY_DOMAIN_NAME` is the domain name of your wire-server instance. +- `INGRESS_IP` is ingress IP address. +- `YOUR_USER_EMAIL` is the email address of the user you want to create. From 4860c4ec97cb061ee994d867c7187721790a50aa Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Wed, 13 Sep 2023 03:17:37 +0200 Subject: [PATCH 126/225] reverting previous commit as sent to wrong branch --- docs/src/how-to/administrate/users.md | 47 --------------------------- 1 file changed, 47 deletions(-) diff --git a/docs/src/how-to/administrate/users.md b/docs/src/how-to/administrate/users.md index 90dd2cef3d1..b1ec7d1c694 100644 --- a/docs/src/how-to/administrate/users.md +++ b/docs/src/how-to/administrate/users.md @@ -588,50 +588,3 @@ Where: - `$EMAIL_CODE` is the validation code received by email after running the previous script/command - `$TEAM_CURRENCY` is the currency of the team - `$TEAM_NAME` is the name of the team - -## Workaround for deploying a first user. - -If you are running wire-server in an offline environment, where there is no mail server available, you might need to manually gather the email validation code for user creation. - -First do: - -```sh -curl --resolve nginz-https.MY_DOMAIN_NAME:31773:INGRESS_IP https://nginz-https.MY_DOMAIN_NAME:31773/register -XPOST -H"Content-Type: application/json" --data '{"email":"YOUR_USER_EMAIL", "name":"YOUR_USER_NAME"}' -k -``` - -Where: - -- `MY_DOMAIN_NAME` is the domain name of your wire-server instance. -- `INGRESS_IP` is the ingress IP address. -- `YOUR_USER_EMAIL` is the email address of the user you want to create. -- `YOUR_USER_NAME` is the display name of the user you want to create. - -This will result in an email being sent internally, but if you do not have an email server handling those, they simply stack up in the `demo-smtp` kubernetes pod. - -You need to get the name of that pod by running: - -```sh -kubectl get pod -lapp=demo-smtp -``` - -Once you have it, for example if it is `demo-smtp-5bb6449497-4w7c4`, you can run: - -```sh -d kubectl exec demo-smtp-5bb6449497-4w7c4 – sh -c 'grep -H -E "^[TX]" /var/spool/exim4/input/*-D' -``` - -This will display emails that have been sent, and you should be able to see the code from there, is is a 6 digit number, for example `607708`. - -From the email, you will also get the key, which will look something like `AoL0FrCGSnurvAJFUv3x5YkiA4hFbpVDlKpwGTzAuNU=`. - -Finally, you use that code to validate the user creation: - -```sh -curl --resolve nginz-https.MY_DOMAIN_NAME:31773:INGRESS_IP https://nginz-https.MY_DOMAIN_NAME:31773/activate -XPOST -H"Content-Type: application/json" --data '{"email":"YOUR_USER_EMAIL", "key":"AoL0FrCGSnurvAJFUv3x5YkiA4hFbpVDlKpwGTzAuNU=", "code": "607708"}' -k -``` - -Where: - -- `MY_DOMAIN_NAME` is the domain name of your wire-server instance. -- `INGRESS_IP` is ingress IP address. -- `YOUR_USER_EMAIL` is the email address of the user you want to create. From a3ba8ef51832139d3e155d29b2461ec602ebe5de Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 13 Sep 2023 18:15:43 +0200 Subject: [PATCH 127/225] Update sftd docs: include uri scheme in allowOrigin (#3584) * Update sftd docs: include uri scheme in allowOrigin * fixup --- docs/src/how-to/install/sft.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/how-to/install/sft.md b/docs/src/how-to/install/sft.md index e4560c72168..9074bd93a21 100644 --- a/docs/src/how-to/install/sft.md +++ b/docs/src/how-to/install/sft.md @@ -18,7 +18,7 @@ tags: sftd: host: sftd.example.com # Replace example.com with your domain - allowOrigin: webapp.example.com # Should be the address you used for the webapp deployment + allowOrigin: https://webapp.example.com # Should be the address you used for the webapp deployment (Note: you must include the uri scheme "https://") ``` In your `secrets.yaml` you should set the TLS keys for sftd domain: From 805f1ff4985b403c4cb7ccc1c4b08cb6784ed03e Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 15 Sep 2023 13:47:33 +0200 Subject: [PATCH 128/225] WPB-4629 impossible to add users to a conversation if one of the members is from an offline backend (#3585) --- changelog.d/3-bug-fixes/WPB-4629 | 1 + integration/test/Test/Conversation.hs | 17 ++++++++++++++++ integration/test/Test/Federation.hs | 4 +++- services/galley/src/Galley/API/Action.hs | 25 ++++++++++++------------ 4 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-4629 diff --git a/changelog.d/3-bug-fixes/WPB-4629 b/changelog.d/3-bug-fixes/WPB-4629 new file mode 100644 index 00000000000..5d1724fe66e --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-4629 @@ -0,0 +1 @@ +Fixed add user to conversation when one of the other participating backends is offline diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index f956480dc85..dc37273afdb 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -529,3 +529,20 @@ testMultiIngressGuestLinks = do bindResponse (getConversationCode user conv (Just "unknown.example.com")) $ \resp -> do res <- getJSON 403 resp res %. "label" `shouldMatch` "access-denied" + +testAddUserWhenOtherBackendOffline :: HasCallStack => App () +testAddUserWhenOtherBackendOffline = do + let overrides = + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} + <> fullSearchWithAll + ([alice, alex], conv) <- + startDynamicBackends [overrides] $ \domains -> do + own <- make OwnDomain & asString + [alice, alex, charlie] <- + createAndConnectUsers $ [own, own] <> domains + + let newConv = defProteus {qualifiedUsers = [charlie]} + conv <- postConversation alice newConv >>= getJSON 201 + pure ([alice, alex], conv) + bindResponse (addMembers alice conv [alex]) $ \resp -> do + resp.status `shouldMatchInt` 200 diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs index a16b3e9077f..5a70f3ad1f0 100644 --- a/integration/test/Test/Federation.hs +++ b/integration/test/Test/Federation.hs @@ -72,10 +72,12 @@ testNotificationsForOfflineBackends = do -- Adding users to an up backend conversation should not work when one of -- the participating backends is down. This is due to not being able to -- check non-fully connected graph between all participating backends + -- however, if the backend of the user to be added is already part of the conversation, we do not need to do the check + -- and the user can be added as long as the backend is reachable otherUser3 <- randomUser OtherDomain def connectUsers delUser otherUser3 bindResponse (addMembers delUser upBackendConv [otherUser3]) $ \resp -> - resp.status `shouldMatchInt` 533 + resp.status `shouldMatchInt` 200 -- Adding users from down backend to a conversation should also fail bindResponse (addMembers delUser upBackendConv [downUser2]) $ \resp -> diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 811e78decdf..710309b8093 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -59,6 +59,7 @@ import Data.List.NonEmpty qualified as NE import Data.Map qualified as Map import Data.Misc import Data.Qualified +import Data.Set ((\\)) import Data.Set qualified as Set import Data.Singletons import Data.Time.Clock @@ -86,7 +87,7 @@ import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.UserList import Galley.Validation -import Imports +import Imports hiding ((\\)) import Polysemy import Polysemy.Error import Polysemy.Input @@ -442,17 +443,17 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do Local UserId -> Sem r () checkRemoteBackendsConnected lusr = do - let invitedDomains = tDomain <$> snd (partitionQualified lusr $ NE.toList invited) - existingDomains = tDomain . rmId <$> convRemoteMembers (tUnqualified lconv) - - -- Note: - -- - -- In some cases, this federation status check might be redundant (for - -- example if there are only local users in the conversation). However, - -- it is important that we attempt to connect to the backends of the new - -- users here, because that results in the correct error when those - -- backends are not reachable. - checkFederationStatus (RemoteDomains . Set.fromList $ invitedDomains <> existingDomains) + let invitedRemoteUsers = filter ((/= tDomain lconv) . tDomain) $ snd (partitionQualified lusr $ NE.toList invited) + invitedRemoteDomains = Set.fromList $ tDomain <$> invitedRemoteUsers + existingRemoteDomains = Set.fromList $ tDomain . rmId <$> convRemoteMembers (tUnqualified lconv) + allInvitedAlreadyInConversation = null $ invitedRemoteDomains \\ existingRemoteDomains + + if not allInvitedAlreadyInConversation + then checkFederationStatus (RemoteDomains (invitedRemoteDomains <> existingRemoteDomains)) + else -- even if there are no new remotes, we still need to check they are reachable + void . (ensureNoUnreachableBackends =<<) $ + E.runFederatedConcurrentlyEither @_ @'Brig invitedRemoteUsers $ \_ -> + pure () conv :: Data.Conversation conv = tUnqualified lconv From fd258a96751aff8d39b8482b03ec3e89aeac76a0 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 18 Sep 2023 14:23:16 +0200 Subject: [PATCH 129/225] fake-aws-s3 chart: Upgrade to minio 5.0.13 (#3565) --- charts/fake-aws-s3/requirements.yaml | 2 +- charts/fake-aws-s3/values.yaml | 6 +----- deploy/dockerephemeral/docker-compose.yaml | 3 +-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/charts/fake-aws-s3/requirements.yaml b/charts/fake-aws-s3/requirements.yaml index da4723909d2..f62c11a7b74 100644 --- a/charts/fake-aws-s3/requirements.yaml +++ b/charts/fake-aws-s3/requirements.yaml @@ -1,4 +1,4 @@ dependencies: - name: minio - version: 3.2.0 + version: 5.0.13 repository: https://charts.min.io/ diff --git a/charts/fake-aws-s3/values.yaml b/charts/fake-aws-s3/values.yaml index a736eb82cb0..bf36bdf4a93 100644 --- a/charts/fake-aws-s3/values.yaml +++ b/charts/fake-aws-s3/values.yaml @@ -1,9 +1,5 @@ -# See defaults in https://github.com/helm/charts/tree/master/stable/minio +# See defaults in https://github.com/minio/minio/blob/RELEASE.2023-07-07T07-13-57Z/helm/minio/values.yaml minio: - mcImage: - repository: quay.io/minio/mc - tag: RELEASE.2021-10-07T04-19-58Z - pullPolicy: IfNotPresent fullnameOverride: fake-aws-s3 service: port: "9000" diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index e02620b7677..17321961014 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -57,8 +57,7 @@ services: fake_s3: container_name: demo_wire_s3 -# image: minio/minio:RELEASE.2018-05-25T19-49-13Z - image: julialongtin/minio:0.0.9 + image: minio/minio:RELEASE.2023-07-07T07-13-57Z ports: - "127.0.0.1:4570:9000" environment: From 3f88513045c264f7752dee7afe18b29fa3c39d52 Mon Sep 17 00:00:00 2001 From: fisx Date: Tue, 19 Sep 2023 10:44:29 +0200 Subject: [PATCH 130/225] Disable de-federation to avoid running into a scalability issue (#3582) https://wearezeta.atlassian.net/browse/WPB-4668 Co-authored-by: Akshay Mankar --- changelog.d/1-api-changes/WPB-3611 | 5 - .../WPB-4668-disable-defederation | 1 + .../duplicate-member-notifications | 1 - changelog.d/6-federation/WPB-3611 | 1 - docs/src/understand/configure-federation.md | 9 +- integration/default.nix | 4 + integration/integration.cabal | 3 +- integration/test/API/BrigInternal.hs | 19 - integration/test/API/GalleyInternal.hs | 9 - integration/test/SetupHelpers.hs | 42 +-- integration/test/Test/Brig.hs | 69 +--- integration/test/Test/Conversation.hs | 202 ++--------- integration/test/Test/Defederation.hs | 85 ----- integration/test/Test/Demo.hs | 7 +- integration/test/Testlib/Env.hs | 8 + integration/test/Testlib/ResourcePool.hs | 17 +- integration/test/Testlib/RunServices.hs | 7 +- integration/test/Testlib/Types.hs | 4 +- libs/schema-profunctor/src/Data/Schema.hs | 19 - .../src/Wire/API/Federation/API/Galley.hs | 1 - .../wire-api/src/Wire/API/Event/Federation.hs | 69 +--- .../wire-api/src/Wire/API/FederationUpdate.hs | 11 +- .../src/Wire/API/Routes/Internal/Brig.hs | 30 -- .../background-worker/background-worker.cabal | 7 - services/background-worker/default.nix | 9 - .../src/Wire/BackgroundWorker.hs | 15 - .../src/Wire/BackgroundWorker/Env.hs | 10 +- .../src/Wire/BackgroundWorker/Health.hs | 13 +- .../src/Wire/Defederation.hs | 143 -------- .../Wire/BackendNotificationPusherSpec.hs | 40 --- .../test/Test/Wire/DefederationSpec.hs | 51 --- .../background-worker/test/Test/Wire/Util.hs | 1 - services/brig/src/Brig/API/Internal.hs | 61 +--- .../brig/test/integration/API/Internal.hs | 72 +--- .../integration/Test/Federator/IngressSpec.hs | 11 +- .../integration/Test/Federator/InwardSpec.hs | 15 +- services/galley/galley.cabal | 1 - services/galley/src/Galley/API/Federation.hs | 96 +---- services/galley/src/Galley/API/Internal.hs | 196 +--------- services/galley/src/Galley/App.hs | 1 - .../Galley/Cassandra/Conversation/Members.hs | 24 +- .../galley/src/Galley/Cassandra/Queries.hs | 13 - services/galley/src/Galley/Effects.hs | 2 - .../Effects/DefederationNotifications.hs | 17 - .../galley/src/Galley/Effects/MemberStore.hs | 10 - services/galley/src/Galley/Intra/Effects.hs | 97 +---- .../galley/src/Galley/Intra/Push/Internal.hs | 2 +- services/galley/test/integration/API.hs | 338 +----------------- services/galley/test/integration/API/Util.hs | 48 +-- .../galley/test/integration/Federation.hs | 304 +--------------- services/galley/test/integration/Main.hs | 7 - 51 files changed, 148 insertions(+), 2079 deletions(-) delete mode 100644 changelog.d/1-api-changes/WPB-3611 create mode 100644 changelog.d/1-api-changes/WPB-4668-disable-defederation delete mode 100644 changelog.d/3-bug-fixes/duplicate-member-notifications delete mode 100644 changelog.d/6-federation/WPB-3611 delete mode 100644 integration/test/Test/Defederation.hs delete mode 100644 services/background-worker/src/Wire/Defederation.hs delete mode 100644 services/background-worker/test/Test/Wire/DefederationSpec.hs delete mode 100644 services/galley/src/Galley/Effects/DefederationNotifications.hs diff --git a/changelog.d/1-api-changes/WPB-3611 b/changelog.d/1-api-changes/WPB-3611 deleted file mode 100644 index 95fa03edec4..00000000000 --- a/changelog.d/1-api-changes/WPB-3611 +++ /dev/null @@ -1,5 +0,0 @@ -Added a new notification event type, "federation.connectionRemoved" -This event contains a pair of domains that are no longer federating, and is used to inform other federation members of the change. -This notification is sent twice to local clients of federation members who receive this notification. Once before and once after cleaning up local conversaions where users from both domains are present. - -Added a new Galley federation endpoint "/federation/on-connection-removed" to receive the connection removed notification. \ No newline at end of file diff --git a/changelog.d/1-api-changes/WPB-4668-disable-defederation b/changelog.d/1-api-changes/WPB-4668-disable-defederation new file mode 100644 index 00000000000..baef31417a9 --- /dev/null +++ b/changelog.d/1-api-changes/WPB-4668-disable-defederation @@ -0,0 +1 @@ +Remove de-federation (to avoid a scalability issue). \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/duplicate-member-notifications b/changelog.d/3-bug-fixes/duplicate-member-notifications deleted file mode 100644 index 120b5bc7ebf..00000000000 --- a/changelog.d/3-bug-fixes/duplicate-member-notifications +++ /dev/null @@ -1 +0,0 @@ -Defederation notifications, federation.delete and federation.connectionRemoved, now deduplicate the user list so that we don't send them more notifications than required. \ No newline at end of file diff --git a/changelog.d/6-federation/WPB-3611 b/changelog.d/6-federation/WPB-3611 deleted file mode 100644 index 4d485843b0f..00000000000 --- a/changelog.d/6-federation/WPB-3611 +++ /dev/null @@ -1 +0,0 @@ -Defederating from a remote server will now inform your remaining federation members, allowing them to clean up their local conversations and inform their clients. \ No newline at end of file diff --git a/docs/src/understand/configure-federation.md b/docs/src/understand/configure-federation.md index 3477a10242a..455aafd6437 100644 --- a/docs/src/understand/configure-federation.md +++ b/docs/src/understand/configure-federation.md @@ -457,13 +457,8 @@ the sysadmin: * [`PUT`](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/put_i_federation_remotes__domain_) -* [`DELETE`](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/delete_i_federation_remotes__domain_) - - **WARNING:** If you delete a connection, all users from that - remote will be removed from local conversations, and all - conversations hosted by that remote will be removed from the local - backend. Connections between local and remote users that are - removed will be archived, and can be re-established should you - decide to add the same backend later. +* **NOTE:** De-federating (`DELETE`) has been removed from the API to + avoid a scalability issue. Watch out for a fix in the changelog! The `remotes` list looks like this: diff --git a/integration/default.nix b/integration/default.nix index 1aa88b39fda..8019fdd1f87 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -15,6 +15,8 @@ , Cabal , case-insensitive , containers +, cql +, cql-io , cryptonite , data-default , directory @@ -80,6 +82,8 @@ mkDerivation { bytestring-conversion case-insensitive containers + cql + cql-io cryptonite data-default directory diff --git a/integration/integration.cabal b/integration/integration.cabal index 0041bca3610..7e989f5a988 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -105,7 +105,6 @@ library Test.Brig Test.Client Test.Conversation - Test.Defederation Test.Demo Test.Federation Test.Federator @@ -143,6 +142,8 @@ library , bytestring-conversion , case-insensitive , containers + , cql + , cql-io , cryptonite , data-default , directory diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index d2788c64bb4..c675c835585 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -99,25 +99,6 @@ updateFedConn' owndom dom fedConn = do conn <- make fedConn submit "PUT" $ addJSON conn req -deleteFedConn :: (HasCallStack, MakesValue owndom) => owndom -> String -> App Response -deleteFedConn owndom dom = do - bindResponse (deleteFedConn' owndom dom) $ \res -> do - res.status `shouldMatchRange` (200, 299) - pure res - -deleteFedConn' :: (HasCallStack, MakesValue owndom) => owndom -> String -> App Response -deleteFedConn' owndom dom = do - req <- rawBaseRequest owndom Brig Unversioned ("/i/federation/remotes/" <> dom) - submit "DELETE" req - -deleteAllFedConns :: (HasCallStack, MakesValue dom) => dom -> App () -deleteAllFedConns dom = do - readFedConns dom >>= \resp -> - resp.json %. "remotes" - & asList - >>= traverse (\v -> v %. "domain" & asString) - >>= mapM_ (deleteFedConn dom) - registerOAuthClient :: (HasCallStack, MakesValue user, MakesValue name, MakesValue url) => user -> name -> url -> App Response registerOAuthClient user name url = do req <- baseRequest user Brig Unversioned "i/oauth/clients" diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index 0d7f8c9f602..f2cc8a8cf65 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -51,12 +51,3 @@ getFederationStatus user domains = submit "GET" $ req & addJSONObject ["domains" .= domainList] - -deleteFederationDomain :: - ( HasCallStack - ) => - String -> - App Response -deleteFederationDomain domain = do - req <- rawBaseRequest OwnDomain Galley Unversioned $ joinHttpPath ["i", "federation", domain] - submit "DELETE" req diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index d5e324ea41c..9053a98c3d1 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + module SetupHelpers where import API.Brig qualified as Brig @@ -9,7 +11,6 @@ import Data.Aeson hiding ((.=)) import Data.Aeson.Types qualified as Aeson import Data.Default import Data.Function -import Data.List qualified as List import Data.UUID.V1 (nextUUID) import Data.UUID.V4 (nextRandom) import GHC.Stack @@ -75,14 +76,6 @@ getAllConvs u = do resp.json result %. "found" & asList -resetFedConns :: (HasCallStack, MakesValue owndom) => owndom -> App () -resetFedConns owndom = do - bindResponse (Internal.readFedConns owndom) $ \resp -> do - rdoms :: [String] <- do - rawlist <- resp.json %. "remotes" & asList - (asString . (%. "domain")) `mapM` rawlist - Internal.deleteFedConn' owndom `mapM_` rdoms - randomId :: HasCallStack => App String randomId = liftIO (show <$> nextRandom) @@ -95,40 +88,15 @@ randomUserId domain = do uid <- randomId pure $ object ["id" .= uid, "domain" .= d] -addFullSearchFor :: [String] -> Value -> App Value -addFullSearchFor domains val = - modifyField - "optSettings.setFederationDomainConfigs" - ( \configs -> do - cfg <- assertJust "" configs - xs <- cfg & asList - pure (xs <> [object ["domain" .= domain, "search_policy" .= "full_search"] | domain <- domains]) - ) - val - -fullSearchWithAll :: ServiceOverrides -fullSearchWithAll = - def - { brigCfg = \val -> do - ownDomain <- asString =<< val %. "optSettings.setFederationDomain" - env <- ask - let remoteDomains = List.delete ownDomain $ [env.domain1, env.domain2] <> env.dynamicDomains - addFullSearchFor remoteDomains val - } - -withFederatingBackendsAllowDynamic :: HasCallStack => Int -> ((String, String, String) -> App a) -> App a -withFederatingBackendsAllowDynamic n k = do +withFederatingBackendsAllowDynamic :: HasCallStack => ((String, String, String) -> App a) -> App a +withFederatingBackendsAllowDynamic k = do let setFederationConfig = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends [ def {brigCfg = setFederationConfig}, def {brigCfg = setFederationConfig}, def {brigCfg = setFederationConfig} ] - $ \dynDomains -> do - domains@[domainA, domainB, domainC] <- pure dynDomains - sequence_ [Internal.createFedConn x (Internal.FedConn y "full_search") | x <- domains, y <- domains, x /= y] - liftIO $ threadDelay (n * 1000 * 1000) -- wait for federation status to be updated + $ \[domainA, domainB, domainC] -> k (domainA, domainB, domainC) diff --git a/integration/test/Test/Brig.hs b/integration/test/Test/Brig.hs index a229f8cfd6d..4b5623a3ecc 100644 --- a/integration/test/Test/Brig.hs +++ b/integration/test/Test/Brig.hs @@ -4,6 +4,7 @@ import API.Brig qualified as Public import API.BrigInternal qualified as Internal import API.Common qualified as API import API.GalleyInternal qualified as Internal +import Control.Concurrent (threadDelay) import Data.Aeson qualified as Aeson import Data.Aeson.Types hiding ((.=)) import Data.Set qualified as Set @@ -29,14 +30,7 @@ testSearchContactForExternalUsers = do testCrudFederationRemotes :: HasCallStack => App () testCrudFederationRemotes = do otherDomain <- asString OtherDomain - let overrides = - def - { brigCfg = - setField - "optSettings.setFederationDomainConfigs" - [object ["domain" .= otherDomain, "search_policy" .= "full_search"]] - } - withModifiedBackend overrides $ \ownDomain -> do + withModifiedBackend def $ \ownDomain -> do let parseFedConns :: HasCallStack => Response -> App [Value] parseFedConns resp = -- Pick out the list of federation domain configs @@ -45,74 +39,40 @@ testCrudFederationRemotes = do -- Enforce that the values are objects and not something else >>= traverse (fmap Object . asObject) - addOnce :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => fedConn -> [fedConn2] -> App () - addOnce fedConn want = do + addTest :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => fedConn -> [fedConn2] -> App () + addTest fedConn want = do bindResponse (Internal.createFedConn ownDomain fedConn) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 200 res2 <- parseFedConns =<< Internal.readFedConns ownDomain sort res2 `shouldMatch` sort want - addFail :: HasCallStack => MakesValue fedConn => fedConn -> App () - addFail fedConn = do - bindResponse (Internal.createFedConn' ownDomain fedConn) $ \res -> do - addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 533 - - deleteOnce :: (Ord fedConn, ToJSON fedConn, MakesValue fedConn) => String -> [fedConn] -> App () - deleteOnce domain want = do - bindResponse (Internal.deleteFedConn ownDomain domain) $ \res -> do - addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 200 - res2 <- parseFedConns =<< Internal.readFedConns ownDomain - sort res2 `shouldMatch` sort want - - deleteFail :: HasCallStack => String -> App () - deleteFail del = do - bindResponse (Internal.deleteFedConn' ownDomain del) $ \res -> do - addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 533 - - updateOnce :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => String -> fedConn -> [fedConn2] -> App () - updateOnce domain fedConn want = do + updateTest :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => String -> fedConn -> [fedConn2] -> App () + updateTest domain fedConn want = do bindResponse (Internal.updateFedConn ownDomain domain fedConn) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 200 res2 <- parseFedConns =<< Internal.readFedConns ownDomain sort res2 `shouldMatch` sort want - updateFail :: (MakesValue fedConn, HasCallStack) => String -> fedConn -> App () - updateFail domain fedConn = do - bindResponse (Internal.updateFedConn' ownDomain domain fedConn) $ \res -> do - addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 533 - dom1 :: String <- (<> ".example.com") . UUID.toString <$> liftIO UUID.nextRandom - dom2 :: String <- (<> ".example.com") . UUID.toString <$> liftIO UUID.nextRandom - let remote1, remote1', remote1'' :: Internal.FedConn + let remote1, remote1' :: Internal.FedConn remote1 = Internal.FedConn dom1 "no_search" remote1' = remote1 {Internal.searchStrategy = "full_search"} - remote1'' = remote1 {Internal.domain = dom2} cfgRemotesExpect :: Internal.FedConn cfgRemotesExpect = Internal.FedConn (cs otherDomain) "full_search" - remote1J <- make remote1 - remote1J' <- make remote1' - - resetFedConns ownDomain + liftIO $ threadDelay 5_000_000 cfgRemotes <- parseFedConns =<< Internal.readFedConns ownDomain - cfgRemotes `shouldMatch` [cfgRemotesExpect] + cfgRemotes `shouldMatch` ([] @Value) -- entries present in the config file can be idempotently added if identical, but cannot be - -- updated, deleted or updated. - addOnce cfgRemotesExpect [cfgRemotesExpect] - addFail (cfgRemotesExpect {Internal.searchStrategy = "no_search"}) - deleteFail (Internal.domain cfgRemotesExpect) - updateFail (Internal.domain cfgRemotesExpect) (cfgRemotesExpect {Internal.searchStrategy = "no_search"}) + -- updated. + addTest cfgRemotesExpect [cfgRemotesExpect] -- create - addOnce remote1 $ (remote1J : cfgRemotes) - addOnce remote1 $ (remote1J : cfgRemotes) -- idempotency + addTest remote1 [cfgRemotesExpect, remote1] + addTest remote1 [cfgRemotesExpect, remote1] -- idempotency -- update - updateOnce (Internal.domain remote1) remote1' (remote1J' : cfgRemotes) - updateFail (Internal.domain remote1) remote1'' - -- delete - deleteOnce (Internal.domain remote1) cfgRemotes - deleteOnce (Internal.domain remote1) cfgRemotes -- idempotency + updateTest (Internal.domain remote1) remote1' [cfgRemotesExpect, remote1'] testCrudOAuthClient :: HasCallStack => App () testCrudOAuthClient = do @@ -185,7 +145,6 @@ testRemoteUserSearch :: HasCallStack => App () testRemoteUserSearch = do let overrides = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends [def {brigCfg = overrides}, def {brigCfg = overrides}] $ \dynDomains -> do domains@[d1, d2] <- pure dynDomains diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index dc37273afdb..e918aa05ab8 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -3,11 +3,10 @@ module Test.Conversation where -import API.Brig (getConnection, getConnections, postConnection) -import API.BrigInternal +import API.Brig (getConnections, postConnection) +import API.BrigInternal as Internal import API.Galley import API.GalleyInternal -import API.Gundeck (getNotifications) import Control.Applicative import Control.Concurrent (threadDelay) import Data.Aeson qualified as Aeson @@ -21,7 +20,6 @@ testDynamicBackendsFullyConnectedWhenAllowAll :: HasCallStack => App () testDynamicBackendsFullyConnectedWhenAllowAll = do let overrides = def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - <> fullSearchWithAll startDynamicBackends [overrides, overrides, overrides] $ \dynDomains -> do [domainA, domainB, domainC] <- pure dynDomains uidA <- randomUser domainA def {team = True} @@ -61,7 +59,6 @@ testDynamicBackendsFullyConnectedWhenAllowDynamic :: HasCallStack => App () testDynamicBackendsFullyConnectedWhenAllowDynamic = do let overrides = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends [ def {brigCfg = overrides}, @@ -90,14 +87,10 @@ testDynamicBackendsNotFullyConnected = do def { brigCfg = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) } startDynamicBackends [overrides, overrides, overrides] $ - \dynDomains -> do - domains@[domainA, domainB, domainC] <- pure dynDomains - -- clean federation config - sequence_ [deleteFedConn x y | x <- domains, y <- domains, x /= y] + \[domainA, domainB, domainC] -> do -- A is connected to B and C, but B and C are not connected to each other void $ createFedConn domainA $ FedConn domainB "full_search" void $ createFedConn domainB $ FedConn domainA "full_search" @@ -139,7 +132,6 @@ testCreateConversationFullyConnected :: HasCallStack => App () testCreateConversationFullyConnected = do let setFederationConfig = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends [ def {brigCfg = setFederationConfig}, @@ -157,7 +149,6 @@ testCreateConversationNonFullyConnected :: HasCallStack => App () testCreateConversationNonFullyConnected = do let setFederationConfig = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends [ def {brigCfg = setFederationConfig}, @@ -165,155 +156,29 @@ testCreateConversationNonFullyConnected = do def {brigCfg = setFederationConfig} ] $ \dynDomains -> do - domains@[domainA, domainB, domainC] <- pure dynDomains - connectAllDomainsAndWaitToSync 1 domains - [u1, u2, u3] <- createAndConnectUsers [domainA, domainB, domainC] - -- stop federation between B and C - void $ deleteFedConn domainB domainC - void $ deleteFedConn domainC domainB + [domainA, domainB, domainC] <- pure dynDomains + + -- A is connected to B and C, but B and C are not connected to each other + void $ createFedConn domainA $ FedConn domainB "full_search" + void $ createFedConn domainB $ FedConn domainA "full_search" + void $ createFedConn domainA $ FedConn domainC "full_search" + void $ createFedConn domainC $ FedConn domainA "full_search" liftIO $ threadDelay (2 * 1000 * 1000) + + u1 <- randomUser domainA def + u2 <- randomUser domainB def + u3 <- randomUser domainC def + connectUsers u1 u2 + connectUsers u1 u3 + bindResponse (postConversation u1 (defProteus {qualifiedUsers = [u2, u3]})) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "non_federating_backends" `shouldMatchSet` [domainB, domainC] -testDefederationGroupConversation :: HasCallStack => App () -testDefederationGroupConversation = do - let setFederationConfig = - setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" - >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - startDynamicBackends - [ def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig} - ] - $ \dynDomains -> do - domains@[domainA, domainB] <- pure dynDomains - connectAllDomainsAndWaitToSync 1 domains - [uA, uB] <- createAndConnectUsers [domainA, domainB] - withWebSocket uA $ \ws -> do - -- create group conversation owned by domainB - convId <- bindResponse (postConversation uB (defProteus {qualifiedUsers = [uA]})) $ \r -> do - r.status `shouldMatchInt` 201 - r.json %. "qualified_id" - - -- check conversation exists and uB is a member from POV of uA - bindResponse (getConversation uA convId) $ \r -> do - r.status `shouldMatchInt` 200 - members <- r.json %. "members.others" & asList - qIds <- for members (\m -> m %. "qualified_id") - uBQId <- objQidObject uB - qIds `shouldMatchSet` [uBQId] - - -- check conversation exists and uA is a member from POV of uB - bindResponse (getConversation uB convId) $ \r -> do - r.status `shouldMatchInt` 200 - members <- r.json %. "members.others" & asList - qIds <- for members (\m -> m %. "qualified_id") - uAQId <- objQidObject uA - qIds `shouldMatchSet` [uAQId] - - -- domainA stops federating with domainB - void $ deleteFedConn domainA domainB - - -- assert conversation deleted from domainA - retryT $ - bindResponse (getConversation uA convId) $ \r -> - r.status `shouldMatchInt` 404 - - -- assert federation.delete event is sent twice - void $ - awaitNMatches - 2 - 3 - ( \n -> do - correctType <- nPayload n %. "type" `isEqual` "federation.delete" - if correctType - then nPayload n %. "domain" `isEqual` domainB - else pure False - ) - ws - - -- assert no conversation.delete event is sent to uA - eventPayloads <- - getNotifications uA "cA" def - >>= getJSON 200 - >>= \n -> n %. "notifications" & asList >>= \ns -> for ns nPayload - - forM_ eventPayloads $ \p -> - p %. "type" `shouldNotMatch` "conversation.delete" - -testDefederationOneOnOne :: HasCallStack => App () -testDefederationOneOnOne = do - let setFederationConfig = - setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" - >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - startDynamicBackends - [ def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig} - ] - $ \dynDomains -> do - domains@[domainA, domainB] <- pure dynDomains - connectAllDomainsAndWaitToSync 1 domains - [uA, uB] <- createAndConnectUsers [domainA, domainB] - -- figure out on which backend the 1:1 conversation is created - qConvId <- getConnection uA uB >>= \c -> c.json %. "qualified_conversation" - - -- check conversation exists and uB is a member from POV of uA - bindResponse (getConversation uA qConvId) $ \r -> do - r.status `shouldMatchInt` 200 - members <- r.json %. "members.others" & asList - qIds <- for members (\m -> m %. "qualified_id") - uBQId <- objQidObject uB - qIds `shouldMatchSet` [uBQId] - - -- check conversation exists and uA is a member from POV of uB - bindResponse (getConversation uB qConvId) $ \r -> do - r.status `shouldMatchInt` 200 - members <- r.json %. "members.others" & asList - qIds <- for members (\m -> m %. "qualified_id") - uAQId <- objQidObject uA - qIds `shouldMatchSet` [uAQId] - - conversationOwningDomain <- objDomain qConvId - - when (domainA == conversationOwningDomain) $ do - -- conversation is created on domainA - assertFederationTerminatingUserNoConvDeleteEvent uB qConvId domainB domainA - - when (domainB == conversationOwningDomain) $ do - -- conversation is created on domainB - assertFederationTerminatingUserNoConvDeleteEvent uA qConvId domainA domainB - - when (domainA /= conversationOwningDomain && domainB /= conversationOwningDomain) $ do - -- this should not happen - error "impossible" - where - assertFederationTerminatingUserNoConvDeleteEvent :: Value -> Value -> String -> String -> App () - assertFederationTerminatingUserNoConvDeleteEvent user convId ownDomain otherDomain = do - withWebSocket user $ \ws -> do - void $ deleteFedConn ownDomain otherDomain - - -- assert conversation deleted eventually - retryT $ - bindResponse (getConversation user convId) $ \r -> - r.status `shouldMatchInt` 404 - - -- assert federation.delete event is sent twice - void $ awaitNMatches 2 3 (\n -> nPayload n %. "type" `isEqual` "federation.delete") ws - - -- assert no conversation.delete event is sent to uA - eventPayloads <- - getNotifications user "user-client" def - >>= getJSON 200 - >>= \n -> n %. "notifications" & asList >>= \ns -> for ns nPayload - - forM_ eventPayloads $ \p -> - p %. "type" `shouldNotMatch` "conversation.delete" - testAddMembersFullyConnectedProteus :: HasCallStack => App () testAddMembersFullyConnectedProteus = do - withFederatingBackendsAllowDynamic 2 $ \(domainA, domainB, domainC) -> do + withFederatingBackendsAllowDynamic $ \(domainA, domainB, domainC) -> do + connectAllDomainsAndWaitToSync 2 [domainA, domainB, domainC] [u1, u2, u3] <- createAndConnectUsers [domainA, domainB, domainC] -- create conversation with no users cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 @@ -327,14 +192,22 @@ testAddMembersFullyConnectedProteus = do testAddMembersNonFullyConnectedProteus :: HasCallStack => App () testAddMembersNonFullyConnectedProteus = do - withFederatingBackendsAllowDynamic 2 $ \(domainA, domainB, domainC) -> do - [u1, u2, u3] <- createAndConnectUsers [domainA, domainB, domainC] + withFederatingBackendsAllowDynamic $ \(domainA, domainB, domainC) -> do + void $ createFedConn domainA (FedConn domainB "full_search") + void $ createFedConn domainB (FedConn domainA "full_search") + void $ createFedConn domainA (FedConn domainC "full_search") + void $ createFedConn domainC (FedConn domainA "full_search") + liftIO $ threadDelay (2 * 1000 * 1000) -- wait for federation status to be updated + + -- add users + u1 <- randomUser domainA def + u2 <- randomUser domainB def + u3 <- randomUser domainC def + connectUsers u1 u2 + connectUsers u1 u3 + -- create conversation with no users cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 - -- stop federation between B and C - void $ deleteFedConn domainB domainC - void $ deleteFedConn domainC domainB - liftIO $ threadDelay (2 * 1000 * 1000) -- wait for federation status to be updated -- add members from remote backends members <- for [u2, u3] (%. "qualified_id") bindResponse (addMembers u1 cid members) $ \resp -> do @@ -345,7 +218,6 @@ testConvWithUnreachableRemoteUsers :: HasCallStack => App () testConvWithUnreachableRemoteUsers = do let overrides = def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - <> fullSearchWithAll ([alice, alex, bob, charlie, dylan], domains) <- startDynamicBackends [overrides, overrides] $ \domains -> do own <- make OwnDomain & asString @@ -366,7 +238,6 @@ testAddReachableWithUnreachableRemoteUsers :: HasCallStack => App () testAddReachableWithUnreachableRemoteUsers = do let overrides = def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - <> fullSearchWithAll ([alex, bob], conv, domains) <- startDynamicBackends [overrides, overrides] $ \domains -> do own <- make OwnDomain & asString @@ -392,7 +263,6 @@ testAddUnreachable :: HasCallStack => App () testAddUnreachable = do let overrides = def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - <> fullSearchWithAll ([alex, charlie], [charlieDomain, dylanDomain], conv) <- startDynamicBackends [overrides, overrides] $ \domains -> do own <- make OwnDomain & asString @@ -416,7 +286,6 @@ testAddingUserNonFullyConnectedFederation = do def { brigCfg = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" } startDynamicBackends [overrides] $ \[dynBackend] -> do own <- asString OwnDomain @@ -424,10 +293,6 @@ testAddingUserNonFullyConnectedFederation = do -- Ensure that dynamic backend only federates with own domain, but not other -- domain. - -- - -- FUTUREWORK: deleteAllFedConns at the time of acquiring a backend, so - -- tests don't affect each other. - deleteAllFedConns dynBackend void $ createFedConn dynBackend (FedConn own "full_search") alice <- randomUser own def @@ -534,7 +399,6 @@ testAddUserWhenOtherBackendOffline :: HasCallStack => App () testAddUserWhenOtherBackendOffline = do let overrides = def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - <> fullSearchWithAll ([alice, alex], conv) <- startDynamicBackends [overrides] $ \domains -> do own <- make OwnDomain & asString diff --git a/integration/test/Test/Defederation.hs b/integration/test/Test/Defederation.hs deleted file mode 100644 index 513efe535f6..00000000000 --- a/integration/test/Test/Defederation.hs +++ /dev/null @@ -1,85 +0,0 @@ -module Test.Defederation where - -import API.BrigInternal -import API.BrigInternal qualified as Internal -import API.Galley (defProteus, getConversation, postConversation, qualifiedUsers) -import Control.Applicative -import Data.Aeson qualified as Aeson -import GHC.Stack -import SetupHelpers -import Testlib.Prelude - -testDefederationRemoteNotifications :: HasCallStack => App () -testDefederationRemoteNotifications = do - let remoteDomain = "example.example.com" - -- Setup federation between OtherDomain and the remote domain - bindResponse (createFedConn OtherDomain $ object ["domain" .= remoteDomain, "search_policy" .= "full_search"]) $ \resp -> - resp.status `shouldMatchInt` 200 - - -- Setup a remote user we can get notifications for. - user <- randomUser OtherDomain def - - withWebSocket user $ \ws -> do - -- Defederate from a domain that doesn't exist. This won't do anything to the databases - -- But it will send out notifications that we can wait on. - -- Begin the whole process at Brig, the same as an operator would. - void $ deleteFedConn OwnDomain remoteDomain - void $ awaitNMatches 2 3 (\n -> nPayload n %. "type" `isEqual` "federation.connectionRemoved") ws - -testDefederationNonFullyConnectedGraph :: HasCallStack => App () -testDefederationNonFullyConnectedGraph = do - let setFederationConfig = - setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" - >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - startDynamicBackends - [ def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig} - ] - $ \dynDomains -> do - domains@[domainA, domainB, domainC] <- pure dynDomains - connectAllDomainsAndWaitToSync 1 domains - - -- create a few extra users and connections to make sure that does not lead to any extra `connectionRemoved` notifications - [uA, uA2, _, _, uB, uC] <- createAndConnectUsers [domainA, domainA, domainA, domainA, domainB, domainC] >>= traverse objQidObject - - -- create group conversation owned by domainA with users from domainB and domainC - convId <- bindResponse (postConversation uA (defProteus {qualifiedUsers = [uA2, uB, uC]})) $ \r -> do - r.status `shouldMatchInt` 201 - r.json %. "qualified_id" - - -- check conversation exists on all backends - checkConv convId uA [uB, uC, uA2] - checkConv convId uB [uA, uC, uA2] - checkConv convId uC [uA, uB, uA2] - - withWebSocket uA $ \wsA -> do - -- one of the 2 non-conversation-owning domains (domainB and domainC) - -- defederate from the other non-conversation-owning domain - void $ Internal.deleteFedConn domainB domainC - - -- assert that clients from domainA receive federation.connectionRemoved events - -- Notifications being delivered exactly twice - void $ awaitNMatches 2 20 (isConnectionRemoved [domainB, domainC]) wsA - - -- remote members should be removed from local conversation eventually - retryT $ checkConv convId uA [uA2] - where - isConnectionRemoved :: [String] -> Value -> App Bool - isConnectionRemoved domains n = do - correctType <- nPayload n %. "type" `isEqual` "federation.connectionRemoved" - if correctType - then do - domsV <- nPayload n %. "domains" & asList - domsStr <- for domsV asString <&> sort - pure $ domsStr == sort domains - else pure False - - checkConv :: Value -> Value -> [Value] -> App () - checkConv convId user expectedOtherMembers = do - bindResponse (getConversation user convId) $ \r -> do - r.status `shouldMatchInt` 200 - members <- r.json %. "members.others" & asList - qIds <- for members (\m -> m %. "qualified_id") - qIds `shouldMatchSet` expectedOtherMembers diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 69d3cf1b0c4..3709ea47999 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -9,6 +9,7 @@ import Control.Monad.Cont import GHC.Stack import SetupHelpers import Testlib.Prelude +import UnliftIO.Concurrent (threadDelay) -- | Legalhold clients cannot be deleted. testCantDeleteLHClient :: HasCallStack => App () @@ -173,12 +174,16 @@ testIndependentESIndices = do testDynamicBackendsFederation :: HasCallStack => App () testDynamicBackendsFederation = do - startDynamicBackends [def <> fullSearchWithAll, def <> fullSearchWithAll] $ \dynDomains -> do + startDynamicBackends [def, def] $ \dynDomains -> do [aDynDomain, anotherDynDomain] <- pure dynDomains + _ <- Internal.createFedConn anotherDynDomain (Internal.FedConn aDynDomain "full_search") + threadDelay 2_000_000 + u1 <- randomUser aDynDomain def u2 <- randomUser anotherDynDomain def uid2 <- objId u2 Internal.refreshIndex anotherDynDomain + bindResponse (Public.searchContacts u1 (u2 %. "name") anotherDynDomain) $ \resp -> do resp.status `shouldMatchInt` 200 docs <- resp.json %. "documents" >>= asList diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index b531a43bf54..326f13034b2 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -4,12 +4,14 @@ module Testlib.Env where import Control.Monad.Codensity import Control.Monad.IO.Class +import Data.Function ((&)) import Data.Functor import Data.IORef import Data.Map qualified as Map import Data.Set (Set) import Data.Set qualified as Set import Data.Yaml qualified as Yaml +import Database.CQL.IO qualified as Cassandra import Network.HTTP.Client qualified as HTTP import System.Exit import System.FilePath @@ -49,10 +51,16 @@ mkGlobalEnv cfgFile = do else Nothing manager <- HTTP.newManager HTTP.defaultManagerSettings + let cassSettings = + Cassandra.defSettings + & Cassandra.setContacts intConfig.cassandra.host [] + & Cassandra.setPortNumber (fromIntegral intConfig.cassandra.port) + cassClient <- Cassandra.init cassSettings resourcePool <- createBackendResourcePool (Map.elems intConfig.dynamicBackends) intConfig.rabbitmq + cassClient pure GlobalEnv { gServiceMap = diff --git a/integration/test/Testlib/ResourcePool.hs b/integration/test/Testlib/ResourcePool.hs index 582e36e7529..c7483ca9478 100644 --- a/integration/test/Testlib/ResourcePool.hs +++ b/integration/test/Testlib/ResourcePool.hs @@ -22,6 +22,7 @@ import Data.Set qualified as Set import Data.String import Data.Text qualified as T import Data.Tuple +import Database.CQL.IO import GHC.Stack (HasCallStack) import Network.AMQP.Extended import Network.RabbitMqAdmin @@ -46,13 +47,17 @@ acquireResources n pool = Codensity $ \f -> bracket acquire release $ \s -> do waitQSemN pool.sem n atomicModifyIORef pool.resources $ swap . Set.splitAt n -createBackendResourcePool :: [DynamicBackendConfig] -> RabbitMQConfig -> IO (ResourcePool BackendResource) -createBackendResourcePool dynConfs rabbitmq = +createBackendResourcePool :: [DynamicBackendConfig] -> RabbitMQConfig -> ClientState -> IO (ResourcePool BackendResource) +createBackendResourcePool dynConfs rabbitmq cassClient = let resources = backendResources dynConfs + cleanupBackend :: BackendResource -> IO () + cleanupBackend resource = do + deleteAllRabbitMQQueues rabbitmq resource + runClient cassClient $ deleteAllDynamicBackendConfigs resource in ResourcePool <$> newQSemN (length dynConfs) <*> newIORef resources - <*> pure (deleteAllRabbitMQQueues rabbitmq) + <*> pure cleanupBackend deleteAllRabbitMQQueues :: RabbitMQConfig -> BackendResource -> IO () deleteAllRabbitMQQueues rc resource = do @@ -68,6 +73,12 @@ deleteAllRabbitMQQueues rc resource = do for_ queues $ \queue -> deleteQueue client (T.pack resource.berVHost) queue.name +deleteAllDynamicBackendConfigs :: BackendResource -> Client () +deleteAllDynamicBackendConfigs resource = write cql (defQueryParams LocalQuorum ()) + where + cql :: PrepQuery W () () + cql = fromString $ "TRUNCATE " <> resource.berBrigKeyspace <> ".federation_remotes" + backendResources :: [DynamicBackendConfig] -> Set.Set BackendResource backendResources dynConfs = (zip dynConfs [1 ..]) diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index 5f2b5a3eb34..9e07814aa53 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -4,7 +4,6 @@ module Testlib.RunServices where import Control.Concurrent import Control.Monad.Codensity (lowerCodensity) -import SetupHelpers import System.Directory import System.Environment (getArgs) import System.Exit (exitWith) @@ -62,10 +61,6 @@ main = do lowerCodensity $ do _modifyEnv <- traverseConcurrentlyCodensity - ( \resource -> - -- We add the 'fullSerachWithAll' overrrides is a hack to get - -- around https://wearezeta.atlassian.net/browse/WPB-3796 - startDynamicBackend resource fullSearchWithAll - ) + (`startDynamicBackend` def) [backendA, backendB] liftIO run diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 847b8eaa107..12403339816 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -111,7 +111,8 @@ data IntegrationConfig = IntegrationConfig { backendOne :: BackendConfig, backendTwo :: BackendConfig, dynamicBackends :: Map String DynamicBackendConfig, - rabbitmq :: RabbitMQConfig + rabbitmq :: RabbitMQConfig, + cassandra :: HostPort } deriving (Show, Generic) @@ -123,6 +124,7 @@ instance FromJSON IntegrationConfig where <*> o .: fromString "backendTwo" <*> o .: fromString "dynamicBackends" <*> o .: fromString "rabbitmq" + <*> o .: fromString "cassandra" data ServiceMap = ServiceMap { brig :: HostPort, diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index 548ed8bfbe0..6ff30f7ed38 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -62,7 +62,6 @@ module Data.Schema fieldOverF, fieldWithDocModifierF, array, - pair, set, nonEmptyArray, map_, @@ -464,24 +463,6 @@ array sch = SchemaP (SchemaDoc s) (SchemaIn r) (SchemaOut w) s = mkArray (schemaDoc sch) w x = A.Array . V.fromList <$> mapM (schemaOut sch) x --- | A schema for a JSON pair. --- This is serialised as JSON array of exactly 2 elements --- of the same type. Any more or less is an error. -pair :: - (HasArray ndoc doc, HasName ndoc) => - ValueSchema ndoc a -> - ValueSchema doc (a, a) -pair sch = SchemaP (SchemaDoc s) (SchemaIn r) (SchemaOut w) - where - name = maybe "pair" ("pair of " <>) (getName (schemaDoc sch)) - r = A.withArray (T.unpack name) $ \arr -> do - l <- mapM (schemaIn sch) $ V.toList arr - case l of - [a, b] -> pure (a, b) - _ -> fail $ "Expected exactly 2 elements, but got " <> show (length l) - s = mkArray (schemaDoc sch) - w (a, b) = A.Array . V.fromList <$> mapM (schemaOut sch) [a, b] - set :: (HasArray ndoc doc, HasName ndoc, Ord a) => ValueSchema ndoc a -> 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 faf5a71fbcb..3c4d1db2fa6 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 @@ -131,7 +131,6 @@ type GalleyApi = TypingDataUpdateRequest TypingDataUpdateResponse :<|> FedEndpoint "on-typing-indicator-updated" TypingDataUpdated EmptyResponse - :<|> FedEndpoint "on-connection-removed" Domain EmptyResponse data TypingDataUpdateRequest = TypingDataUpdateRequest { typingStatus :: TypingStatus, diff --git a/libs/wire-api/src/Wire/API/Event/Federation.hs b/libs/wire-api/src/Wire/API/Event/Federation.hs index cc38b1e2795..17d5120c0ce 100644 --- a/libs/wire-api/src/Wire/API/Event/Federation.hs +++ b/libs/wire-api/src/Wire/API/Event/Federation.hs @@ -1,12 +1,9 @@ -{-# LANGUAGE TemplateHaskell #-} - module Wire.API.Event.Federation ( Event (..), + EventType (..), ) where -import Control.Arrow ((&&&)) -import Control.Lens (makePrisms) import Data.Aeson (FromJSON, ToJSON) import Data.Aeson qualified as A import Data.Aeson.KeyMap qualified as KeyMap @@ -15,71 +12,41 @@ import Data.Json.Util (ToJSONObject (toJSONObject)) import Data.Schema import Data.Swagger qualified as S import Imports -import Test.QuickCheck.Gen import Wire.Arbitrary -data Event - = FederationDelete Domain - | FederationConnectionRemoved (Domain, Domain) - deriving stock (Eq, Show, Generic) - -$(makePrisms ''Event) +data Event = Event + { _eventType :: EventType, + _eventDomain :: Domain + } + deriving (Eq, Show, Ord, Generic) instance Arbitrary Event where arbitrary = - oneof - [ FederationDelete <$> arbitrary, - FederationConnectionRemoved <$> arbitrary - ] + Event + <$> arbitrary + <*> arbitrary data EventType - = FederationTypeDelete - | FederationTypeConnectionRemoved - deriving (Eq, Show, Ord, Enum, Bounded, Generic) + = FederationDelete + deriving (Eq, Show, Ord, Generic) deriving (Arbitrary) via (GenericUniform EventType) deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema EventType instance ToSchema EventType where schema = - enum @Text "FederationEventType" $ + enum @Text "EventType" $ mconcat - [ element "federation.delete" FederationTypeDelete, - element "federation.connectionRemoved" FederationTypeConnectionRemoved + [ element "federation.delete" FederationDelete ] -eventType :: Event -> EventType -eventType (FederationDelete _) = FederationTypeDelete -eventType (FederationConnectionRemoved _) = FederationTypeConnectionRemoved - -taggedEventDataSchema :: ObjectSchema SwaggerDoc (EventType, Event) -taggedEventDataSchema = - bind - (fst .= field "type" schema) - -- The fields we need to look at change based on the event - -- type, so we need to dispatch here to get monadic-ish behaviour. - -- - -- federation.delete is expecting a "domain" field that contains a bare domain string. - -- federation.connectionRemoved is expecting a "domains" field that contains exactly a pair of domains in a list - ( snd .= dispatch dataSchema - ) - where - dataSchema :: EventType -> ObjectSchema SwaggerDoc Event - dataSchema FederationTypeDelete = tag _FederationDelete deleteSchema - dataSchema FederationTypeConnectionRemoved = tag _FederationConnectionRemoved connectionRemovedSchema - --- These schemas have different fields they are targeting. -deleteSchema :: ObjectSchema SwaggerDoc Domain -deleteSchema = field "domain" schema - -connectionRemovedSchema :: ObjectSchema SwaggerDoc (Domain, Domain) -connectionRemovedSchema = field "domains" (pair schema) - --- Schemas for the events, as they have different structures. eventObjectSchema :: ObjectSchema SwaggerDoc Event -eventObjectSchema = snd <$> (eventType &&& id) .= taggedEventDataSchema +eventObjectSchema = + Event + <$> _eventType .= field "type" schema + <*> _eventDomain .= field "domain" schema instance ToSchema Event where - schema = object "FederationEvent" eventObjectSchema + schema = object "Event" eventObjectSchema instance ToJSONObject Event where toJSONObject = diff --git a/libs/wire-api/src/Wire/API/FederationUpdate.hs b/libs/wire-api/src/Wire/API/FederationUpdate.hs index 0bafee435af..aeb700fa52b 100644 --- a/libs/wire-api/src/Wire/API/FederationUpdate.hs +++ b/libs/wire-api/src/Wire/API/FederationUpdate.hs @@ -2,21 +2,18 @@ module Wire.API.FederationUpdate ( syncFedDomainConfigs, SyncFedDomainConfigsCallback (..), emptySyncFedDomainConfigsCallback, - deleteFederationRemoteGalley, - fetch, ) where import Control.Concurrent.Async import Control.Exception import Control.Retry qualified as R -import Data.Domain import Data.Set qualified as Set import Data.Text import Data.Typeable (cast) import Imports import Network.HTTP.Client (defaultManagerSettings, newManager) -import Servant.Client (BaseUrl (BaseUrl), ClientEnv (ClientEnv), ClientError, ClientM, Scheme (Http), runClientM) +import Servant.Client (BaseUrl (BaseUrl), ClientEnv (ClientEnv), ClientError, Scheme (Http), runClientM) import Servant.Client.Internal.HttpClient (defaultMakeClientRequest) import System.Logger qualified as L import Util.Options @@ -34,9 +31,6 @@ syncFedDomainConfigs (Endpoint h p) log' cb = do updateDomainsThread <- async $ loop log' clientEnv cb ioref pure (ioref, updateDomainsThread) -deleteFedRemoteGalley :: Domain -> ClientM () -deleteFedRemoteGalley dom = namedClient @IAPI.API @"delete-federation-remote-from-galley" dom - -- | Initial function for getting the set of domains from brig, and an update interval initialize :: L.Logger -> ClientEnv -> IO FederationDomainConfigs initialize logger clientEnv = @@ -56,9 +50,6 @@ initialize logger clientEnv = Just c -> pure c Nothing -> throwIO $ ErrorCall "*** Failed to reach brig for federation setup, giving up!" -deleteFederationRemoteGalley :: Domain -> ClientEnv -> IO (Either ClientError ()) -deleteFederationRemoteGalley dom = runClientM $ deleteFedRemoteGalley dom - loop :: L.Logger -> ClientEnv -> SyncFedDomainConfigsCallback -> IORef FederationDomainConfigs -> IO () loop logger clientEnv (SyncFedDomainConfigsCallback callback) env = forever $ catch go $ \(e :: SomeException) -> do 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 beab0e82a20..8a343a285b3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -764,40 +764,10 @@ type FederationRemotesAPI = :> ReqBody '[JSON] FederationDomainConfig :> Put '[JSON] () ) - :<|> Named - "delete-federation-remotes" - ( Description FederationRemotesAPIDescription - :> Description FederationRemotesAPIDeleteDescription - :> "federation" - :> "remotes" - :> Capture "domain" Domain - :> Delete '[JSON] () - ) - -- This is nominally similar to delete-federation-remotes, - -- but is called from Galley to delete the one-on-one coversations. - -- This is needed as Galley doesn't have access to the tables - -- that hold these values. We don't want these deletes to happen - -- in delete-federation-remotes as brig might fall over and leave - -- some records hanging around. Galley uses a Rabbit queue to track - -- what is has done and can recover from a service falling over. - :<|> Named - "delete-federation-remote-from-galley" - ( Description FederationRemotesAPIDescription - :> Description FederationRemotesAPIDeleteDescription - :> "federation" - :> "remote" - :> Capture "domain" Domain - :> "galley" - :> Delete '[JSON] () - ) type FederationRemotesAPIDescription = "See https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections for background. " -type FederationRemotesAPIDeleteDescription = - "**WARNING!** If you remove a remote connection, all users from that remote will be removed from local conversations, and all \ - \group conversations hosted by that remote will be removed from the local backend. This cannot be reverted! " - swaggerDoc :: Swagger swaggerDoc = toSwagger (Proxy @API) diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index 2dc7c897f45..f0c7083d066 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -18,7 +18,6 @@ library Wire.BackgroundWorker.Health Wire.BackgroundWorker.Options Wire.BackgroundWorker.Util - Wire.Defederation hs-source-dirs: src default-language: GHC2021 @@ -32,17 +31,13 @@ library , amqp , async , base - , bilge - , bytestring-conversion , containers , exceptions , extended , HsOpenSSL , http-client - , http-types , http2-manager , imports - , lens , metrics-core , metrics-wai , monad-control @@ -175,7 +170,6 @@ test-suite background-worker-test other-modules: Main Test.Wire.BackendNotificationPusherSpec - Test.Wire.DefederationSpec Test.Wire.Util build-depends: @@ -191,7 +185,6 @@ test-suite background-worker-test , http-client , http-media , http-types - , HUnit , imports , prometheus-client , QuickCheck diff --git a/services/background-worker/default.nix b/services/background-worker/default.nix index de2217e45b6..ce67f35095a 100644 --- a/services/background-worker/default.nix +++ b/services/background-worker/default.nix @@ -7,9 +7,7 @@ , amqp , async , base -, bilge , bytestring -, bytestring-conversion , containers , exceptions , extended @@ -21,9 +19,7 @@ , http-media , http-types , http2-manager -, HUnit , imports -, lens , lib , metrics-core , metrics-wai @@ -57,17 +53,13 @@ mkDerivation { amqp async base - bilge - bytestring-conversion containers exceptions extended HsOpenSSL http-client - http-types http2-manager imports - lens metrics-core metrics-wai monad-control @@ -97,7 +89,6 @@ mkDerivation { http-client http-media http-types - HUnit imports prometheus-client QuickCheck diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index 117b135aa6b..7709cb8bd52 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -17,31 +17,16 @@ import Wire.BackendNotificationPusher qualified as BackendNotificationPusher import Wire.BackgroundWorker.Env import Wire.BackgroundWorker.Health qualified as Health import Wire.BackgroundWorker.Options -import Wire.Defederation as Defederation --- FUTUREWORK: Start an http service with status and metrics endpoints run :: Opts -> IO () run opts = do (env, syncThread) <- mkEnv opts - (defedChanRef, defedConsumerRef) <- runAppT env $ Defederation.startWorker opts.rabbitmq (notifChanRef, notifConsumersRef) <- runAppT env $ BackendNotificationPusher.startWorker opts.rabbitmq let -- cleanup will run in a new thread when the signal is caught, so we need to use IORefs and -- specific exception types to message threads to clean up l = logger env cleanup = do cancel syncThread - -- Cancel the consumers and wait for them to finish their processing step. - -- Defederation thread - Log.info (logger env) $ Log.msg (Log.val "Cancelling the defederation thread") - readIORef defedChanRef >>= traverse_ \chan -> do - Log.info (logger env) $ Log.msg (Log.val "Got channel") - readIORef defedConsumerRef >>= traverse_ \(consumer, runningFlag) -> do - Log.info l $ Log.msg (Log.val "Cancelling consumer") - Q.cancelConsumer chan consumer - Log.info l $ Log.msg $ Log.val "Taking MVar. Waiting for current operation to finish" - takeMVar runningFlag - Log.info l $ Log.msg $ Log.val "Closing RabbitMQ channel" - Q.closeChannel chan -- Notification pusher thread Log.info (logger env) $ Log.msg (Log.val "Cancelling the notification pusher thread") readIORef notifChanRef >>= traverse_ \chan -> do diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index 86a5b99ed57..37bbaffad01 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -12,7 +12,6 @@ import Data.Map.Strict qualified as Map import Data.Metrics qualified as Metrics import HTTP2.Client.Manager import Imports -import Network.AMQP (Channel) import Network.AMQP.Extended import Network.HTTP.Client import Network.RabbitMqAdmin qualified as RabbitMqAdmin @@ -33,7 +32,6 @@ type IsWorking = Bool -- | Eventually this will be a sum type of all the types of workers data Worker = BackendNotificationPusher - | DefederationWorker deriving (Show, Eq, Ord) data Env = Env @@ -50,10 +48,6 @@ data Env = Env remoteDomains :: IORef FederationDomainConfigs, remoteDomainsChan :: Chan FederationDomainConfigs, backendNotificationMetrics :: BackendNotificationMetrics, - -- This is needed so that the defederation worker can push - -- connection-removed notifications into the notifications channels. - -- This allows us to reuse existing code. This only pushes. - notificationChannel :: MVar Channel, backendNotificationsConfig :: BackendNotificationsConfig, statuses :: IORef (Map Worker IsWorking) } @@ -96,12 +90,10 @@ mkEnv opts = do statuses <- newIORef $ Map.fromList - [ (BackendNotificationPusher, False), - (DefederationWorker, False) + [ (BackendNotificationPusher, False) ] metrics <- Metrics.metrics backendNotificationMetrics <- mkBackendNotificationMetrics - notificationChannel <- mkRabbitMqChannelMVar logger $ demoteOpts opts.rabbitmq let backendNotificationsConfig = opts.backendNotificationPusher pure (Env {..}, syncThread) diff --git a/services/background-worker/src/Wire/BackgroundWorker/Health.hs b/services/background-worker/src/Wire/BackgroundWorker/Health.hs index 26c8374654b..dc0cc0a97d7 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Health.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Health.hs @@ -7,12 +7,13 @@ import Servant.Server.Generic import Wire.BackgroundWorker.Env data HealthAPI routes = HealthAPI - { status :: routes :- "i" :> "status" :> Get '[PlainText] NoContent + { status :: routes :- "i" :> "status" :> Get '[PlainText] NoContent, + statusWorkers :: routes :- "i" :> "status" :> "workers" :> Get '[PlainText] NoContent } deriving (Generic) -statusImpl :: AppT Handler NoContent -statusImpl = do +statusWorkersImpl :: AppT Handler NoContent +statusWorkersImpl = do notWorkingWorkers <- Map.keys . Map.filter not <$> (readIORef =<< asks statuses) if null notWorkingWorkers then pure NoContent @@ -22,4 +23,8 @@ api :: Env -> HealthAPI AsServer api env = fromServant $ hoistServer (Proxy @(ToServant HealthAPI AsApi)) (runAppT env) (toServant apiInAppT) where apiInAppT :: HealthAPI (AsServerT (AppT Handler)) - apiInAppT = HealthAPI {status = statusImpl} + apiInAppT = + HealthAPI + { status = pure NoContent, + statusWorkers = statusWorkersImpl + } diff --git a/services/background-worker/src/Wire/Defederation.hs b/services/background-worker/src/Wire/Defederation.hs deleted file mode 100644 index e8da0e9366b..00000000000 --- a/services/background-worker/src/Wire/Defederation.hs +++ /dev/null @@ -1,143 +0,0 @@ -module Wire.Defederation where - -import Bilge.Retry -import Control.Concurrent.Async -import Control.Lens (to, (^.)) -import Control.Monad.Catch -import Control.Retry -import Data.Aeson qualified as A -import Data.ByteString.Conversion -import Data.Domain -import Data.Text (unpack) -import Data.Text.Encoding -import Imports -import Network.AMQP qualified as Q -import Network.AMQP.Extended -import Network.AMQP.Lifted qualified as QL -import Network.HTTP.Client -import Network.HTTP.Types -import Servant.Client (BaseUrl (..), ClientEnv, Scheme (Http), mkClientEnv) -import System.Logger.Class qualified as Log -import Util.Options -import Util.Options qualified as O -import Wire.API.Federation.BackendNotifications -import Wire.API.Routes.FederationDomainConfig qualified as Fed -import Wire.BackgroundWorker.Env -import Wire.BackgroundWorker.Util - -deleteFederationDomain :: MVar () -> Q.Channel -> AppT IO Q.ConsumerTag -deleteFederationDomain runningFlag chan = do - lift $ ensureQueue chan defederationQueue - QL.consumeMsgs chan (routingKey defederationQueue) Q.Ack $ deleteFederationDomainInner runningFlag - -x3 :: RetryPolicy -x3 = limitRetries 3 <> exponentialBackoff 100000 - --- Exposed for testing purposes so we can decode without further processing the message. -deleteFederationDomainInner' :: (RabbitMQEnvelope e) => (e -> DefederationDomain -> AppT IO ()) -> (Q.Message, e) -> AppT IO () -deleteFederationDomainInner' go (msg, envelope) = do - either - ( \e -> do - void $ logErr e - -- ensure that the message is _NOT_ requeued - -- This means that we won't process this message again - -- as it is unparsable. - liftIO $ reject envelope False - ) - (go envelope) - $ A.eitherDecode @DefederationDomain (Q.msgBody msg) - where - logErr err = - Log.err $ - Log.msg (Log.val "Failed to delete federation domain") - . Log.field "error" err - -mkBrigEnv :: AppT IO ClientEnv -mkBrigEnv = do - Endpoint brigHost brigPort <- asks brig - mkClientEnv - <$> asks httpManager - <*> pure (BaseUrl Http (unpack brigHost) (fromIntegral brigPort) "") - -getRemoteDomains :: AppT IO [Domain] -getRemoteDomains = do - ref <- asks remoteDomains - fmap Fed.domain . Fed.remotes <$> readIORef ref - -callGalleyDelete :: - ( MonadReader Env m, - MonadMask m, - ToByteString a, - RabbitMQEnvelope e, - MonadIO m - ) => - MVar () -> - e -> - a -> - m () -callGalleyDelete runningFlag envelope domain = do - env <- ask - -- Jittered exponential backoff with 10ms as starting delay and 60s as max - -- delay. When 60 is reached, every retry will happen after 60s. - let policy = capDelay 60_000_000 $ fullJitterBackoff 10000 - manager = httpManager env - recovering policy httpHandlers $ \_ -> - bracket_ (takeMVar runningFlag) (putMVar runningFlag ()) $ do - -- Non 2xx responses will throw an exception - -- So we are relying on that to be caught by recovering - resp <- liftIO $ httpLbs (req env domain) manager - let code = statusCode $ responseStatus resp - if code >= 200 && code <= 299 - then do - liftIO $ ack envelope - else -- ensure that the message is requeued - -- This message was able to be parsed but something - -- else in our stack failed and we should try again. - liftIO $ reject envelope True - -req :: ToByteString a => Env -> a -> Request -req env dom = - defaultRequest - { method = methodDelete, - secure = False, - host = galley env ^. O.host . to encodeUtf8, - port = galley env ^. O.port . to fromIntegral, - path = "/i/federation/" <> toByteString' dom, - requestHeaders = ("Accept", "application/json") : requestHeaders defaultRequest, - responseTimeout = defederationTimeout env - } - --- What should we do with non-recoverable (unparsable) errors/messages? --- should we deadletter, or do something else? --- Deadlettering has a privacy implication -- FUTUREWORK. -deleteFederationDomainInner :: RabbitMQEnvelope e => MVar () -> (Q.Message, e) -> AppT IO () -deleteFederationDomainInner runningFlag (msg, envelope) = - deleteFederationDomainInner' (const $ callGalleyDelete runningFlag envelope) (msg, envelope) - -startDefederator :: IORef (Maybe (Q.ConsumerTag, MVar ())) -> Q.Channel -> AppT IO () -startDefederator consumerRef chan = do - markAsWorking DefederationWorker - lift $ Q.qos chan 0 1 False - runningFlag <- newMVar () - consumer <- deleteFederationDomain runningFlag chan - liftIO $ atomicWriteIORef consumerRef $ pure (consumer, runningFlag) - liftIO $ forever $ threadDelay maxBound - -startWorker :: RabbitMqAdminOpts -> AppT IO (IORef (Maybe Q.Channel), IORef (Maybe (Q.ConsumerTag, MVar ()))) -startWorker rabbitmqOpts = do - env <- ask - chanRef <- newIORef Nothing - consumerRef <- newIORef Nothing - let clearRefs = do - runAppT env $ markAsNotWorking DefederationWorker - atomicWriteIORef chanRef Nothing - atomicWriteIORef consumerRef Nothing - void . liftIO . async . openConnectionWithRetries env.logger (demoteOpts rabbitmqOpts) $ - RabbitMqHooks - { onNewChannel = \chan -> do - atomicWriteIORef chanRef $ pure chan - runAppT env $ startDefederator consumerRef chan, - onChannelException = const clearRefs, - onConnectionClose = clearRefs - } - pure (chanRef, consumerRef) diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index f38680c1d43..bb37c87fce5 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -92,44 +92,6 @@ spec = do getVectorWith env.backendNotificationMetrics.pushedCounter getCounter `shouldReturn` [(domainText targetDomain, 1)] - it "should push on-connection-removed notifications" $ do - let returnSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) - let origDomain = Domain "origin.example.com" - targetDomain = Domain "target.example.com" - defederatedDomain = Domain "defederated.example.com" - let notif = - BackendNotification - { targetComponent = Galley, - ownDomain = origDomain, - path = "/on-connection-removed", - body = RawJson $ Aeson.encode defederatedDomain - } - envelope <- newMockEnvelope - let msg = - Q.newMsg - { Q.msgBody = Aeson.encode notif, - Q.msgContentType = Just "application/json" - } - runningFlag <- newMVar () - (env, fedReqs) <- - withTempMockFederator [] returnSuccess . runTestAppT $ do - wait =<< pushNotification runningFlag targetDomain (msg, envelope) - ask - - readIORef envelope.acks `shouldReturn` 1 - readIORef envelope.rejections `shouldReturn` [] - fedReqs - `shouldBe` [ FederatedRequest - { frTargetDomain = targetDomain, - frOriginDomain = origDomain, - frComponent = Galley, - frRPC = "on-connection-removed", - frBody = Aeson.encode defederatedDomain - } - ] - getVectorWith env.backendNotificationMetrics.pushedCounter getCounter - `shouldReturn` [(domainText targetDomain, 1)] - it "should reject invalid notifications" $ do let returnSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) envelope <- newMockEnvelope @@ -222,7 +184,6 @@ spec = do httpManager <- newManager defaultManagerSettings remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan - notificationChannel <- newEmptyMVar let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined @@ -245,7 +206,6 @@ spec = do httpManager <- newManager defaultManagerSettings remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan - notificationChannel <- newEmptyMVar let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined diff --git a/services/background-worker/test/Test/Wire/DefederationSpec.hs b/services/background-worker/test/Test/Wire/DefederationSpec.hs deleted file mode 100644 index 8707414d442..00000000000 --- a/services/background-worker/test/Test/Wire/DefederationSpec.hs +++ /dev/null @@ -1,51 +0,0 @@ -module Test.Wire.DefederationSpec where - -import Data.Aeson qualified as Aeson -import Data.Domain -import Federator.MockServer -import Imports -import Network.AMQP qualified as Q -import Test.HUnit.Lang -import Test.Hspec -import Test.Wire.Util -import Wire.API.Federation.API.Common -import Wire.API.Federation.BackendNotifications -import Wire.BackgroundWorker.Util -import Wire.Defederation - -spec :: Spec -spec = do - describe - "Wire.BackendNotificationPusher.deleteFederationDomain" - $ do - it "should fail on message decoding" $ do - envelope <- newFakeEnvelope - let msg = Q.newMsg {Q.msgBody = Aeson.encode @[()] [], Q.msgContentType = Just "application/json"} - respSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) - resps <- - withTempMockFederator [] respSuccess - . runTestAppT - $ deleteFederationDomainInner' (\e _ -> liftIO $ ack e) (msg, envelope) - case resps of - ((), []) -> pure () - _ -> assertFailure "Expected call to federation" - readIORef envelope.acks `shouldReturn` 0 - -- Fail to decode should not be requeued - readIORef envelope.rejections `shouldReturn` [False] - it "should succeed on message decoding" $ do - envelope <- newFakeEnvelope - let msg = - Q.newMsg - { Q.msgBody = Aeson.encode @DefederationDomain (Domain "far-away.example.com"), - Q.msgContentType = Just "application/json" - } - respSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) - resps <- - withTempMockFederator [] respSuccess - . runTestAppT - $ deleteFederationDomainInner' (\e _ -> liftIO $ ack e) (msg, envelope) - case resps of - ((), []) -> pure () - _ -> assertFailure "Expected call to federation" - readIORef envelope.acks `shouldReturn` 1 - readIORef envelope.rejections `shouldReturn` [] diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index d80121fdc69..58454c9582c 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -22,7 +22,6 @@ testEnv = do httpManager <- newManager defaultManagerSettings remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan - notificationChannel <- newEmptyMVar let federatorInternal = Endpoint "localhost" 0 rabbitmqAdminClient = undefined rabbitmqVHost = undefined diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index cd2393132ee..cf13f9d3386 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -63,9 +63,8 @@ import Brig.User.EJPD qualified import Brig.User.Search.Index qualified as Index import Control.Error hiding (bool) import Control.Lens (view, (^.)) -import Data.Aeson hiding (json) import Data.CommaSeparatedList -import Data.Domain (Domain, domainText) +import Data.Domain (Domain) import Data.Handle import Data.Id as Id import Data.Map.Strict qualified as Map @@ -73,13 +72,11 @@ import Data.Qualified import Data.Set qualified as Set import Data.Time.Clock.System import Imports hiding (head) -import Network.AMQP qualified as Q import Network.Wai.Routing hiding (toList) import Network.Wai.Utilities as Utilities import Polysemy import Servant hiding (Handler, JSON, addHeader, respond) import Servant.Swagger.Internal.Orphans () -import System.Logger qualified as Lg import System.Logger.Class qualified as Log import System.Random (randomRIO) import UnliftIO.Async @@ -87,7 +84,6 @@ import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig qualified as E import Wire.API.Federation.API -import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Error (FederationError (..)) import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage @@ -225,8 +221,6 @@ federationRemotesAPI = Named @"add-federation-remotes" addFederationRemote :<|> Named @"get-federation-remotes" getFederationRemotes :<|> Named @"update-federation-remotes" updateFederationRemote - :<|> Named @"delete-federation-remotes" deleteFederationRemote - :<|> Named @"delete-federation-remote-from-galley" deleteFederationRemoteGalley addFederationRemote :: FederationDomainConfig -> ExceptT Brig.API.Error.Error (AppT r) () addFederationRemote fedDomConf = do @@ -332,59 +326,6 @@ assertNoDomainsFromConfigFiles dom = do "keeping track of remote domains in the brig config file is deprecated, but as long as we \ \do that, removing or updating items listed in the config file is not allowed." --- | Remove the entry from the database if present (or do nothing if not). This responds with --- 533 if the entry was also present in the config file, but only *after* it has removed the --- entry from cassandra. --- --- The ordering on this delete then check seems weird, but allows us to default all the --- way back to config file state for a federation domain. -deleteFederationRemote :: Domain -> ExceptT Brig.API.Error.Error (AppT r) () -deleteFederationRemote dom = do - lift . wrapClient . Data.deleteFederationRemote $ dom - assertNoDomainsFromConfigFiles dom - env <- ask - ownDomain <- viewFederationDomain - remoteDomains <- fmap domain . remotes <$> getFederationRemotes - for_ (env ^. rabbitmqChannel) $ \chan -> liftIO . withMVar chan $ \chan' -> do - -- ensureQueue uses routingKey internally - ensureQueue chan' defederationQueue - void $ - Q.publishMsg chan' "" queue $ - Q.newMsg - { -- Check that this message type is compatible with what - -- background worker is expecting - Q.msgBody = encode @DefederationDomain dom, - Q.msgDeliveryMode = pure Q.Persistent, - Q.msgContentType = pure "application/json" - } - -- Send a notification to remaining federation servers, telling them - -- that we are defederating from a given domain, and that they should - -- clean up their conversations and notify clients. - -- Just to be safe! - for_ (filter (/= dom) remoteDomains) $ \remoteDomain -> do - ensureQueue chan' $ domainText remoteDomain - liftIO - $ enqueue chan' ownDomain remoteDomain Q.Persistent - . void - $ fedQueueClient @'Galley @"on-connection-removed" dom - -- Drop the notification queue for the domain. - -- This will also drop all of the messages in the queue - -- as we will no longer be able to communicate with this - -- domain. - num <- Q.deleteQueue chan' . routingKey $ domainText dom - Lg.info (env ^. applog) $ Log.msg @String "Dropped Notifications" . Log.field "domain" (domainText dom) . Log.field "count" (show num) - where - -- Ensure that this is kept in sync with background worker - queue = routingKey defederationQueue - --- | Remove one-on-one conversations for the given remote domain. This is called from Galley as --- part of the defederation process, and should not be called during the initial domain removal --- call to brig. This is so we can ensure that domains are correctly cleaned up if a service --- falls over for whatever reason. -deleteFederationRemoteGalley :: Domain -> ExceptT Brig.API.Error.Error (AppT r) () -deleteFederationRemoteGalley dom = do - lift . wrapClient . Data.deleteRemoteConnectionsDomain $ dom - -- | Responds with 'Nothing' if field is NULL in existing user or user does not exist. getAccountConferenceCallingConfig :: UserId -> (Handler r) (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig) getAccountConferenceCallingConfig uid = diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index c09719e9d70..beead5e4451 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -26,24 +26,19 @@ where import API.Internal.Util import Bilge import Bilge.Assert -import Brig.Data.Connection import Brig.Data.User (lookupFeatureConferenceCalling, lookupStatus, userExists) import Brig.Options qualified as Opt import Cassandra qualified as C import Cassandra qualified as Cass -import Cassandra.Exec (x1) import Cassandra.Util import Control.Exception (ErrorCall (ErrorCall), throwIO) import Control.Lens ((^.), (^?!)) import Data.Aeson.Lens qualified as Aeson import Data.Aeson.Types qualified as Aeson import Data.ByteString.Conversion (toByteString') -import Data.Domain import Data.Id -import Data.Json.Util (toUTCTimeMillis) import Data.Qualified import Data.Set qualified as Set -import Data.Time import GHC.TypeLits (KnownSymbol) import Imports import Test.Tasty @@ -67,74 +62,9 @@ tests opts mgr db brig brigep gundeck galley = do test mgr "suspend and unsuspend user" $ testSuspendUser db brig, test mgr "suspend non existing user and verify no db entry" $ testSuspendNonExistingUser db brig, - test mgr "writetimeToInt64" $ testWritetimeRepresentation opts mgr db brig brigep galley, - test mgr "delete-federation-remote-galley" $ testDeleteFederationRemoteGalley db brig + test mgr "writetimeToInt64" $ testWritetimeRepresentation opts mgr db brig brigep galley ] -testDeleteFederationRemoteGalley :: forall m. (TestConstraints m) => Cass.ClientState -> Brig -> m () -testDeleteFederationRemoteGalley db brig = do - let remoteDomain1 = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-two.example.com" - isRemote1 = (== remoteDomain1) - isRemote2 = (== remoteDomain2) - localUser <- randomUser brig - let localUserId = userId localUser - remoteUserId1 <- randomId - remoteUserId2 <- randomId - now <- liftIO $ getCurrentTime - convId <- randomId - - -- Write the local and remote users into 'connection_remote' - let params1 = (localUserId, remoteDomain1, remoteUserId1, Conn.AcceptedWithHistory, toUTCTimeMillis now, remoteDomain1, convId) - liftIO $ - Cass.runClient db $ - Cass.retry x1 $ - Cass.write remoteConnectionInsert (Cass.params Cass.LocalQuorum params1) - let params2 = (localUserId, remoteDomain2, remoteUserId2, Conn.AcceptedWithHistory, toUTCTimeMillis now, remoteDomain1, convId) - liftIO $ - Cass.runClient db $ - Cass.retry x1 $ - Cass.write remoteConnectionInsert (Cass.params Cass.LocalQuorum params2) - - -- Check that the value exists in the DB as expected. - -- Remote 1 - liftIO - ( Cass.runClient db $ - Cass.retry x1 $ - Cass.query remoteConnectionsSelectUsers (Cass.params Cass.LocalQuorum $ pure localUserId) - ) - >>= liftIO . assertBool "connection_remote entry should exist for the user" . any (isRemote1 . fst) - -- Remote 2 - liftIO - ( Cass.runClient db $ - Cass.retry x1 $ - Cass.query remoteConnectionsSelectUsers (Cass.params Cass.LocalQuorum $ pure localUserId) - ) - >>= liftIO . assertBool "connection_remote entry should exist for the user" . any (isRemote2 . fst) - - -- Make the API call to delete remote domain 1 - delete - ( brig - . paths ["i", "federation", "remote", toByteString' $ domainText remoteDomain1, "galley"] - ) - !!! const 200 === statusCode - - -- Check 'connection_remote' for the local user and ensure - -- that there are no conversations for the remote domain. - liftIO - ( Cass.runClient db $ - Cass.retry x1 $ - Cass.query remoteConnectionsSelectUsers (Cass.params Cass.LocalQuorum $ pure localUserId) - ) - >>= liftIO . assertBool "connection_remote entry should NOT exist for the user" . not . any (isRemote1 . fst) - -- But remote domain 2 is still listed - liftIO - ( Cass.runClient db $ - Cass.retry x1 $ - Cass.query remoteConnectionsSelectUsers (Cass.params Cass.LocalQuorum $ pure localUserId) - ) - >>= liftIO . assertBool "connection_remote entry should exist for the user" . any (isRemote2 . fst) - testSuspendUser :: forall m. (TestConstraints m) => Cass.ClientState -> Brig -> m () testSuspendUser db brig = do user <- randomUser brig diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs index a93a4fa78bf..0d322238276 100644 --- a/services/federator/test/integration/Test/Federator/IngressSpec.hs +++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs @@ -23,7 +23,6 @@ import Control.Monad.Codensity import Data.Aeson qualified as Aeson import Data.Binary.Builder import Data.Domain -import Data.Handle import Data.LegalHold (UserLegalHoldStatus (UserLegalHoldNoConsent)) import Data.Text.Encoding qualified as Text import Federator.Discovery @@ -57,22 +56,20 @@ spec env = do runTestFederator env $ do brig <- view teBrig <$> ask user <- randomUser brig - hdl <- randomHandle - _ <- putHandle brig (userId user) hdl - let expectedProfile = (publicProfile user UserLegalHoldNoConsent) {profileHandle = Just (Handle hdl)} + let expectedProfile = publicProfile user UserLegalHoldNoConsent runTestSem $ do resp <- liftToCodensity . assertNoError @RemoteError $ inwardBrigCallViaIngress - "get-user-by-handle" - (Aeson.fromEncoding (Aeson.toEncoding hdl)) + "get-users-by-ids" + (Aeson.fromEncoding (Aeson.toEncoding [userId user])) embed . lift @Codensity $ do bdy <- streamingResponseStrictBody resp let actualProfile = Aeson.decode (toLazyByteString bdy) responseStatusCode resp `shouldBe` HTTP.status200 - actualProfile `shouldBe` Just expectedProfile + actualProfile `shouldBe` Just [expectedProfile] -- @SF.Federation @TSFI.RESTfulAPI @S2 @S3 @S7 -- diff --git a/services/federator/test/integration/Test/Federator/InwardSpec.hs b/services/federator/test/integration/Test/Federator/InwardSpec.hs index ae267dd67e8..3b4cc55bd9b 100644 --- a/services/federator/test/integration/Test/Federator/InwardSpec.hs +++ b/services/federator/test/integration/Test/Federator/InwardSpec.hs @@ -28,7 +28,6 @@ import Data.Aeson.Types qualified as Aeson import Data.ByteString qualified as BS import Data.ByteString.Conversion (toByteString') import Data.ByteString.Lazy qualified as LBS -import Data.Handle import Data.LegalHold (UserLegalHoldStatus (UserLegalHoldNoConsent)) import Data.Text.Encoding import Federator.Options hiding (federatorExternal) @@ -48,7 +47,7 @@ import Wire.API.User -- they don't spread out over the different sevices. -- | This module contains tests for the interface between federator and brig. The tests call --- federator directly, circumnventing ingress: +-- federator directly, circumventing ingress: -- -- +----------+ -- |federator-| +------+--+ @@ -71,15 +70,15 @@ spec env = runTestFederator env $ do brig <- view teBrig <$> ask user <- randomUser brig - hdl <- randomHandle - _ <- putHandle brig (userId user) hdl - let expectedProfile = (publicProfile user UserLegalHoldNoConsent) {profileHandle = Just (Handle hdl)} + let expectedProfile = publicProfile user UserLegalHoldNoConsent bdy <- responseJsonError - =<< inwardCall "/federation/brig/get-user-by-handle" (encode hdl) - Named @"on-client-removed" (callsFed (exposeAnnotations onClientRemoved)) :<|> Named @"update-typing-indicator" (callsFed (exposeAnnotations updateTypingIndicator)) :<|> Named @"on-typing-indicator-updated" onTypingIndicatorUpdated - :<|> Named @"on-connection-removed" (onFederationConnectionRemoved (toRange (Proxy @500))) onClientRemoved :: ( Member ConversationStore r, @@ -768,78 +752,6 @@ onTypingIndicatorUpdated origDomain F.TypingDataUpdated {..} = do pushTypingIndicatorEvents origUserId time usersInConv Nothing qcnv typingStatus pure EmptyResponse --- Since we already have the origin domain where the defederation event started, --- all it needs to carry in addition is the domain it is defederating from. This --- is all the information that we need to cleanup the database and notify clients. -onFederationConnectionRemoved :: - forall r. - ( Member (Input Env) r, - Member (Embed IO) r, - Member (Input ClientState) r, - Member MemberStore r, - Member DefederationNotifications r - ) => - Range 1 1000 Int32 -> - Domain -> - Domain -> - Sem r EmptyResponse -onFederationConnectionRemoved range originDomain targetDomain = do - fedDomains <- getFederationDomains - let federatedWithBoth = all (`elem` fedDomains) [originDomain, targetDomain] - when federatedWithBoth $ do - sendOnConnectionRemovedNotifications originDomain targetDomain - cleanupRemovedConnections originDomain targetDomain range - sendOnConnectionRemovedNotifications originDomain targetDomain - pure EmptyResponse - -getFederationDomains :: - ( Member (Input Env) r, - Member (Embed IO) r - ) => - Sem r [Domain] -getFederationDomains = do - Endpoint (T.unpack -> h) (fromIntegral -> p) <- inputs _brig - mgr <- inputs _manager - liftIO $ recovering policy httpHandlers $ \_ -> do - resp <- fetch $ mkClientEnv mgr $ BaseUrl Http h p "" - either throwIO (pure . fmap domain . remotes) resp - where - policy = capDelay 60_000_000 $ fullJitterBackoff 200_000 - --- for all conversations owned by backend C, only if there are users from both A and B, --- remove users from A and B from those conversations --- This is similar to Galley.API.Internal.deleteFederationDomain --- However it has some important differences, such as we only remove from our conversations --- where users for both domains are in the same conversation. -cleanupRemovedConnections :: - forall r. - ( Member (Embed IO) r, - Member (Input ClientState) r, - Member MemberStore r - ) => - Domain -> - Domain -> - Range 1 1000 Int32 -> - Sem r () -cleanupRemovedConnections domainA domainB (fromRange -> maxPage) = do - runPaginated Q.selectConvIdsByRemoteDomain (paramsP LocalQuorum (Identity domainA) maxPage) $ \convIds -> - -- `nub $ sort` is a small performance boost, it will drop duplicate convIds from the page results. - -- However we can certainly still process a conversation more than once if it is in multiple pages. - for_ (nub $ sort convIds) $ \(runIdentity -> convId) -> do - -- Check if users from domain B are in the conversation - b <- isJust <$> E.checkConvForRemoteDomain convId domainB - when b $ do - -- Users from both domains exist, delete all of them from the conversation. - E.removeRemoteDomain convId domainA - E.removeRemoteDomain convId domainB - where - runPaginated :: (Tuple p, Tuple a) => PrepQuery R p a -> QueryParams p -> ([a] -> Sem r b) -> Sem r b - runPaginated q ps f = go f <=< embedClient $ paginate q ps - go :: ([a] -> Sem r b) -> Page a -> Sem r b - go f page - | hasMore page = f (result page) >> embedClient (nextPage page) >>= go f - | otherwise = f $ result page - -------------------------------------------------------------------------------- -- Utilities -------------------------------------------------------------------------------- @@ -860,7 +772,3 @@ logFederationError lc e = \ a user from a local conversation: " <> displayException e ) - --- Build the map, keyed by conversations to the list of members -insertIntoMap :: (ConvId, a) -> Map ConvId (N.NonEmpty a) -> Map ConvId (N.NonEmpty a) -insertIntoMap (cnvId, user) m = Map.alter (pure . maybe (pure user) (N.cons user)) cnvId m diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 7f1565036fe..d4aecd636fe 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -21,32 +21,22 @@ module Galley.API.Internal InternalAPI, deleteLoop, safeForever, - -- Exported for tests - deleteFederationDomain, ) where -import Bilge.Retry -import Cassandra (ClientState, Consistency (LocalQuorum), Page (hasMore, nextPage, result), paginate, paramsP) import Control.Exception.Safe (catchAny) import Control.Lens hiding (Getter, Setter, (.=)) -import Control.Retry -import Data.Domain import Data.Id as Id -import Data.List.NonEmpty qualified as N import Data.List1 (maybeList1) -import Data.Map qualified as Map import Data.Qualified import Data.Range import Data.Singletons -import Data.Text (unpack) import Data.Time import Galley.API.Action import Galley.API.Clients qualified as Clients import Galley.API.Create qualified as Create import Galley.API.CustomBackend qualified as CustomBackend import Galley.API.Error -import Galley.API.Federation (insertIntoMap) import Galley.API.LegalHold (unsetTeamLegalholdWhitelistedH) import Galley.API.LegalHold.Conflicts import Galley.API.MLS.Removal @@ -60,20 +50,15 @@ import Galley.API.Teams.Features import Galley.API.Update qualified as Update import Galley.API.Util import Galley.App -import Galley.Cassandra.Queries qualified as Q -import Galley.Cassandra.Store (embedClient) import Galley.Data.Conversation qualified as Data -import Galley.Data.Conversation.Types import Galley.Effects import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.ClientStore import Galley.Effects.ConversationStore -import Galley.Effects.DefederationNotifications (DefederationNotifications, sendDefederationNotifications) import Galley.Effects.FederatorAccess import Galley.Effects.GundeckAccess import Galley.Effects.LegalHoldStore as LegalHoldStore import Galley.Effects.MemberStore -import Galley.Effects.MemberStore qualified as E import Galley.Effects.ProposalStore import Galley.Effects.TeamStore import Galley.Intra.Push qualified as Intra @@ -82,12 +67,10 @@ import Galley.Options hiding (brig) import Galley.Queue qualified as Q import Galley.Types.Bot (AddBot, RemoveBot) import Galley.Types.Bot.Service -import Galley.Types.Conversations.Members (RemoteMember (RemoteMember, rmId)) +import Galley.Types.Conversations.Members (RemoteMember (rmId)) import Galley.Types.UserList import Imports hiding (head) import Network.AMQP qualified as Q -import Network.HTTP.Types -import Network.Wai import Network.Wai.Predicate hiding (Error, err, result, setStatus) import Network.Wai.Predicate qualified as Predicate hiding (result) import Network.Wai.Routing hiding (App, route, toList) @@ -98,10 +81,8 @@ import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P import Servant hiding (JSON, WithStatus) -import Servant.Client (BaseUrl (BaseUrl), ClientEnv (ClientEnv), Scheme (Http), defaultMakeClientRequest) import System.Logger.Class hiding (Path, name) import System.Logger.Class qualified as Log -import Util.Options import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Action import Wire.API.CustomBackend @@ -111,7 +92,6 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error -import Wire.API.FederationUpdate import Wire.API.Provider.Service hiding (Service) import Wire.API.Routes.API import Wire.API.Routes.Internal.Galley @@ -119,7 +99,7 @@ import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Routes.MultiTablePaging (mtpHasMore, mtpPagingState, mtpResults) import Wire.API.Team.Feature hiding (setStatus) import Wire.API.Team.Member -import Wire.Sem.Paging (Paging (pageItems, pageState)) +import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra internalAPI :: API InternalAPI GalleyEffects @@ -325,10 +305,6 @@ internalSitemap = unsafeCallsFed @'Galley @"on-client-removed" $ unsafeCallsFed capture "domain" .&. accept "application" "json" - delete "/i/federation/:domain" (continue . internalDeleteFederationDomainH $ toRange (Proxy @500)) $ - capture "domain" - .&. accept "application" "json" - rmUser :: forall p1 p2 r. ( p1 ~ CassandraPaging, @@ -496,171 +472,3 @@ guardLegalholdPolicyConflictsH :: guardLegalholdPolicyConflictsH glh = do mapError @LegalholdConflicts (const $ Tagged @'MissingLegalholdConsent ()) $ guardLegalholdPolicyConflicts (glhProtectee glh) (glhUserClients glh) - --- Bundle all of the deletes together for easy calling --- Errors & exceptions are thrown to IO to stop the message being ACKed, eventually timing it --- out so that it can be redelivered. -deleteFederationDomain :: - ( Member (Input Env) r, - Member (P.Logger (Msg -> Msg)) r, - Member (Error FederationError) r, - Member MemberStore r, - Member (Input ClientState) r, - Member ConversationStore r, - Member (Embed IO) r, - Member CodeStore r, - Member TeamStore r, - Member (Error InternalError) r - ) => - Range 1 1000 Int32 -> - Domain -> - Sem r () -deleteFederationDomain range d = do - deleteFederationDomainRemoteUserFromLocalConversations range d - deleteFederationDomainLocalUserFromRemoteConversation range d - deleteFederationDomainOneOnOne d - -internalDeleteFederationDomainH :: - ( Member (Input Env) r, - Member (P.Logger (Msg -> Msg)) r, - Member (Error FederationError) r, - Member MemberStore r, - Member ConversationStore r, - Member (Embed IO) r, - Member (Input ClientState) r, - Member CodeStore r, - Member TeamStore r, - Member DefederationNotifications r, - Member (Error InternalError) r - ) => - Range 1 1000 Int32 -> - Domain ::: JSON -> - Sem r Response -internalDeleteFederationDomainH range (domain ::: _) = do - -- We have to send the same event twice. - -- Once before and once after defederation work. - -- https://wearezeta.atlassian.net/wiki/spaces/ENGINEERIN/pages/809238539/Use+case+Stopping+to+federate+with+a+domain - sendDefederationNotifications domain - deleteFederationDomain range domain - sendDefederationNotifications domain - pure (empty & setStatus status200) - --- Remove remote members from local conversations -deleteFederationDomainRemoteUserFromLocalConversations :: - forall r. - ( Member (Input Env) r, - Member (P.Logger (Msg -> Msg)) r, - Member (Error FederationError) r, - Member (Input ClientState) r, - Member (Embed IO) r, - Member MemberStore r, - Member ConversationStore r, - Member CodeStore r, - Member TeamStore r - ) => - Range 1 1000 Int32 -> - Domain -> - Sem r () -deleteFederationDomainRemoteUserFromLocalConversations (fromRange -> maxPage) dom = do - remoteUsers <- - mkConvMem <$$> do - page <- - embedClient $ - paginate Q.selectRemoteMembersByDomain $ - paramsP LocalQuorum (Identity dom) maxPage - getPaginatedData page - env <- input - let lCnvMap = foldr insertIntoMap mempty remoteUsers - localDomain = env ^. Galley.App.options . Galley.Options.settings . federationDomain - for_ (Map.toList lCnvMap) $ \(cnvId, rUsers) -> do - let mapAllErrors :: - Text -> - Sem (Error NoChanges ': ErrorS 'NotATeamMember ': r) () -> - Sem r () - mapAllErrors msgText = - -- This can be thrown in `updateLocalConversationUserUnchecked @'ConversationDeleteTag`. - P.logAndIgnoreErrors @(Tagged 'NotATeamMember ()) (const "Not a team member") msgText - -- This can be thrown in `updateLocalConversationUserUnchecked @'ConversationRemoveMembersTag` - . P.logAndIgnoreErrors @NoChanges (const "No changes") msgText - - mapAllErrors "Federation domain removal" $ do - getConversation cnvId - >>= maybe (pure () {- conv already gone, nothing to do -}) (delConv localDomain rUsers) - where - mkConvMem (convId, usr, role) = (convId, RemoteMember (toRemoteUnsafe dom usr) role) - delConv :: - Domain -> - N.NonEmpty RemoteMember -> - Galley.Data.Conversation.Types.Conversation -> - Sem (Error NoChanges : ErrorS 'NotATeamMember : r) () - delConv localDomain rUsers conv = - do - let lConv = toLocalUnsafe localDomain conv - updateLocalConversationUserUnchecked - @'ConversationRemoveMembersTag - lConv - undefined - $ tUntagged . rmId <$> rUsers -- This field can be undefined as the path for ConversationRemoveMembersTag doens't use it - -- Check if the conversation if type 2 or 3, one-on-one conversations. - -- If it is, then we need to remove the entire conversation as users - -- aren't able to delete those types of conversations themselves. - -- Check that we are in a type 2 or a type 3 conversation - when (cnvmType (convMetadata conv) `elem` [One2OneConv, ConnectConv]) $ - -- If we are, delete it. - updateLocalConversationUserUnchecked - @'ConversationDeleteTag - lConv - undefined - () - --- Remove local members from remote conversations -deleteFederationDomainLocalUserFromRemoteConversation :: - ( Member (Error InternalError) r, - Member (Input ClientState) r, - Member (Embed IO) r, - Member MemberStore r - ) => - Range 1 1000 Int32 -> - Domain -> - Sem r () -deleteFederationDomainLocalUserFromRemoteConversation (fromRange -> maxPage) dom = do - remoteConvs <- - foldr insertIntoMap mempty <$> do - page <- - embedClient $ - paginate Q.selectLocalMembersByDomain $ - paramsP LocalQuorum (Identity dom) maxPage - getPaginatedData page - for_ (Map.toList remoteConvs) $ \(cnv, lUsers) -> do - -- All errors, either exceptions or Either e, get thrown into IO - mapError @NoChanges (const (InternalErrorWithDescription "No Changes: Could not remove a local member from a remote conversation.")) $ do - E.deleteMembersInRemoteConversation (toRemoteUnsafe dom cnv) (N.toList lUsers) - --- These need to be recoverable? --- This is recoverable with the following flow conditions. --- 1) Deletion calls to the Brig endpoint `delete-federation-remote-from-galley` are idempotent for a given domain. --- 2) This call is made from a function that is backed by a RabbitMQ queue. --- The calling function needs to catch thrown exceptions and NACK the deletion --- message. This will allow Rabbit to redeliver the message and give us a second --- go at performing the deletion. -deleteFederationDomainOneOnOne :: (Member (Input Env) r, Member (Embed IO) r) => Domain -> Sem r () -deleteFederationDomainOneOnOne dom = do - env <- input - let c = mkClientEnv (env ^. manager) (env ^. brig) - -- This is the same policy as background-worker for retrying. - policy = capDelay 60_000_000 $ fullJitterBackoff 200_000 - void . liftIO . recovering policy httpHandlers $ \_ -> deleteFederationRemoteGalley dom c - where - mkClientEnv mgr (Endpoint h p) = ClientEnv mgr (BaseUrl Http (unpack h) (fromIntegral p) "") Nothing defaultMakeClientRequest - -getPaginatedData :: - ( Member (Input ClientState) r, - Member (Embed IO) r - ) => - Page a -> - Sem r [a] -getPaginatedData page - | hasMore page = - (result page <>) <$> do - getPaginatedData <=< embedClient $ nextPage page - | otherwise = pure $ result page diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 2163f9a8a50..493eb61283f 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -281,7 +281,6 @@ evalGalley e = . interpretFederatorAccess . interpretExternalAccess . interpretGundeckAccess - . interpretDefederationNotifications . interpretSparAccess . interpretBrigAccess where diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index 34787ef69b1..fe6c18a4fdc 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -208,32 +208,14 @@ lookupRemoteMembers conv = do lookupRemoteMembersByDomain :: Domain -> Client [(ConvId, RemoteMember)] lookupRemoteMembersByDomain dom = do - mkConvMem <$$$> retry x1 $ query Cql.selectRemoteMembersByDomain (params LocalQuorum (Identity dom)) + fmap (fmap mkConvMem) . retry x1 $ query Cql.selectRemoteMembersByDomain (params LocalQuorum (Identity dom)) where mkConvMem (convId, usr, role) = (convId, RemoteMember (toRemoteUnsafe dom usr) role) -lookupRemoteMembersByConvAndDomain :: ConvId -> Domain -> Client [RemoteMember] -lookupRemoteMembersByConvAndDomain conv dom = do - mkMem <$$$> retry x1 $ query Cql.selectRemoteMembersByConvAndDomain (params LocalQuorum (conv, dom)) - where - mkMem (usr, role) = RemoteMember (toRemoteUnsafe dom usr) role - lookupLocalMembersByDomain :: Domain -> Client [(ConvId, UserId)] lookupLocalMembersByDomain dom = do retry x1 $ query Cql.selectLocalMembersByDomain (params LocalQuorum (Identity dom)) -removeRemoteDomain :: ConvId -> Domain -> Client () -removeRemoteDomain convId dom = do - retry x1 $ write Cql.removeRemoteDomain $ params LocalQuorum (convId, dom) - -selectConvIdsByRemoteDomain :: Domain -> Client [ConvId] -selectConvIdsByRemoteDomain dom = do - runIdentity <$$$> retry x1 $ query Cql.selectConvIdsByRemoteDomain $ params LocalQuorum $ Identity dom - -checkConvForRemoteDomain :: ConvId -> Domain -> Client (Maybe ConvId) -checkConvForRemoteDomain convId dom = do - runIdentity <$$$> retry x1 $ query1 Cql.checkConvForRemoteDomain $ params LocalQuorum (convId, dom) - member :: ConvId -> UserId -> @@ -427,8 +409,4 @@ interpretMemberStoreToCassandra = interpret $ \case RemoveMLSClients lcnv quid cs -> embedClient $ removeMLSClients lcnv quid cs LookupMLSClients lcnv -> embedClient $ lookupMLSClients lcnv GetRemoteMembersByDomain dom -> embedClient $ lookupRemoteMembersByDomain dom - GetRemoteMembersByConvAndDomain conv dom -> embedClient $ lookupRemoteMembersByConvAndDomain conv dom GetLocalMembersByDomain dom -> embedClient $ lookupLocalMembersByDomain dom - RemoveRemoteDomain convId dom -> embedClient $ removeRemoteDomain convId dom - SelectConvIdsByRemoteDomain dom -> embedClient $ selectConvIdsByRemoteDomain dom - CheckConvForRemoteDomain convId dom -> embedClient $ checkConvForRemoteDomain convId dom diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 8118d0cd542..c81df4144bc 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -385,24 +385,11 @@ selectRemoteMembers = "select user_remote_domain, user_remote_id, conversation_r updateRemoteMemberConvRoleName :: PrepQuery W (RoleName, ConvId, Domain, UserId) () updateRemoteMemberConvRoleName = {- `IF EXISTS`, but that requires benchmarking -} "update member_remote_user set conversation_role = ? where conv = ? and user_remote_domain = ? and user_remote_id = ?" -removeRemoteDomain :: PrepQuery W (ConvId, Domain) () -removeRemoteDomain = "delete from member_remote_user where conv = ? and user_remote_domain = ?" - -- Used when removing a federation domain, so that we can quickly list all of the affected remote users and conversations -- This returns local conversation IDs and remote users selectRemoteMembersByDomain :: PrepQuery R (Identity Domain) (ConvId, UserId, RoleName) selectRemoteMembersByDomain = "select conv, user_remote_id, conversation_role from member_remote_user where user_remote_domain = ?" -selectRemoteMembersByConvAndDomain :: PrepQuery R (ConvId, Domain) (UserId, RoleName) -selectRemoteMembersByConvAndDomain = "select user_remote_id, conversation_role from member_remote_user where conv = ? and user_remote_domain = ?" - -selectConvIdsByRemoteDomain :: PrepQuery R (Identity Domain) (Identity ConvId) -selectConvIdsByRemoteDomain = "select conv from member_remote_user where user_remote_domain = ?" - --- Return a single element, as this is being used as a SQL exists analog -checkConvForRemoteDomain :: PrepQuery R (ConvId, Domain) (Identity ConvId) -checkConvForRemoteDomain = "select conv from member_remote_user where conv = ? and user_remote_domain = ? limit 1" - -- local user with remote conversations insertUserRemoteConv :: PrepQuery W (UserId, Domain, ConvId) () diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index fc8f406becb..a8dc2a5198c 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -69,7 +69,6 @@ import Galley.Effects.ClientStore import Galley.Effects.CodeStore import Galley.Effects.ConversationStore import Galley.Effects.CustomBackendStore -import Galley.Effects.DefederationNotifications import Galley.Effects.ExternalAccess import Galley.Effects.FederatorAccess import Galley.Effects.FireAndForget @@ -100,7 +99,6 @@ import Wire.Sem.Paging.Cassandra type GalleyEffects1 = '[ BrigAccess, SparAccess, - DefederationNotifications, GundeckAccess, ExternalAccess, FederatorAccess, diff --git a/services/galley/src/Galley/Effects/DefederationNotifications.hs b/services/galley/src/Galley/Effects/DefederationNotifications.hs deleted file mode 100644 index aaef53bc794..00000000000 --- a/services/galley/src/Galley/Effects/DefederationNotifications.hs +++ /dev/null @@ -1,17 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - -module Galley.Effects.DefederationNotifications - ( DefederationNotifications (..), - sendDefederationNotifications, - sendOnConnectionRemovedNotifications, - ) -where - -import Data.Domain (Domain) -import Polysemy - -data DefederationNotifications m a where - SendDefederationNotifications :: Domain -> DefederationNotifications m () - SendOnConnectionRemovedNotifications :: Domain -> Domain -> DefederationNotifications m () - -makeSem ''DefederationNotifications diff --git a/services/galley/src/Galley/Effects/MemberStore.hs b/services/galley/src/Galley/Effects/MemberStore.hs index 604804aab66..c8542a71f3e 100644 --- a/services/galley/src/Galley/Effects/MemberStore.hs +++ b/services/galley/src/Galley/Effects/MemberStore.hs @@ -36,13 +36,8 @@ module Galley.Effects.MemberStore checkLocalMemberRemoteConv, selectRemoteMembers, getRemoteMembersByDomain, - getRemoteMembersByConvAndDomain, getLocalMembersByDomain, - -- * Conversation checks - selectConvIdsByRemoteDomain, - checkConvForRemoteDomain, - -- * Update members setSelfMember, setOtherMember, @@ -53,7 +48,6 @@ module Galley.Effects.MemberStore -- * Delete members deleteMembers, deleteMembersInRemoteConversation, - removeRemoteDomain, ) where @@ -92,11 +86,7 @@ data MemberStore m a where GroupId -> MemberStore m (Map (Qualified UserId) (Set (ClientId, KeyPackageRef))) GetRemoteMembersByDomain :: Domain -> MemberStore m [(ConvId, RemoteMember)] - GetRemoteMembersByConvAndDomain :: ConvId -> Domain -> MemberStore m [RemoteMember] GetLocalMembersByDomain :: Domain -> MemberStore m [(ConvId, UserId)] - RemoveRemoteDomain :: ConvId -> Domain -> MemberStore m () - SelectConvIdsByRemoteDomain :: Domain -> MemberStore m [ConvId] - CheckConvForRemoteDomain :: ConvId -> Domain -> MemberStore m (Maybe ConvId) makeSem ''MemberStore diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs index e6639c4ce96..e79f060b066 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/services/galley/src/Galley/Intra/Effects.hs @@ -20,45 +20,27 @@ module Galley.Intra.Effects interpretSparAccess, interpretBotAccess, interpretGundeckAccess, - interpretDefederationNotifications, ) where -import Cassandra (ClientState, Consistency (LocalQuorum), Page (hasMore, nextPage, result), paginate, paramsP) -import Control.Lens ((.~)) -import Data.Id (ProviderId, ServiceId, UserId) -import Data.Range (Range (fromRange)) -import Data.Set qualified as Set import Galley.API.Error -import Galley.API.Util (localBotsAndUsers) -import Galley.Cassandra.Conversation.Members (toMember) -import Galley.Cassandra.Queries (MemberStatus, selectAllMembers) -import Galley.Cassandra.Store (embedClient) import Galley.Effects.BotAccess (BotAccess (..)) import Galley.Effects.BrigAccess (BrigAccess (..)) -import Galley.Effects.DefederationNotifications (DefederationNotifications (..)) -import Galley.Effects.ExternalAccess (ExternalAccess, deliverAsync) -import Galley.Effects.GundeckAccess (GundeckAccess (..), push1) +import Galley.Effects.GundeckAccess (GundeckAccess (..)) import Galley.Effects.SparAccess (SparAccess (..)) import Galley.Env import Galley.Intra.Client -import Galley.Intra.Push qualified as Intra import Galley.Intra.Push.Internal qualified as G import Galley.Intra.Spar import Galley.Intra.Team import Galley.Intra.User import Galley.Monad -import Galley.Types.Conversations.Members (LocalMember) import Imports import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P import UnliftIO qualified -import Wire.API.Conversation (MutedStatus) -import Wire.API.Conversation.Role (RoleName) -import Wire.API.Event.Federation qualified as Federation -import Wire.API.Team.Member (ListType (ListComplete)) interpretBrigAccess :: ( Member (Embed IO) r, @@ -141,80 +123,3 @@ interpretGundeckAccess :: interpretGundeckAccess = interpret $ \case Push ps -> embedApp $ G.push ps PushSlowly ps -> embedApp $ G.pushSlowly ps - --- FUTUREWORK: --- This functions uses an in-memory set for tracking UserIds that we have already --- sent notifications to. This set will only grow throughout the lifttime of this --- function, and may cause memory & performance problems with millions of users. --- How we are tracking which users have already been sent 0, 1, or 2 defederation --- messages should be rethought to be more fault tollerant, e.g. this method doesn't --- handle the server crashing and restarting. -interpretDefederationNotifications :: - forall r a. - ( Member (Embed IO) r, - Member (Input Env) r, - Member (Input ClientState) r, - Member GundeckAccess r, - Member ExternalAccess r - ) => - Sem (DefederationNotifications ': r) a -> - Sem r a -interpretDefederationNotifications = interpret $ \case - SendDefederationNotifications domain -> - getPage - >>= void . sendNotificationPage mempty (Federation.FederationDelete domain) - SendOnConnectionRemovedNotifications domainA domainB -> - getPage - >>= void . sendNotificationPage mempty (Federation.FederationConnectionRemoved (domainA, domainB)) - where - getPage :: Sem r (Page PageType) - getPage = do - maxPage <- inputs (fromRange . currentFanoutLimit . _options) -- This is based on the limits in removeIfLargeFanout - -- selectAllMembers will return duplicate members when they are in more than one chat - -- however we need the full row to build out the bot members to send notifications - -- to them. We have to do the duplicate filtering here. - embedClient $ paginate selectAllMembers (paramsP LocalQuorum () maxPage) - pushEvents :: Set UserId -> Federation.Event -> [LocalMember] -> Sem r (Set UserId) - pushEvents seenRecipients eventData results = do - let (bots, mems) = localBotsAndUsers results - recipients = Intra.recipient <$> mems - event = Intra.FederationEvent eventData - filteredRecipients = - -- Deduplicate by UserId the page of recipients that we are working on - nubBy (\a b -> a._recipientUserId == b._recipientUserId) - -- Sort the remaining recipients by their IDs - $ - sortBy (\a b -> a._recipientUserId `compare` b._recipientUserId) - -- Filter out any recipient that we have already seen in a previous page - $ - filter (\r -> r._recipientUserId `notElem` seenRecipients) recipients - for_ (Intra.newPush ListComplete Nothing event filteredRecipients) $ \p -> do - -- Futurework: Transient or not? - -- RouteAny is used as it will wake up mobile clients - -- and notify them of the changes to federation state. - push1 $ p & Intra.pushRoute .~ Intra.RouteAny - deliverAsync (bots `zip` repeat (G.pushEventJson event)) - -- Add the users to the set of users we've sent messages to. - pure $ seenRecipients <> Set.fromList ((._recipientUserId) <$> filteredRecipients) - sendNotificationPage :: Set UserId -> Federation.Event -> Page PageType -> Sem r () - sendNotificationPage seenRecipients eventData page = do - let res = result page - mems = mapMaybe toMember res - seenRecipients' <- pushEvents seenRecipients eventData mems - when (hasMore page) $ do - page' <- embedClient $ nextPage page - sendNotificationPage seenRecipients' eventData page' - -type PageType = - ( UserId, - Maybe ServiceId, - Maybe ProviderId, - Maybe MemberStatus, - Maybe MutedStatus, - Maybe Text, - Maybe Bool, - Maybe Text, - Maybe Bool, - Maybe Text, - Maybe RoleName - ) diff --git a/services/galley/src/Galley/Intra/Push/Internal.hs b/services/galley/src/Galley/Intra/Push/Internal.hs index 272a4593493..3575cdf2cef 100644 --- a/services/galley/src/Galley/Intra/Push/Internal.hs +++ b/services/galley/src/Galley/Intra/Push/Internal.hs @@ -60,7 +60,7 @@ data RecipientBy user = Recipient { _recipientUserId :: user, _recipientClients :: RecipientClients } - deriving stock (Functor, Foldable, Traversable, Show, Ord, Eq) + deriving stock (Functor, Foldable, Traversable, Show) makeLenses ''RecipientBy diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 24038e03605..179b3fbb4a6 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -266,23 +266,7 @@ tests s = [ test s "send typing indicators" postTypingIndicators, test s "send typing indicators without domain" postTypingIndicatorsV2, test s "send typing indicators with invalid pyaload" postTypingIndicatorsHandlesNonsense - ], - -- NOTE: These federation notification tests need to run after all of the other tests are finished. - -- This is because they will send notifications to _ALL_ registered clients for the local domain. - -- As a lot of these tests are waiting on specific notifications to come through in a specified - -- order, these tests will cause them to fail. - -- See the Tasty docs on patterns. https://hackage.haskell.org/package/tasty-1.4.3#patterns - after AllFinish "$0 !~ /federation notifications/" $ - testGroup - "federation notifications" - -- Run these tests in order by having them wait on each other. - -- The names need to be distint enough so that there isn't a loop with the regexes - [ test s "delete federation notifications" testDefederationNotifications, - after AllFinish "$0 ~ /delete federation notifications/" $ test s "connection removed notifications normal" testConnectionRemovedNotifications, - after AllFinish "$0 ~ /connection removed notifications normal/" $ test s "connection removed notifications no-op" testConnectionRemovedNotificationsNoop, - after AllFinish "$0 ~ /connection removed notifications no-op/" $ test s "connection removed notifications domain A bias" testConnectionRemovedNotificationsNoopDomainA, - after AllFinish "$0 ~ /connection removed notifications domain A bias/" $ test s "connection removed notifications domain B bias" testConnectionRemovedNotificationsNoopDomainB - ] + ] ] rb1, rb2, rb3, rb4 :: Remote Backend rb1 = @@ -4421,323 +4405,3 @@ testOne2OneConversationRequest shouldBeLocal actor desired = do pure $ statusCode resp == 200 liftIO $ found @?= ((actor, desired) == (LocalActor, Included)) ) - --- Testing defederation notifications. The important thing to note for all --- of this is that when defederating from a remote domain only _2_ notifications --- are sent, and both are identical. One notification is at the start of --- defederation, and one is sent at the end of defederation. No other --- notifications about users being removed from conversations, or conversations --- being deleted are sent. We are do not want to DOS either our local clients, --- nor our own services. -testDefederationNotifications :: TestM () -testDefederationNotifications = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - let remoteDomain = Domain "far-away.example.com" - -- This variable should be commented out if the below - -- section is used to insert users to the database. - users = [] - -- This section of code is useful to massively increase - -- the amount of users in the testing database. This is - -- useful for checking that notifications are being fanned - -- out correctly, and that all users are sent a - -- notification. If the database already has a large - -- amount of users then this can be left out and will also - -- allow this test to run faster. - -- count = 10000 - -- users <- replicateM count randomQualifiedUser - -- replicateM_ count $ do - -- connectWithRemoteUser (qUnqualified alice) =<< - -- Qualified <$> randomId <*> pure remoteDomain - - -- dee is a remote guest - dee <- Qualified <$> randomId <*> pure remoteDomain - - connectWithRemoteUser (qUnqualified alice) dee - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee], - newConvTeam = Just (ConvTeamInfo tid) - } - users) $ \(wsA : wsB : wsC : wsD : wsUsers) -> do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply ()) $ do - -- Delete the domain that Dee lives on - deleteFederation remoteDomain !!! const 200 === statusCode - -- First notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC] <> wsUsers) $ - wsAssertFederationDeleted remoteDomain - -- Second notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC] <> wsUsers) $ - wsAssertFederationDeleted remoteDomain - -- dee's remote doesn't receive a notification - WS.assertNoEvent (5 # Second) [wsD] - -- There should be not requests out to the federtaion domain - liftIO $ reqs @?= [] - - -- only alice, bob, and charlie remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - cmOthers (cnvMembers conv2)) @?= sort [bob, charlie] - --- Testing defederation notifications. The important thing to note for all --- of this is that when defederating from a remote domain only _2_ notifications --- are sent, and both are identical. One notification is at the start of --- defederation, and one is sent at the end of defederation. No other --- notifications about users being removed from conversations, or conversations --- being deleted are sent. We are do not want to DOS either our local clients, --- nor our own services. --- There are four tests here. - --- * A normal run where we have users from both remote domains in a conversation. Both remote users should be removed. - --- * A no-op run where we have no remote users in the conversation. The conversation remains unchanged. - --- * A domain A biased run where we have a conversation with a remote member from domain A, but none from domain B. The conversation remains unchanged. - --- * A domain B biased run where we have a conversation with a remote member from domain B, but none from domain A. The conversation remains unchanged. - -testConnectionRemovedNotifications :: TestM () -testConnectionRemovedNotifications = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - let remoteDomain1 = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - remoteDomain3 = Domain "far-away-3.example.com" - -- dee and erin are remote guests - dee <- Qualified <$> randomId <*> pure remoteDomain1 - erin <- Qualified <$> randomId <*> pure remoteDomain2 - -- frank is a remote we are going to keep around. - frank <- Qualified <$> randomId <*> pure remoteDomain3 - - -- Set up the federation - addFederation remoteDomain1 !!! const 200 === statusCode - addFederation remoteDomain2 !!! const 200 === statusCode - addFederation remoteDomain3 !!! const 200 === statusCode - - connectWithRemoteUser (qUnqualified alice) dee - connectWithRemoteUser (qUnqualified alice) erin - connectWithRemoteUser (qUnqualified alice) frank - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee, erin, frank], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply ()) $ do - -- Remove the connection - connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode - -- First notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- Second notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- dee, erin, and frank's remotes don't receive a notification - WS.assertNoEvent (5 # Second) [wsD, wsE, wsF] - -- There should be not requests out to the federtaion domain - liftIO $ reqs @?= [] - - -- only alice, bob, charlie, and frank remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - cmOthers (cnvMembers conv2)) @?= sort [bob, charlie, frank] - -testConnectionRemovedNotificationsNoop :: TestM () -testConnectionRemovedNotificationsNoop = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - let remoteDomain1 = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - - -- Setup federation - addFederation remoteDomain1 !!! const 200 === statusCode - addFederation remoteDomain2 !!! const 200 === statusCode - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply ()) $ do - -- Remove the connection - connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode - -- First notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- Second notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- There should be not requests out to the federtaion domain - liftIO $ reqs @?= [] - - -- only alice, bob, and charlie remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - cmOthers (cnvMembers conv2)) @?= sort [bob, charlie] - -testConnectionRemovedNotificationsNoopDomainA :: TestM () -testConnectionRemovedNotificationsNoopDomainA = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - let remoteDomain1 = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - - -- Setup federation - addFederation remoteDomain1 !!! const 200 === statusCode - addFederation remoteDomain2 !!! const 200 === statusCode - - -- dee is a remote guest - dee <- Qualified <$> randomId <*> pure remoteDomain1 - - connectWithRemoteUser (qUnqualified alice) dee - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply ()) $ do - -- Remove the connection - connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode - -- First notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- Second notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- dee and remote doesn't receive a notification - WS.assertNoEvent (5 # Second) [wsD] - -- There should be not requests out to the federtaion domain - liftIO $ reqs @?= [] - - -- alice, bob, charlie, and dee remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - cmOthers (cnvMembers conv2)) @?= sort [bob, charlie, dee] - -testConnectionRemovedNotificationsNoopDomainB :: TestM () -testConnectionRemovedNotificationsNoopDomainB = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - let remoteDomain1 = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - - -- Setup federation - addFederation remoteDomain1 !!! const 200 === statusCode - addFederation remoteDomain2 !!! const 200 === statusCode - - -- erin is a remote guest - erin <- Qualified <$> randomId <*> pure remoteDomain2 - - connectWithRemoteUser (qUnqualified alice) erin - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, erin], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply ()) $ do - -- Remove the connection - connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode - -- First notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- Second notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- erin's remote doesn't receive a notification - WS.assertNoEvent (5 # Second) [wsE] - -- There should be not requests out to the federtaion domain - liftIO $ reqs @?= [] - - -- alice, bob, charlie, and erin remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - cmOthers (cnvMembers conv2)) @?= sort [bob, charlie, erin] - --- @END diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index b126c0aca72..2bb2f8b40ef 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -128,7 +128,6 @@ import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.API.Message import Wire.API.Message.Proto qualified as Proto -import Wire.API.Routes.FederationDomainConfig (FederationDomainConfig (..)) import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi @@ -147,7 +146,6 @@ import Wire.API.User.Auth hiding (Access) import Wire.API.User.Client import Wire.API.User.Client qualified as Client import Wire.API.User.Client.Prekey -import Wire.API.User.Search (FederatedUserSearchPolicy (..)) ------------------------------------------------------------------------------- -- API Operations @@ -1406,31 +1404,6 @@ deleteFederation dom = do delete $ g . paths ["/i/federation", toByteString' dom] -addFederation :: - (MonadHttp m, HasBrig m, MonadIO m) => - Domain -> - m ResponseLBS -addFederation dom = do - b <- viewBrig - post $ - b - . paths ["/i/federation/remotes"] - . json (FederationDomainConfig dom FullSearch) - -connectionRemovedFederation :: - (MonadHttp m, HasGalley m, MonadIO m) => - Domain -> - Domain -> - m ResponseLBS -connectionRemovedFederation origin target = do - g <- viewGalley - post $ - g - . paths ["federation", "on-connection-removed"] - . content "application/json" - . header "Wire-Origin-Domain" (toByteString' origin) - . json target - putQualifiedAccessUpdate :: (MonadHttp m, HasGalley m, MonadIO m) => UserId -> @@ -1810,25 +1783,8 @@ assertFederationDeletedEvent :: Fed.Event -> IO () assertFederationDeletedEvent dom e = do - e @?= Fed.FederationDelete dom - -wsAssertFederationConnectionRemoved :: - HasCallStack => - Domain -> - Domain -> - Notification -> - IO () -wsAssertFederationConnectionRemoved domA domB n = do - ntfTransient n @?= False - assertFederationConnectionRemovedEvent domA domB $ List1.head (WS.unpackPayload n) - -assertFederationConnectionRemovedEvent :: - Domain -> - Domain -> - Fed.Event -> - IO () -assertFederationConnectionRemovedEvent domA domB e = do - e @?= Fed.FederationConnectionRemoved (domA, domB) + Fed._eventType e @?= Fed.FederationDelete + Fed._eventDomain e @?= dom -- FUTUREWORK: See if this one can be implemented in terms of: -- diff --git a/services/galley/test/integration/Federation.hs b/services/galley/test/integration/Federation.hs index 2971b59ecc8..4f218570599 100644 --- a/services/galley/test/integration/Federation.hs +++ b/services/galley/test/integration/Federation.hs @@ -2,54 +2,31 @@ module Federation where -import API.Util -import Bilge.Assert -import Bilge.Response import Cassandra qualified as C -import Cassandra.Exec (x1) -import Control.Lens (view, (^.)) +import Control.Lens ((^.)) import Control.Monad.Catch -import Control.Monad.Codensity (lowerCodensity) import Data.ByteString qualified as LBS import Data.Domain import Data.Id -import Data.List.NonEmpty -import Data.List1 qualified as List1 import Data.Qualified -import Data.Range (toRange) import Data.Set qualified as Set -import Data.Singletons -import Data.Time (getCurrentTime) import Data.UUID qualified as UUID -import Federator.MockServer -import Galley.API.Internal import Galley.API.Util import Galley.App -import Galley.Cassandra.Queries import Galley.Data.Conversation.Types qualified as Types -import Galley.Monad import Galley.Options -import Galley.Run -import Galley.Types.Conversations.Members (LocalMember (..), RemoteMember (..), defMemberStatus, localMemberToOther) +import Galley.Types.Conversations.Members (LocalMember (..), RemoteMember (..), defMemberStatus) import Imports -import Test.Tasty.Cannon (TimeoutUnit (..), (#)) -import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit import TestSetup import UnliftIO.Retry import Wire.API.Conversation import Wire.API.Conversation qualified as Public -import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol (Protocol (..)) -import Wire.API.Conversation.Role (roleNameWireAdmin, roleNameWireMember) -import Wire.API.Event.Conversation -import Wire.API.Federation.API.Brig (NonConnectedBackends (NonConnectedBackends)) -import Wire.API.Federation.API.Galley (ConversationUpdate (..), GetConversationsResponse (..)) -import Wire.API.Internal.Notification +import Wire.API.Conversation.Role (roleNameWireMember) import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.MultiTablePaging qualified as Public -import Wire.API.User.Search x3 :: RetryPolicy x3 = limitRetries 3 <> exponentialBackoff 100000 @@ -83,223 +60,12 @@ isConvMemberLTests = do liftIO $ assertBool "Qualified UserId (local)" $ isConvMemberL lconv $ tUntagged lUserId liftIO $ assertBool "Qualified UserId (remote)" $ isConvMemberL lconv $ tUntagged rUserId -updateFedDomainsTestNoop' :: TestM () -updateFedDomainsTestNoop' = do - s <- ask - let opts = s ^. tsGConf - -- Don't need the actual server, and we certainly don't want it running. - -- But this is how the env is made, so it is what we do - (_, env) <- liftIO $ lowerCodensity $ mkApp opts - -- Common variables. - -- FUTUREWORK, NEWTICKET: These uuid strings side step issues with the tests hanging. - -- FUTUREWORK, NEWTICKET: Figure out the underlying issue as to why these tests occasionally hang. - let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates - remoteDomain = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain - -- Setup a conversation for a known remote domain. - -- Include that domain in the old and new lists so - -- if the function is acting up we know it will be - -- working on the domain. - updateFedDomainsTestNoop env remoteDomain interval - -updateFedDomainsTestAddRemote' :: TestM () -updateFedDomainsTestAddRemote' = do - s <- ask - let opts = s ^. tsGConf - -- Don't need the actual server, and we certainly don't want it running. - -- But this is how the env is made, so it is what we do - (_, env) <- liftIO $ lowerCodensity $ mkApp opts - -- Common variables. - let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates - remoteDomain = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain - - -- Adding a new federation domain, this too should be a no-op - updateFedDomainsAddRemote env remoteDomain remoteDomain2 interval - -updateFedDomainsTestRemoveRemoteFromLocal' :: TestM () -updateFedDomainsTestRemoveRemoteFromLocal' = do - s <- ask - let opts = s ^. tsGConf - -- Don't need the actual server, and we certainly don't want it running. - -- But this is how the env is made, so it is what we do - (_, env) <- liftIO $ lowerCodensity $ mkApp opts - -- Common variables. - let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates - remoteDomain = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain - - -- Remove a remote domain from local conversations - updateFedDomainRemoveRemoteFromLocal env remoteDomain remoteDomain2 interval - -updateFedDomainsTestRemoveLocalFromRemote' :: TestM () -updateFedDomainsTestRemoveLocalFromRemote' = do - s <- ask - let opts = s ^. tsGConf - -- Don't need the actual server, and we certainly don't want it running. - -- But this is how the env is made, so it is what we do - (_, env) <- liftIO $ lowerCodensity $ mkApp opts - -- Common variables. - let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates - remoteDomain = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain - - -- Remove a local domain from remote conversations - updateFedDomainRemoveLocalFromRemote env remoteDomain interval - fromFedList :: FederationDomainConfigs -> Set Domain fromFedList = Set.fromList . fmap domain . remotes -deleteFederationDomains :: FederationDomainConfigs -> FederationDomainConfigs -> App () -deleteFederationDomains old new = do - let prev = fromFedList old - curr = fromFedList new - deletedDomains = Set.difference prev curr - env <- ask - -- Call into the galley code - for_ deletedDomains $ liftIO . evalGalleyToIO env . deleteFederationDomain (toRange $ Proxy @500) - constHandlers :: (MonadIO m) => [RetryStatus -> Handler m Bool] constHandlers = [const $ Handler $ (\(_ :: SomeException) -> pure True)] -updateFedDomainRemoveRemoteFromLocal :: Env -> Domain -> Domain -> Int -> TestM () -updateFedDomainRemoveRemoteFromLocal env remoteDomain remoteDomain2 interval = recovering x3 constHandlers $ const $ do - let new = FederationDomainConfigs AllowDynamic [FederationDomainConfig remoteDomain2 FullSearch] interval - old = new {remotes = FederationDomainConfig remoteDomain FullSearch : remotes new} - qalice <- randomQualifiedUser - bobId <- randomId - charlieId <- randomId - let alice = qUnqualified qalice - remoteBob = Qualified bobId remoteDomain - remoteCharlie = Qualified charlieId remoteDomain2 - -- Create a local conversation - conv <- postConv alice [] (Just "remote gossip") [] Nothing Nothing - let qConvId = decodeQualifiedConvId conv - connectWithRemoteUser alice remoteBob - connectWithRemoteUser alice remoteCharlie - _ <- withTempMockFederator' ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty) $ postQualifiedMembers alice (remoteCharlie <| remoteBob :| []) qConvId - -- Remove the remote user from the local domain - liftIO $ runApp env $ deleteFederationDomains old new - -- Check that the conversation still exists. - getConvQualified alice qConvId !!! do - const 200 === statusCode - let findRemote :: Qualified UserId -> Conversation -> Maybe (Qualified UserId) - findRemote u = find (== u) . fmap omQualifiedId . cmOthers . cnvMembers - -- Check that only one remote user was removed. - const (Right Nothing) === (fmap (findRemote remoteBob) <$> responseJsonEither) - const (Right $ pure remoteCharlie) === (fmap (findRemote remoteCharlie) <$> responseJsonEither) - const (Right qalice) === (fmap (memId . cmSelf . cnvMembers) <$> responseJsonEither) - -updateFedDomainRemoveLocalFromRemote :: Env -> Domain -> Int -> TestM () -updateFedDomainRemoveLocalFromRemote env remoteDomain interval = recovering x3 constHandlers $ const $ do - c <- view tsCannon - let new = FederationDomainConfigs AllowDynamic [] interval - old = new {remotes = FederationDomainConfig remoteDomain FullSearch : remotes new} - -- Make our users - qalice <- randomQualifiedUser - qbob <- Qualified <$> randomId <*> pure remoteDomain - let alice = qUnqualified qalice - update = memberUpdate {mupHidden = Just False} - -- Create a remote conversation - -- START: code from putRemoteConvMemberOk - qconv <- Qualified <$> randomId <*> pure remoteDomain - connectWithRemoteUser alice qbob - - fedGalleyClient <- view tsFedGalleyClient - now <- liftIO getCurrentTime - let cu = - ConversationUpdate - { cuTime = now, - cuOrigUserId = qbob, - cuConvId = qUnqualified qconv, - cuAlreadyPresentUsers = [], - cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qalice) roleNameWireMember) - } - void $ runFedClient @"on-conversation-updated" fedGalleyClient remoteDomain cu - -- Expected member state - let memberAlice = - Member - { memId = qalice, - memService = Nothing, - memOtrMutedStatus = mupOtrMuteStatus update, - memOtrMutedRef = mupOtrMuteRef update, - memOtrArchived = Just True == mupOtrArchive update, - memOtrArchivedRef = mupOtrArchiveRef update, - memHidden = Just True == mupHidden update, - memHiddenRef = mupHiddenRef update, - memConvRoleName = roleNameWireMember - } - -- Update member state & verify push notification - WS.bracketR c alice $ \ws -> do - putMember alice update qconv !!! const 200 === statusCode - void . liftIO . WS.assertMatch (5 # Second) ws $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= MemberStateUpdate - evtFrom e @?= qalice - case evtData e of - EdMemberUpdate mis -> do - assertEqual "otr_muted_status" (mupOtrMuteStatus update) (misOtrMutedStatus mis) - assertEqual "otr_muted_ref" (mupOtrMuteRef update) (misOtrMutedRef mis) - assertEqual "otr_archived" (mupOtrArchive update) (misOtrArchived mis) - assertEqual "otr_archived_ref" (mupOtrArchiveRef update) (misOtrArchivedRef mis) - assertEqual "hidden" (mupHidden update) (misHidden mis) - assertEqual "hidden_ref" (mupHiddenRef update) (misHiddenRef mis) - x -> assertFailure $ "Unexpected event data: " ++ show x - - -- Fetch remote conversation - let bobAsLocal = - LocalMember - (qUnqualified qbob) - defMemberStatus - Nothing - roleNameWireAdmin - let mockConversation = - mkProteusConv - (qUnqualified qconv) - (qUnqualified qbob) - roleNameWireMember - [localMemberToOther remoteDomain bobAsLocal] - remoteConversationResponse = GetConversationsResponse [mockConversation] - (rs, _) <- - withTempMockFederator' - (mockReply remoteConversationResponse) - $ getConvQualified alice qconv - responseJsonUnsafe rs - liftIO $ do - assertBool "user" (isJust alice') - let newAlice = fromJust alice' - assertEqual "id" (memId memberAlice) (memId newAlice) - assertEqual "otr_muted_status" (memOtrMutedStatus memberAlice) (memOtrMutedStatus newAlice) - assertEqual "otr_muted_ref" (memOtrMutedRef memberAlice) (memOtrMutedRef newAlice) - assertEqual "otr_archived" (memOtrArchived memberAlice) (memOtrArchived newAlice) - assertEqual "otr_archived_ref" (memOtrArchivedRef memberAlice) (memOtrArchivedRef newAlice) - assertEqual "hidden" (memHidden memberAlice) (memHidden newAlice) - assertEqual "hidden_ref" (memHiddenRef memberAlice) (memHiddenRef newAlice) - -- END: code from putRemoteConvMemberOk - - -- Remove the remote user from the local domain - liftIO $ runApp env $ deleteFederationDomains old new - convIds <- - liftIO $ - C.runClient (env ^. cstate) $ - C.retry x1 $ - C.query selectUserRemoteConvs (C.params C.LocalQuorum (pure alice)) - case find (== qUnqualified qconv) $ snd <$> convIds of - Nothing -> pure () - Just c' -> liftIO $ assertFailure $ "Found conversation where none was expected: " <> show c' - pageToConvIdPage :: Public.LocalOrRemoteTable -> C.PageWithState (Qualified ConvId) -> Public.ConvIdsPage pageToConvIdPage table page@C.PageWithState {..} = Public.MultiTablePage @@ -307,67 +73,3 @@ pageToConvIdPage table page@C.PageWithState {..} = mtpHasMore = C.pwsHasMore page, mtpPagingState = Public.ConversationPagingState table (LBS.toStrict . C.unPagingState <$> pwsState) } - -updateFedDomainsAddRemote :: Env -> Domain -> Domain -> Int -> TestM () -updateFedDomainsAddRemote env remoteDomain remoteDomain2 interval = do - s <- ask - let opts = s ^. tsGConf - localDomain = opts ^. settings . federationDomain - old = FederationDomainConfigs AllowDynamic [FederationDomainConfig remoteDomain FullSearch] interval - new = old {remotes = FederationDomainConfig remoteDomain2 FullSearch : remotes old} - -- Just check against the domains, as the search - -- strategies are outside of this testing scope - newDoms = domain <$> new.remotes - oldDoms = domain <$> old.remotes - liftIO $ assertBool "old and new are different" $ oldDoms /= newDoms - liftIO $ assertBool "old is shorter than new" $ Imports.length oldDoms < Imports.length newDoms - liftIO $ assertBool "new contains old" $ all (`elem` newDoms) oldDoms - liftIO $ assertBool "new elements not in old" $ any (`notElem` oldDoms) newDoms - qalice <- randomQualifiedUser - bobId <- randomId - let alice = qUnqualified qalice - remoteBob = Qualified bobId remoteDomain - -- Create a conversation - - conv <- postConv alice [] (Just "remote gossip") [] Nothing Nothing - -- liftIO $ assertBool ("conv = " <> show conv) False - let convId = decodeConvId conv - let qConvId = Qualified convId localDomain - connectWithRemoteUser alice remoteBob - _ <- withTempMockFederator' ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty) $ postQualifiedMembers alice (remoteBob :| []) qConvId - - -- No-op - liftIO $ runApp env $ deleteFederationDomains old new - -- Check that the conversation still exists. - getConvQualified (qUnqualified qalice) (Qualified convId localDomain) !!! do - const 200 === statusCode - let findRemote :: Conversation -> Maybe (Qualified UserId) - findRemote = find (== remoteBob) . fmap omQualifiedId . cmOthers . cnvMembers - const (Right $ pure remoteBob) === (fmap findRemote <$> responseJsonEither) - const (Right qalice) === (fmap (memId . cmSelf . cnvMembers) <$> responseJsonEither) - -updateFedDomainsTestNoop :: Env -> Domain -> Int -> TestM () -updateFedDomainsTestNoop env remoteDomain interval = do - s <- ask - let opts = s ^. tsGConf - localDomain = opts ^. settings . federationDomain - old = FederationDomainConfigs AllowDynamic [FederationDomainConfig remoteDomain FullSearch] interval - new = old - qalice <- randomQualifiedUser - bobId <- randomId - let alice = qUnqualified qalice - remoteBob = Qualified bobId remoteDomain - -- Create a conversation - convId <- decodeConvId <$> postConv alice [] (Just "remote gossip") [] Nothing Nothing - let qConvId = Qualified convId localDomain - connectWithRemoteUser alice remoteBob - _ <- withTempMockFederator' ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty) $ postQualifiedMembers alice (remoteBob :| []) qConvId - -- No-op - liftIO $ runApp env $ deleteFederationDomains old new - -- Check that the conversation still exists. - getConvQualified (qUnqualified qalice) (Qualified convId localDomain) !!! do - const 200 === statusCode - let findRemote :: Conversation -> Maybe (Qualified UserId) - findRemote = find (== remoteBob) . fmap omQualifiedId . cmOthers . cnvMembers - const (Right $ pure remoteBob) === (fmap findRemote <$> responseJsonEither) - const (Right qalice) === (fmap (memId . cmSelf . cnvMembers) <$> responseJsonEither) diff --git a/services/galley/test/integration/Main.hs b/services/galley/test/integration/Main.hs index 6d465089cbe..c578808998d 100644 --- a/services/galley/test/integration/Main.hs +++ b/services/galley/test/integration/Main.hs @@ -98,13 +98,6 @@ main = withOpenSSL $ runTests go mempty (pathsConsistencyCheck . treeToPaths . compile $ Galley.API.sitemap), API.tests setup, - testGroup - "Federation Domains" - [ test setup "No-Op" updateFedDomainsTestNoop', - test setup "Add Remote" updateFedDomainsTestAddRemote', - test setup "Remove Remote From Local" updateFedDomainsTestRemoveRemoteFromLocal', - test setup "Remove Local From Remote" updateFedDomainsTestRemoveLocalFromRemote' - ], test setup "isConvMemberL" isConvMemberLTests ] getOpts gFile iFile = do From 85cb5a9078da31d553df501e4687124d492f21f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 20 Sep 2023 11:30:22 +0200 Subject: [PATCH 131/225] [WPB-3664] Bug fix: Notify remote backends of their users removed from conversation when reachable again (#3537) * Formatting * Test utilities for changing a conv name * Add a test confirming the bug report * An action to enqueue notifications concurrently * Enqueue member removal notification for remotes * Add a changelog * Test case formatting * Migrate test roleUpdateWithRemotesUnavailable * Migrate test putReceiptModeWithRemotesOk * Migrate test putReceiptModeWithRemotesUnavailable * Migrate test testRoleUpdateWithRemotesOk * Migrate test roleUpdateRemoteMember * Migrate test putQualifiedConvRenameWithRemotesUnavailable This one is already covered by testSynchroniseUserRemovalNotification * Migrate test putQualifiedConvRenameWithRemotesOk * Migrate test deleteLocalMemberConvLocalQualifiedOk * Migrate test deleteRemoteMemberConvLocalQualifiedOk * Migrate test deleteUnavailableRemoteMemberConvLocalQualifiedOk * Add the copyright header to a test module * Move a test utility (allPreds) * Test utility: create a team with members * Migrate test testAccessUpdateGuestRemoved * Migrate test messageTimerChangeWithRemotes * Migrate test messageTimerUnavailableRemotes * Migrate test testAccessUpdateGuestRemovedRemotesUnavailable * Migrate test accessUpdateWithRemotes * Migrate test testAddRemoteMember * Migrate test testDeleteTeamConversationWithRemoteMembers * Migrate test testDeleteTeamConversationWithUnavailableRemoteMembers * Move a test utility (assertLeaveNotification) * Migrate test "POST /federation/leave-conversation : Success" * Migrate test "POST /federation/on-user-deleted-conversations : Remove deleted remote user from local conversations" * Migrate test updateConversationByRemoteAdmin * Tests: support giving a role when adding * Use cannon API for notifications when possible * Use startDynamicBackends when possible * Fix assertion * Migrate test testAddRemoteUsersToLocalConv * Test add member endpoint at version 1 * Add return value to enqueueNotification * Use cannon assertions in offline backends test * Check that remote notifications are received * Test removal of users from unreachable backends * Use correct domains for default backends Taking the domains in the `backendA` and `backendB` resources only works locally. * fixup! Use cannon assertions in offline backends test --------- Co-authored-by: Paolo Capriotti Co-authored-by: Akshay Mankar --- .../remote-member-removal-notification | 1 + integration/default.nix | 2 + integration/integration.cabal | 4 + integration/test/API/Galley.hs | 139 +++- integration/test/Notifications.hs | 59 +- integration/test/SetupHelpers.hs | 45 +- integration/test/Test/AccessUpdate.hs | 122 ++++ integration/test/Test/Conversation.hs | 358 +++++++++- integration/test/Test/Demo.hs | 6 +- integration/test/Test/Federation.hs | 151 +++-- integration/test/Test/MessageTimer.hs | 55 ++ integration/test/Test/Roles.hs | 65 ++ integration/test/Testlib/Assertions.hs | 4 + integration/test/Testlib/Prelude.hs | 11 + .../API/Federation/BackendNotifications.hs | 2 +- .../src/Wire/API/Conversation/Action.hs | 6 +- services/brig/src/Brig/Federation/Client.hs | 6 +- .../test/integration/Federation/End2end.hs | 60 -- services/galley/src/Galley/API/Action.hs | 60 +- services/galley/src/Galley/API/Federation.hs | 16 +- services/galley/src/Galley/API/LegalHold.hs | 27 +- services/galley/src/Galley/API/MLS/Message.hs | 6 +- services/galley/src/Galley/API/Teams.hs | 4 +- .../galley/src/Galley/API/Teams/Features.hs | 1 + services/galley/src/Galley/API/Update.hs | 76 ++- services/galley/src/Galley/Effects.hs | 3 + .../Effects/BackendNotificationQueueAccess.hs | 10 +- .../Galley/Intra/BackendNotificationQueue.hs | 20 +- services/galley/test/integration/API.hs | 641 ------------------ .../galley/test/integration/API/Federation.hs | 294 -------- .../test/integration/API/MessageTimer.hs | 97 --- services/galley/test/integration/API/Roles.hs | 246 ------- 32 files changed, 1074 insertions(+), 1523 deletions(-) create mode 100644 changelog.d/3-bug-fixes/remote-member-removal-notification create mode 100644 integration/test/Test/AccessUpdate.hs create mode 100644 integration/test/Test/MessageTimer.hs create mode 100644 integration/test/Test/Roles.hs diff --git a/changelog.d/3-bug-fixes/remote-member-removal-notification b/changelog.d/3-bug-fixes/remote-member-removal-notification new file mode 100644 index 00000000000..a94c916a689 --- /dev/null +++ b/changelog.d/3-bug-fixes/remote-member-removal-notification @@ -0,0 +1 @@ +This fixes a bug where a remote member is removed from a conversation while their backend is unreachable, and the backend does not receive the removal notification once it is reachable again. diff --git a/integration/default.nix b/integration/default.nix index 8019fdd1f87..b42304134c1 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -19,6 +19,7 @@ , cql-io , cryptonite , data-default +, data-timeout , directory , errors , exceptions @@ -86,6 +87,7 @@ mkDerivation { cql-io cryptonite data-default + data-timeout directory errors exceptions diff --git a/integration/integration.cabal b/integration/integration.cabal index 7e989f5a988..5ff99f89941 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -100,6 +100,7 @@ library Notifications RunAllTests SetupHelpers + Test.AccessUpdate Test.AssetDownload Test.B2B Test.Brig @@ -108,8 +109,10 @@ library Test.Demo Test.Federation Test.Federator + Test.MessageTimer Test.Notifications Test.Presence + Test.Roles Test.User Testlib.App Testlib.Assertions @@ -146,6 +149,7 @@ library , cql-io , cryptonite , data-default + , data-timeout , directory , errors , exceptions diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index bb3aa2ac0d5..b646212e14b 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -5,6 +5,7 @@ module API.Galley where import Control.Lens hiding ((.=)) import Control.Monad.Reader import Data.Aeson qualified as Aeson +import Data.Aeson.Types qualified as Aeson import Data.ByteString.Lazy qualified as LBS import Data.ProtoLens qualified as Proto import Data.ProtoLens.Labels () @@ -80,6 +81,21 @@ postConversation user cc = do ccv <- make cc submit "POST" $ req & addJSON ccv +deleteTeamConversation :: + ( HasCallStack, + MakesValue user, + MakesValue conv + ) => + String -> + conv -> + user -> + App Response +deleteTeamConversation tid qcnv user = do + cnv <- snd <$> objQid qcnv + let path = joinHttpPath ["teams", tid, "conversations", cnv] + req <- baseRequest user Galley Versioned path + submit "DELETE" req + putConversationProtocol :: ( HasCallStack, MakesValue user, @@ -217,12 +233,39 @@ getGroupInfo user conv = do req <- baseRequest user Galley Versioned path submit "GET" req -addMembers :: (HasCallStack, MakesValue user, MakesValue conv) => user -> conv -> [Value] -> App Response -addMembers usr qcnv newMembers = do +data AddMembers = AddMembers + { users :: [Value], + role :: Maybe String, + version :: Maybe Int + } + +instance Default AddMembers where + def = AddMembers {users = [], role = Nothing, version = Nothing} + +addMembers :: + (HasCallStack, MakesValue user, MakesValue conv) => + user -> + conv -> + AddMembers -> + App Response +addMembers usr qcnv opts = do (convDomain, convId) <- objQid qcnv - qUsers <- mapM objQidObject newMembers - req <- baseRequest usr Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "members"]) - submit "POST" (req & addJSONObject ["qualified_users" .= qUsers]) + qUsers <- mapM objQidObject opts.users + let path = case opts.version of + Just v | v <= 1 -> ["conversations", convId, "members", "v2"] + _ -> ["conversations", convDomain, convId, "members"] + req <- + baseRequest + usr + Galley + (maybe Versioned ExplicitVersion opts.version) + (joinHttpPath path) + submit "POST" $ + req + & addJSONObject + ( ["qualified_users" .= qUsers] + <> ["conversation_role" .= r | r <- toList opts.role] + ) removeMember :: (HasCallStack, MakesValue remover, MakesValue conv, MakesValue removed) => remover -> conv -> removed -> App Response removeMember remover qcnv removed = do @@ -263,3 +306,89 @@ getConversationCode user conv mbZHost = do & addQueryParams [("cnv", convId)] & maybe id zHost mbZHost ) + +changeConversationName :: + (HasCallStack, MakesValue user, MakesValue conv, MakesValue name) => + user -> + conv -> + name -> + App Response +changeConversationName user qcnv name = do + (convDomain, convId) <- objQid qcnv + let path = joinHttpPath ["conversations", convDomain, convId, "name"] + nameReq <- make name + req <- baseRequest user Galley Versioned path + submit "PUT" (req & addJSONObject ["name" .= nameReq]) + +updateRole :: + ( HasCallStack, + MakesValue callerUser, + MakesValue targetUser, + MakesValue roleUpdate, + MakesValue qcnv + ) => + callerUser -> + targetUser -> + roleUpdate -> + qcnv -> + App Response +updateRole caller target role qcnv = do + (cnvDomain, cnvId) <- objQid qcnv + (tarDomain, tarId) <- objQid target + roleReq <- make role + req <- + baseRequest + caller + Galley + Versioned + ( joinHttpPath ["conversations", cnvDomain, cnvId, "members", tarDomain, tarId] + ) + submit "PUT" (req & addJSONObject ["conversation_role" .= roleReq]) + +updateReceiptMode :: + ( HasCallStack, + MakesValue user, + MakesValue conv, + MakesValue mode + ) => + user -> + conv -> + mode -> + App Response +updateReceiptMode user qcnv mode = do + (cnvDomain, cnvId) <- objQid qcnv + modeReq <- make mode + let path = joinHttpPath ["conversations", cnvDomain, cnvId, "receipt-mode"] + req <- baseRequest user Galley Versioned path + submit "PUT" (req & addJSONObject ["receipt_mode" .= modeReq]) + +updateAccess :: + ( HasCallStack, + MakesValue user, + MakesValue conv + ) => + user -> + conv -> + [Aeson.Pair] -> + App Response +updateAccess user qcnv update = do + (cnvDomain, cnvId) <- objQid qcnv + let path = joinHttpPath ["conversations", cnvDomain, cnvId, "access"] + req <- baseRequest user Galley Versioned path + submit "PUT" (req & addJSONObject update) + +updateMessageTimer :: + ( HasCallStack, + MakesValue user, + MakesValue conv + ) => + user -> + conv -> + Word64 -> + App Response +updateMessageTimer user qcnv update = do + (cnvDomain, cnvId) <- objQid qcnv + updateReq <- make update + let path = joinHttpPath ["conversations", cnvDomain, cnvId, "message-timer"] + req <- baseRequest user Galley Versioned path + submit "PUT" (addJSONObject ["message_timer" .= updateReq] req) diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index 0364bee7ceb..a6ffb6505b1 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -65,8 +65,63 @@ isMemberJoinNotif n = fieldEquals n "payload.0.type" "conversation.member-join" isConvLeaveNotif :: MakesValue a => a -> App Bool isConvLeaveNotif n = fieldEquals n "payload.0.type" "conversation.member-leave" -isNotifConv :: (MakesValue conv, MakesValue a) => conv -> a -> App Bool +isNotifConv :: (MakesValue conv, MakesValue a, HasCallStack) => conv -> a -> App Bool isNotifConv conv n = fieldEquals n "payload.0.qualified_conversation" (objQidObject conv) -isNotifForUser :: (MakesValue user, MakesValue a) => user -> a -> App Bool +isNotifForUser :: (MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool isNotifForUser user n = fieldEquals n "payload.0.data.qualified_user_ids.0" (objQidObject user) + +isNotifFromUser :: (MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool +isNotifFromUser user n = fieldEquals n "payload.0.qualified_from" (objQidObject user) + +isConvNameChangeNotif :: (HasCallStack, MakesValue a) => a -> App Bool +isConvNameChangeNotif n = fieldEquals n "payload.0.type" "conversation.rename" + +isMemberUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isMemberUpdateNotif n = fieldEquals n "payload.0.type" "conversation.member-update" + +isReceiptModeUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isReceiptModeUpdateNotif n = + fieldEquals n "payload.0.type" "conversation.receipt-mode-update" + +isConvMsgTimerUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isConvMsgTimerUpdateNotif n = + fieldEquals n "payload.0.type" "conversation.message-timer-update" + +isConvAccessUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isConvAccessUpdateNotif n = + fieldEquals n "payload.0.type" "conversation.access-update" + +isConvCreateNotif :: MakesValue a => a -> App Bool +isConvCreateNotif n = fieldEquals n "payload.0.type" "conversation.create" + +isConvDeleteNotif :: MakesValue a => a -> App Bool +isConvDeleteNotif n = fieldEquals n "payload.0.type" "conversation.delete" + +assertLeaveNotification :: + ( HasCallStack, + MakesValue fromUser, + MakesValue conv, + MakesValue user, + MakesValue kickedUser + ) => + fromUser -> + conv -> + user -> + String -> + kickedUser -> + App () +assertLeaveNotification fromUser conv user client leaver = + void $ + awaitNotification + user + client + noValue + 2 + ( allPreds + [ isConvLeaveNotif, + isNotifConv conv, + isNotifForUser leaver, + isNotifFromUser fromUser + ] + ) diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 9053a98c3d1..b3c6b2dd5b5 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -4,6 +4,7 @@ module SetupHelpers where import API.Brig qualified as Brig import API.BrigInternal qualified as Internal +import API.Common import API.Galley import Control.Concurrent (threadDelay) import Control.Monad.Reader @@ -32,15 +33,43 @@ deleteUser user = bindResponse (Brig.deleteUser user) $ \resp -> do resp.status `shouldMatchInt` 200 -- | returns (user, team id) -createTeam :: (HasCallStack, MakesValue domain) => domain -> App (Value, String) -createTeam domain = do +createTeam :: (HasCallStack, MakesValue domain) => domain -> Int -> App (Value, String, [Value]) +createTeam domain memberCount = do res <- Internal.createUser domain def {Internal.team = True} - user <- res.json - tid <- user %. "team" & asString - -- TODO - -- SQS.assertTeamActivate "create team" tid - -- refreshIndex - pure (user, tid) + owner <- res.json + tid <- owner %. "team" & asString + members <- for [2 .. memberCount] $ \_ -> createTeamMember owner tid + pure (owner, tid, members) + +createTeamMember :: + (HasCallStack, MakesValue inviter) => + inviter -> + String -> + App Value +createTeamMember inviter tid = do + newUserEmail <- randomEmail + let invitationJSON = ["role" .= "member", "email" .= newUserEmail] + invitationReq <- + baseRequest inviter Brig Versioned $ + joinHttpPath ["teams", tid, "invitations"] + invitation <- getJSON 201 =<< submit "POST" (addJSONObject invitationJSON invitationReq) + invitationId <- objId invitation + invitationCodeReq <- + rawBaseRequest inviter Brig Unversioned "/i/teams/invitation-code" + <&> addQueryParams [("team", tid), ("invitation_id", invitationId)] + invitationCode <- bindResponse (submit "GET" invitationCodeReq) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "code" & asString + let registerJSON = + [ "name" .= newUserEmail, + "email" .= newUserEmail, + "password" .= defPassword, + "team_code" .= invitationCode + ] + registerReq <- + rawBaseRequest inviter Brig Versioned "/register" + <&> addJSONObject registerJSON + getJSON 201 =<< submit "POST" registerReq connectUsers :: ( HasCallStack, diff --git a/integration/test/Test/AccessUpdate.hs b/integration/test/Test/AccessUpdate.hs new file mode 100644 index 00000000000..7202717a8a7 --- /dev/null +++ b/integration/test/Test/AccessUpdate.hs @@ -0,0 +1,122 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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.AccessUpdate where + +import API.Brig +import API.Galley +import Control.Monad.Codensity +import Control.Monad.Reader +import GHC.Stack +import Notifications +import SetupHelpers +import Testlib.Prelude +import Testlib.ResourcePool + +-- @SF.Federation @SF.Separation @TSFI.RESTfulAPI @S2 +-- +-- The test asserts that, among others, remote users are removed from a +-- conversation when an access update occurs that disallows guests from +-- accessing. +testAccessUpdateGuestRemoved :: HasCallStack => App () +testAccessUpdateGuestRemoved = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 + charlie <- randomUser OwnDomain def + dee <- randomUser OtherDomain def + mapM_ (connectUsers alice) [charlie, dee] + [aliceClient, bobClient, charlieClient, deeClient] <- + mapM + (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) + [alice, bob, charlie, dee] + conv <- + postConversation + alice + defProteus + { qualifiedUsers = [bob, charlie, dee], + team = Just tid + } + >>= getJSON 201 + + let update = ["access" .= ([] :: [String]), "access_role" .= ["team_member"]] + void $ updateAccess alice conv update >>= getJSON 200 + + mapM_ (assertLeaveNotification alice conv alice aliceClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv bob bobClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv charlie charlieClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv dee deeClient) [charlie, dee] + + bindResponse (getConversation alice conv) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject bob + +-- @END + +testAccessUpdateGuestRemovedUnreachableRemotes :: HasCallStack => App () +testAccessUpdateGuestRemovedUnreachableRemotes = do + resourcePool <- asks resourcePool + (alice, tid, [bob]) <- createTeam OwnDomain 2 + charlie <- randomUser OwnDomain def + connectUsers alice charlie + [aliceClient, bobClient, charlieClient] <- + mapM + (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) + [alice, bob, charlie] + (conv, dee) <- runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + dee <- randomUser dynBackend.berDomain def + connectUsers alice dee + conv <- + postConversation + alice + ( defProteus + { qualifiedUsers = [bob, charlie, dee], + team = Just tid + } + ) + >>= getJSON 201 + pure (conv, dee) + + let update = ["access" .= ([] :: [String]), "access_role" .= ["team_member"]] + void $ updateAccess alice conv update >>= getJSON 200 + + mapM_ (assertLeaveNotification alice conv alice aliceClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv bob bobClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv charlie charlieClient) [charlie, dee] + + bindResponse (getConversation alice conv) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject bob + +testAccessUpdateWithRemotes :: HasCallStack => App () +testAccessUpdateWithRemotes = do + [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OwnDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) + >>= getJSON 201 + let update_access_value = ["code"] + update_access_role_value = ["team_member", "non_team_member", "guest", "service"] + update = ["access" .= update_access_value, "access_role" .= update_access_role_value] + withWebSockets [alice, bob, charlie] $ \wss -> do + void $ updateAccess alice conv update >>= getJSON 200 + for_ wss $ \ws -> do + notif <- awaitMatch 10 isConvAccessUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + notif %. "payload.0.data.access" `shouldMatch` update_access_value + notif %. "payload.0.data.access_role_v2" `shouldMatch` update_access_role_value diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index e918aa05ab8..afd2e07aaad 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -1,20 +1,41 @@ {-# OPTIONS_GHC -Wno-ambiguous-fields #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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.Conversation where -import API.Brig (getConnections, postConnection) -import API.BrigInternal as Internal +import API.Brig +import API.BrigInternal import API.Galley import API.GalleyInternal import Control.Applicative import Control.Concurrent (threadDelay) +import Control.Monad.Codensity +import Control.Monad.Reader import Data.Aeson qualified as Aeson import Data.Text qualified as T import GHC.Stack -import SetupHelpers +import Notifications +import SetupHelpers hiding (deleteUser) import Testlib.One2One (generateRemoteAndConvIdWithDomain) import Testlib.Prelude +import Testlib.ResourcePool testDynamicBackendsFullyConnectedWhenAllowAll :: HasCallStack => App () testDynamicBackendsFullyConnectedWhenAllowAll = do @@ -184,7 +205,7 @@ testAddMembersFullyConnectedProteus = do cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 -- add members from remote backends members <- for [u2, u3] (%. "qualified_id") - bindResponse (addMembers u1 cid members) $ \resp -> do + bindResponse (addMembers u1 cid def {users = members}) $ \resp -> do resp.status `shouldMatchInt` 200 users <- resp.json %. "data.users" >>= asList addedUsers <- forM users (%. "qualified_id") @@ -210,10 +231,66 @@ testAddMembersNonFullyConnectedProteus = do cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 -- add members from remote backends members <- for [u2, u3] (%. "qualified_id") - bindResponse (addMembers u1 cid members) $ \resp -> do + bindResponse (addMembers u1 cid def {users = members}) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "non_federating_backends" `shouldMatchSet` [domainB, domainC] +testAddMember :: HasCallStack => App () +testAddMember = do + alice <- randomUser OwnDomain def + aliceId <- alice %. "qualified_id" + -- create conversation with no users + cid <- postConversation alice defProteus >>= getJSON 201 + bob <- randomUser OwnDomain def + bobId <- bob %. "qualified_id" + let addMember = addMembers alice cid def {role = Just "wire_member", users = [bobId]} + bindResponse addMember $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "not-connected" + connectUsers alice bob + bindResponse addMember $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "type" `shouldMatch` "conversation.member-join" + resp.json %. "qualified_from" `shouldMatch` objQidObject alice + resp.json %. "qualified_conversation" `shouldMatch` objQidObject cid + users <- resp.json %. "data.users" >>= asList + addedUsers <- forM users (%. "qualified_id") + addedUsers `shouldMatchSet` [bobId] + + -- check that both users can see the conversation + bindResponse (getConversation alice cid) $ \resp -> do + resp.status `shouldMatchInt` 200 + mems <- resp.json %. "members.others" & asList + mem <- assertOne mems + mem %. "qualified_id" `shouldMatch` bobId + mem %. "conversation_role" `shouldMatch` "wire_member" + + bindResponse (getConversation bob cid) $ \resp -> do + resp.status `shouldMatchInt` 200 + mems <- resp.json %. "members.others" & asList + mem <- assertOne mems + mem %. "qualified_id" `shouldMatch` aliceId + mem %. "conversation_role" `shouldMatch` "wire_admin" + +testAddMemberV1 :: HasCallStack => Domain -> App () +testAddMemberV1 domain = do + [alice, bob] <- createAndConnectUsers [OwnDomain, domain] + conv <- postConversation alice defProteus >>= getJSON 201 + bobId <- bob %. "qualified_id" + let opts = + def + { version = Just 1, + role = Just "wire_member", + users = [bobId] + } + bindResponse (addMembers alice conv opts) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "type" `shouldMatch` "conversation.member-join" + resp.json %. "qualified_from" `shouldMatch` objQidObject alice + resp.json %. "qualified_conversation" `shouldMatch` objQidObject conv + users <- resp.json %. "data.users" >>= asList + traverse (%. "qualified_id") users `shouldMatchSet` [bobId] + testConvWithUnreachableRemoteUsers :: HasCallStack => App () testConvWithUnreachableRemoteUsers = do let overrides = @@ -250,7 +327,7 @@ testAddReachableWithUnreachableRemoteUsers = do pure ([alex, bob], conv, domains) bobId <- bob %. "qualified_id" - bindResponse (addMembers alex conv [bobId]) $ \resp -> do + bindResponse (addMembers alex conv def {users = [bobId]}) $ \resp -> do -- This test is updated to reflect the changes in `performConversationJoin` -- `performConversationJoin` now does a full check between all federation members -- that will be in the conversation when adding users to a conversation. This is @@ -274,7 +351,7 @@ testAddUnreachable = do pure ([alex, charlie], domains, conv) charlieId <- charlie %. "qualified_id" - bindResponse (addMembers alex conv [charlieId]) $ \resp -> do + bindResponse (addMembers alex conv def {users = [charlieId]}) $ \resp -> do resp.status `shouldMatchInt` 533 -- All of the domains that are in the conversation, or will be in the conversation, -- need to be reachable so we can check that the graph for those domains is fully connected. @@ -307,7 +384,7 @@ testAddingUserNonFullyConnectedFederation = do bobId <- bob %. "qualified_id" charlieId <- charlie %. "qualified_id" - bindResponse (addMembers alice conv [bobId, charlieId]) $ \resp -> do + bindResponse (addMembers alice conv def {users = [bobId, charlieId]}) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "non_federating_backends" `shouldMatchSet` [other, dynBackend] @@ -334,7 +411,7 @@ testMultiIngressGuestLinks = do do configuredURI <- readServiceConfig Galley & (%. "settings.conversationCodeURI") & asText - (user, _) <- createTeam OwnDomain + (user, _, _) <- createTeam OwnDomain 1 conv <- postConversation user (allowGuests defProteus) >>= getJSON 201 bindResponse (postConversationCode user conv Nothing Nothing) $ \resp -> do @@ -368,7 +445,7 @@ testMultiIngressGuestLinks = do } ) $ \domain -> do - (user, _) <- createTeam domain + (user, _, _) <- createTeam domain 1 conv <- postConversation user (allowGuests defProteus) >>= getJSON 201 bindResponse (postConversationCode user conv Nothing (Just "red.example.com")) $ \resp -> do @@ -408,5 +485,264 @@ testAddUserWhenOtherBackendOffline = do let newConv = defProteus {qualifiedUsers = [charlie]} conv <- postConversation alice newConv >>= getJSON 201 pure ([alice, alex], conv) - bindResponse (addMembers alice conv [alex]) $ \resp -> do + bindResponse (addMembers alice conv def {users = [alex]}) $ \resp -> do resp.status `shouldMatchInt` 200 + +testSynchroniseUserRemovalNotification :: HasCallStack => App () +testSynchroniseUserRemovalNotification = do + resourcePool <- asks resourcePool + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> do + (conv, charlie, client) <- + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + charlie <- randomUser dynBackend.berDomain def + client <- objId $ bindResponse (addClient charlie def) $ getJSON 201 + mapM_ (connectUsers charlie) [alice, bob] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) + >>= getJSON 201 + pure (conv, charlie, client) + + let newConvName = "The new conversation name" + bindResponse (changeConversationName alice conv newConvName) $ \resp -> + resp.status `shouldMatchInt` 200 + bindResponse (removeMember alice conv charlie) $ \resp -> + resp.status `shouldMatchInt` 200 + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + nameNotif <- awaitNotification charlie client noValue 2 isConvNameChangeNotif + nameNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + nameNotif %. "payload.0.data.name" `shouldMatch` newConvName + leaveNotif <- awaitNotification charlie client noValue 2 isConvLeaveNotif + leaveNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + +testConvRenaming :: HasCallStack => App () +testConvRenaming = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob]}) + >>= getJSON 201 + let newConvName = "The new conversation name" + withWebSockets [alice, bob] $ \wss -> do + for_ wss $ \ws -> do + void $ changeConversationName alice conv newConvName >>= getBody 200 + nameNotif <- awaitMatch 10 isConvNameChangeNotif ws + nameNotif %. "payload.0.data.name" `shouldMatch` newConvName + nameNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + +testReceiptModeWithRemotesOk :: HasCallStack => App () +testReceiptModeWithRemotesOk = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob]}) + >>= getJSON 201 + withWebSockets [alice, bob] $ \wss -> do + void $ updateReceiptMode alice conv (43 :: Int) >>= getBody 200 + for_ wss $ \ws -> do + notif <- awaitMatch 10 isReceiptModeUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + notif %. "payload.0.data.receipt_mode" `shouldMatchInt` 43 + +testReceiptModeWithRemotesUnreachable :: HasCallStack => App () +testReceiptModeWithRemotesUnreachable = do + ownDomain <- asString OwnDomain + alice <- randomUser ownDomain def + conv <- startDynamicBackends [mempty] $ \[dynBackend] -> do + bob <- randomUser dynBackend def + connectUsers alice bob + postConversation alice (defProteus {qualifiedUsers = [bob]}) + >>= getJSON 201 + withWebSocket alice $ \ws -> do + void $ updateReceiptMode alice conv (43 :: Int) >>= getBody 200 + notif <- awaitMatch 10 isReceiptModeUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + notif %. "payload.0.data.receipt_mode" `shouldMatchInt` 43 + +testDeleteLocalMember :: HasCallStack => App () +testDeleteLocalMember = do + [alice, alex, bob] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [alex, bob]}) + >>= getJSON 201 + bindResponse (removeMember alice conv alex) $ \resp -> do + r <- getJSON 200 resp + r %. "type" `shouldMatch` "conversation.member-leave" + r %. "qualified_conversation" `shouldMatch` objQidObject conv + r %. "qualified_from" `shouldMatch` objQidObject alice + r %. "data.qualified_user_ids.0" `shouldMatch` objQidObject alex + -- Now that Alex is gone, try removing her once again + bindResponse (removeMember alice conv alex) $ \r -> do + r.status `shouldMatchInt` 204 + r.jsonBody `shouldMatch` (Nothing @Aeson.Value) + +testDeleteRemoteMember :: HasCallStack => App () +testDeleteRemoteMember = do + [alice, alex, bob] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [alex, bob]}) + >>= getJSON 201 + bindResponse (removeMember alice conv bob) $ \resp -> do + r <- getJSON 200 resp + r %. "type" `shouldMatch` "conversation.member-leave" + r %. "qualified_conversation" `shouldMatch` objQidObject conv + r %. "qualified_from" `shouldMatch` objQidObject alice + r %. "data.qualified_user_ids.0" `shouldMatch` objQidObject bob + -- Now that Bob is gone, try removing him once again + bindResponse (removeMember alice conv bob) $ \r -> do + r.status `shouldMatchInt` 204 + r.jsonBody `shouldMatch` (Nothing @Aeson.Value) + +testDeleteRemoteMemberRemoteUnreachable :: HasCallStack => App () +testDeleteRemoteMemberRemoteUnreachable = do + [alice, bob, bart] <- createAndConnectUsers [OwnDomain, OtherDomain, OtherDomain] + conv <- startDynamicBackends [mempty] $ \[dynBackend] -> do + charlie <- randomUser dynBackend def + connectUsers alice charlie + postConversation + alice + (defProteus {qualifiedUsers = [bob, bart, charlie]}) + >>= getJSON 201 + void $ withWebSockets [alice, bob] $ \wss -> do + void $ removeMember alice conv bob >>= getBody 200 + for wss $ \ws -> do + leaveNotif <- awaitMatch 10 isConvLeaveNotif ws + leaveNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + leaveNotif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + leaveNotif %. "payload.0.data.qualified_user_ids.0" `shouldMatch` objQidObject bob + -- Now that Bob is gone, try removing him once again + bindResponse (removeMember alice conv bob) $ \r -> do + r.status `shouldMatchInt` 204 + r.jsonBody `shouldMatch` (Nothing @Aeson.Value) + +testDeleteTeamConversationWithRemoteMembers :: HasCallStack => App () +testDeleteTeamConversationWithRemoteMembers = do + (alice, team, _) <- createTeam OwnDomain 1 + conv <- postConversation alice (defProteus {team = Just team}) >>= getJSON 201 + bob <- randomUser OtherDomain def + connectUsers alice bob + mem <- bob %. "qualified_id" + void $ addMembers alice conv def {users = [mem]} >>= getBody 200 + + void $ withWebSockets [alice, bob] $ \wss -> do + void $ deleteTeamConversation team conv alice >>= getBody 200 + for wss $ \ws -> do + notif <- awaitMatch 10 isConvDeleteNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + +testDeleteTeamConversationWithUnreachableRemoteMembers :: HasCallStack => App () +testDeleteTeamConversationWithUnreachableRemoteMembers = do + resourcePool <- asks resourcePool + (alice, team, _) <- createTeam OwnDomain 1 + conv <- postConversation alice (defProteus {team = Just team}) >>= getJSON 201 + + let assertNotification :: (HasCallStack, MakesValue n) => n -> App () + assertNotification notif = do + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + + runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> do + (bob, bobClient) <- runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + -- FUTUREWORK: get rid of this once the background worker is able to listen to all queues + do + ownDomain <- make OwnDomain & asString + otherDomain <- make OtherDomain & asString + let domains = [ownDomain, otherDomain, dynBackend.berDomain] + sequence_ + [ createFedConn x (FedConn y "full_search") + | x <- domains, + y <- domains, + x /= y + ] + + bob <- randomUser dynBackend.berDomain def + bobClient <- objId $ bindResponse (addClient bob def) $ getJSON 201 + connectUsers alice bob + mem <- bob %. "qualified_id" + void $ addMembers alice conv def {users = [mem]} >>= getBody 200 + pure (bob, bobClient) + withWebSocket alice $ \ws -> do + void $ deleteTeamConversation team conv alice >>= getBody 200 + notif <- awaitMatch 10 isConvDeleteNotif ws + assertNotification notif + void $ runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + notif <- awaitNotification bob bobClient noValue 2 isConvDeleteNotif + assertNotification notif + +testLeaveConversationSuccess :: HasCallStack => App () +testLeaveConversationSuccess = do + [alice, bob, chad, dee] <- + createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain, OtherDomain] + [aClient, bClient] <- forM [alice, bob] $ \user -> + objId $ bindResponse (addClient user def) $ getJSON 201 + let overrides = + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} + startDynamicBackends [overrides] $ \[dynDomain] -> do + eve <- randomUser dynDomain def + eClient <- objId $ bindResponse (addClient eve def) $ getJSON 201 + connectUsers alice eve + conv <- + postConversation + alice + ( defProteus + { qualifiedUsers = [bob, chad, dee, eve] + } + ) + >>= getJSON 201 + void $ removeMember chad conv chad >>= getBody 200 + assertLeaveNotification chad conv alice aClient chad + assertLeaveNotification chad conv bob bClient chad + assertLeaveNotification chad conv eve eClient chad + +testOnUserDeletedConversations :: HasCallStack => App () +testOnUserDeletedConversations = do + let overrides = + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} + startDynamicBackends [overrides] $ \[dynDomain] -> do + [ownDomain, otherDomain] <- forM [OwnDomain, OtherDomain] asString + [alice, alex, bob, bart, chad] <- + createAndConnectUsers [ownDomain, ownDomain, otherDomain, otherDomain, dynDomain] + bobId <- bob %. "qualified_id" + ooConvId <- do + l <- getAllConvs alice + let isWith users c = do + t <- (==) <$> (c %. "type" & asInt) <*> pure 2 + others <- c %. "members.others" & asList + qIds <- for others (%. "qualified_id") + pure $ qIds == users && t + c <- head <$> filterM (isWith [bobId]) l + c %. "qualified_id" + + mainConvBefore <- + postConversation alice (defProteus {qualifiedUsers = [alex, bob, bart, chad]}) + >>= getJSON 201 + + void $ withWebSocket alex $ \ws -> do + void $ deleteUser bob >>= getBody 200 + n <- awaitMatch 10 isConvLeaveNotif ws + n %. "payload.0.qualified_from" `shouldMatch` bobId + n %. "payload.0.qualified_conversation" `shouldMatch` (mainConvBefore %. "qualified_id") + + do + -- Bob is not in the one-to-one conversation with Alice any more + conv <- getConversation alice ooConvId >>= getJSON 200 + shouldBeEmpty $ conv %. "members.others" + do + -- Bob is not in the main conversation any more + mainConvAfter <- getConversation alice (mainConvBefore %. "qualified_id") >>= getJSON 200 + mems <- mainConvAfter %. "members.others" & asList + memIds <- for mems (%. "qualified_id") + expectedIds <- for [alex, bart, chad] (%. "qualified_id") + memIds `shouldMatchSet` expectedIds + +testUpdateConversationByRemoteAdmin :: HasCallStack => App () +testUpdateConversationByRemoteAdmin = do + [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) + >>= getJSON 201 + void $ updateRole alice bob "wire_admin" (conv %. "qualified_id") >>= getBody 200 + void $ withWebSockets [alice, bob, charlie] $ \wss -> do + void $ updateReceiptMode bob conv (41 :: Int) >>= getBody 200 + for_ wss $ \ws -> awaitMatch 10 isReceiptModeUpdateNotif ws diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 3709ea47999..547c6598220 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -43,7 +43,7 @@ testModifiedBrig = do testModifiedGalley :: HasCallStack => App () testModifiedGalley = do - (_user, tid) <- createTeam OwnDomain + (_user, tid, _) <- createTeam OwnDomain 1 let getFeatureStatus :: (MakesValue domain) => domain -> String -> App Value getFeatureStatus domain team = do @@ -56,7 +56,7 @@ testModifiedGalley = do withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default"} $ \domain -> do - (_user, tid') <- createTeam domain + (_user, tid', _) <- createTeam domain 1 getFeatureStatus domain tid' `shouldMatch` "enabled" testModifiedCannon :: HasCallStack => App () @@ -84,7 +84,7 @@ testModifiedServices = do } withModifiedBackend serviceMap $ \domain -> do - (_user, tid) <- createTeam domain + (_user, tid, _) <- createTeam domain 1 bindResponse (Internal.getTeamFeature domain "searchVisibility" tid) $ \res -> do res.status `shouldMatchInt` 200 res.json %. "status" `shouldMatch` "enabled" diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs index 5a70f3ad1f0..97ec011f028 100644 --- a/integration/test/Test/Federation.hs +++ b/integration/test/Test/Federation.hs @@ -4,6 +4,7 @@ module Test.Federation where import API.Brig qualified as API +import API.BrigInternal qualified as API import API.Galley import Control.Lens import Control.Monad.Codensity @@ -27,10 +28,22 @@ testNotificationsForOfflineBackends = do otherClient <- objId $ bindResponse (API.addClient otherUser def) $ getJSON 201 otherClient2 <- objId $ bindResponse (API.addClient otherUser2 def) $ getJSON 201 - -- We call it 'downBackend' because it is down for the most of this test + -- We call it 'downBackend' because it is down for most of this test -- except for setup and assertions. Perhaps there is a better name. runCodensity (acquireResources 1 resourcePool) $ \[downBackend] -> do (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) <- runCodensity (startDynamicBackend downBackend mempty) $ \_ -> do + -- FUTUREWORK: get rid of this once the background worker is able to listen to all queues + do + ownDomain <- make OwnDomain & asString + otherDomain <- make OtherDomain & asString + let domains = [ownDomain, otherDomain, downBackend.berDomain] + sequence_ + [ API.createFedConn x (API.FedConn y "full_search") + | x <- domains, + y <- domains, + x /= y + ] + downUser1 <- randomUser downBackend.berDomain def downUser2 <- randomUser downBackend.berDomain def downClient1 <- objId $ bindResponse (API.addClient downUser1 def) $ getJSON 201 @@ -41,69 +54,74 @@ testNotificationsForOfflineBackends = do downBackendConv <- bindResponse (postConversation downUser1 (defProteus {qualifiedUsers = [otherUser, delUser]})) $ getJSON 201 pure (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) - -- Even when a participating backend is down, messages to conversations - -- owned by other backends should go. - successfulMsgForOtherUsers <- mkProteusRecipients otherUser [(otherUser, [otherClient]), (otherUser2, [otherClient2])] "success message for other user" - successfulMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "success message for down user" - let successfulMsg = - Proto.defMessage @Proto.QualifiedNewOtrMessage - & #sender . Proto.client .~ (delClient ^?! hex) - & #recipients .~ [successfulMsgForOtherUsers, successfulMsgForDownUser] - & #reportAll .~ Proto.defMessage - bindResponse (postProteusMessage delUser upBackendConv successfulMsg) assertSuccess - - -- When conversation owning backend is down, messages will fail to be sent. - failedMsgForOtherUser <- mkProteusRecipient otherUser otherClient "failed message for other user" - failedMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "failed message for down user" - let failedMsg = - Proto.defMessage @Proto.QualifiedNewOtrMessage - & #sender . Proto.client .~ (delClient ^?! hex) - & #recipients .~ [failedMsgForOtherUser, failedMsgForDownUser] - & #reportAll .~ Proto.defMessage - bindResponse (postProteusMessage delUser downBackendConv failedMsg) $ \resp -> - -- Due to the way federation breaks in local env vs K8s, it can return 521 - -- (local) or 533 (K8s). - resp.status `shouldMatchOneOf` [Number 521, Number 533] - - -- Conversation creation with people from down backend should fail - bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, downUser1]})) $ \resp -> - resp.status `shouldMatchInt` 533 - - -- Adding users to an up backend conversation should not work when one of - -- the participating backends is down. This is due to not being able to - -- check non-fully connected graph between all participating backends - -- however, if the backend of the user to be added is already part of the conversation, we do not need to do the check - -- and the user can be added as long as the backend is reachable - otherUser3 <- randomUser OtherDomain def - connectUsers delUser otherUser3 - bindResponse (addMembers delUser upBackendConv [otherUser3]) $ \resp -> - resp.status `shouldMatchInt` 200 - - -- Adding users from down backend to a conversation should also fail - bindResponse (addMembers delUser upBackendConv [downUser2]) $ \resp -> - resp.status `shouldMatchInt` 533 - - -- Removing users from an up backend conversation should work even when one - -- of the participating backends is down. - bindResponse (removeMember delUser upBackendConv otherUser2) $ \resp -> - resp.status `shouldMatchInt` 200 - - -- User deletions should eventually make it to the other backend. - deleteUser delUser - - let isOtherUser2LeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser otherUser2] - isDelUserLeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser delUser] - - do - newMsgNotif <- awaitNotification otherUser otherClient noValue 1 isNewMessageNotif - newMsgNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv - newMsgNotif %. "payload.0.data.text" `shouldMatchBase64` "success message for other user" - - void $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isOtherUser2LeaveUpConvNotif - void $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDelUserLeaveUpConvNotif - - delUserDeletedNotif <- nPayload $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDeleteUserNotif - objQid delUserDeletedNotif `shouldMatch` objQid delUser + withWebSocket otherUser $ \ws -> do + -- Even when a participating backend is down, messages to conversations + -- owned by other backends should go. + successfulMsgForOtherUsers <- mkProteusRecipients otherUser [(otherUser, [otherClient]), (otherUser2, [otherClient2])] "success message for other user" + successfulMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "success message for down user" + let successfulMsg = + Proto.defMessage @Proto.QualifiedNewOtrMessage + & #sender . Proto.client .~ (delClient ^?! hex) + & #recipients .~ [successfulMsgForOtherUsers, successfulMsgForDownUser] + & #reportAll .~ Proto.defMessage + bindResponse (postProteusMessage delUser upBackendConv successfulMsg) assertSuccess + + -- When the conversation owning backend is down, messages will fail to be sent. + failedMsgForOtherUser <- mkProteusRecipient otherUser otherClient "failed message for other user" + failedMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "failed message for down user" + let failedMsg = + Proto.defMessage @Proto.QualifiedNewOtrMessage + & #sender . Proto.client .~ (delClient ^?! hex) + & #recipients .~ [failedMsgForOtherUser, failedMsgForDownUser] + & #reportAll .~ Proto.defMessage + bindResponse (postProteusMessage delUser downBackendConv failedMsg) $ \resp -> + -- Due to the way federation breaks in local env vs K8s, it can return 521 + -- (local) or 533 (K8s). + resp.status `shouldMatchOneOf` [Number 521, Number 533] + + -- Conversation creation with people from down backend should fail + bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, downUser1]})) $ \resp -> + resp.status `shouldMatchInt` 533 + + -- Adding users to an up backend conversation should not work when one of + -- the participating backends is down. This is due to not being able to + -- check non-fully connected graph between all participating backends + -- however, if the backend of the user to be added is already part of the conversation, we do not need to do the check + -- and the user can be added as long as the backend is reachable + otherUser3 <- randomUser OtherDomain def + connectUsers delUser otherUser3 + bindResponse (addMembers delUser upBackendConv def {users = [otherUser3]}) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- Adding users from down backend to a conversation should fail + bindResponse (addMembers delUser upBackendConv def {users = [downUser2]}) $ \resp -> + resp.status `shouldMatchInt` 533 + + -- Removing users from an up backend conversation should work even when one + -- of the participating backends is down. + bindResponse (removeMember delUser upBackendConv otherUser2) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- Even removing a user from the down backend itself should work. + bindResponse (removeMember delUser upBackendConv delUser) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- User deletions should eventually make it to the other backend. + deleteUser delUser + + let isOtherUser2LeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser otherUser2] + isDelUserLeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser delUser] + + do + newMsgNotif <- awaitMatch 10 isNewMessageNotif ws + newMsgNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv + newMsgNotif %. "payload.0.data.text" `shouldMatchBase64` "success message for other user" + + void $ awaitMatch 10 isOtherUser2LeaveUpConvNotif ws + void $ awaitMatch 10 isDelUserLeaveUpConvNotif ws + + delUserDeletedNotif <- nPayload $ awaitMatch 10 isDeleteUserNotif ws + objQid delUserDeletedNotif `shouldMatch` objQid delUser runCodensity (startDynamicBackend downBackend mempty) $ \_ -> do newMsgNotif <- awaitNotification downUser1 downClient1 noValue 5 isNewMessageNotif @@ -124,8 +142,3 @@ testNotificationsForOfflineBackends = do delUserDeletedNotif <- nPayload $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isDeleteUserNotif objQid delUserDeletedNotif `shouldMatch` objQid delUser - -allPreds :: (Applicative f) => [a -> f Bool] -> a -> f Bool -allPreds [] _ = pure True -allPreds [p] x = p x -allPreds (p1 : ps) x = (&&) <$> p1 x <*> allPreds ps x diff --git a/integration/test/Test/MessageTimer.hs b/integration/test/Test/MessageTimer.hs new file mode 100644 index 00000000000..6a401e3d68e --- /dev/null +++ b/integration/test/Test/MessageTimer.hs @@ -0,0 +1,55 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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.MessageTimer where + +import API.Galley +import Control.Monad.Codensity +import Control.Monad.Reader +import GHC.Stack +import Notifications +import SetupHelpers +import Testlib.Prelude +import Testlib.ResourcePool + +testMessageTimerChangeWithRemotes :: HasCallStack => App () +testMessageTimerChangeWithRemotes = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + conv <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 + withWebSockets [alice, bob] $ \wss -> do + void $ updateMessageTimer alice conv 1000 >>= getBody 200 + for_ wss $ \ws -> do + notif <- awaitMatch 10 isConvMsgTimerUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + +testMessageTimerChangeWithUnreachableRemotes :: HasCallStack => App () +testMessageTimerChangeWithUnreachableRemotes = do + resourcePool <- asks resourcePool + alice <- randomUser OwnDomain def + conv <- runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + bob <- randomUser dynBackend.berDomain def + connectUsers alice bob + postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 + withWebSocket alice $ \ws -> do + void $ updateMessageTimer alice conv 1000 >>= getBody 200 + notif <- awaitMatch 10 isConvMsgTimerUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice diff --git a/integration/test/Test/Roles.hs b/integration/test/Test/Roles.hs new file mode 100644 index 00000000000..906d9d9632c --- /dev/null +++ b/integration/test/Test/Roles.hs @@ -0,0 +1,65 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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.Roles where + +import API.Galley +import Control.Monad.Reader +import GHC.Stack +import Notifications +import SetupHelpers +import Testlib.Prelude + +testRoleUpdateWithRemotesOk :: HasCallStack => App () +testRoleUpdateWithRemotesOk = do + [bob, charlie, alice] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + conv <- + postConversation bob (defProteus {qualifiedUsers = [charlie, alice]}) + >>= getJSON 201 + adminRole <- make "wire_admin" + + withWebSockets [bob, charlie, alice] $ \wss -> do + void $ updateRole bob charlie adminRole conv >>= getBody 200 + bindResponse (getConversation bob conv) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject charlie + resp.json %. "members.others.0.conversation_role" `shouldMatch` "wire_admin" + for_ wss $ \ws -> do + notif <- awaitMatch 10 isMemberUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject bob + +testRoleUpdateWithRemotesUnreachable :: HasCallStack => App () +testRoleUpdateWithRemotesUnreachable = do + [bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain] + startDynamicBackends [mempty] $ \[dynBackend] -> do + alice <- randomUser dynBackend def + mapM_ (connectUsers alice) [bob, charlie] + conv <- + postConversation bob (defProteus {qualifiedUsers = [charlie, alice]}) + >>= getJSON 201 + adminRole <- make "wire_admin" + + withWebSockets [bob, charlie] $ \wss -> do + void $ updateRole bob charlie adminRole conv >>= getBody 200 + + for_ wss $ \ws -> do + notif <- awaitMatch 10 isMemberUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject bob diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index 299c1ae20e8..8be47a687b1 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -4,6 +4,7 @@ module Testlib.Assertions where import Control.Exception as E import Control.Monad.Reader +import Data.Aeson (Value) import Data.Aeson qualified as Aeson import Data.Aeson.Encode.Pretty qualified as Aeson import Data.ByteString.Base64 qualified as B64 @@ -127,6 +128,9 @@ shouldMatchSet a b = do lb <- fmap sort (asList b) la `shouldMatch` lb +shouldBeEmpty :: (MakesValue a, HasCallStack) => a -> App () +shouldBeEmpty a = a `shouldMatch` (mempty :: [Value]) + shouldMatchOneOf :: (MakesValue a, MakesValue b, HasCallStack) => a -> diff --git a/integration/test/Testlib/Prelude.hs b/integration/test/Testlib/Prelude.hs index 05a04f366a3..27c9db153cd 100644 --- a/integration/test/Testlib/Prelude.hs +++ b/integration/test/Testlib/Prelude.hs @@ -66,6 +66,9 @@ module Testlib.Prelude -- * Functor (<$$>), (<$$$>), + + -- * Applicative + allPreds, ) where @@ -222,3 +225,11 @@ infix 4 <$$> (<$$$>) = fmap . fmap . fmap infix 4 <$$$> + +---------------------------------------------------------------------- +-- Applicative + +allPreds :: (Applicative f) => [a -> f Bool] -> a -> f Bool +allPreds [] _ = pure True +allPreds [p] x = p x +allPreds (p1 : ps) x = (&&) <$> p1 x <*> allPreds ps x diff --git a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs index 3560e2c5e44..3fa1aba2871 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs @@ -77,7 +77,7 @@ sendNotification env component path body = runFederatorClient env . void $ clientIn (Proxy @BackendNotificationAPI) (Proxy @(FederatorClient c)) (withoutFirstSlash path) body -enqueue :: Q.Channel -> Domain -> Domain -> Q.DeliveryMode -> FedQueueClient c () -> IO () +enqueue :: Q.Channel -> Domain -> Domain -> Q.DeliveryMode -> FedQueueClient c a -> IO a enqueue channel originDomain targetDomain deliveryMode (FedQueueClient action) = runReaderT action FedQueueEnv {..} diff --git a/libs/wire-api/src/Wire/API/Conversation/Action.hs b/libs/wire-api/src/Wire/API/Conversation/Action.hs index 8d8b9883cc8..30156061710 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action.hs @@ -69,7 +69,11 @@ data SomeConversationAction where instance Show SomeConversationAction where show (SomeConversationAction tag action) = - $(sCases ''ConversationActionTag [|tag|] [|show action|]) + "SomeConversationAction {tag = " + <> show (fromSing tag) + <> ", action = " + <> $(sCases ''ConversationActionTag [|tag|] [|show action|]) + <> "}" instance Eq SomeConversationAction where (SomeConversationAction tag1 action1) == (SomeConversationAction tag2 action2) = diff --git a/services/brig/src/Brig/Federation/Client.hs b/services/brig/src/Brig/Federation/Client.hs index e824b4f53e9..66792c83030 100644 --- a/services/brig/src/Brig/Federation/Client.hs +++ b/services/brig/src/Brig/Federation/Client.hs @@ -154,7 +154,9 @@ notifyUserDeleted self remotes = do remoteDomain = tDomain remotes view rabbitmqChannel >>= \case Just chanVar -> do - enqueueNotification (tDomain self) remoteDomain Q.Persistent chanVar $ void $ fedQueueClient @'Brig @"on-user-deleted-connections" notif + enqueueNotification (tDomain self) remoteDomain Q.Persistent chanVar $ + void $ + fedQueueClient @'Brig @"on-user-deleted-connections" notif Nothing -> Log.err $ Log.msg ("Federation error while notifying remote backends of a user deletion." :: ByteString) @@ -163,7 +165,7 @@ notifyUserDeleted self remotes = do . Log.field "error" (show FederationNotConfigured) -- | Enqueues notifications in RabbitMQ. Retries 3 times with a delay of 1s. -enqueueNotification :: (MonadReader Env m, MonadIO m, MonadMask m, Log.MonadLogger m) => Domain -> Domain -> Q.DeliveryMode -> MVar Q.Channel -> FedQueueClient c () -> m () +enqueueNotification :: (MonadIO m, MonadMask m, Log.MonadLogger m) => Domain -> Domain -> Q.DeliveryMode -> MVar Q.Channel -> FedQueueClient c () -> m () enqueueNotification ownDomain remoteDomain deliveryMode chanVar action = do let policy = limitRetries 3 <> constantDelay 1_000_000 recovering policy [logRetries (const $ pure True) logError] (const go) diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index 25d95fd6bae..cad9c16082d 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -31,7 +31,6 @@ import Data.Domain import Data.Handle import Data.Id import Data.Json.Util (toBase64Text) -import Data.List.NonEmpty (NonEmpty ((:|))) import Data.List1 as List1 import Data.Map qualified as Map import Data.ProtoLens qualified as Protolens @@ -49,7 +48,6 @@ import Util import Util.Options (Endpoint) import Wire.API.Asset import Wire.API.Conversation -import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Event.Conversation @@ -99,7 +97,6 @@ spec _brigOpts mg brig galley cargohold cannon _federator brigTwo galleyTwo carg test mg "claim multi-prekey bundle" $ testClaimMultiPrekeyBundleSuccess brig brigTwo, test mg "list user clients" $ testListUserClients brig brigTwo, test mg "list own conversations" $ testListConversations brig brigTwo galley galleyTwo, - test mg "add remote users to local conversation" $ testAddRemoteUsersToLocalConv brig galley brigTwo galleyTwo, test mg "remove remote user from a local conversation" $ testRemoveRemoteUserFromLocalConv brig galley brigTwo galleyTwo, test mg "leave a remote conversation" $ leaveRemoteConversation brig galley brigTwo galleyTwo, test mg "include remote users to new conversation" $ testRemoteUsersInNewConv brig galley brigTwo galleyTwo, @@ -249,63 +246,6 @@ testClaimMultiPrekeyBundleSuccess brig1 brig2 = do const 200 === statusCode const (Just ucm) === responseJsonMaybe -testAddRemoteUsersToLocalConv :: Brig -> Galley -> Brig -> Galley -> Http () -testAddRemoteUsersToLocalConv brig1 galley1 brig2 galley2 = do - alice <- randomUser brig1 - bob <- randomUser brig2 - - let newConv = - NewConv - [] - [] - (checked "gossip") - mempty - Nothing - Nothing - Nothing - Nothing - roleNameWireAdmin - ProtocolProteusTag - convId <- - fmap cnvQualifiedId . responseJsonError - =<< post - ( galley1 - . path "/conversations" - . zUser (userId alice) - . zConn "conn" - . header "Z-Type" "access" - . json newConv - ) - - connectUsersEnd2End brig1 brig2 (userQualifiedId alice) (userQualifiedId bob) - - let invite = InviteQualified (userQualifiedId bob :| []) roleNameWireAdmin - post - ( apiVersion "v1" - . galley1 - . paths ["conversations", (toByteString' . qUnqualified) convId, "members", "v2"] - . zUser (userId alice) - . zConn "conn" - . header "Z-Type" "access" - . json invite - ) - !!! (const 200 === statusCode) - - -- test GET /conversations/:domain/:cnv -- Alice's domain is used here - liftIO $ putStrLn "search for conversation on backend 1..." - res <- getConvQualified galley1 (userId alice) convId Galley -> Brig -> Galley -> Http () testRemoveRemoteUserFromLocalConv brig1 galley1 brig2 galley2 = do alice <- randomUser brig1 diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 710309b8093..2efa6ca4d75 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -71,6 +71,7 @@ import Galley.Data.Conversation qualified as Data import Galley.Data.Scope (Scope (ReusableCode)) import Galley.Data.Services import Galley.Effects +import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.BotAccess qualified as E import Galley.Effects.BrigAccess qualified as E import Galley.Effects.CodeStore qualified as E @@ -88,6 +89,7 @@ import Galley.Types.Conversations.Members import Galley.Types.UserList import Galley.Validation import Imports hiding ((\\)) +import Network.AMQP qualified as Q import Polysemy import Polysemy.Error import Polysemy.Input @@ -349,6 +351,7 @@ ensureAllowed tag loc action conv origUser = do performAction :: forall tag r. ( HasConversationActionEffects tag r, + Member BackendNotificationQueueAccess r, Member (Error FederationError) r ) => Sing tag -> @@ -419,7 +422,8 @@ performAction tag origUser lconv action = do performConversationJoin :: forall r. - ( HasConversationActionEffects 'ConversationJoinTag r + ( HasConversationActionEffects 'ConversationJoinTag r, + Member BackendNotificationQueueAccess r ) => Qualified UserId -> Local Conversation -> @@ -529,7 +533,8 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do performConversationAccessData :: ( HasConversationActionEffects 'ConversationAccessDataTag r, - Member (Error FederationError) r + Member (Error FederationError) r, + Member BackendNotificationQueueAccess r ) => Qualified UserId -> Local Conversation -> @@ -615,13 +620,13 @@ data LocalConversationUpdate = LocalConversationUpdate updateLocalConversation :: forall tag r. - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied (ConversationActionPermission tag))) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'ConvNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Log.Msg -> Log.Msg)) r, @@ -656,12 +661,12 @@ updateLocalConversation lcnv qusr con action = do updateLocalConversationUnchecked :: forall tag r. ( SingI tag, + Member BackendNotificationQueueAccess r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied (ConversationActionPermission tag))) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Log.Msg -> Log.Msg)) r, @@ -704,6 +709,7 @@ updateLocalConversationUserUnchecked :: forall tag r. ( SingI tag, HasConversationActionEffects tag r, + Member BackendNotificationQueueAccess r, Member (Error FederationError) r ) => Local Conversation -> @@ -762,7 +768,7 @@ addMembersToLocalConversation lcnv users role = do notifyConversationAction :: forall tag r. - ( Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, Member ExternalAccess r, Member GundeckAccess r, Member (Input UTCTime) r, @@ -790,24 +796,23 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do (SomeConversationAction tag action) update <- do + let remoteTargets = toList (bmRemotes targets) updates <- - E.runFederatedConcurrentlyEither (toList (bmRemotes targets)) $ - \ruids -> do - let update = mkUpdate (tUnqualified ruids) - -- if notifyOrigDomain is false, filter out user from quid's domain, - -- because quid's backend will update local state and notify its users - -- itself using the ConversationUpdate returned by this function - if notifyOrigDomain || tDomain ruids /= qDomain quid - then fedClient @'Galley @"on-conversation-updated" update $> Nothing - else pure (Just update) - let f = fromMaybe (mkUpdate []) . asum . map tUnqualified . rights - update = f updates - failedUpdates = lefts updates - for_ failedUpdates $ - logError - "on-conversation-updated" - "An error occurred while communicating with federated server: " - pure update + enqueueNotificationsConcurrently Q.Persistent remoteTargets $ \ruids -> do + let update = mkUpdate (tUnqualified ruids) + -- if notifyOrigDomain is false, filter out user from quid's domain, + -- because quid's backend will update local state and notify its users + -- itself using the ConversationUpdate returned by this function + if notifyOrigDomain || tDomain ruids /= qDomain quid + then fedQueueClient @'Galley @"on-conversation-updated" update $> Nothing + else pure (Just update) + case partitionEithers updates of + (ls :: [Remote ([UserId], FederationError)], rs) -> do + for_ ls $ + logError + "on-conversation-updated" + "An error occurred while communicating with federated server: " + pure $ fromMaybe (mkUpdate []) . asum . map tUnqualified $ rs -- notify local participants and bots pushConversationEvent con e (qualifyAs lcnv (bmLocals targets)) (bmBots targets) @@ -816,10 +821,12 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do -- to the originating domain (if it is remote) pure $ LocalConversationUpdate e update where - logError :: (Show a) => String -> String -> (a, FederationError) -> Sem r () + logError :: String -> String -> Remote (a, FederationError) -> Sem r () logError field msg e = P.warn $ - Log.field "federation call" field . Log.msg (msg <> show e) + Log.field "federation call" field + . Log.field "domain" (_domainText (tDomain e)) + . Log.msg (msg <> displayException (snd (tUnqualified e))) -- | Update the local database with information on conversation members joining -- or leaving. Finally, push out notifications to local users. @@ -938,7 +945,8 @@ addLocalUsersToRemoteConv remoteConvId qAdder localUsers = do -- leave, but then sends notifications as if the user was removed by someone -- else. kickMember :: - ( Member (Error FederationError) r, + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, Member (Error InternalError) r, Member ExternalAccess r, Member FederatorAccess r, diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 2642e428a78..18bb120620b 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -51,7 +51,6 @@ import Galley.API.Util import Galley.App import Galley.Data.Conversation qualified as Data import Galley.Effects -import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.ConversationStore qualified as E import Galley.Effects.FireAndForget qualified as E import Galley.Effects.MemberStore qualified as E @@ -219,7 +218,8 @@ onConversationUpdated requestingDomain cu = do -- as of now this will not generate the necessary events on the leaver's domain leaveConversation :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error InternalError) r, Member ExternalAccess r, Member FederatorAccess r, @@ -365,7 +365,8 @@ sendMessage originDomain msr = do throwErr = throw . InvalidPayload . LT.pack onUserDeleted :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member FederatorAccess r, Member FireAndForget r, Member ExternalAccess r, @@ -421,7 +422,8 @@ onUserDeleted origDomain udcn = do updateConversation :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member CodeStore r, Member BotAccess r, Member FireAndForget r, @@ -535,7 +537,8 @@ handleMLSMessageErrors = . mapToGalleyError @MLSBundleStaticErrors sendMLSCommitBundle :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member ExternalAccess r, Member (Error FederationError) r, @@ -568,7 +571,8 @@ sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do <$> postMLSCommitBundle loc (tUntagged sender) Nothing qcnv Nothing bundle sendMLSMessage :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member ExternalAccess r, Member (Error FederationError) r, diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index b3b3d6b34be..91a9ff45df8 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -139,7 +139,8 @@ getSettings lzusr tid = do removeSettingsInternalPaging :: forall r. - ( Member BotAccess r, + ( Member BackendNotificationQueueAccess r, + Member BotAccess r, Member BrigAccess r, Member CodeStore r, Member ConversationStore r, @@ -181,7 +182,8 @@ removeSettings :: forall p r. ( Paging p, Bounded (PagingBounds p TeamMember), - ( Member BotAccess r, + ( Member BackendNotificationQueueAccess r, + Member BotAccess r, Member BrigAccess r, Member CodeStore r, Member ConversationStore r, @@ -243,7 +245,8 @@ removeSettings' :: forall p r. ( Paging p, Bounded (PagingBounds p TeamMember), - ( Member BotAccess r, + ( Member BackendNotificationQueueAccess r, + Member BotAccess r, Member BrigAccess r, Member CodeStore r, Member ConversationStore r, @@ -335,7 +338,8 @@ getUserStatus _lzusr tid uid = do -- @withdrawExplicitConsentH@ (lots of corner cases we'd have to implement for that to pan -- out). grantConsent :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -372,7 +376,8 @@ grantConsent lusr tid = do -- | Request to provision a device on the legal hold service for a user requestDevice :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -450,7 +455,8 @@ requestDevice lzusr tid uid = do -- since they are replaced if needed when registering new LH devices. approveDevice :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, Member (Error FederationError) r, @@ -528,7 +534,8 @@ approveDevice lzusr connId tid uid (Public.ApproveLegalHoldForUserRequest mPassw disableForUser :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, Member (Error FederationError) r, @@ -585,7 +592,8 @@ disableForUser lzusr tid uid (Public.DisableLegalHoldForUserRequest mPassword) = -- or disabled, make sure the affected connections are screened for policy conflict (anybody -- with no-consent), and put those connections in the appropriate blocked state. changeLegalholdStatus :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -702,7 +710,8 @@ unsetTeamLegalholdWhitelistedH tid = do -- contains the hypothetical new LH status of `uid`'s so it can be consulted instead of the -- one from the database. handleGroupConvPolicyConflicts :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 643048311c0..781e8258f29 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -547,7 +547,8 @@ postMLSMessageToRemoteConv loc qusr _senderClient con smsg rcnv = do MLSMessageResponseNonFederatingBackends e -> throw e type HasProposalEffects r = - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error InternalError) r, Member (Error MLSProposalFailure) r, @@ -1094,7 +1095,8 @@ checkExternalProposalUser qusr prop = do executeProposalAction :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error InternalError) r, Member (ErrorS 'ConvNotFound) r, diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 921990a56fd..c9d800f59a4 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -1082,7 +1082,8 @@ getTeamConversation zusr tid cid = do >>= noteS @'ConvNotFound deleteTeamConversation :: - ( Member CodeStore r, + ( Member BackendNotificationQueueAccess r, + Member CodeStore r, Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS 'ConvNotFound) r, @@ -1090,7 +1091,6 @@ deleteTeamConversation :: Member (ErrorS 'NotATeamMember) r, Member (ErrorS ('ActionDenied 'DeleteConversation)) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member TeamStore r, diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 8b87f4850c0..4dd3dc85b14 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -284,6 +284,7 @@ instance SetFeatureConfig LegalholdConfig where type SetConfigForTeamConstraints LegalholdConfig (r :: EffectRow) = ( Bounded (PagingBounds InternalPaging TeamMember), + Member BackendNotificationQueueAccess r, Member BotAccess r, Member BrigAccess r, Member CodeStore r, diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 595a89d8858..4ead604633e 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -95,7 +95,6 @@ import Galley.Data.Conversation qualified as Data import Galley.Data.Services as Data import Galley.Data.Types hiding (Conversation) import Galley.Effects -import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.ClientStore qualified as E import Galley.Effects.CodeStore qualified as E import Galley.Effects.ConversationStore qualified as E @@ -255,7 +254,8 @@ handleUpdateResult = \case Unchanged -> empty & setStatus status204 type UpdateConversationAccessEffects = - '[ BotAccess, + '[ BackendNotificationQueueAccess, + BotAccess, BrigAccess, CodeStore, ConversationStore, @@ -309,7 +309,8 @@ updateConversationAccessUnqualified lusr con cnv update = update updateConversationReceiptMode :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -382,7 +383,8 @@ updateRemoteConversation rcnv lusr conn action = getUpdateResult $ do updateLocalStateOfRemoteConv (qualifyAs rcnv convUpdate) (Just conn) >>= note NoChanges updateConversationReceiptModeUnqualified :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -405,13 +407,13 @@ updateConversationReceiptModeUnqualified :: updateConversationReceiptModeUnqualified lusr zcon cnv = updateConversationReceiptMode lusr zcon (tUntagged (qualifyAs lusr cnv)) updateConversationMessageTimer :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (ErrorS ('ActionDenied 'ModifyConversationMessageTimer)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -438,13 +440,13 @@ updateConversationMessageTimer lusr zcon qcnv update = qcnv updateConversationMessageTimerUnqualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (ErrorS ('ActionDenied 'ModifyConversationMessageTimer)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -457,7 +459,8 @@ updateConversationMessageTimerUnqualified :: updateConversationMessageTimerUnqualified lusr zcon cnv = updateConversationMessageTimer lusr zcon (tUntagged (qualifyAs lusr cnv)) deleteLocalConversation :: - ( Member CodeStore r, + ( Member BackendNotificationQueueAccess r, + Member CodeStore r, Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS 'NotATeamMember) r, @@ -465,7 +468,6 @@ deleteLocalConversation :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member TeamStore r, @@ -675,7 +677,8 @@ checkReusableCode convCode = do joinConversationByReusableCode :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member CodeStore r, Member ConversationStore r, Member (ErrorS 'CodeNotFound) r, @@ -686,7 +689,6 @@ joinConversationByReusableCode :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TooManyMembers) r, - Member FederatorAccess r, Member ExternalAccess r, Member GundeckAccess r, Member (Input Opts) r, @@ -708,8 +710,8 @@ joinConversationByReusableCode lusr zcon req = do joinConversationById :: forall r. - ( Member BrigAccess r, - Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, @@ -734,8 +736,8 @@ joinConversationById lusr zcon cnv = do joinConversation :: forall r. - ( Member BrigAccess r, - Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, @@ -777,7 +779,8 @@ joinConversation lusr zcon conv access = do action addMembers :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'AddConversationMember)) r, @@ -816,7 +819,8 @@ addMembers lusr zcon qcnv (InviteQualified users role) = do ConversationJoin users role addMembersUnqualifiedV2 :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -855,7 +859,8 @@ addMembersUnqualifiedV2 lusr zcon cnv (InviteQualified users role) = do ConversationJoin users role addMembersUnqualified :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -956,7 +961,8 @@ updateUnqualifiedSelfMember lusr zcon cnv update = do updateSelfMember lusr zcon (tUntagged lcnv) update updateOtherMemberLocalConv :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -964,7 +970,6 @@ updateOtherMemberLocalConv :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, @@ -983,7 +988,8 @@ updateOtherMemberLocalConv lcnv lusr con qvictim update = void . getUpdateResult ConversationMemberUpdate qvictim update updateOtherMemberUnqualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -991,7 +997,6 @@ updateOtherMemberUnqualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, @@ -1009,7 +1014,8 @@ updateOtherMemberUnqualified lusr zcon cnv victim update = do updateOtherMemberLocalConv lcnv lusr zcon (tUntagged lvictim) update updateOtherMember :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -1017,7 +1023,6 @@ updateOtherMember :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, @@ -1044,7 +1049,8 @@ updateOtherMemberRemoteConv :: updateOtherMemberRemoteConv _ _ _ _ _ = throw FederationNotImplemented removeMemberUnqualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, @@ -1070,7 +1076,8 @@ removeMemberUnqualified lusr con cnv victim = do removeMemberQualified lusr con (tUntagged lcnv) (tUntagged lvictim) removeMemberQualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, @@ -1137,7 +1144,8 @@ removeMemberFromRemoteConv cnv lusr victim -- | Remove a member from a local conversation. removeMemberFromLocalConv :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'LeaveConversation)) r, @@ -1329,14 +1337,14 @@ postOtrMessageUnqualified sender zcon cnv = (runLocalInput sender . postQualifiedOtrMessage User (tUntagged sender) (Just zcon) lcnv) updateConversationName :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -1355,14 +1363,14 @@ updateConversationName lusr zcon qcnv convRename = do convRename updateUnqualifiedConversationName :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -1377,14 +1385,14 @@ updateUnqualifiedConversationName lusr zcon cnv rename = do updateLocalConversationName lusr zcon lcnv rename updateLocalConversationName :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index a8dc2a5198c..535a60eaded 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -56,6 +56,9 @@ module Galley.Effects -- * Polysemy re-exports Member, Members, + + -- * Queueing effects + BackendNotificationQueueAccess, ) where diff --git a/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs b/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs index c7ecdfcb771..ac006ded4c5 100644 --- a/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs +++ b/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs @@ -15,7 +15,13 @@ data BackendNotificationQueueAccess m a where KnownComponent c => Remote x -> Q.DeliveryMode -> - FedQueueClient c () -> - BackendNotificationQueueAccess m (Either FederationError ()) + FedQueueClient c a -> + BackendNotificationQueueAccess m (Either FederationError a) + EnqueueNotificationsConcurrently :: + (KnownComponent c, Foldable f, Functor f) => + Q.DeliveryMode -> + f (Remote x) -> + (Remote [x] -> FedQueueClient c a) -> + BackendNotificationQueueAccess m [Either (Remote ([x], FederationError)) (Remote a)] makeSem ''BackendNotificationQueueAccess diff --git a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs index fb2e02605fc..b9affe48585 100644 --- a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs +++ b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs @@ -5,6 +5,7 @@ module Galley.Intra.BackendNotificationQueue (interpretBackendNotificationQueueA import Control.Lens (view) import Control.Monad.Catch import Control.Retry +import Data.Bifunctor import Data.Domain import Data.Qualified import Galley.Effects.BackendNotificationQueueAccess (BackendNotificationQueueAccess (..)) @@ -16,7 +17,7 @@ import Network.AMQP qualified as Q import Polysemy import Polysemy.Input import System.Logger.Class qualified as Log -import UnliftIO.Timeout (timeout) +import UnliftIO import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Error @@ -29,8 +30,10 @@ interpretBackendNotificationQueueAccess :: interpretBackendNotificationQueueAccess = interpret $ \case EnqueueNotification remote deliveryMode action -> do embedApp $ enqueueNotification (tDomain remote) deliveryMode action + EnqueueNotificationsConcurrently m xs rpc -> do + embedApp $ enqueueNotificationsConcurrently m xs rpc -enqueueNotification :: Domain -> Q.DeliveryMode -> FedQueueClient c () -> App (Either FederationError ()) +enqueueNotification :: Domain -> Q.DeliveryMode -> FedQueueClient c a -> App (Either FederationError a) enqueueNotification remoteDomain deliveryMode action = do mChanVar <- view rabbitmqChannel ownDomain <- view (options . settings . federationDomain) @@ -56,6 +59,19 @@ enqueueNotification remoteDomain deliveryMode action = do Just chan -> do liftIO $ enqueue chan ownDomain remoteDomain deliveryMode action +enqueueNotificationsConcurrently :: + (Foldable f, Functor f) => + Q.DeliveryMode -> + f (Remote x) -> + (Remote [x] -> FedQueueClient c a) -> + App [(Either (Remote ([x], FederationError)) (Remote a))] +enqueueNotificationsConcurrently m xs f = + pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> + bimap + (qualifyAs r . (tUnqualified r,)) + (qualifyAs r) + <$> enqueueNotification (tDomain r) m (f r) + data NoRabbitMqChannel = NoRabbitMqChannel deriving (Show) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 179b3fbb4a6..014ffa63820 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -51,7 +51,6 @@ import Data.ByteString qualified as BS import Data.ByteString.Conversion import Data.Code qualified as Code import Data.Domain -import Data.Either.Extra (eitherToMaybe) import Data.Id import Data.Json.Util (toBase64Text, toUTCTimeMillis) import Data.List.NonEmpty (NonEmpty (..)) @@ -106,7 +105,6 @@ import Wire.API.Routes.Version import Wire.API.Routes.Versioned import Wire.API.Team.Feature qualified as Public import Wire.API.Team.Member qualified as Teams -import Wire.API.User import Wire.API.User.Client import Wire.API.UserMap (UserMap (..)) @@ -177,9 +175,6 @@ tests s = test s "generate guest link forbidden when no guest or non-team-member access role" generateGuestLinkFailIfNoNonTeamMemberOrNoGuestAccess, 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 "delete conversation with remote members" testDeleteTeamConversationWithRemoteMembers, - test s "delete conversation with unavailable remote members" testDeleteTeamConversationWithUnavailableRemoteMembers, test s "get conversations/:domain/:cnv - local" testGetQualifiedLocalConv, test s "get conversations/:domain/:cnv - local, not found" testGetQualifiedLocalConvNotFound, test s "get conversations/:domain/:cnv - local, not participating" testGetQualifiedLocalConvNotParticipating, @@ -193,9 +188,6 @@ tests s = test s "delete conversations/:domain/:cnv/members/:domain/:usr - fail, self conv" deleteMembersQualifiedFailSelf, test s "delete conversations/:domain:/cnv/members/:domain/:usr - fail, 1:1 conv" deleteMembersQualifiedFailO2O, test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with all locals" deleteMembersConvLocalQualifiedOk, - test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with locals and remote, delete local" deleteLocalMemberConvLocalQualifiedOk, - test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with locals and remote, delete remote" deleteRemoteMemberConvLocalQualifiedOk, - test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with locals and remote, delete unavailable remote" deleteUnavailableRemoteMemberConvLocalQualifiedOk, test s "delete conversations/:domain/:cnv/members/:domain/:usr - remote conv, leave conv" leaveRemoteConvQualifiedOk, test s "delete conversations/:domain/:cnv/members/:domain/:usr - remote conv, leave conv, non-existent" leaveNonExistentRemoteConv, test s "delete conversations/:domain/:cnv/members/:domain/:usr - remote conv, leave conv, denied" leaveRemoteConvDenied, @@ -204,8 +196,6 @@ tests s = test s "rename conversation (deprecated endpoint)" putConvDeprecatedRenameOk, test s "rename conversation" putConvRenameOk, test s "rename qualified conversation" putQualifiedConvRenameOk, - test s "rename qualified conversation with remote members" putQualifiedConvRenameWithRemotesOk, - test s "rename qualified conversation with unavailable remote" putQualifiedConvRenameWithRemotesUnavailable, test s "rename qualified conversation failure" putQualifiedConvRenameFailure, test s "other member update role" putOtherMemberOk, test s "qualified other member update role" putQualifiedOtherMemberOk, @@ -218,8 +208,6 @@ tests s = test s "remote conversation member update (otr hidden)" putRemoteConvMemberHiddenOk, test s "remote conversation member update (everything)" putRemoteConvMemberAllOk, test s "conversation receipt mode update" putReceiptModeOk, - test s "conversation receipt mode update with remote members" putReceiptModeWithRemotesOk, - test s "conversation receipt mode update with unavailable remote members" putReceiptModeWithRemotesUnavailable, test s "remote conversation receipt mode update" putRemoteReceiptModeOk, test s "leave connect conversation" leaveConnectConversation, test s "post conversations/:cnv/otr/message: message delivery and missing clients" postCryptoMessageVerifyMsgSentAndRejectIfMissingClient, @@ -240,8 +228,6 @@ tests s = test s "join code-access conversation - password" postJoinCodeConvWithPassword, test s "convert invite to code-access conversation" postConvertCodeConv, test s "convert code to team-access conversation" postConvertTeamConv, - test s "local and remote guests are removed when access changes" testAccessUpdateGuestRemoved, - test s "local and remote guests are removed when access changes remotes unavailable" testAccessUpdateGuestRemovedRemotesUnavailable, test s "team member can't join via guest link if access role removed" testTeamMemberCantJoinViaGuestLinkIfAccessRoleRemoved, test s "cannot join private conversation" postJoinConvFail, test s "revoke guest links for team conversation" testJoinTeamConvGuestLinksDisabled, @@ -1628,183 +1614,6 @@ postConvertTeamConv = do -- team members (dave) can still join postJoinCodeConv dave j !!! const 200 === statusCode --- @SF.Federation @SF.Separation @TSFI.RESTfulAPI @S2 --- --- The test asserts that, among others, remote users are removed from a --- conversation when an access update occurs that disallows guests from --- accessing. -testAccessUpdateGuestRemoved :: TestM () -testAccessUpdateGuestRemoved = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - -- dee is a remote guest - let remoteDomain = Domain "far-away.example.com" - dee <- Qualified <$> randomId <*> pure remoteDomain - - connectWithRemoteUser (qUnqualified alice) dee - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply EmptyResponse) $ do - putQualifiedAccessUpdate - (qUnqualified alice) - (cnvQualifiedId conv) - (ConversationAccessData mempty (Set.fromList [TeamMemberAccessRole])) - !!! const 200 === statusCode - - -- charlie and dee are kicked out - -- - -- note that removing users happens asynchronously, so this check should - -- happen while the mock federator is still available - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [charlie] - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [dee] - - -- dee's remote receives a notification - let compareLists [] ys = [] @?= ys - compareLists (x : xs) ys = case break (== x) ys of - (ys1, _ : ys2) -> compareLists xs (ys1 <> ys2) - _ -> assertFailure $ "Could not find " <> show x <> " in " <> show ys - liftIO $ - compareLists - ( map - ( \fr -> do - cu <- eitherDecode @ConversationUpdate (frBody fr) - pure (cu.cuOrigUserId, cu.cuAction) - ) - ( filter - ( \fr -> - frComponent fr == Galley - && frRPC fr == "on-conversation-updated" - ) - reqs - ) - ) - [ Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure charlie)), - Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure dee)), - Right - ( alice, - SomeConversationAction - (sing @'ConversationAccessDataTag) - ConversationAccessData - { cupAccess = mempty, - cupAccessRoles = Set.fromList [TeamMemberAccessRole] - } - ) - ] - - -- only alice and bob remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - randomId <*> pure remoteDomain - - connectWithRemoteUser (qUnqualified alice) dee - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (throw $ MockErrorResponse HTTP.status503 "Down for maintenance") $ do - -- This request should still succeed even with an unresponsive federation member. - putQualifiedAccessUpdate - (qUnqualified alice) - (cnvQualifiedId conv) - (ConversationAccessData mempty (Set.fromList [TeamMemberAccessRole])) - !!! const 200 === statusCode - -- charlie and dee are kicked out - -- - -- note that removing users happens asynchronously, so this check should - -- happen while the mock federator is still available - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [charlie] - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [dee] - - let compareLists [] ys = [] @?= ys - compareLists (x : xs) ys = case break (== x) ys of - (ys1, _ : ys2) -> compareLists xs (ys1 <> ys2) - _ -> assertFailure $ "Could not find " <> show x <> " in " <> show ys - liftIO $ - compareLists - ( map - ( \fr -> do - cu <- eitherDecode @ConversationUpdate (frBody fr) - pure (cu.cuOrigUserId, cu.cuAction) - ) - ( filter - ( \fr -> - frComponent fr == Galley - && frRPC fr == "on-conversation-updated" - ) - reqs - ) - ) - [ Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure charlie)), - Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure dee)), - Right - ( alice, - SomeConversationAction - (sing @'ConversationAccessDataTag) - ConversationAccessData - { cupAccess = mempty, - cupAccessRoles = Set.fromList [TeamMemberAccessRole] - } - ) - ] - -- only alice and bob remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - viewFederationDomain deleteMemberQualified alice qalice qc !!! const 403 === statusCode -testAddRemoteMember :: TestM () -testAddRemoteMember = do - qalice <- randomQualifiedUser - let alice = qUnqualified qalice - let localDomain = qDomain qalice - bobId <- randomId - let remoteDomain = Domain "far-away.example.com" - remoteBob = Qualified bobId remoteDomain - convId <- decodeConvId <$> postConv alice [] (Just "remote gossip") [] Nothing Nothing - let qconvId = Qualified convId localDomain - - postQualifiedMembers alice (remoteBob :| []) qconvId !!! do - const 403 === statusCode - const (Right (Just "not-connected")) === fmap (view (at "label")) . responseJsonEither @Object - - connectWithRemoteUser alice remoteBob - - (resp, reqs) <- - withTempMockFederator' (respond remoteBob) $ - postQualifiedMembers alice (remoteBob :| []) qconvId - getConvQualified alice qconvId - liftIO $ do - let actual = cmOthers $ cnvMembers conv - let expected = [OtherMember remoteBob Nothing roleNameWireAdmin] - assertEqual "other members should include remoteBob" expected actual - where - respond :: Qualified UserId -> Mock LByteString - respond bob = - asum - [ getNotFullyConnectedBackendsMock <|> guardComponent Brig *> mockReply [mkProfile bob (Name "bob")] - ] - -testDeleteTeamConversationWithRemoteMembers :: TestM () -testDeleteTeamConversationWithRemoteMembers = do - (alice, tid) <- createBindingTeam - localDomain <- viewFederationDomain - let qalice = Qualified alice localDomain - - bobId <- randomId - let remoteDomain = Domain "far-away.example.com" - remoteBob = Qualified bobId remoteDomain - - convId <- decodeConvId <$> postTeamConv tid alice [] (Just "remote gossip") [] Nothing Nothing - let qconvId = Qualified convId localDomain - - connectWithRemoteUser alice remoteBob - - let mock = getNotFullyConnectedBackendsMock <|> "api-version" ~> EmptyResponse - (_, received) <- withTempMockFederator' mock $ do - postQualifiedMembers alice (remoteBob :| []) qconvId - !!! const 200 === statusCode - - deleteTeamConv tid convId alice - !!! const 200 === statusCode - - liftIO $ do - let convUpdates = mapMaybe (eitherToMaybe . parseFedRequest) received - convUpdate <- case filter ((== SomeConversationAction (sing @'ConversationDeleteTag) ()) . cuAction) convUpdates of - [] -> assertFailure "No ConversationUpdate requests received" - [convDelete] -> pure convDelete - _ -> assertFailure "Multiple ConversationUpdate requests received" - cuAlreadyPresentUsers convUpdate @?= [bobId] - cuOrigUserId convUpdate @?= qalice - -testDeleteTeamConversationWithUnavailableRemoteMembers :: TestM () -testDeleteTeamConversationWithUnavailableRemoteMembers = do - (alice, tid) <- createBindingTeam - localDomain <- viewFederationDomain - let qalice = Qualified alice localDomain - - bobId <- randomId - let remoteDomain = Domain "far-away.example.com" - remoteBob = Qualified bobId remoteDomain - - convId <- decodeConvId <$> postTeamConv tid alice [] (Just "remote gossip") [] Nothing Nothing - let qconvId = Qualified convId localDomain - - connectWithRemoteUser alice remoteBob - - let mock = - getNotFullyConnectedBackendsMock - <|> - -- Mock an unavailable federation server for the deletion call - (guardRPC "on-conversation-updated" *> throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) - <|> (guardRPC "delete-team-conversation" *> throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) - (_, received) <- withTempMockFederator' mock $ do - postQualifiedMembers alice (remoteBob :| []) qconvId - !!! const 200 === statusCode - - deleteTeamConv tid convId alice - !!! const 200 === statusCode - liftIO $ do - let convUpdates = mapMaybe (eitherToMaybe . parseFedRequest) received - convUpdate <- case filter ((== SomeConversationAction (sing @'ConversationDeleteTag) ()) . cuAction) convUpdates of - [] -> assertFailure "No ConversationUpdate requests received" - [convDelete] -> pure convDelete - _ -> assertFailure "Multiple ConversationUpdate requests received" - cuAlreadyPresentUsers convUpdate @?= [bobId] - cuOrigUserId convUpdate @?= qalice - testGetQualifiedLocalConv :: TestM () testGetQualifiedLocalConv = do alice <- randomUser @@ -3122,180 +2819,6 @@ deleteMembersConvLocalQualifiedOk = do deleteMemberQualified alice qAlice qconv !!! const 200 === statusCode deleteMemberQualified alice qAlice qconv !!! const 404 === statusCode --- Creates a conversation with three users. Alice and Bob are on the local --- domain, while Eve is on a remote domain. It uses a qualified endpoint for --- removing Bob from the conversation: --- --- DELETE /conversations/:domain/:cnv/members/:domain/:usr -deleteLocalMemberConvLocalQualifiedOk :: TestM () -deleteLocalMemberConvLocalQualifiedOk = do - localDomain <- viewFederationDomain - [alice, bob] <- randomUsers 2 - eve <- randomId - let [qAlice, qBob] = (`Qualified` localDomain) <$> [alice, bob] - remoteDomain = Domain "far-away.example.com" - qEve = Qualified eve remoteDomain - - connectUsers alice (singleton bob) - connectWithRemoteUser alice qEve - convId <- - decodeConvId - <$> postConvWithRemoteUsers - alice - Nothing - defNewProteusConv {newConvQualifiedUsers = [qBob, qEve]} - let qconvId = Qualified convId localDomain - - let mockReturnEve = - mockedFederatedBrigResponse [(qEve, "Eve")] - <|> mockReply EmptyResponse - (respDel, fedRequests) <- - withTempMockFederator' mockReturnEve $ - deleteMemberQualified alice qBob qconvId - let [galleyFederatedRequest] = fedRequestsForDomain remoteDomain Galley fedRequests - assertRemoveUpdate galleyFederatedRequest qconvId qAlice [qUnqualified qEve] qBob - - liftIO $ do - statusCode respDel @?= 200 - case responseJsonEither respDel of - Left err -> assertFailure err - Right e -> assertLeaveEvent qconvId qAlice [qBob] e - - -- Now that Bob is gone, try removing him once again - deleteMemberQualified alice qBob qconvId !!! do - const 204 === statusCode - const Nothing === responseBody - --- Creates a conversation with five users. Alice and Bob are on the local --- domain. Chad and Dee are on far-away-1.example.com. Eve is on --- far-away-2.example.com. It uses a qualified endpoint to remove Chad from the --- conversation: --- --- DELETE /conversations/:domain/:cnv/members/:domain/:usr -deleteRemoteMemberConvLocalQualifiedOk :: TestM () -deleteRemoteMemberConvLocalQualifiedOk = do - localDomain <- viewFederationDomain - [alice, bob] <- randomUsers 2 - let [qAlice, qBob] = (`Qualified` localDomain) <$> [alice, bob] - remoteDomain1 = Domain "far-away-1.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - qChad <- (`Qualified` remoteDomain1) <$> randomId - qDee <- (`Qualified` remoteDomain1) <$> randomId - qEve <- (`Qualified` remoteDomain2) <$> randomId - connectUsers alice (singleton bob) - mapM_ (connectWithRemoteUser alice) [qChad, qDee, qEve] - - let mockedResponse = do - guardRPC "get-users-by-ids" - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")], - guard (d == remoteDomain2) - *> mockReply [mkProfile qEve (Name "Eve")] - ] - (convId, _) <- - withTempMockFederator' (getNotFullyConnectedBackendsMock <|> mockedResponse <|> mockReply EmptyResponse) $ - fmap decodeConvId $ - postConvQualified - alice - Nothing - defNewProteusConv {newConvQualifiedUsers = [qBob, qChad, qDee, qEve]} - mockedResponse <|> mockReply EmptyResponse) $ - deleteMemberQualified alice qChad qconvId - liftIO $ do - statusCode respDel @?= 200 - case responseJsonEither respDel of - Left err -> assertFailure err - Right e -> assertLeaveEvent qconvId qAlice [qChad] e - - let [remote1GalleyFederatedRequest] = fedRequestsForDomain remoteDomain1 Galley federatedRequests - [remote2GalleyFederatedRequest] = fedRequestsForDomain remoteDomain2 Galley federatedRequests - assertRemoveUpdate remote1GalleyFederatedRequest qconvId qAlice [qUnqualified qChad, qUnqualified qDee] qChad - assertRemoveUpdate remote2GalleyFederatedRequest qconvId qAlice [qUnqualified qEve] qChad - - -- Now that Chad is gone, try removing him once again - deleteMemberQualified alice qChad qconvId !!! do - const 204 === statusCode - const Nothing === responseBody - --- Creates a conversation with five users. Alice and Bob are on the local --- domain. Chad and Dee are on far-away-1.example.com. Eve is on --- far-away-2.example.com. It uses a qualified endpoint to remove Chad from the --- conversation. The federator for far-away-2.example.com isn't availabe: --- --- DELETE /conversations/:domain/:cnv/members/:domain/:usr -deleteUnavailableRemoteMemberConvLocalQualifiedOk :: TestM () -deleteUnavailableRemoteMemberConvLocalQualifiedOk = do - localDomain <- viewFederationDomain - [alice, bob] <- randomUsers 2 - let [qAlice, qBob] = (`Qualified` localDomain) <$> [alice, bob] - remoteDomain1 = Domain "far-away-1.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - qChad <- (`Qualified` remoteDomain1) <$> randomId - qDee <- (`Qualified` remoteDomain1) <$> randomId - qEve <- (`Qualified` remoteDomain2) <$> randomId - connectUsers alice (singleton bob) - mapM_ (connectWithRemoteUser alice) [qChad, qDee, qEve] - - let mockedGetUsers remote2Response = do - guardRPC "get-users-by-ids" - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")], - guard (d == remoteDomain2) - *> remote2Response - ] - mockedCreateConvGetUsers = - mockedGetUsers (mockReply [mkProfile qEve (Name "Eve")]) - mockedRemMemGetUsers = - mockedGetUsers (throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) - mockedOther = do - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply EmptyResponse, - guard (d == remoteDomain2) - *> asum - [ guardRPC "on-conversation-created" *> mockReply EmptyResponse, - guardRPC "on-conversation-updated" *> mockReply EmptyResponse, - throw $ MockErrorResponse HTTP.status503 "Down for maintenance." - ] - ] - convId <- - fmap decodeConvId $ - postConvWithRemoteUsersGeneric - (mockedCreateConvGetUsers <|> mockedOther) - alice - Nothing - defNewProteusConv {newConvQualifiedUsers = [qBob, qChad, qDee, qEve]} - mockedOther) $ - deleteMemberQualified alice qChad qconvId - liftIO $ do - statusCode respDel @?= 200 - case responseJsonEither respDel of - Left err -> assertFailure err - Right e -> assertLeaveEvent qconvId qAlice [qChad] e - - let [remote1GalleyFederatedRequest] = fedRequestsForDomain remoteDomain1 Galley federatedRequests - [remote2GalleyFederatedRequest] = fedRequestsForDomain remoteDomain2 Galley federatedRequests - assertRemoveUpdate remote1GalleyFederatedRequest qconvId qAlice [qUnqualified qChad, qUnqualified qDee] qChad - assertRemoveUpdate remote2GalleyFederatedRequest qconvId qAlice [qUnqualified qEve] qChad - - -- Now that Chad is gone, try removing him once again - deleteMemberQualified alice qChad qconvId !!! do - const 204 === statusCode - const Nothing === responseBody - -- Alice, a local user, leaves a remote conversation. Bob's domain is the same -- as that of the conversation. The test uses the following endpoint: -- @@ -3461,86 +2984,6 @@ putQualifiedConvRenameOk = do evtFrom e @?= qbob evtData e @?= EdConvRename (ConversationRename "gossip++") -putQualifiedConvRenameWithRemotesOk :: TestM () -putQualifiedConvRenameWithRemotesOk = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putQualifiedConversationName bob qconv "gossip++" !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction @?= SomeConversationAction (sing @'ConversationRenameTag) (ConversationRename "gossip++") - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvRename - evtFrom e @?= qbob - evtData e @?= EdConvRename (ConversationRename "gossip++") - -putQualifiedConvRenameWithRemotesUnavailable :: TestM () -putQualifiedConvRenameWithRemotesUnavailable = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse HTTP.status503 "Down for maintenance") $ - putQualifiedConversationName bob qconv "gossip++" !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction @?= SomeConversationAction (sing @'ConversationRenameTag) (ConversationRename "gossip++") - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvRename - evtFrom e @?= qbob - evtData e @?= EdConvRename (ConversationRename "gossip++") - putConvDeprecatedRenameOk :: TestM () putConvDeprecatedRenameOk = do c <- view tsCannon @@ -3987,90 +3430,6 @@ putRemoteReceiptModeOk = do WS.assertMatch_ (5 # Second) wsAdam $ \n -> do liftIO $ wsAssertConvReceiptModeUpdate qconv qalice newReceiptMode n -putReceiptModeWithRemotesOk :: TestM () -putReceiptModeWithRemotesOk = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putQualifiedReceiptMode bob qconv (ReceiptMode 43) !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction - @?= SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) (ConversationReceiptModeUpdate (ReceiptMode 43)) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvReceiptModeUpdate - evtFrom e @?= qbob - evtData e - @?= EdConvReceiptModeUpdate - (ConversationReceiptModeUpdate (ReceiptMode 43)) - -putReceiptModeWithRemotesUnavailable :: TestM () -putReceiptModeWithRemotesUnavailable = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse HTTP.status503 "Down for maintenance") $ - putQualifiedReceiptMode bob qconv (ReceiptMode 43) !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction - @?= SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) (ConversationReceiptModeUpdate (ReceiptMode 43)) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvReceiptModeUpdate - evtFrom e @?= qbob - evtData e - @?= EdConvReceiptModeUpdate - (ConversationReceiptModeUpdate (ReceiptMode 43)) - postTypingIndicatorsV2 :: TestM () postTypingIndicatorsV2 = do c <- view tsCannon diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 73930b4bd59..d0b470937c7 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -23,7 +23,6 @@ import Bilge hiding (head) import Bilge.Assert import Control.Exception import Control.Lens hiding ((#)) -import Data.Aeson qualified as A import Data.ByteString.Conversion (toByteString') import Data.Domain import Data.Id @@ -34,7 +33,6 @@ import Data.List1 qualified as List1 import Data.Map qualified as Map import Data.ProtoLens qualified as Protolens import Data.Qualified -import Data.Range import Data.Set qualified as Set import Data.Singletons import Data.Time.Clock @@ -83,13 +81,10 @@ tests s = test s "POST /federation/on-conversation-updated : Notify local user about receipt mode update" notifyReceiptMode, test s "POST /federation/on-conversation-updated : Notify local user about access update" notifyAccess, test s "POST /federation/on-conversation-updated : Notify local users about a deleted conversation" notifyDeletedConversation, - test s "POST /federation/leave-conversation : Success" leaveConversationSuccess, test s "POST /federation/leave-conversation : Non-existent" leaveConversationNonExistent, test s "POST /federation/leave-conversation : Invalid type" leaveConversationInvalidType, 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/on-conversation-updated : Notify local user about conversation rename with an unavailable federator" notifyConvRenameUnavailable, test s "POST /federation/on-conversation-updated : Notify local user about message timer update with an unavailable federator" notifyMessageTimerUnavailable, test s "POST /federation/on-conversation-updated : Notify local user about receipt mode update with an unavailable federator" notifyReceiptModeUnavailable, @@ -711,69 +706,6 @@ addRemoteUser = do WS.assertNoEvent (1 # Second) [wsC] WS.assertNoEvent (1 # Second) [wsF] -leaveConversationSuccess :: TestM () -leaveConversationSuccess = do - localDomain <- viewFederationDomain - c <- view tsCannon - [alice, bob] <- randomUsers 2 - let qBob = Qualified bob localDomain - remoteDomain1 = Domain "far-away-1.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - qChad <- (`Qualified` remoteDomain1) <$> randomId - qDee <- (`Qualified` remoteDomain1) <$> randomId - qEve <- (`Qualified` remoteDomain2) <$> randomId - connectUsers alice (singleton bob) - connectWithRemoteUser alice qChad - connectWithRemoteUser alice qDee - connectWithRemoteUser alice qEve - - let mock = do - guardRPC "get-users-by-ids" - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")], - guard (d == remoteDomain2) - *> mockReply [mkProfile qEve (Name "Eve")] - ] - - convId <- - decodeConvId - <$> postConvWithRemoteUsersGeneric - (mock <|> mockReply EmptyResponse) - alice - Nothing - defNewProteusConv - { newConvQualifiedUsers = [qBob, qChad, qDee, qEve] - } - let qconvId = Qualified convId localDomain - - (_, federatedRequests) <- - WS.bracketR2 c alice bob $ \(wsAlice, wsBob) -> do - withTempMockFederator' ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty <|> mock <|> mockReply EmptyResponse) $ do - g <- viewGalley - let leaveRequest = FedGalley.LeaveConversationRequest convId (qUnqualified qChad) - respBS <- - post - ( g - . paths ["federation", "leave-conversation"] - . content "application/json" - . header "Wire-Origin-Domain" (toByteString' remoteDomain1) - . json leaveRequest - ) - Causes Alice to be notified --- - groupConvId -> Causes Alice and Alex to be notified --- - extraConvId -> Ignored --- - noBobConvId -> Ignored -onUserDeleted :: TestM () -onUserDeleted = do - cannon <- view tsCannon - let bDomain = Domain "b.far-away.example.com" - cDomain = Domain "c.far-away.example.com" - - alice <- qTagUnsafe <$> randomQualifiedUser - alex <- randomQualifiedUser - (bob, ooConvId) <- generateRemoteAndConvIdWithDomain bDomain True alice - bart <- randomQualifiedId bDomain - carl <- randomQualifiedId cDomain - - connectWithRemoteUser (tUnqualified alice) (tUntagged bob) - connectUsers (tUnqualified alice) (pure (qUnqualified alex)) - connectWithRemoteUser (tUnqualified alice) bart - connectWithRemoteUser (tUnqualified alice) carl - - -- create 1-1 conversation between alice and bob - createOne2OneConvWithRemote alice bob - - -- create group conversation with everybody - groupConvId <- WS.bracketR cannon (tUnqualified alice) $ \wsAlice -> do - convId <- - decodeQualifiedConvId - <$> ( postConvWithRemoteUsers - (tUnqualified alice) - Nothing - defNewProteusConv {newConvQualifiedUsers = [tUntagged bob, alex, bart, carl]} - do - convId <- - fmap decodeQualifiedConvId $ - postConvQualified - (tUnqualified alice) - Nothing - defNewProteusConv {newConvQualifiedUsers = [alex]} - do - (resp, rpcCalls) <- withTempMockFederator' (mockReply EmptyResponse) $ do - let udcn = - FedGalley.UserDeletedConversationsNotification - { FedGalley.user = tUnqualified bob, - FedGalley.conversations = - unsafeRange - [ qUnqualified ooConvId, - qUnqualified groupConvId, - extraConvId, - qUnqualified noBobConvId - ] - } - g <- viewGalley - responseJsonError - =<< post - ( g - . paths ["federation", "on-user-deleted-conversations"] - . content "application/json" - . header "Wire-Origin-Domain" (toByteString' (tDomain bob)) - . json udcn - ) - show rpcCalls) 1 (length rpcCalls) - - -- Assertions about RPC to 'cDomain' - cDomainRPC <- assertOne $ filter (\c -> frTargetDomain c == cDomain) rpcCalls - cDomainRPCReq <- assertRight $ parseFedRequest cDomainRPC - FedGalley.cuOrigUserId cDomainRPCReq @?= tUntagged bob - FedGalley.cuConvId cDomainRPCReq @?= qUnqualified groupConvId - FedGalley.cuAlreadyPresentUsers cDomainRPCReq @?= [qUnqualified carl] - FedGalley.cuAction cDomainRPCReq @?= SomeConversationAction (sing @'ConversationLeaveTag) () - --- | We test only ReceiptMode update here --- --- A : local domain, owns the conversation --- B : bob is an admin of the converation --- C : charlie is a regular member of the conversation -updateConversationByRemoteAdmin :: TestM () -updateConversationByRemoteAdmin = do - c <- view tsCannon - (alice, qalice) <- randomUserTuple - - let bdomain = Domain "b.example.com" - cdomain = Domain "c.example.com" - qbob <- randomQualifiedId bdomain - qcharlie <- randomQualifiedId cdomain - mapM_ (connectWithRemoteUser alice) [qbob, qcharlie] - - let convName = "Test Conv" - WS.bracketR c alice $ \wsAlice -> do - (rsp, _federatedRequests) <- do - let mock = ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty) <|> mockReply EmptyResponse - withTempMockFederator' mock $ do - postConvQualified alice Nothing defNewProteusConv {newConvName = checked convName, newConvQualifiedUsers = [qbob, qcharlie]} - assertFailure ("Expected ConversationUpdateResponseUpdate but got " <> show err) - ConversationUpdateResponseNoChanges -> assertFailure "Expected ConversationUpdateResponseUpdate but got ConversationUpdateResponseNoChanges" - ConversationUpdateResponseUpdate up -> pure up - ConversationUpdateResponseNonFederatingBackends _ -> assertFailure "Expected ConversationUpdateResponseUpdate but got ConversationUpdateResponseNonFederatingBackends" - ConversationUpdateResponseUnreachableBackends _ -> assertFailure "Expected ConversationUpdateResponseUpdate but got ConversationUpdateResponseUnreachableBackends" - - liftIO $ do - cuOrigUserId cnvUpdate' @?= qbob - cuAlreadyPresentUsers cnvUpdate' @?= [qUnqualified qbob] - cuAction cnvUpdate' @?= action - - -- backend A generates a notification for alice - void $ - WS.awaitMatch (5 # Second) wsAlice $ \n -> do - liftIO $ wsAssertConvReceiptModeUpdate cnv qalice newReceiptMode n - - -- backend B does *not* get notified of the conversation update ony of bob's promotion - liftIO $ do - [(_fr, cUpdate)] <- mapM parseConvUpdate $ filter (\r -> frTargetDomain r == bdomain) federatedRequests - assertBool "Action is not a ConversationMemberUpdate" (isJust (getConvAction (sing @'ConversationMemberUpdateTag) (cuAction cUpdate))) - - -- conversation has been modified by action - updatedConv :: Conversation <- fmap responseJsonUnsafe $ getConvQualified alice cnv frTargetDomain r == cdomain) federatedRequests - - (_fr1, _cu1, _up1) <- assertOne $ mapMaybe (\(fr, up) -> getConvAction (sing @'ConversationMemberUpdateTag) (cuAction up) <&> (fr,up,)) dUpdates - - (_fr2, convUpdate, receiptModeUpdate) <- assertOne $ mapMaybe (\(fr, up) -> getConvAction (sing @'ConversationReceiptModeUpdateTag) (cuAction up) <&> (fr,up,)) dUpdates - - cruReceiptMode receiptModeUpdate @?= newReceiptMode - cuOrigUserId convUpdate @?= qbob - cuConvId convUpdate @?= qUnqualified cnv - cuAlreadyPresentUsers convUpdate @?= [qUnqualified qcharlie] - - WS.assertMatch_ (5 # Second) wsAlice $ \n -> do - wsAssertConvReceiptModeUpdate cnv qbob newReceiptMode n - where - _toOtherMember qid = OtherMember qid Nothing roleNameWireAdmin - _convView cnv usr = responseJsonUnsafeWithMsg "conversation" <$> getConv usr cnv - - parseConvUpdate :: FederatedRequest -> IO (FederatedRequest, ConversationUpdate) - parseConvUpdate rpc = do - frComponent rpc @?= Galley - frRPC rpc @?= "on-conversation-updated" - let convUpdate :: ConversationUpdate = fromRight (error $ "Could not parse ConversationUpdate from " <> show (frBody rpc)) $ A.eitherDecode (frBody rpc) - pure (rpc, convUpdate) - getConvAction :: Sing tag -> SomeConversationAction -> Maybe (ConversationAction tag) getConvAction tquery (SomeConversationAction tag action) = case (tag, tquery) of diff --git a/services/galley/test/integration/API/MessageTimer.hs b/services/galley/test/integration/API/MessageTimer.hs index fcf5db5e909..66538f13093 100644 --- a/services/galley/test/integration/API/MessageTimer.hs +++ b/services/galley/test/integration/API/MessageTimer.hs @@ -23,34 +23,19 @@ where import API.Util import Bilge hiding (timeout) import Bilge.Assert -import Control.Exception import Control.Lens (view) -import Data.Aeson (eitherDecode) -import Data.Domain -import Data.Id import Data.List1 -import Data.List1 qualified as List1 import Data.Misc import Data.Qualified -import Data.Singletons -import Federator.MockServer import Imports hiding (head) -import Network.HTTP.Types qualified as Http import Network.Wai.Utilities.Error import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import Test.Tasty.Cannon qualified as WS -import Test.Tasty.HUnit import TestHelpers import TestSetup import Wire.API.Conversation -import Wire.API.Conversation.Action import Wire.API.Conversation.Role -import Wire.API.Event.Conversation -import Wire.API.Federation.API.Common -import Wire.API.Federation.API.Galley qualified as F -import Wire.API.Federation.Component -import Wire.API.Internal.Notification (Notification (..)) tests :: IO TestSetup -> TestTree tests s = @@ -63,8 +48,6 @@ tests s = ], test s "timer can be changed" messageTimerChange, test s "timer can be changed with the qualified endpoint" messageTimerChangeQualified, - test s "timer changes are propagated to remote users" messageTimerChangeWithRemotes, - test s "timer changes unavailable remotes" messageTimerUnavailableRemotes, test s "timer can't be set by conv member without allowed action" messageTimerChangeWithoutAllowedAction, test s "timer can't be set in 1:1 conversations" messageTimerChangeO2O, test s "setting the timer generates an event" messageTimerEvent @@ -143,86 +126,6 @@ messageTimerChangeQualified = do getConvQualified jane qcid !!! const timer1year === (cnvMessageTimer <=< responseJsonUnsafe) -messageTimerChangeWithRemotes :: TestM () -messageTimerChangeWithRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putMessageTimerUpdateQualified bob qconv (ConversationMessageTimerUpdate timer1sec) - !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMessageTimerUpdateTag) (ConversationMessageTimerUpdate timer1sec) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvMessageTimerUpdate - evtFrom e @?= qbob - evtData e @?= EdConvMessageTimerUpdate (ConversationMessageTimerUpdate timer1sec) - -messageTimerUnavailableRemotes :: TestM () -messageTimerUnavailableRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse Http.status503 "Down for maintenance") $ - putMessageTimerUpdateQualified bob qconv (ConversationMessageTimerUpdate timer1sec) - !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMessageTimerUpdateTag) (ConversationMessageTimerUpdate timer1sec) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvMessageTimerUpdate - evtFrom e @?= qbob - evtData e @?= EdConvMessageTimerUpdate (ConversationMessageTimerUpdate timer1sec) - messageTimerChangeWithoutAllowedAction :: TestM () messageTimerChangeWithoutAllowedAction = do -- Create a team and a guest user diff --git a/services/galley/test/integration/API/Roles.hs b/services/galley/test/integration/API/Roles.hs index 3e807e8eff3..f74a264b71e 100644 --- a/services/galley/test/integration/API/Roles.hs +++ b/services/galley/test/integration/API/Roles.hs @@ -20,20 +20,14 @@ module API.Roles where import API.Util import Bilge hiding (timeout) import Bilge.Assert -import Control.Exception import Control.Lens (view) import Data.Aeson hiding (json) import Data.ByteString.Conversion (toByteString') -import Data.Domain import Data.Id import Data.List1 -import Data.List1 qualified as List1 import Data.Qualified import Data.Set qualified as Set -import Data.Singletons -import Federator.MockServer import Imports -import Network.HTTP.Types qualified as Http import Network.Wai.Utilities.Error import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) @@ -42,13 +36,7 @@ import Test.Tasty.HUnit import TestHelpers import TestSetup import Wire.API.Conversation -import Wire.API.Conversation.Action import Wire.API.Conversation.Role -import Wire.API.Event.Conversation -import Wire.API.Federation.API.Common -import Wire.API.Federation.API.Galley qualified as F -import Wire.API.Federation.Component -import Wire.API.Internal.Notification (Notification (..)) tests :: IO TestSetup -> TestTree tests s = @@ -56,10 +44,6 @@ tests s = "Conversation roles" [ test s "conversation roles admin (and downgrade)" handleConversationRoleAdmin, test s "conversation roles member (and upgrade)" handleConversationRoleMember, - test s "conversation role update with remote users present" roleUpdateWithRemotes, - test s "conversation role update with remote users present remotes unavailable" roleUpdateWithRemotesUnavailable, - test s "conversation access update with remote users present" accessUpdateWithRemotes, - test s "conversation role update of remote member" roleUpdateRemoteMember, test s "get all conversation roles" testAllConversationRoles, test s "access role update with v2" testAccessRoleUpdateV2, test s "test access roles of new conversations" testConversationAccessRole @@ -161,236 +145,6 @@ handleConversationRoleMember = do wsAssertMemberUpdateWithRole qcid qalice bob roleNameWireAdmin wireAdminChecks cid bob alice chuck -roleUpdateRemoteMember :: TestM () -roleUpdateRemoteMember = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- Qualified <$> randomId <*> pure remoteDomain - let bob = qUnqualified qbob - - traverse_ (connectWithRemoteUser bob) [qalice, qcharlie] - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putOtherMemberQualified - bob - qcharlie - (OtherMemberUpdate (Just roleNameWireMember)) - qconv - !!! const 200 === statusCode - - req <- assertOne requests - let mu = - MemberUpdateData - { misTarget = qcharlie, - misOtrMutedStatus = Nothing, - misOtrMutedRef = Nothing, - misOtrArchived = Nothing, - misOtrArchivedRef = Nothing, - misHidden = Nothing, - misHiddenRef = Nothing, - misConvRoleName = Just roleNameWireMember - } - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMemberUpdateTag) (ConversationMemberUpdate qcharlie (OtherMemberUpdate (Just roleNameWireMember))) - sort (F.cuAlreadyPresentUsers cu) @?= sort [qUnqualified qalice, qUnqualified qcharlie] - - liftIO . WS.assertMatch_ (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= MemberStateUpdate - evtFrom e @?= qbob - evtData e @?= EdMemberUpdate mu - - conv <- responseJsonError =<< getConvQualified bob qconv omQualifiedId m == qcharlie) (cmOthers (cnvMembers conv)) - liftIO $ - charlieAsMember - @=? Just - OtherMember - { omQualifiedId = qcharlie, - omService = Nothing, - omConvRoleName = roleNameWireMember - } - -roleUpdateWithRemotes :: TestM () -roleUpdateWithRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- randomQualifiedUser - let bob = qUnqualified qbob - charlie = qUnqualified qcharlie - - connectUsers bob (singleton charlie) - connectWithRemoteUser bob qalice - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putOtherMemberQualified - bob - qcharlie - (OtherMemberUpdate (Just roleNameWireAdmin)) - qconv - !!! const 200 === statusCode - - req <- assertOne requests - let mu = - MemberUpdateData - { misTarget = qcharlie, - misOtrMutedStatus = Nothing, - misOtrMutedRef = Nothing, - misOtrArchived = Nothing, - misOtrArchivedRef = Nothing, - misHidden = Nothing, - misHiddenRef = Nothing, - misConvRoleName = Just roleNameWireAdmin - } - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMemberUpdateTag) (ConversationMemberUpdate qcharlie (OtherMemberUpdate (Just roleNameWireAdmin))) - F.cuAlreadyPresentUsers cu @?= [qUnqualified qalice] - - liftIO . WS.assertMatchN_ (5 # Second) [wsB, wsC] $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= MemberStateUpdate - evtFrom e @?= qbob - evtData e @?= EdMemberUpdate mu - -roleUpdateWithRemotesUnavailable :: TestM () -roleUpdateWithRemotesUnavailable = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- randomQualifiedUser - let bob = qUnqualified qbob - charlie = qUnqualified qcharlie - - connectUsers bob (singleton charlie) - connectWithRemoteUser bob qalice - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse Http.status503 "Down for maintenance") $ - putOtherMemberQualified - bob - qcharlie - (OtherMemberUpdate (Just roleNameWireAdmin)) - qconv - !!! const 200 === statusCode - - req <- assertOne requests - let mu = - MemberUpdateData - { misTarget = qcharlie, - misOtrMutedStatus = Nothing, - misOtrMutedRef = Nothing, - misOtrArchived = Nothing, - misOtrArchivedRef = Nothing, - misHidden = Nothing, - misHiddenRef = Nothing, - misConvRoleName = Just roleNameWireAdmin - } - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMemberUpdateTag) (ConversationMemberUpdate qcharlie (OtherMemberUpdate (Just roleNameWireAdmin))) - F.cuAlreadyPresentUsers cu @?= [qUnqualified qalice] - - liftIO . WS.assertMatchN_ (5 # Second) [wsB, wsC] $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= MemberStateUpdate - evtFrom e @?= qbob - evtData e @?= EdMemberUpdate mu - -accessUpdateWithRemotes :: TestM () -accessUpdateWithRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- randomQualifiedUser - let bob = qUnqualified qbob - charlie = qUnqualified qcharlie - - connectUsers bob (singleton charlie) - connectWithRemoteUser bob qalice - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - let access = ConversationAccessData (Set.singleton CodeAccess) (Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole, GuestAccessRole, ServiceAccessRole]) - WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putQualifiedAccessUpdate bob qconv access - !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu @?= SomeConversationAction (sing @'ConversationAccessDataTag) access - F.cuAlreadyPresentUsers cu @?= [qUnqualified qalice] - - liftIO . WS.assertMatchN_ (5 # Second) [wsB, wsC] $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvAccessUpdate - evtFrom e @?= qbob - evtData e @?= EdConvAccessUpdate access - -- | Given an admin, another admin and a member run all -- the necessary checks targeting the admin wireAdminChecks :: From faa90f76f253921bcb51ccce8a2f63c07761006f Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Wed, 20 Sep 2023 13:10:27 +0300 Subject: [PATCH 132/225] federator: allow creating ServiceMonitor resources This will mark federator Metrics for scraping, if `metrics.serviceMonitor.enabled` is set to true, similar to all other charts. WPB-4778 --- changelog.d/5-internal/WPB-4406 | 2 +- .../federator/templates/servicemonitor.yaml | 19 +++++++++++++++++++ charts/federator/values.yaml | 4 ++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 charts/federator/templates/servicemonitor.yaml diff --git a/changelog.d/5-internal/WPB-4406 b/changelog.d/5-internal/WPB-4406 index 5313ca41b5d..6c064b7594b 100644 --- a/changelog.d/5-internal/WPB-4406 +++ b/changelog.d/5-internal/WPB-4406 @@ -1,2 +1,2 @@ - Extending the information returned in errors for Federator. Paths and response bodies, if available, are included in error logs. -- Prometheus metrics for outgoing and incoming federation requests added. +- Prometheus metrics for outgoing and incoming federation requests added. They can be enabled by setting `metrics.serviceMonitor.enabled`, like in other charts. diff --git a/charts/federator/templates/servicemonitor.yaml b/charts/federator/templates/servicemonitor.yaml new file mode 100644 index 00000000000..cb98cd0368a --- /dev/null +++ b/charts/federator/templates/servicemonitor.yaml @@ -0,0 +1,19 @@ +{{- if .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: gundeck + labels: + app: gundeck + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + endpoints: + - port: internal + path: /i/metrics + selector: + matchLabels: + app: federator + release: {{ .Release.Name }} +{{- end }} diff --git a/charts/federator/values.yaml b/charts/federator/values.yaml index 406bb3b17c6..9546be2d10c 100644 --- a/charts/federator/values.yaml +++ b/charts/federator/values.yaml @@ -8,6 +8,10 @@ service: internalFederatorPort: 8080 externalFederatorPort: 8081 +metrics: + serviceMonitor: + enabled: false + tls: # if enabled, federator will get its client certificate and private key from # the secret used by the federator ingress From 2e3a6ec3eaac434006df9e8a18a92c2a8855c3f2 Mon Sep 17 00:00:00 2001 From: Owen Harvey Date: Wed, 20 Sep 2023 21:37:53 +1000 Subject: [PATCH 133/225] WPB-4240: Migrate from swagger2 to openapi3 (#3570) --------- Co-authored-by: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Co-authored-by: Igor Ranieri --- changelog.d/4-docs/WPB-4240 | 1 + changelog.d/5-internal/WPB-4240 | 4 + libs/brig-types/brig-types.cabal | 2 +- libs/brig-types/default.nix | 4 +- .../test/unit/Test/Brig/Roundtrip.hs | 4 +- libs/deriving-swagger2/default.nix | 4 +- .../deriving-swagger2/deriving-swagger2.cabal | 4 +- .../deriving-swagger2/src/Deriving/Swagger.hs | 18 ++-- libs/extended/default.nix | 4 +- libs/extended/extended.cabal | 2 +- libs/extended/src/Servant/API/Extended.hs | 8 +- .../extended/src/Servant/API/Extended/RawM.hs | 6 +- libs/schema-profunctor/default.nix | 6 +- .../schema-profunctor/schema-profunctor.cabal | 4 +- libs/schema-profunctor/src/Data/Schema.hs | 34 +++--- .../test/unit/Test/Data/Schema.hs | 10 +- libs/types-common/default.nix | 4 +- libs/types-common/src/Data/Code.hs | 4 +- .../src/Data/CommaSeparatedList.hs | 6 +- libs/types-common/src/Data/Domain.hs | 2 +- libs/types-common/src/Data/Handle.hs | 2 +- libs/types-common/src/Data/Id.hs | 4 +- libs/types-common/src/Data/Json/Util.hs | 8 +- libs/types-common/src/Data/LegalHold.hs | 2 +- libs/types-common/src/Data/List1.hs | 6 +- libs/types-common/src/Data/Misc.hs | 10 +- libs/types-common/src/Data/Nonce.hs | 4 +- libs/types-common/src/Data/Qualified.hs | 14 ++- libs/types-common/src/Data/Range.hs | 13 ++- libs/types-common/src/Data/Text/Ascii.hs | 4 +- libs/types-common/types-common.cabal | 2 +- libs/wai-utilities/default.nix | 4 +- .../src/Network/Wai/Utilities/Headers.hs | 2 +- libs/wai-utilities/wai-utilities.cabal | 2 +- libs/wire-api-federation/default.nix | 4 +- .../src/Wire/API/Federation/Version.hs | 2 +- .../wire-api-federation.cabal | 2 +- libs/wire-api/default.nix | 10 +- libs/wire-api/src/Wire/API/Asset.hs | 6 +- libs/wire-api/src/Wire/API/Call/Config.hs | 2 +- libs/wire-api/src/Wire/API/Connection.hs | 4 +- libs/wire-api/src/Wire/API/Conversation.hs | 24 +++-- .../src/Wire/API/Conversation/Action.hs | 2 +- .../wire-api/src/Wire/API/Conversation/Bot.hs | 2 +- .../src/Wire/API/Conversation/Code.hs | 2 +- .../src/Wire/API/Conversation/Member.hs | 5 +- .../src/Wire/API/Conversation/Role.hs | 2 +- .../src/Wire/API/Conversation/Typing.hs | 2 +- libs/wire-api/src/Wire/API/CustomBackend.hs | 2 +- libs/wire-api/src/Wire/API/Deprecated.hs | 60 +++++++++++ libs/wire-api/src/Wire/API/Error.hs | 41 ++++--- libs/wire-api/src/Wire/API/Error/Brig.hs | 5 +- libs/wire-api/src/Wire/API/Error/Cannon.hs | 5 +- libs/wire-api/src/Wire/API/Error/Cargohold.hs | 5 +- libs/wire-api/src/Wire/API/Error/Empty.hs | 2 +- libs/wire-api/src/Wire/API/Error/Galley.hs | 34 ++++-- libs/wire-api/src/Wire/API/Error/Gundeck.hs | 5 +- .../src/Wire/API/Event/Conversation.hs | 7 +- .../src/Wire/API/Event/FeatureConfig.hs | 2 +- .../wire-api/src/Wire/API/Event/Federation.hs | 2 +- libs/wire-api/src/Wire/API/Event/Team.hs | 2 +- .../wire-api/src/Wire/API/FederationStatus.hs | 2 +- .../src/Wire/API/Internal/BulkPush.hs | 2 +- .../src/Wire/API/Internal/Notification.hs | 2 +- libs/wire-api/src/Wire/API/MLS/CipherSuite.hs | 4 +- .../wire-api/src/Wire/API/MLS/CommitBundle.hs | 2 +- libs/wire-api/src/Wire/API/MLS/Credential.hs | 6 +- libs/wire-api/src/Wire/API/MLS/Group.hs | 2 +- libs/wire-api/src/Wire/API/MLS/KeyPackage.hs | 2 +- libs/wire-api/src/Wire/API/MLS/Keys.hs | 2 +- libs/wire-api/src/Wire/API/MLS/Message.hs | 2 +- .../src/Wire/API/MLS/PublicGroupState.hs | 2 +- .../src/Wire/API/MLS/Serialisation.hs | 2 +- .../src/Wire/API/MLS/SubConversation.hs | 2 +- libs/wire-api/src/Wire/API/MLS/Welcome.hs | 2 +- .../src/Wire/API/MakesFederatedCall.hs | 48 +++++---- libs/wire-api/src/Wire/API/Message.hs | 6 +- libs/wire-api/src/Wire/API/Notification.hs | 4 +- libs/wire-api/src/Wire/API/OAuth.hs | 10 +- libs/wire-api/src/Wire/API/Properties.hs | 6 +- libs/wire-api/src/Wire/API/Provider/Bot.hs | 2 +- .../wire-api/src/Wire/API/Provider/Service.hs | 2 +- .../src/Wire/API/Provider/Service/Tag.hs | 36 ++++++- libs/wire-api/src/Wire/API/Push/V2/Token.hs | 4 +- libs/wire-api/src/Wire/API/RawJson.hs | 4 +- libs/wire-api/src/Wire/API/Routes/API.hs | 8 +- .../wire-api/src/Wire/API/Routes/AssetBody.hs | 6 +- libs/wire-api/src/Wire/API/Routes/Bearer.hs | 10 +- libs/wire-api/src/Wire/API/Routes/CSV.hs | 12 +++ libs/wire-api/src/Wire/API/Routes/Cookies.hs | 6 +- .../Wire/API/Routes/FederationDomainConfig.hs | 2 +- .../src/Wire/API/Routes/Internal/Brig.hs | 12 +-- .../API/Routes/Internal/Brig/Connection.hs | 2 +- .../src/Wire/API/Routes/Internal/Brig/EJPD.hs | 2 +- .../Wire/API/Routes/Internal/Brig/OAuth.hs | 2 +- .../API/Routes/Internal/Brig/SearchIndex.hs | 2 +- .../src/Wire/API/Routes/Internal/Cannon.hs | 8 +- .../src/Wire/API/Routes/Internal/Cargohold.hs | 8 +- .../src/Wire/API/Routes/Internal/Galley.hs | 8 +- .../Internal/Galley/ConversationsIntra.hs | 2 +- .../Galley/TeamFeatureNoConfigMulti.hs | 2 +- .../API/Routes/Internal/Galley/TeamsIntra.hs | 2 +- .../src/Wire/API/Routes/Internal/LegalHold.hs | 9 +- .../src/Wire/API/Routes/Internal/Spar.hs | 8 +- .../src/Wire/API/Routes/LowLevelStream.hs | 21 ++-- .../src/Wire/API/Routes/MultiTablePaging.hs | 9 +- .../Wire/API/Routes/MultiTablePaging/State.hs | 2 +- .../wire-api/src/Wire/API/Routes/MultiVerb.hs | 100 ++++++++++++------ libs/wire-api/src/Wire/API/Routes/Named.hs | 11 +- libs/wire-api/src/Wire/API/Routes/Public.hs | 41 +++---- .../src/Wire/API/Routes/Public/Brig.hs | 18 ++-- .../src/Wire/API/Routes/Public/Brig/Bot.hs | 2 +- .../src/Wire/API/Routes/Public/Brig/OAuth.hs | 2 +- .../src/Wire/API/Routes/Public/Cargohold.hs | 2 +- .../src/Wire/API/Routes/Public/Galley.hs | 2 +- .../src/Wire/API/Routes/Public/Galley/Bot.hs | 2 +- .../API/Routes/Public/Galley/Conversation.hs | 10 +- .../API/Routes/Public/Galley/CustomBackend.hs | 2 +- .../Wire/API/Routes/Public/Galley/Feature.hs | 2 +- .../API/Routes/Public/Galley/LegalHold.hs | 2 +- .../src/Wire/API/Routes/Public/Galley/MLS.hs | 3 +- .../API/Routes/Public/Galley/Messaging.hs | 2 +- .../src/Wire/API/Routes/Public/Galley/Team.hs | 2 +- .../Routes/Public/Galley/TeamConversation.hs | 2 +- .../API/Routes/Public/Galley/TeamMember.hs | 2 +- .../src/Wire/API/Routes/Public/Spar.hs | 8 +- .../src/Wire/API/Routes/Public/Util.hs | 2 +- .../src/Wire/API/Routes/QualifiedCapture.hs | 13 ++- libs/wire-api/src/Wire/API/Routes/Version.hs | 7 +- .../wire-api/src/Wire/API/Routes/Versioned.hs | 16 +-- .../wire-api/src/Wire/API/Routes/WebSocket.hs | 10 +- libs/wire-api/src/Wire/API/ServantProto.hs | 2 +- libs/wire-api/src/Wire/API/SwaggerHelper.hs | 66 +++++++++++- libs/wire-api/src/Wire/API/SwaggerServant.hs | 6 +- libs/wire-api/src/Wire/API/SystemSettings.hs | 4 +- libs/wire-api/src/Wire/API/Team.hs | 15 ++- .../src/Wire/API/Team/Conversation.hs | 2 +- libs/wire-api/src/Wire/API/Team/Feature.hs | 10 +- libs/wire-api/src/Wire/API/Team/Invitation.hs | 4 +- libs/wire-api/src/Wire/API/Team/LegalHold.hs | 6 +- .../src/Wire/API/Team/LegalHold/External.hs | 2 +- .../src/Wire/API/Team/LegalHold/Internal.hs | 2 +- libs/wire-api/src/Wire/API/Team/Member.hs | 10 +- libs/wire-api/src/Wire/API/Team/Permission.hs | 2 +- libs/wire-api/src/Wire/API/Team/Role.hs | 4 +- .../src/Wire/API/Team/SearchVisibility.hs | 2 +- libs/wire-api/src/Wire/API/Team/Size.hs | 2 +- libs/wire-api/src/Wire/API/Unreachable.hs | 2 +- libs/wire-api/src/Wire/API/User.hs | 8 +- libs/wire-api/src/Wire/API/User/Activation.hs | 4 +- libs/wire-api/src/Wire/API/User/Auth.hs | 4 +- .../src/Wire/API/User/Auth/LegalHold.hs | 2 +- .../wire-api/src/Wire/API/User/Auth/ReAuth.hs | 2 +- libs/wire-api/src/Wire/API/User/Auth/Sso.hs | 2 +- libs/wire-api/src/Wire/API/User/Client.hs | 6 +- .../Wire/API/User/Client/DPoPAccessToken.hs | 4 +- .../src/Wire/API/User/Client/Prekey.hs | 2 +- libs/wire-api/src/Wire/API/User/Handle.hs | 2 +- libs/wire-api/src/Wire/API/User/Identity.hs | 6 +- .../src/Wire/API/User/IdentityProvider.hs | 10 +- libs/wire-api/src/Wire/API/User/Orphans.hs | 10 +- libs/wire-api/src/Wire/API/User/Password.hs | 4 +- libs/wire-api/src/Wire/API/User/Profile.hs | 2 +- libs/wire-api/src/Wire/API/User/RichInfo.hs | 2 +- libs/wire-api/src/Wire/API/User/Saml.hs | 2 +- libs/wire-api/src/Wire/API/User/Scim.hs | 10 +- libs/wire-api/src/Wire/API/User/Search.hs | 8 +- libs/wire-api/src/Wire/API/UserMap.hs | 4 +- libs/wire-api/src/Wire/API/Wrapped.hs | 4 +- .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 4 +- .../test/unit/Test/Wire/API/Swagger.hs | 4 +- libs/wire-api/wire-api.cabal | 7 +- nix/haskell-pins.nix | 16 +-- nix/manual-overrides.nix | 2 +- services/brig/brig.cabal | 4 +- services/brig/default.nix | 8 +- services/brig/src/Brig/API/Internal.hs | 2 +- services/brig/src/Brig/API/Public.hs | 7 +- services/brig/src/Brig/API/Public/Swagger.hs | 14 ++- services/brig/src/Brig/User/EJPD.hs | 2 +- services/spar/default.nix | 8 +- services/spar/spar.cabal | 4 +- services/spar/test/Arbitrary.hs | 2 +- services/spar/test/Test/Spar/APISpec.hs | 2 +- tools/fedcalls/default.nix | 10 +- tools/fedcalls/fedcalls.cabal | 7 +- tools/fedcalls/src/Main.hs | 88 ++++++++------- tools/stern/default.nix | 8 +- tools/stern/src/Stern/API/Routes.hs | 8 +- tools/stern/src/Stern/Types.hs | 8 +- tools/stern/stern.cabal | 4 +- 191 files changed, 890 insertions(+), 579 deletions(-) create mode 100644 changelog.d/4-docs/WPB-4240 create mode 100644 changelog.d/5-internal/WPB-4240 create mode 100644 libs/wire-api/src/Wire/API/Deprecated.hs diff --git a/changelog.d/4-docs/WPB-4240 b/changelog.d/4-docs/WPB-4240 new file mode 100644 index 00000000000..d7dd76196ec --- /dev/null +++ b/changelog.d/4-docs/WPB-4240 @@ -0,0 +1 @@ +Updating the route documentation from Swagger 2 to OpenAPI 3. \ No newline at end of file diff --git a/changelog.d/5-internal/WPB-4240 b/changelog.d/5-internal/WPB-4240 new file mode 100644 index 00000000000..bca7dcb1fc6 --- /dev/null +++ b/changelog.d/5-internal/WPB-4240 @@ -0,0 +1,4 @@ +Updating the route documentation library from swagger2 to openapi3. + +This also introduced a breaking change in how we track what federation calls each route makes. +The openapi3 library doesn't support extension fields, and as such tags are being used instead in a similar way. \ No newline at end of file diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index fb0afa4af77..faac2030515 100644 --- a/libs/brig-types/brig-types.cabal +++ b/libs/brig-types/brig-types.cabal @@ -156,8 +156,8 @@ test-suite brig-types-tests , brig-types , bytestring-conversion >=0.3.1 , imports + , openapi3 , QuickCheck >=2.9 - , swagger2 >=2.5 , tasty , tasty-hunit , tasty-quickcheck diff --git a/libs/brig-types/default.nix b/libs/brig-types/default.nix index 49028b0de48..173b83591b0 100644 --- a/libs/brig-types/default.nix +++ b/libs/brig-types/default.nix @@ -13,8 +13,8 @@ , gitignoreSource , imports , lib +, openapi3 , QuickCheck -, swagger2 , tasty , tasty-hunit , tasty-quickcheck @@ -47,8 +47,8 @@ mkDerivation { base bytestring-conversion imports + openapi3 QuickCheck - swagger2 tasty tasty-hunit tasty-quickcheck diff --git a/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs b/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs index 9ea421c6c2f..13cfc3570e6 100644 --- a/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs +++ b/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs @@ -20,7 +20,7 @@ module Test.Brig.Roundtrip where import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) import Data.Aeson.Types (parseEither) import Data.ByteString.Conversion -import Data.Swagger (ToSchema, validatePrettyToJSON) +import Data.OpenApi (ToSchema, validatePrettyToJSON) import Imports import Test.Tasty (TestTree) import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (.&&.), (===)) @@ -40,7 +40,7 @@ testRoundTrip = testProperty msg trip testRoundTripWithSwagger :: forall a. - (Arbitrary a, Typeable a, ToJSON a, FromJSON a, ToSchema a, Eq a, Show a) => + (Arbitrary a, ToJSON a, FromJSON a, ToSchema a, Eq a, Show a) => TestTree testRoundTripWithSwagger = testProperty msg (trip .&&. scm) where diff --git a/libs/deriving-swagger2/default.nix b/libs/deriving-swagger2/default.nix index fdf39de254a..5359dbec579 100644 --- a/libs/deriving-swagger2/default.nix +++ b/libs/deriving-swagger2/default.nix @@ -8,12 +8,12 @@ , gitignoreSource , imports , lib -, swagger2 +, openapi3 }: mkDerivation { pname = "deriving-swagger2"; version = "0.1.0"; src = gitignoreSource ./.; - libraryHaskellDepends = [ base extra imports swagger2 ]; + libraryHaskellDepends = [ base extra imports openapi3 ]; license = lib.licenses.agpl3Only; } diff --git a/libs/deriving-swagger2/deriving-swagger2.cabal b/libs/deriving-swagger2/deriving-swagger2.cabal index 4d68184d8c4..6e5b3f9de4a 100644 --- a/libs/deriving-swagger2/deriving-swagger2.cabal +++ b/libs/deriving-swagger2/deriving-swagger2.cabal @@ -62,9 +62,9 @@ library -Wredundant-constraints -Wunused-packages build-depends: - base >=4 && <5 + base >=4 && <5 , extra , imports - , swagger2 >=0.6 + , openapi3 default-language: GHC2021 diff --git a/libs/deriving-swagger2/src/Deriving/Swagger.hs b/libs/deriving-swagger2/src/Deriving/Swagger.hs index 3f0fc3b56f9..95a0c121a3e 100644 --- a/libs/deriving-swagger2/src/Deriving/Swagger.hs +++ b/libs/deriving-swagger2/src/Deriving/Swagger.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE RankNTypes #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -22,10 +24,10 @@ module Deriving.Swagger where import Data.Char qualified as Char import Data.Kind (Constraint) import Data.List.Extra (stripSuffix) +import Data.OpenApi.Internal.Schema (GToSchema) +import Data.OpenApi.Internal.TypeShape +import Data.OpenApi.Schema import Data.Proxy (Proxy (..)) -import Data.Swagger (SchemaOptions, ToSchema (..), constructorTagModifier, defaultSchemaOptions, fieldLabelModifier, genericDeclareNamedSchema) -import Data.Swagger.Internal.Schema (GToSchema) -import Data.Swagger.Internal.TypeShape (TypeHasSimpleShape) import GHC.Generics (Generic (Rep)) import GHC.TypeLits (ErrorMessage (Text), KnownSymbol, Symbol, TypeError, symbolVal) import Imports @@ -81,6 +83,7 @@ import Imports -- | A newtype wrapper which gives ToSchema instances with modified options. -- 't' has to have an instance of the 'SwaggerOptions' class. newtype CustomSwagger t a = CustomSwagger {unCustomSwagger :: a} + deriving (Generic, Typeable) class SwaggerOptions xs where swaggerOptions :: SchemaOptions @@ -94,14 +97,7 @@ instance (StringModifier f, SwaggerOptions xs) => SwaggerOptions (FieldLabelModi instance (StringModifier f, SwaggerOptions xs) => SwaggerOptions (ConstructorTagModifier f ': xs) where swaggerOptions = (swaggerOptions @xs) {constructorTagModifier = getStringModifier @f} -instance - ( SwaggerOptions t, - Generic a, - GToSchema (Rep a), - TypeHasSimpleShape a "genericDeclareNamedSchemaUnrestricted" - ) => - ToSchema (CustomSwagger t a) - where +instance (SwaggerOptions t, Generic a, Typeable a, GToSchema (Rep a), Typeable (CustomSwagger t a), TypeHasSimpleShape a "genericDeclareNamedSchemaUnrestricted") => ToSchema (CustomSwagger t a) where declareNamedSchema _ = genericDeclareNamedSchema (swaggerOptions @t) (Proxy @a) -- ** Specify __what__ to modify diff --git a/libs/extended/default.nix b/libs/extended/default.nix index d2fd00ab9cb..b44a955a35f 100644 --- a/libs/extended/default.nix +++ b/libs/extended/default.nix @@ -27,8 +27,8 @@ , servant , servant-client , servant-client-core +, servant-openapi3 , servant-server -, servant-swagger , temporary , text , tinylog @@ -60,8 +60,8 @@ mkDerivation { servant servant-client servant-client-core + servant-openapi3 servant-server - servant-swagger text tinylog unliftio diff --git a/libs/extended/extended.cabal b/libs/extended/extended.cabal index 2271f8d1312..389b59b9447 100644 --- a/libs/extended/extended.cabal +++ b/libs/extended/extended.cabal @@ -98,8 +98,8 @@ library , servant , servant-client , servant-client-core + , servant-openapi3 , servant-server - , servant-swagger , text , tinylog , unliftio diff --git a/libs/extended/src/Servant/API/Extended.hs b/libs/extended/src/Servant/API/Extended.hs index 322b029f1b4..c1e87f38beb 100644 --- a/libs/extended/src/Servant/API/Extended.hs +++ b/libs/extended/src/Servant/API/Extended.hs @@ -31,8 +31,8 @@ import Network.Wai import Servant.API import Servant.API.ContentTypes import Servant.API.Modifiers +import Servant.OpenApi import Servant.Server.Internal -import Servant.Swagger import Prelude () -- | Like 'ReqBody'', but takes parsers that throw 'ServerError', not 'String'. @tag@ is used @@ -108,10 +108,10 @@ instance Right v -> pure v instance - HasSwagger (ReqBody' '[Required, Strict] cts a :> api) => - HasSwagger (ReqBodyCustomError cts tag a :> api) + HasOpenApi (ReqBody' '[Required, Strict] cts a :> api) => + HasOpenApi (ReqBodyCustomError cts tag a :> api) where - toSwagger Proxy = toSwagger (Proxy @(ReqBody' '[Required, Strict] cts a :> api)) + toOpenApi Proxy = toOpenApi (Proxy @(ReqBody' '[Required, Strict] cts a :> api)) instance RoutesToPaths rest => RoutesToPaths (ReqBodyCustomError' mods list tag a :> rest) where getRoutes = getRoutes @rest diff --git a/libs/extended/src/Servant/API/Extended/RawM.hs b/libs/extended/src/Servant/API/Extended/RawM.hs index 9f1e1a6395f..f5108d12329 100644 --- a/libs/extended/src/Servant/API/Extended/RawM.hs +++ b/libs/extended/src/Servant/API/Extended/RawM.hs @@ -10,11 +10,11 @@ import Data.Proxy import Imports import Network.Wai import Servant.API (Raw) +import Servant.OpenApi import Servant.Server hiding (respond) import Servant.Server.Internal.Delayed import Servant.Server.Internal.RouteResult import Servant.Server.Internal.Router -import Servant.Swagger type ApplicationM m = Request -> (Response -> IO ResponseReceived) -> m ResponseReceived @@ -51,8 +51,8 @@ instance HasServer RawM context where hoistServerWithContext _ _ f srvM req respond = f (srvM req respond) -instance HasSwagger RawM where - toSwagger _ = toSwagger (Proxy @Raw) +instance HasOpenApi RawM where + toOpenApi _ = toOpenApi (Proxy @Raw) instance RoutesToPaths RawM where getRoutes = [] diff --git a/libs/schema-profunctor/default.nix b/libs/schema-profunctor/default.nix index a498d97378b..bede1bdeae6 100644 --- a/libs/schema-profunctor/default.nix +++ b/libs/schema-profunctor/default.nix @@ -14,8 +14,8 @@ , insert-ordered-containers , lens , lib +, openapi3 , profunctors -, swagger2 , tasty , tasty-hunit , text @@ -34,8 +34,8 @@ mkDerivation { containers imports lens + openapi3 profunctors - swagger2 text transformers vector @@ -47,7 +47,7 @@ mkDerivation { imports insert-ordered-containers lens - swagger2 + openapi3 tasty tasty-hunit text diff --git a/libs/schema-profunctor/schema-profunctor.cabal b/libs/schema-profunctor/schema-profunctor.cabal index c9c534c0165..236a68a841b 100644 --- a/libs/schema-profunctor/schema-profunctor.cabal +++ b/libs/schema-profunctor/schema-profunctor.cabal @@ -69,8 +69,8 @@ library , containers , imports , lens + , openapi3 , profunctors - , swagger2 >=2 && <2.9 , text , transformers , vector @@ -139,8 +139,8 @@ test-suite schemas-tests , imports , insert-ordered-containers , lens + , openapi3 , schema-profunctor - , swagger2 , tasty , tasty-hunit , text diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index 6ff30f7ed38..9ae1187481f 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -100,12 +100,11 @@ import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as NonEmpty import Data.Map qualified as Map import Data.Monoid hiding (Product) +import Data.OpenApi qualified as S +import Data.OpenApi.Declare qualified as S import Data.Profunctor (Star (..)) import Data.Proxy (Proxy (..)) import Data.Set qualified as Set -import Data.Swagger qualified as S -import Data.Swagger.Declare qualified as S -import Data.Swagger.Internal qualified as S import Data.Text qualified as T import Data.Text.Lazy qualified as TL import Data.Vector qualified as V @@ -624,7 +623,7 @@ text name = (A.withText (T.unpack name) pure) (pure . A.String) where - d = mempty & S.type_ ?~ S.SwaggerString + d = mempty & S.type_ ?~ S.OpenApiString -- | A schema for a textual value with possible failure. parsedText :: @@ -764,7 +763,7 @@ instance HasSchemaRef doc => HasField doc SwaggerDoc where where f ref = mempty - & S.type_ ?~ S.SwaggerObject + & S.type_ ?~ S.OpenApiObject & S.properties . at name ?~ ref & S.required .~ [name] @@ -780,8 +779,8 @@ instance HasSchemaRef ndoc => HasArray ndoc SwaggerDoc where f :: S.Referenced S.Schema -> S.Schema f ref = mempty - & S.type_ ?~ S.SwaggerArray - & S.items ?~ S.SwaggerItemsObject ref + & S.type_ ?~ S.OpenApiArray + & S.items ?~ S.OpenApiItemsObject ref instance HasSchemaRef ndoc => HasMap ndoc SwaggerDoc where mkMap = fmap f . schemaRef @@ -789,7 +788,7 @@ instance HasSchemaRef ndoc => HasMap ndoc SwaggerDoc where f :: S.Referenced S.Schema -> S.Schema f ref = mempty - & S.type_ ?~ S.SwaggerObject + & S.type_ ?~ S.OpenApiObject & S.additionalProperties ?~ S.AdditionalPropertiesSchema ref class HasMinItems s a where @@ -799,19 +798,19 @@ instance HasMinItems SwaggerDoc (Maybe Integer) where minItems = declared . S.minItems instance HasEnum Text NamedSwaggerDoc where - mkEnum = mkSwaggerEnum S.SwaggerString + mkEnum = mkSwaggerEnum S.OpenApiString instance HasEnum Integer NamedSwaggerDoc where - mkEnum = mkSwaggerEnum S.SwaggerInteger + mkEnum = mkSwaggerEnum S.OpenApiInteger instance HasEnum Natural NamedSwaggerDoc where - mkEnum = mkSwaggerEnum S.SwaggerInteger + mkEnum = mkSwaggerEnum S.OpenApiInteger instance HasEnum Bool NamedSwaggerDoc where - mkEnum = mkSwaggerEnum S.SwaggerBoolean + mkEnum = mkSwaggerEnum S.OpenApiBoolean mkSwaggerEnum :: - S.SwaggerType 'S.SwaggerKindSchema -> + S.OpenApiType -> Text -> [A.Value] -> NamedSwaggerDoc @@ -839,11 +838,12 @@ class ToSchema a where -- Newtype wrappers for deriving via newtype Schema a = Schema {getSchema :: a} + deriving (Generic) schemaToSwagger :: forall a. ToSchema a => Proxy a -> Declare S.NamedSchema schemaToSwagger _ = runDeclare (schemaDoc (schema @a)) -instance ToSchema a => S.ToSchema (Schema a) where +instance (Typeable a, ToSchema a) => S.ToSchema (Schema a) where declareNamedSchema _ = schemaToSwagger (Proxy @a) -- | JSON serialiser for an instance of 'ToSchema'. @@ -920,8 +920,14 @@ instance S.HasSchema d S.Schema => S.HasSchema (SchemaP d v w a b) S.Schema wher instance S.HasDescription NamedSwaggerDoc (Maybe Text) where description = declared . S.schema . S.description +instance S.HasDeprecated NamedSwaggerDoc (Maybe Bool) where + deprecated = declared . S.schema . S.deprecated + instance {-# OVERLAPPABLE #-} S.HasDescription s a => S.HasDescription (WithDeclare s) a where description = declared . S.description +instance {-# OVERLAPPABLE #-} S.HasDeprecated s a => S.HasDeprecated (WithDeclare s) a where + deprecated = declared . S.deprecated + instance {-# OVERLAPPABLE #-} S.HasExample s a => S.HasExample (WithDeclare s) a where example = declared . S.example diff --git a/libs/schema-profunctor/test/unit/Test/Data/Schema.hs b/libs/schema-profunctor/test/unit/Test/Data/Schema.hs index 5ee7af68a77..d29b69b2365 100644 --- a/libs/schema-profunctor/test/unit/Test/Data/Schema.hs +++ b/libs/schema-profunctor/test/unit/Test/Data/Schema.hs @@ -27,10 +27,10 @@ import Data.Aeson.QQ import Data.Aeson.Types qualified as A import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.List.NonEmpty (NonEmpty ((:|))) +import Data.OpenApi qualified as S +import Data.OpenApi.Declare qualified as S import Data.Proxy import Data.Schema hiding (getName) -import Data.Swagger qualified as S -import Data.Swagger.Declare qualified as S import Data.Text qualified as Text import Imports import Test.Tasty @@ -290,7 +290,7 @@ testNonEmptySchema = Nothing -> assertFailure "expected schema to have a property called 'nl'" Just (S.Ref _) -> assertFailure "expected property 'nl' to have inline schema" Just (S.Inline nlSch) -> do - assertEqual "type should be Array" (Just S.SwaggerArray) (nlSch ^. S.type_) + assertEqual "type should be Array" (Just S.OpenApiArray) (nlSch ^. S.type_) assertEqual "minItems should be 1" (Just 1) (nlSch ^. S.minItems) testRefField :: TestTree @@ -332,7 +332,7 @@ testEnumType = assertEqual "Text enum has Swagger type \"string\"" (s1 ^. S.type_) - (Just S.SwaggerString) + (Just S.OpenApiString) let e2 :: ValueSchema NamedSwaggerDoc Integer e2 = enum @Integer "IntEnum" (element (3 :: Integer) (3 :: Integer)) @@ -340,7 +340,7 @@ testEnumType = assertEqual "Integer enum has Swagger type \"integer\"" (s2 ^. S.type_) - (Just S.SwaggerInteger) + (Just S.OpenApiInteger) testNullable :: TestTree testNullable = diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index 8b4da990200..a5c57f5f05d 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -32,6 +32,7 @@ , lens-datetime , lib , mime +, openapi3 , optparse-applicative , pem , protobuf @@ -40,7 +41,6 @@ , random , schema-profunctor , servant-server -, swagger2 , tagged , tasty , tasty-hunit @@ -86,6 +86,7 @@ mkDerivation { lens lens-datetime mime + openapi3 optparse-applicative pem protobuf @@ -94,7 +95,6 @@ mkDerivation { random schema-profunctor servant-server - swagger2 tagged tasty tasty-hunit diff --git a/libs/types-common/src/Data/Code.hs b/libs/types-common/src/Data/Code.hs index 1820d85f403..ba176629701 100644 --- a/libs/types-common/src/Data/Code.hs +++ b/libs/types-common/src/Data/Code.hs @@ -31,11 +31,11 @@ import Data.Aeson.TH import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Conversion import Data.Json.Util +import Data.OpenApi qualified as S +import Data.OpenApi.ParamSchema import Data.Proxy (Proxy (Proxy)) import Data.Range import Data.Schema -import Data.Swagger qualified as S -import Data.Swagger.ParamSchema import Data.Text (pack) import Data.Text.Ascii import Data.Text.Encoding (encodeUtf8) diff --git a/libs/types-common/src/Data/CommaSeparatedList.hs b/libs/types-common/src/Data/CommaSeparatedList.hs index 8e3ebd0edd8..8c13c49f4cf 100644 --- a/libs/types-common/src/Data/CommaSeparatedList.hs +++ b/libs/types-common/src/Data/CommaSeparatedList.hs @@ -22,9 +22,9 @@ module Data.CommaSeparatedList where import Control.Lens ((?~)) import Data.Bifunctor qualified as Bifunctor import Data.ByteString.Conversion (FromByteString, List, fromList, parser, runParser) +import Data.OpenApi import Data.Proxy (Proxy (..)) import Data.Range (Bounds, Range) -import Data.Swagger (CollectionFormat (CollectionCSV), SwaggerItems (SwaggerItemsPrimitive), SwaggerType (SwaggerString), ToParamSchema (..), items, type_) import Data.Text qualified as Text import Data.Text.Encoding (encodeUtf8) import Imports @@ -40,10 +40,10 @@ instance FromByteString (List a) => FromHttpApiData (CommaSeparatedList a) where CommaSeparatedList . fromList <$> Bifunctor.first Text.pack (runParser parser $ encodeUtf8 t) instance ToParamSchema (CommaSeparatedList a) where - toParamSchema _ = mempty & type_ ?~ SwaggerString + toParamSchema _ = mempty & type_ ?~ OpenApiString -- | TODO: is this obsoleted by the instances in "Data.Range"? instance (ToParamSchema a, ToParamSchema (Range n m [a])) => ToParamSchema (Range n m (CommaSeparatedList a)) where toParamSchema _ = toParamSchema (Proxy @(Range n m [a])) - & items ?~ SwaggerItemsPrimitive (Just CollectionCSV) (toParamSchema (Proxy @a)) + & items ?~ OpenApiItemsArray [Inline $ toParamSchema (Proxy @a)] diff --git a/libs/types-common/src/Data/Domain.hs b/libs/types-common/src/Data/Domain.hs index 8f96dc18bcb..6f9d0884405 100644 --- a/libs/types-common/src/Data/Domain.hs +++ b/libs/types-common/src/Data/Domain.hs @@ -31,8 +31,8 @@ import Data.ByteString qualified as BS import Data.ByteString.Builder qualified as Builder import Data.ByteString.Char8 qualified as BS.Char8 import Data.ByteString.Conversion +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding qualified as Text.E import Imports hiding (isAlphaNum) diff --git a/libs/types-common/src/Data/Handle.hs b/libs/types-common/src/Data/Handle.hs index 0d1e5220076..29d1570cc32 100644 --- a/libs/types-common/src/Data/Handle.hs +++ b/libs/types-common/src/Data/Handle.hs @@ -31,8 +31,8 @@ import Data.Bifunctor (Bifunctor (first)) import Data.ByteString qualified as BS import Data.ByteString.Conversion (FromByteString (parser), ToByteString) import Data.Hashable (Hashable) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding qualified as Text.E import Imports diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index cae7c686ce7..528725f888b 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -71,11 +71,11 @@ import Data.ByteString.Lazy qualified as L import Data.Char qualified as Char import Data.Default (Default (..)) import Data.Hashable (Hashable) +import Data.OpenApi qualified as S +import Data.OpenApi.Internal.ParamSchema (ToParamSchema (..)) import Data.ProtocolBuffers.Internal import Data.Proxy import Data.Schema -import Data.Swagger qualified as S -import Data.Swagger.Internal.ParamSchema (ToParamSchema (..)) import Data.Text qualified as T import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.Lazy (toStrict) diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index 62e7168d728..408dfe41cbc 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -62,8 +62,8 @@ import Data.ByteString.Builder qualified as BB import Data.ByteString.Conversion qualified as BS import Data.ByteString.Lazy qualified as L import Data.Fixed +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Data.Text.Encoding.Error qualified as Text @@ -161,7 +161,7 @@ instance ToJSONObject A.Object where instance S.ToParamSchema A.Object where toParamSchema _ = - mempty & S.type_ ?~ S.SwaggerString + mempty & S.type_ ?~ S.OpenApiString instance ToSchema A.Object where schema = @@ -209,7 +209,7 @@ instance ToHttpApiData Base64ByteString where toUrlPiece = Text.decodeUtf8With Text.lenientDecode . B64U.encodeUnpadded . fromBase64ByteString instance S.ToParamSchema Base64ByteString where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString -- base64("example") ~> "ZXhhbXBsZQo=" base64SchemaN :: ValueSchema NamedSwaggerDoc ByteString @@ -245,7 +245,7 @@ instance ToHttpApiData Base64ByteStringL where toUrlPiece = toUrlPiece . base64ToStrict instance S.ToParamSchema Base64ByteStringL where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString base64SchemaLN :: ValueSchema NamedSwaggerDoc LByteString base64SchemaLN = L.toStrict .= fmap L.fromStrict base64SchemaN diff --git a/libs/types-common/src/Data/LegalHold.hs b/libs/types-common/src/Data/LegalHold.hs index 7b328820e6c..02955c03f3d 100644 --- a/libs/types-common/src/Data/LegalHold.hs +++ b/libs/types-common/src/Data/LegalHold.hs @@ -20,8 +20,8 @@ module Data.LegalHold where import Cassandra.CQL import Control.Lens ((?~)) import Data.Aeson hiding (constructorTagModifier) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Test.QuickCheck diff --git a/libs/types-common/src/Data/List1.hs b/libs/types-common/src/Data/List1.hs index f77d578fed9..8a1d31555d2 100644 --- a/libs/types-common/src/Data/List1.hs +++ b/libs/types-common/src/Data/List1.hs @@ -25,8 +25,8 @@ import Cassandra import Data.Aeson import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as N +import Data.OpenApi qualified as Swagger import Data.Schema as S -import Data.Swagger qualified as Swagger import Imports import Test.QuickCheck (Arbitrary) import Test.QuickCheck.Instances () @@ -72,8 +72,8 @@ instance ToSchema a => ToSchema (List1 a) where instance Swagger.ToParamSchema (List1 a) where toParamSchema _ = mempty - { Swagger._paramSchemaType = Just Swagger.SwaggerArray, - Swagger._paramSchemaMinLength = Just 1 + { Swagger._schemaType = Just Swagger.OpenApiArray, + Swagger._schemaMinLength = Just 1 } instance (Cql a) => Cql (List1 a) where diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index 1b81d37aa31..8acd18dee2b 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -77,9 +77,9 @@ import Data.ByteString.Char8 (unpack) import Data.ByteString.Conversion import Data.ByteString.Lazy (toStrict) import Data.IP (IP (IPv4, IPv6), toIPv4, toIPv6b) +import Data.OpenApi qualified as S import Data.Range import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding (decodeUtf8, encodeUtf8) import GHC.TypeLits (Nat) @@ -100,7 +100,7 @@ newtype IpAddr = IpAddr {ipAddr :: IP} deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema IpAddr) instance S.ToParamSchema IpAddr where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData IpAddr where parseQueryParam p = first Text.pack (runParser parser (encodeUtf8 p)) @@ -296,7 +296,7 @@ data Rsa newtype Fingerprint a = Fingerprint { fingerprintBytes :: ByteString } - deriving stock (Eq, Show, Generic) + deriving stock (Eq, Show, Generic, Typeable) deriving newtype (FromByteString, ToByteString, NFData) deriving via @@ -314,7 +314,7 @@ deriving via deriving via (Schema (Fingerprint a)) instance - (ToSchema (Fingerprint a)) => + (Typeable (Fingerprint a), ToSchema (Fingerprint a)) => S.ToSchema (Fingerprint a) instance ToSchema (Fingerprint Rsa) where @@ -378,7 +378,7 @@ deriving via (Schema (PlainTextPassword' tag)) instance ToSchema (PlainTextPassw deriving via (Schema (PlainTextPassword' tag)) instance ToSchema (PlainTextPassword' tag) => ToJSON (PlainTextPassword' tag) -deriving via (Schema (PlainTextPassword' tag)) instance ToSchema (PlainTextPassword' tag) => S.ToSchema (PlainTextPassword' tag) +deriving via (Schema (PlainTextPassword' tag)) instance (KnownNat tag, ToSchema (PlainTextPassword' tag)) => S.ToSchema (PlainTextPassword' tag) instance Show (PlainTextPassword' minLen) where show _ = "PlainTextPassword' " diff --git a/libs/types-common/src/Data/Nonce.hs b/libs/types-common/src/Data/Nonce.hs index 91befc4c3e8..1f094bab764 100644 --- a/libs/types-common/src/Data/Nonce.hs +++ b/libs/types-common/src/Data/Nonce.hs @@ -31,10 +31,10 @@ import Data.Aeson qualified as A import Data.ByteString.Base64.URL qualified as Base64 import Data.ByteString.Conversion import Data.ByteString.Lazy (fromStrict, toStrict) +import Data.OpenApi qualified as S +import Data.OpenApi.ParamSchema import Data.Proxy (Proxy (Proxy)) import Data.Schema -import Data.Swagger qualified as S -import Data.Swagger.ParamSchema import Data.UUID as UUID (UUID, fromByteString, toByteString) import Data.UUID.V4 (nextRandom) import Imports diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index 964f91e1ef5..1c1ba088e10 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -48,15 +48,16 @@ module Data.Qualified ) where -import Control.Lens (Lens, lens, (?~)) +import Control.Lens (Lens, lens, over, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bifunctor (first) import Data.Domain (Domain) import Data.Handle (Handle (..)) import Data.Id import Data.Map qualified as Map +import Data.OpenApi (deprecated) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports hiding (local) import Test.QuickCheck (Arbitrary (arbitrary)) @@ -163,8 +164,11 @@ isLocal loc = foldQualified loc (const True) (const False) ---------------------------------------------------------------------- -deprecatedSchema :: S.HasDescription doc (Maybe Text) => Text -> ValueSchema doc a -> ValueSchema doc a -deprecatedSchema new = doc . description ?~ ("Deprecated, use " <> new) +deprecatedSchema :: (S.HasDeprecated doc (Maybe Bool), S.HasDescription doc (Maybe Text)) => Text -> ValueSchema doc a -> ValueSchema doc a +deprecatedSchema new = + over doc $ + (description ?~ ("Deprecated, use " <> new)) + . (deprecated ?~ True) qualifiedSchema :: HasSchemaRef doc => @@ -198,7 +202,7 @@ instance KnownIdTag t => ToJSON (Qualified (Id t)) where instance KnownIdTag t => FromJSON (Qualified (Id t)) where parseJSON = schemaParseJSON -instance KnownIdTag t => S.ToSchema (Qualified (Id t)) where +instance (Typeable t, KnownIdTag t) => S.ToSchema (Qualified (Id t)) where declareNamedSchema = schemaToSwagger instance ToJSON (Qualified Handle) where diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index e4c5be14781..898df2142c1 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -74,13 +74,13 @@ import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as N import Data.List1 (List1, toNonEmpty) import Data.Map qualified as Map +import Data.OpenApi (Schema, ToParamSchema (..)) +import Data.OpenApi qualified as S import Data.Proxy -import Data.Schema +import Data.Schema hiding (Schema) import Data.Sequence (Seq) import Data.Sequence qualified as Seq import Data.Set qualified as Set -import Data.Swagger (ParamSchema, ToParamSchema (..)) -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Ascii (AsciiChar, AsciiChars, AsciiText, fromAsciiChars) import Data.Text.Ascii qualified as Ascii @@ -152,6 +152,9 @@ numRangedSchemaDocModifier n m = S.schema %~ ((S.minimum_ ?~ fromIntegral n) . ( instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d [a] where rangedSchemaDocModifier _ = listRangedSchemaDocModifier +-- Sets are similar to lists, so use that as our defininition +instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d (Set a) where rangedSchemaDocModifier _ = listRangedSchemaDocModifier + instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d Text where rangedSchemaDocModifier _ = stringRangedSchemaDocModifier instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d String where rangedSchemaDocModifier _ = stringRangedSchemaDocModifier @@ -232,7 +235,7 @@ instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m TL.Text) where & S.maxLength ?~ fromKnownNat (Proxy @n) & S.minLength ?~ fromKnownNat (Proxy @m) -instance S.ToSchema a => S.ToSchema (Range n m a) where +instance (KnownNat n, S.ToSchema a, KnownNat m) => S.ToSchema (Range n m a) where declareNamedSchema _ = S.declareNamedSchema (Proxy @a) @@ -316,7 +319,7 @@ rappend (Range a) (Range b) = Range (a <> b) rsingleton :: a -> Range 1 1 [a] rsingleton = Range . pure -rangedNumToParamSchema :: forall a n m t. (ToParamSchema a, Num a, KnownNat n, KnownNat m) => Proxy (Range n m a) -> ParamSchema t +rangedNumToParamSchema :: forall a n m. (ToParamSchema a, Num a, KnownNat n, KnownNat m) => Proxy (Range n m a) -> Schema rangedNumToParamSchema _ = toParamSchema (Proxy @a) & S.minimum_ ?~ fromKnownNat (Proxy @n) diff --git a/libs/types-common/src/Data/Text/Ascii.hs b/libs/types-common/src/Data/Text/Ascii.hs index 70712eabdba..0fac4b07e2f 100644 --- a/libs/types-common/src/Data/Text/Ascii.hs +++ b/libs/types-common/src/Data/Text/Ascii.hs @@ -86,8 +86,8 @@ import Data.ByteString.Base64.URL qualified as B64Url import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Conversion import Data.Hashable (Hashable) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding (decodeLatin1, decodeUtf8') import Imports @@ -156,7 +156,7 @@ instance AsciiChars c => ToJSON (AsciiText c) where instance AsciiChars c => FromJSON (AsciiText c) where parseJSON = schemaParseJSON -instance AsciiChars c => S.ToSchema (AsciiText c) where +instance (Typeable c, AsciiChars c) => S.ToSchema (AsciiText c) where declareNamedSchema = schemaToSwagger instance AsciiChars c => Cql (AsciiText c) where diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 9f2eb9391c7..4ce602225f1 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -116,6 +116,7 @@ library , lens >=4.10 , lens-datetime >=0.3 , mime >=0.4.0.2 + , openapi3 , optparse-applicative >=0.10 , pem , protobuf >=0.2 @@ -124,7 +125,6 @@ library , random >=1.1 , schema-profunctor , servant-server - , swagger2 , tagged >=0.8 , tasty >=0.11 , tasty-hunit diff --git a/libs/wai-utilities/default.nix b/libs/wai-utilities/default.nix index 33988b17bfd..bc345ab3586 100644 --- a/libs/wai-utilities/default.nix +++ b/libs/wai-utilities/default.nix @@ -18,12 +18,12 @@ , lib , metrics-core , metrics-wai +, openapi3 , pipes , prometheus-client , schema-profunctor , servant-server , streaming-commons -, swagger2 , text , tinylog , types-common @@ -52,12 +52,12 @@ mkDerivation { kan-extensions metrics-core metrics-wai + openapi3 pipes prometheus-client schema-profunctor servant-server streaming-commons - swagger2 text tinylog types-common diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs index 2cf2b2e644e..f1673e7de13 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs @@ -18,7 +18,7 @@ module Network.Wai.Utilities.Headers where import Data.ByteString.Conversion (FromByteString (..), ToByteString (..), fromByteString', toByteString') -import Data.Swagger.ParamSchema (ToParamSchema (..)) +import Data.OpenApi.ParamSchema (ToParamSchema (..)) import Data.Text as T import Imports import Servant (FromHttpApiData (..), Proxy (Proxy), ToHttpApiData (..)) diff --git a/libs/wai-utilities/wai-utilities.cabal b/libs/wai-utilities/wai-utilities.cabal index 44a3769dbf1..1c1ae75cbcc 100644 --- a/libs/wai-utilities/wai-utilities.cabal +++ b/libs/wai-utilities/wai-utilities.cabal @@ -86,12 +86,12 @@ library , kan-extensions , metrics-core >=0.1 , metrics-wai >=0.5.7 + , openapi3 , pipes >=4.1 , prometheus-client , schema-profunctor , servant-server , streaming-commons >=0.1 - , swagger2 , text >=0.11 , tinylog >=0.8 , types-common >=0.12 diff --git a/libs/wire-api-federation/default.nix b/libs/wire-api-federation/default.nix index 2ac0f43e549..0275616b33e 100644 --- a/libs/wire-api-federation/default.nix +++ b/libs/wire-api-federation/default.nix @@ -26,6 +26,7 @@ , lib , metrics-wai , mtl +, openapi3 , QuickCheck , schema-profunctor , servant @@ -34,7 +35,6 @@ , servant-server , singletons , singletons-th -, swagger2 , text , time , transformers @@ -66,6 +66,7 @@ mkDerivation { lens metrics-wai mtl + openapi3 QuickCheck schema-profunctor servant @@ -73,7 +74,6 @@ mkDerivation { servant-client-core servant-server singletons-th - swagger2 text time transformers diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs index 3a433e3fb2a..0f3e113db95 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs @@ -21,10 +21,10 @@ module Wire.API.Federation.Version where import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.OpenApi qualified as S import Data.Schema import Data.Set qualified as Set import Data.Singletons.TH -import Data.Swagger qualified as S import Imports import Wire.API.VersionInfo diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index 53862a2f92f..b19ff51c99d 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -97,6 +97,7 @@ library , lens , metrics-wai , mtl + , openapi3 , QuickCheck >=2.13 , schema-profunctor , servant >=0.16 @@ -104,7 +105,6 @@ library , servant-client-core , servant-server , singletons-th - , swagger2 , text >=0.11 , time >=1.8 , transformers diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index e6ec675d546..b76f3159a94 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -61,6 +61,7 @@ , metrics-wai , mime , mtl +, openapi3 , pem , polysemy , pretty @@ -81,13 +82,12 @@ , servant-client-core , servant-conduit , servant-multipart +, servant-openapi3 , servant-server -, servant-swagger , singletons , singletons-base , singletons-th , sop-core -, swagger2 , tagged , tasty , tasty-hspec @@ -167,6 +167,7 @@ mkDerivation { metrics-wai mime mtl + openapi3 pem polysemy proto-lens @@ -185,13 +186,12 @@ mkDerivation { servant-client-core servant-conduit servant-multipart + servant-openapi3 servant-server - servant-swagger singletons singletons-base singletons-th sop-core - swagger2 tagged text time @@ -239,6 +239,7 @@ mkDerivation { lens memory metrics-wai + openapi3 pem pretty process @@ -247,7 +248,6 @@ mkDerivation { schema-profunctor servant servant-server - swagger2 tasty tasty-hspec tasty-hunit diff --git a/libs/wire-api/src/Wire/API/Asset.hs b/libs/wire-api/src/Wire/API/Asset.hs index d8505a038f1..1658056c6d0 100644 --- a/libs/wire-api/src/Wire/API/Asset.hs +++ b/libs/wire-api/src/Wire/API/Asset.hs @@ -74,11 +74,11 @@ import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as LBS import Data.Id import Data.Json.Util (UTCTimeMillis (fromUTCTimeMillis), toUTCTimeMillis) +import Data.OpenApi qualified as S import Data.Proxy import Data.Qualified import Data.SOP import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Ascii (AsciiBase64Url) import Data.Text.Encoding qualified as T @@ -109,7 +109,7 @@ deriving via Schema (Asset' key) instance ToSchema (Asset' key) => (ToJSON (Asse deriving via Schema (Asset' key) instance ToSchema (Asset' key) => (FromJSON (Asset' key)) -deriving via Schema (Asset' key) instance ToSchema (Asset' key) => (S.ToSchema (Asset' key)) +deriving via Schema (Asset' key) instance (Typeable key, ToSchema (Asset' key)) => (S.ToSchema (Asset' key)) -- Generate expiry time with millisecond precision instance Arbitrary key => Arbitrary (Asset' key) where @@ -394,7 +394,7 @@ instance FromHttpApiData (AssetLocation Absolute) where instance S.ToParamSchema (AssetLocation r) where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.format ?~ "url" -- | An asset as returned by the download API: if the asset is local, only a diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index a458891e34c..18289ca1706 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -81,8 +81,8 @@ import Data.ByteString.Conversion qualified as BC import Data.IP qualified as IP import Data.List.NonEmpty (NonEmpty) import Data.Misc (HttpsUrl (..), IpAddr (IpAddr), Port (..)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Ascii import Data.Text.Encoding qualified as TE diff --git a/libs/wire-api/src/Wire/API/Connection.hs b/libs/wire-api/src/Wire/API/Connection.hs index a093c10c72e..138b6c3eb4b 100644 --- a/libs/wire-api/src/Wire/API/Connection.hs +++ b/libs/wire-api/src/Wire/API/Connection.hs @@ -45,10 +45,10 @@ import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Id import Data.Json.Util (UTCTimeMillis) +import Data.OpenApi qualified as S import Data.Qualified (Qualified (qUnqualified), deprecatedSchema) import Data.Range import Data.Schema -import Data.Swagger qualified as S import Data.Text as Text import Imports import Servant.API @@ -142,7 +142,7 @@ data Relation deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Relation) instance S.ToParamSchema Relation where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString -- | 'updateConnectionInternal', requires knowledge of the previous state (before -- 'MissingLegalholdConsent'), but the clients don't need that information. To avoid having diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index f28e178727a..bfe91520ec7 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -96,12 +96,13 @@ import Data.List.NonEmpty (NonEmpty) import Data.List1 import Data.Map qualified as Map import Data.Misc +import Data.OpenApi (deprecated) +import Data.OpenApi qualified as S import Data.Qualified import Data.Range (Range, fromRange, rangedSchema) import Data.SOP import Data.Schema import Data.Set qualified as Set -import Data.Swagger qualified as S import Data.UUID qualified as UUID import Data.UUID.V5 qualified as UUIDV5 import Imports @@ -567,14 +568,15 @@ instance ToSchema AccessRole where instance ToSchema AccessRoleLegacy where schema = - (S.schema . description ?~ desc) $ - enum @Text "AccessRoleLegacy" $ - mconcat - [ element "private" PrivateAccessRole, - element "team" TeamAccessRole, - element "activated" ActivatedAccessRole, - element "non_activated" NonActivatedAccessRole - ] + (S.schema . S.deprecated ?~ True) $ + (S.schema . description ?~ desc) $ + enum @Text "AccessRoleLegacy" $ + mconcat + [ element "private" PrivateAccessRole, + element "team" TeamAccessRole, + element "activated" ActivatedAccessRole, + element "non_activated" NonActivatedAccessRole + ] where desc = "Which users can join conversations (deprecated, use `access_role_v2` instead).\ @@ -670,7 +672,9 @@ newConvSchema sch = <$> newConvUsers .= ( fieldWithDocModifier "users" - (description ?~ usersDesc) + ( (deprecated ?~ True) + . (description ?~ usersDesc) + ) (array schema) <|> pure [] ) diff --git a/libs/wire-api/src/Wire/API/Conversation/Action.hs b/libs/wire-api/src/Wire/API/Conversation/Action.hs index 30156061710..4ede3530c6d 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action.hs @@ -38,10 +38,10 @@ import Data.Aeson.KeyMap qualified as A import Data.Id import Data.Kind import Data.List.NonEmpty qualified as NonEmptyList +import Data.OpenApi qualified as S import Data.Qualified (Qualified) import Data.Schema hiding (tag) import Data.Singletons.TH -import Data.Swagger qualified as S import Data.Time.Clock import Imports import Wire.API.Conversation diff --git a/libs/wire-api/src/Wire/API/Conversation/Bot.hs b/libs/wire-api/src/Wire/API/Conversation/Bot.hs index 2fd4a442cb3..f46a83869d4 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Bot.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Bot.hs @@ -28,8 +28,8 @@ where import Data.Aeson qualified as A import Data.Id +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.Event.Conversation (Event) import Wire.API.User.Client.Prekey (Prekey) diff --git a/libs/wire-api/src/Wire/API/Conversation/Code.hs b/libs/wire-api/src/Wire/API/Conversation/Code.hs index b99b4012df2..51a142ddd09 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Code.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Code.hs @@ -40,8 +40,8 @@ import Data.ByteString.Conversion (toByteString') -- FUTUREWORK: move content of Data.Code here? import Data.Code as Code import Data.Misc +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import URI.ByteString qualified as URI import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Conversation/Member.hs b/libs/wire-api/src/Wire/API/Conversation/Member.hs index 2bb1d7e3817..bb913a7ef38 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Member.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Member.hs @@ -38,9 +38,10 @@ import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Id +import Data.OpenApi (deprecated) +import Data.OpenApi qualified as S import Data.Qualified import Data.Schema -import Data.Swagger qualified as S import Imports import Test.QuickCheck qualified as QC import Wire.API.Conversation.Role @@ -141,7 +142,7 @@ instance ToSchema OtherMember where <* (qUnqualified . omQualifiedId) .= optional (field "id" schema) <*> omService .= maybe_ (optFieldWithDocModifier "service" (description ?~ desc) schema) <*> omConvRoleName .= (field "conversation_role" schema <|> pure roleNameWireAdmin) - <* const (0 :: Int) .= optional (fieldWithDocModifier "status" (description ?~ "deprecated") schema) -- TODO: remove + <* const (0 :: Int) .= optional (fieldWithDocModifier "status" ((deprecated ?~ True) . (description ?~ "deprecated")) schema) -- TODO: remove where desc = "The reference to the owning service, if the member is a 'bot'." diff --git a/libs/wire-api/src/Wire/API/Conversation/Role.hs b/libs/wire-api/src/Wire/API/Conversation/Role.hs index 1df79697f80..edb97c23f42 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Role.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Role.hs @@ -68,11 +68,11 @@ import Data.Aeson.TH qualified as A import Data.Attoparsec.Text import Data.ByteString.Conversion import Data.Hashable +import Data.OpenApi qualified as S import Data.Range (fromRange, genRangeText) import Data.Schema import Data.Set qualified as Set import Data.Singletons.TH -import Data.Swagger qualified as S import Deriving.Swagger qualified as S import GHC.TypeLits import Imports diff --git a/libs/wire-api/src/Wire/API/Conversation/Typing.hs b/libs/wire-api/src/Wire/API/Conversation/Typing.hs index 65e728c87c6..076dbde5e47 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Typing.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Typing.hs @@ -21,8 +21,8 @@ module Wire.API.Conversation.Typing where import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/CustomBackend.hs b/libs/wire-api/src/Wire/API/CustomBackend.hs index 73c3a525a06..f7c12e0140d 100644 --- a/libs/wire-api/src/Wire/API/CustomBackend.hs +++ b/libs/wire-api/src/Wire/API/CustomBackend.hs @@ -24,8 +24,8 @@ where import Control.Lens ((?~)) import Data.Misc (HttpsUrl) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Deriving.Aeson import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Deprecated.hs b/libs/wire-api/src/Wire/API/Deprecated.hs new file mode 100644 index 00000000000..c68120be996 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Deprecated.hs @@ -0,0 +1,60 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.Deprecated + ( Deprecated, + ) +where + +import Control.Lens +import Data.Kind (Type) +import Data.Metrics.Servant +import Data.OpenApi hiding (HasServer) +import Imports +import Servant +import Servant.Client +import Servant.OpenApi + +-- Annotate that the route is deprecated +data Deprecated deriving (Typeable) + +-- All of these instances are very similar to the instances +-- for Summary. These don't impact the API directly, but are +-- for marking the deprecated flag in the openapi output. +instance HasLink sub => HasLink (Deprecated :> sub :: Type) where + type MkLink (Deprecated :> sub) a = MkLink sub a + toLink = + let simpleToLink toA _ = toLink toA (Proxy :: Proxy sub) + in simpleToLink + +instance HasOpenApi api => HasOpenApi (Deprecated :> api :: Type) where + toOpenApi _ = + toOpenApi (Proxy @api) + & allOperations . deprecated ?~ True + +instance HasServer api ctx => HasServer (Deprecated :> api) ctx where + type ServerT (Deprecated :> api) m = ServerT api m + route _ = route $ Proxy @api + hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy @api) pc nt s + +instance HasClient m api => HasClient m (Deprecated :> api) where + type Client m (Deprecated :> api) = Client m api + clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api) + hoistClientMonad pm _ f cl = hoistClientMonad pm (Proxy :: Proxy api) f cl + +instance (RoutesToPaths rest) => RoutesToPaths (Deprecated :> rest) where + getRoutes = getRoutes @rest diff --git a/libs/wire-api/src/Wire/API/Error.hs b/libs/wire-api/src/Wire/API/Error.hs index 304f11596ec..8946a785721 100644 --- a/libs/wire-api/src/Wire/API/Error.hs +++ b/libs/wire-api/src/Wire/API/Error.hs @@ -49,12 +49,13 @@ where import Control.Lens (at, (%~), (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A +import Data.HashMap.Strict.InsOrd import Data.Kind import Data.Metrics.Servant +import Data.OpenApi qualified as S import Data.Proxy import Data.SOP import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Lazy qualified as LT import GHC.TypeLits @@ -65,7 +66,7 @@ import Network.Wai.Utilities.JSONResponse import Polysemy import Polysemy.Error import Servant -import Servant.Swagger +import Servant.OpenApi import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named) import Wire.API.Routes.Version @@ -185,23 +186,23 @@ instance (HasServer api ctx) => HasServer (CanThrowMany es :> api) ctx where hoistServerWithContext _ = hoistServerWithContext (Proxy @api) instance - (HasSwagger api, IsSwaggerError e) => - HasSwagger (CanThrow e :> api) + (HasOpenApi api, IsSwaggerError e) => + HasOpenApi (CanThrow e :> api) where - toSwagger _ = addToSwagger @e (toSwagger (Proxy @api)) + toOpenApi _ = addToOpenApi @e (toOpenApi (Proxy @api)) type instance SpecialiseToVersion v (CanThrowMany es :> api) = CanThrowMany es :> SpecialiseToVersion v api -instance HasSwagger api => HasSwagger (CanThrowMany '() :> api) where - toSwagger _ = toSwagger (Proxy @api) +instance HasOpenApi api => HasOpenApi (CanThrowMany '() :> api) where + toOpenApi _ = toOpenApi (Proxy @api) instance - (HasSwagger (CanThrowMany es :> api), IsSwaggerError e) => - HasSwagger (CanThrowMany '(e, es) :> api) + (HasOpenApi (CanThrowMany es :> api), IsSwaggerError e) => + HasOpenApi (CanThrowMany '(e, es) :> api) where - toSwagger _ = addToSwagger @e (toSwagger (Proxy @(CanThrowMany es :> api))) + toOpenApi _ = addToOpenApi @e (toOpenApi (Proxy @(CanThrowMany es :> api))) type family DeclaredErrorEffects api :: EffectRow where DeclaredErrorEffects (CanThrow e :> api) = (ErrorEffect e ': DeclaredErrorEffects api) @@ -211,15 +212,23 @@ type family DeclaredErrorEffects api :: EffectRow where DeclaredErrorEffects (Named n api) = DeclaredErrorEffects api DeclaredErrorEffects api = '[] -errorResponseSwagger :: forall e. KnownError e => S.Response +errorResponseSwagger :: forall e. (Typeable e, KnownError e) => S.Response errorResponseSwagger = mempty & S.description .~ (eMessage err <> " (label: `" <> eLabel err <> "`)") - & S.schema ?~ S.Inline (S.toSchema (Proxy @(SStaticError e))) + -- Defaulting this to JSON, as openapi3 needs something to map a schema against. + -- This _should_ be overridden with the actual media types once we are at the + -- point of rendering out the schemas for MultiVerb. + -- Check the instance of `S.HasOpenApi (MultiVerb method (cs :: [Type]) as r)` + & S.content .~ singleton mediaType mediaTypeObject where err = dynError @e + mediaType = contentType $ Proxy @JSON + mediaTypeObject = + mempty + & S.schema ?~ S.Inline (S.toSchema (Proxy @(SStaticError e))) -addErrorResponseToSwagger :: Int -> S.Response -> S.Swagger -> S.Swagger +addErrorResponseToSwagger :: Int -> S.Response -> S.OpenApi -> S.OpenApi addErrorResponseToSwagger code resp = S.allOperations . S.responses @@ -233,7 +242,7 @@ addErrorResponseToSwagger code resp = addRef (Just (S.Inline resp1)) = S.Inline (combineResponseSwagger resp1 resp) addRef (Just r@(S.Ref _)) = r -addStaticErrorToSwagger :: forall e. KnownError e => S.Swagger -> S.Swagger +addStaticErrorToSwagger :: forall e. (Typeable e, KnownError e) => S.OpenApi -> S.OpenApi addStaticErrorToSwagger = addErrorResponseToSwagger (fromIntegral (eCode (dynError @e))) @@ -244,7 +253,7 @@ type family MapError (e :: k) :: StaticError type family ErrorEffect (e :: k) :: Effect class IsSwaggerError e where - addToSwagger :: S.Swagger -> S.Swagger + addToOpenApi :: S.OpenApi -> S.OpenApi -- | An effect for a static error type with no data. type ErrorS e = Error (Tagged e ()) @@ -323,7 +332,7 @@ instance KnownError (MapError e) => AsConstructor '[] (ErrorResponse e) where toConstructor _ = Nil fromConstructor _ = dynError @(MapError e) -instance KnownError (MapError e) => IsSwaggerResponse (ErrorResponse e) where +instance (KnownError (MapError e), Typeable (MapError e)) => IsSwaggerResponse (ErrorResponse e) where responseSwagger = pure $ errorResponseSwagger @(MapError e) instance diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 8544a58d50d..85a280b171c 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -17,6 +17,7 @@ module Wire.API.Error.Brig where +import Data.Data import Wire.API.Error data BrigError @@ -86,8 +87,8 @@ data BrigError | ServiceDisabled | InvalidBot -instance KnownError (MapError e) => IsSwaggerError (e :: BrigError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: BrigError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) type instance MapError 'ServiceDisabled = 'StaticError 403 "service-disabled" "The desired service is currently disabled." diff --git a/libs/wire-api/src/Wire/API/Error/Cannon.hs b/libs/wire-api/src/Wire/API/Error/Cannon.hs index 7cdca830697..6dea237c1fc 100644 --- a/libs/wire-api/src/Wire/API/Error/Cannon.hs +++ b/libs/wire-api/src/Wire/API/Error/Cannon.hs @@ -17,14 +17,15 @@ module Wire.API.Error.Cannon where +import Data.Data import Wire.API.Error data CannonError = ClientGone | PresenceNotRegistered -instance KnownError (MapError e) => IsSwaggerError (e :: CannonError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: CannonError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) type instance MapError 'ClientGone = 'StaticError 410 "general" "client gone" diff --git a/libs/wire-api/src/Wire/API/Error/Cargohold.hs b/libs/wire-api/src/Wire/API/Error/Cargohold.hs index 26087509d12..0c4f17015cc 100644 --- a/libs/wire-api/src/Wire/API/Error/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Error/Cargohold.hs @@ -17,6 +17,7 @@ module Wire.API.Error.Cargohold where +import Data.Typeable import Wire.API.Error data CargoholdError @@ -26,8 +27,8 @@ data CargoholdError | InvalidLength | NoMatchingAssetEndpoint -instance KnownError (MapError e) => IsSwaggerError (e :: CargoholdError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: CargoholdError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) type instance MapError 'AssetNotFound = 'StaticError 404 "not-found" "Asset not found" diff --git a/libs/wire-api/src/Wire/API/Error/Empty.hs b/libs/wire-api/src/Wire/API/Error/Empty.hs index 474841ef1fd..290c75c978d 100644 --- a/libs/wire-api/src/Wire/API/Error/Empty.hs +++ b/libs/wire-api/src/Wire/API/Error/Empty.hs @@ -18,7 +18,7 @@ module Wire.API.Error.Empty where import Control.Lens ((.~)) -import Data.Swagger qualified as S +import Data.OpenApi qualified as S import Data.Text qualified as Text import GHC.TypeLits import Imports diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index 48aba881d42..3e7896b4544 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -37,11 +37,12 @@ import Control.Lens ((%~), (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Containers.ListUtils import Data.Domain +import Data.HashMap.Strict.InsOrd (singleton) +import Data.OpenApi qualified as S import Data.Proxy import Data.Qualified import Data.Schema import Data.Singletons.TH (genSingletons) -import Data.Swagger qualified as S import Data.Tagged import GHC.TypeLits import Imports @@ -51,6 +52,7 @@ import Network.Wai.Utilities.JSONResponse import Polysemy import Polysemy.Error import Prelude.Singletons (Show_) +import Servant.API.ContentTypes (JSON, contentType) import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Brig qualified as BrigError @@ -139,8 +141,8 @@ data GalleyError $(genSingletons [''GalleyError]) -instance KnownError (MapError e) => IsSwaggerError (e :: GalleyError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: GalleyError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) instance KnownError (MapError e) => APIError (Tagged (e :: GalleyError) ()) where toResponse _ = toResponse $ dynError @(MapError e) @@ -324,7 +326,7 @@ type instance MapError 'VerificationCodeAuthFailed = 'StaticError 403 "code-auth type instance MapError 'VerificationCodeRequired = 'StaticError 403 "code-authentication-required" "Verification code required" instance IsSwaggerError AuthenticationError where - addToSwagger = + addToOpenApi = addStaticErrorToSwagger @(MapError 'ReAuthFailed) . addStaticErrorToSwagger @(MapError 'VerificationCodeAuthFailed) . addStaticErrorToSwagger @(MapError 'VerificationCodeRequired) @@ -351,7 +353,7 @@ data TeamFeatureError instance IsSwaggerError TeamFeatureError where -- Do not display in Swagger - addToSwagger = id + addToOpenApi = id type instance MapError 'AppLockInactivityTimeoutTooLow = 'StaticError 400 "inactivity-timeout-too-low" "Applock inactivity timeout must be at least 30 seconds" @@ -398,7 +400,7 @@ type instance ErrorEffect MLSProposalFailure = Error MLSProposalFailure -- Proposal failures are only reported generically in Swagger instance IsSwaggerError MLSProposalFailure where - addToSwagger = S.allOperations . S.description %~ Just . (<> desc) . fold + addToOpenApi = S.allOperations . S.description %~ Just . (<> desc) . fold where desc = "\n\n**Note**: this endpoint can execute proposals, and therefore \ @@ -449,11 +451,16 @@ instance ToSchema NonFederatingBackends where nonFederatingBackendsFromList instance IsSwaggerError NonFederatingBackends where - addToSwagger = + addToOpenApi = addErrorResponseToSwagger (HTTP.statusCode nonFederatingBackendsStatus) $ mempty & S.description .~ "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" - & S.schema ?~ S.Inline (S.toSchema (Proxy @NonFederatingBackends)) + & S.content .~ singleton mediaType mediaTypeObject + where + mediaType = contentType $ Proxy @JSON + mediaTypeObject = + mempty + & S.schema ?~ S.Inline (S.toSchema (Proxy @NonFederatingBackends)) type instance ErrorEffect NonFederatingBackends = Error NonFederatingBackends @@ -486,11 +493,18 @@ instance ToSchema UnreachableBackends where <$> (.backends) .= field "unreachable_backends" (array schema) instance IsSwaggerError UnreachableBackends where - addToSwagger = + addToOpenApi = addErrorResponseToSwagger (HTTP.statusCode unreachableBackendsStatus) $ mempty & S.description .~ "Some domains are unreachable" - & S.schema ?~ S.Inline (S.toSchema (Proxy @UnreachableBackends)) + -- Defaulting this to JSON, as openapi3 needs something to map a schema against. + -- This _should_ be overridden with the actual media types once we are at the + -- point of rendering out the schemas for MultiVerb. + -- Check the instance of `S.HasOpenApi (MultiVerb method (cs :: [Type]) as r)` + & S.content .~ singleton mediaType mediaTypeObject + where + mediaType = contentType $ Proxy @JSON + mediaTypeObject = mempty & S.schema ?~ S.Inline (S.toSchema (Proxy @UnreachableBackends)) type instance ErrorEffect UnreachableBackends = Error UnreachableBackends diff --git a/libs/wire-api/src/Wire/API/Error/Gundeck.hs b/libs/wire-api/src/Wire/API/Error/Gundeck.hs index f28432f45f1..ac9b6ce363f 100644 --- a/libs/wire-api/src/Wire/API/Error/Gundeck.hs +++ b/libs/wire-api/src/Wire/API/Error/Gundeck.hs @@ -17,6 +17,7 @@ module Wire.API.Error.Gundeck where +import Data.Typeable import Wire.API.Error data GundeckError @@ -28,8 +29,8 @@ data GundeckError | TokenNotFound | NotificationNotFound -instance KnownError (MapError e) => IsSwaggerError (e :: GundeckError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: GundeckError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) type instance MapError 'AddTokenErrorNoBudget = 'StaticError 413 "sns-thread-budget-reached" "Too many concurrent calls to SNS; is SNS down?" diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index 2aeb06e131d..265bf5a5d8b 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -71,10 +71,11 @@ import Data.Aeson qualified as A import Data.Aeson.KeyMap qualified as KeyMap import Data.Id import Data.Json.Util +import Data.OpenApi (deprecated) +import Data.OpenApi qualified as S import Data.Qualified import Data.SOP import Data.Schema -import Data.Swagger qualified as S import Data.Time import Imports import Test.QuickCheck qualified as QC @@ -234,7 +235,9 @@ instance ToSchema SimpleMembers where .= optional ( fieldWithDocModifier "user_ids" - (description ?~ "deprecated") + ( (description ?~ "deprecated") + . (deprecated ?~ True) + ) (array schema) ) diff --git a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs index e6982d8f45f..32e67dcfaf6 100644 --- a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs +++ b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs @@ -26,8 +26,8 @@ import Data.Aeson (toJSON) import Data.Aeson qualified as A import Data.Aeson.KeyMap qualified as KeyMap import Data.Json.Util (ToJSONObject (toJSONObject)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import GHC.TypeLits (KnownSymbol) import Imports import Test.QuickCheck.Gen (oneof) diff --git a/libs/wire-api/src/Wire/API/Event/Federation.hs b/libs/wire-api/src/Wire/API/Event/Federation.hs index 17d5120c0ce..55130a9ec84 100644 --- a/libs/wire-api/src/Wire/API/Event/Federation.hs +++ b/libs/wire-api/src/Wire/API/Event/Federation.hs @@ -9,8 +9,8 @@ import Data.Aeson qualified as A import Data.Aeson.KeyMap qualified as KeyMap import Data.Domain import Data.Json.Util (ToJSONObject (toJSONObject)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.Arbitrary diff --git a/libs/wire-api/src/Wire/API/Event/Team.hs b/libs/wire-api/src/Wire/API/Event/Team.hs index a6404dd851b..d5dac32eb39 100644 --- a/libs/wire-api/src/Wire/API/Event/Team.hs +++ b/libs/wire-api/src/Wire/API/Event/Team.hs @@ -42,8 +42,8 @@ import Data.Aeson.KeyMap qualified as KeyMap import Data.Aeson.Types (Parser) import Data.Id (ConvId, TeamId, UserId) import Data.Json.Util +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Time (UTCTime) import Imports import Test.QuickCheck qualified as QC diff --git a/libs/wire-api/src/Wire/API/FederationStatus.hs b/libs/wire-api/src/Wire/API/FederationStatus.hs index 257d95c96b3..b0e7a3c9859 100644 --- a/libs/wire-api/src/Wire/API/FederationStatus.hs +++ b/libs/wire-api/src/Wire/API/FederationStatus.hs @@ -10,8 +10,8 @@ import Data.Aeson (FromJSON (..), ToJSON (..), (.:)) import Data.Aeson qualified as A import Data.Aeson.Types qualified as A import Data.Domain +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.Arbitrary diff --git a/libs/wire-api/src/Wire/API/Internal/BulkPush.hs b/libs/wire-api/src/Wire/API/Internal/BulkPush.hs index 8c07c074a2c..0ffb9eec618 100644 --- a/libs/wire-api/src/Wire/API/Internal/BulkPush.hs +++ b/libs/wire-api/src/Wire/API/Internal/BulkPush.hs @@ -20,9 +20,9 @@ module Wire.API.Internal.BulkPush where import Control.Lens import Data.Aeson import Data.Id +import Data.OpenApi qualified as Swagger import Data.Schema (ValueSchema) import Data.Schema qualified as S -import Data.Swagger qualified as Swagger import Imports import Wire.API.Internal.Notification diff --git a/libs/wire-api/src/Wire/API/Internal/Notification.hs b/libs/wire-api/src/Wire/API/Internal/Notification.hs index 3c252180668..849c8125460 100644 --- a/libs/wire-api/src/Wire/API/Internal/Notification.hs +++ b/libs/wire-api/src/Wire/API/Internal/Notification.hs @@ -45,8 +45,8 @@ import Control.Lens (makeLenses) import Data.Aeson import Data.Id import Data.List1 +import Data.OpenApi qualified as Swagger import Data.Schema qualified as S -import Data.Swagger qualified as Swagger import Imports hiding (cs) import Wire.API.Notification diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index 4327cee928d..c6d93ac2e4e 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -25,10 +25,10 @@ import Crypto.Hash.Algorithms import Crypto.KDF.HKDF qualified as HKDF import Crypto.PubKey.Ed25519 qualified as Ed25519 import Data.Aeson (parseJSON, toJSON) +import Data.OpenApi qualified as S +import Data.OpenApi.Internal.Schema qualified as S import Data.Proxy import Data.Schema -import Data.Swagger qualified as S -import Data.Swagger.Internal.Schema qualified as S import Data.Word import Imports import Wire.API.MLS.Credential diff --git a/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs b/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs index a6c4e6753cd..8cff3683009 100644 --- a/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs +++ b/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs @@ -20,9 +20,9 @@ module Wire.API.MLS.CommitBundle where import Control.Lens (view, (.~), (?~)) import Data.Bifunctor (first) import Data.ByteString qualified as BS +import Data.OpenApi qualified as S import Data.ProtoLens (decodeMessage, encodeMessage) import Data.ProtoLens qualified (Message (defMessage)) -import Data.Swagger qualified as S import Data.Text qualified as T import Imports import Proto.Mls qualified diff --git a/libs/wire-api/src/Wire/API/MLS/Credential.hs b/libs/wire-api/src/Wire/API/MLS/Credential.hs index dc229102392..b3c92866f53 100644 --- a/libs/wire-api/src/Wire/API/MLS/Credential.hs +++ b/libs/wire-api/src/Wire/API/MLS/Credential.hs @@ -32,9 +32,9 @@ import Data.Binary.Parser import Data.Binary.Parser.Char8 import Data.Domain import Data.Id +import Data.OpenApi qualified as S import Data.Qualified import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as T import Data.UUID import Imports @@ -123,7 +123,7 @@ instance FromJSONKey SignatureSchemeTag where fromJSONKey = Aeson.FromJSONKeyTextParser parseSignatureScheme instance S.ToParamSchema SignatureSchemeTag where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData SignatureSchemeTag where parseQueryParam = note "Unknown signature scheme" . signatureSchemeFromName @@ -206,7 +206,7 @@ instance FromJSONKey SignaturePurpose where either fail pure . signaturePurposeFromName instance S.ToParamSchema SignaturePurpose where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData SignaturePurpose where parseQueryParam = first T.pack . signaturePurposeFromName diff --git a/libs/wire-api/src/Wire/API/MLS/Group.hs b/libs/wire-api/src/Wire/API/MLS/Group.hs index fbbefd015e1..4cff768d633 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group.hs @@ -23,9 +23,9 @@ import Data.ByteArray (convert) import Data.ByteString.Conversion import Data.Id import Data.Json.Util +import Data.OpenApi qualified as S import Data.Qualified import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.MLS.Serialisation import Wire.Arbitrary diff --git a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs index 51a7267450f..a692179a9bc 100644 --- a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs +++ b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs @@ -49,9 +49,9 @@ import Data.ByteString qualified as B import Data.ByteString.Lazy qualified as LBS import Data.Id import Data.Json.Util +import Data.OpenApi qualified as S import Data.Qualified import Data.Schema -import Data.Swagger qualified as S import Imports hiding (cs) import Test.QuickCheck import Web.HttpApiData diff --git a/libs/wire-api/src/Wire/API/MLS/Keys.hs b/libs/wire-api/src/Wire/API/MLS/Keys.hs index 3bb54a9be20..9819b2c1f64 100644 --- a/libs/wire-api/src/Wire/API/MLS/Keys.hs +++ b/libs/wire-api/src/Wire/API/MLS/Keys.hs @@ -29,8 +29,8 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.ByteArray import Data.Json.Util import Data.Map qualified as Map +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.MLS.Credential diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs index 517ef2a7fa6..b24e8742988 100644 --- a/libs/wire-api/src/Wire/API/MLS/Message.hs +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -51,9 +51,9 @@ import Data.Binary.Put import Data.ByteArray qualified as BA import Data.Json.Util import Data.Kind +import Data.OpenApi qualified as S import Data.Schema import Data.Singletons.TH -import Data.Swagger qualified as S import Imports hiding (cs) import Test.QuickCheck hiding (label) import Wire.API.Event.Conversation diff --git a/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs b/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs index 870b46f549d..fb467228d3b 100644 --- a/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs +++ b/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs @@ -22,7 +22,7 @@ import Data.Binary import Data.Binary.Get import Data.Binary.Put import Data.ByteString.Lazy qualified as LBS -import Data.Swagger qualified as S +import Data.OpenApi qualified as S import Imports import Test.QuickCheck hiding (label) import Wire.API.MLS.CipherSuite diff --git a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs index 946bcbc7c39..f0e7f997c26 100644 --- a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs @@ -59,9 +59,9 @@ import Data.ByteString qualified as BS import Data.ByteString.Lazy qualified as LBS import Data.Json.Util import Data.Kind +import Data.OpenApi qualified as S import Data.Proxy import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Imports diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index 69ec37ade05..8d46721f7b5 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -26,8 +26,8 @@ import Control.Lens.Tuple (_1) import Control.Monad.Except import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Id +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as T import Imports import Servant (FromHttpApiData (..), ToHttpApiData (toQueryParam)) diff --git a/libs/wire-api/src/Wire/API/MLS/Welcome.hs b/libs/wire-api/src/Wire/API/MLS/Welcome.hs index ac95b53dee0..2bb8e117eed 100644 --- a/libs/wire-api/src/Wire/API/MLS/Welcome.hs +++ b/libs/wire-api/src/Wire/API/MLS/Welcome.hs @@ -17,7 +17,7 @@ module Wire.API.MLS.Welcome where -import Data.Swagger qualified as S +import Data.OpenApi qualified as S import Imports hiding (cs) import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit diff --git a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs index fbc133d6728..ba24fd4ee16 100644 --- a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs +++ b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs @@ -31,20 +31,22 @@ module Wire.API.MakesFederatedCall ) where +import Control.Lens ((<>~)) import Data.Aeson import Data.Constraint +import Data.HashSet.InsOrd (singleton) import Data.Kind import Data.Metrics.Servant +import Data.OpenApi qualified as S import Data.Proxy import Data.Schema -import Data.Swagger.Operation (addExtensions) import Data.Text qualified as T import GHC.TypeLits import Imports import Servant.API import Servant.Client +import Servant.OpenApi import Servant.Server -import Servant.Swagger import Test.QuickCheck (Arbitrary) import TransitiveAnns.Types import Unsafe.Coerce (unsafeCoerce) @@ -158,24 +160,32 @@ type instance -- | 'MakesFederatedCall' annotates the swagger documentation with an extension -- tag @x-wire-makes-federated-calls-to@. -instance (HasSwagger api, KnownSymbol name, KnownSymbol (ShowComponent comp)) => HasSwagger (MakesFederatedCall comp name :> api :: Type) where - toSwagger _ = - toSwagger (Proxy @api) - & addExtensions - mergeJSONArray - [ ( "wire-makes-federated-call-to", - Array - [ Array - [ String $ T.pack $ symbolVal $ Proxy @(ShowComponent comp), - String $ T.pack $ symbolVal $ Proxy @name - ] - ] +instance (HasOpenApi api, KnownSymbol name, KnownSymbol (ShowComponent comp)) => HasOpenApi (MakesFederatedCall comp name :> api :: Type) where + toOpenApi _ = + toOpenApi (Proxy @api) + -- Since extensions aren't in the openapi3 library yet, + -- and the PRs for their support seem be going no where quickly, I'm using + -- tags instead. https://github.com/biocad/openapi3/pull/43 + -- Basically, this is similar to the old system, except we don't have nested JSON to + -- work with. So I'm using the magic string and sticking the call name on the end + -- and sticking the component in the description. This ordering is important as we + -- can't have duplicate tag names on an object. + + -- Set the tags at the top of OpenApi object + & S.tags + <>~ singleton + ( S.Tag + name + (pure $ T.pack (symbolVal $ Proxy @(ShowComponent comp))) + Nothing ) - ] - -mergeJSONArray :: Value -> Value -> Value -mergeJSONArray (Array x) (Array y) = Array $ x <> y -mergeJSONArray _ _ = error "impossible! bug in construction of federated calls JSON" + -- Set the tags on the specific path we're looking at + -- This is where the tag is actually registered on the path + -- so it can be picked up by fedcalls. + & S.allOperations . S.tags <>~ setName + where + name = "wire-makes-federated-call-to-" <> T.pack (symbolVal $ Proxy @name) + setName = singleton name instance HasClient m api => HasClient m (MakesFederatedCall comp name :> api :: Type) where type Client m (MakesFederatedCall comp name :> api) = Client m api diff --git a/libs/wire-api/src/Wire/API/Message.hs b/libs/wire-api/src/Wire/API/Message.hs index e258cc3e74a..3b651796e21 100644 --- a/libs/wire-api/src/Wire/API/Message.hs +++ b/libs/wire-api/src/Wire/API/Message.hs @@ -67,6 +67,7 @@ import Data.Domain (Domain, domainText, mkDomain) import Data.Id import Data.Json.Util import Data.Map.Strict qualified as Map +import Data.OpenApi qualified as S import Data.ProtoLens qualified as ProtoLens import Data.ProtoLens.Field qualified as ProtoLens import Data.ProtocolBuffers qualified as Protobuf @@ -74,7 +75,6 @@ import Data.Qualified (Qualified (..)) import Data.Schema import Data.Serialize (runGet) import Data.Set qualified as Set -import Data.Swagger qualified as S import Data.Text.Read qualified as Reader import Data.UUID qualified as UUID import Imports @@ -553,7 +553,7 @@ data IgnoreMissing deriving (Show, Eq) instance S.ToParamSchema IgnoreMissing where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData IgnoreMissing where parseQueryParam = \case @@ -566,7 +566,7 @@ data ReportMissing | ReportMissingList (Set UserId) instance S.ToParamSchema ReportMissing where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData ReportMissing where parseQueryParam = \case diff --git a/libs/wire-api/src/Wire/API/Notification.hs b/libs/wire-api/src/Wire/API/Notification.hs index b404e05db61..1b7601bce10 100644 --- a/libs/wire-api/src/Wire/API/Notification.hs +++ b/libs/wire-api/src/Wire/API/Notification.hs @@ -47,10 +47,10 @@ import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Id import Data.Json.Util import Data.List.NonEmpty (NonEmpty) +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi qualified as S import Data.SOP import Data.Schema -import Data.Swagger (ToParamSchema (..)) -import Data.Swagger qualified as S import Data.Time.Clock (UTCTime) import Data.UUID qualified as UUID import Imports diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index b34b3ae38d6..293d154885c 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -31,11 +31,11 @@ import Data.ByteString.Lazy (toStrict) import Data.Either.Combinators (mapLeft) import Data.HashMap.Strict qualified as HM import Data.Id as Id +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi qualified as S import Data.Range import Data.Schema import Data.Set qualified as Set -import Data.Swagger (ToParamSchema (..)) -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Ascii import Data.Text.Encoding qualified as TE @@ -45,7 +45,7 @@ import GHC.TypeLits (Nat, symbolVal) import Imports hiding (exp, head) import Prelude.Singletons (Show_) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Test.QuickCheck (Arbitrary (..)) import URI.ByteString import URI.ByteString.QQ qualified as URI.QQ @@ -641,8 +641,8 @@ data OAuthError | OAuthInvalidRefreshToken | OAuthInvalidGrant -instance KnownError (MapError e) => IsSwaggerError (e :: OAuthError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: OAuthError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) type instance MapError 'OAuthClientNotFound = 'StaticError 404 "not-found" "OAuth client not found" diff --git a/libs/wire-api/src/Wire/API/Properties.hs b/libs/wire-api/src/Wire/API/Properties.hs index 70e03c71437..debcf9016d7 100644 --- a/libs/wire-api/src/Wire/API/Properties.hs +++ b/libs/wire-api/src/Wire/API/Properties.hs @@ -30,7 +30,7 @@ import Data.Aeson (FromJSON (..), ToJSON (..), Value) import Data.Aeson qualified as A import Data.ByteString.Conversion import Data.Hashable (Hashable) -import Data.Swagger qualified as S +import Data.OpenApi qualified as S import Data.Text.Ascii import Imports import Servant @@ -43,7 +43,7 @@ instance S.ToSchema PropertyKeysAndValues where declareNamedSchema _ = pure $ S.NamedSchema (Just "PropertyKeysAndValues") $ - mempty & S.type_ ?~ S.SwaggerObject + mempty & S.type_ ?~ S.OpenApiObject newtype PropertyKey = PropertyKey {propertyKeyName :: AsciiPrintable} @@ -64,7 +64,7 @@ newtype PropertyKey = PropertyKey instance S.ToParamSchema PropertyKey where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.format ?~ "printable" -- | A raw, unparsed property value. diff --git a/libs/wire-api/src/Wire/API/Provider/Bot.hs b/libs/wire-api/src/Wire/API/Provider/Bot.hs index 0db3cabeaff..e8a1f5b1c4a 100644 --- a/libs/wire-api/src/Wire/API/Provider/Bot.hs +++ b/libs/wire-api/src/Wire/API/Provider/Bot.hs @@ -34,8 +34,8 @@ import Control.Lens (makeLenses) import Data.Aeson qualified as A import Data.Handle (Handle) import Data.Id +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.Conversation.Member (OtherMember (..)) import Wire.API.User.Profile (ColourId, Name) diff --git a/libs/wire-api/src/Wire/API/Provider/Service.hs b/libs/wire-api/src/Wire/API/Provider/Service.hs index 28a1e5609a1..c1110a58f25 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service.hs @@ -61,11 +61,11 @@ import Data.Id import Data.Json.Util ((#)) import Data.List1 (List1) import Data.Misc (HttpsUrl (..), PlainTextPassword6) +import Data.OpenApi qualified as S import Data.PEM (PEM, pemParseBS, pemWriteLBS) import Data.Proxy import Data.Range (Range) import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Ascii import Data.Text.Encoding qualified as Text diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index 522c519ff87..07f0910ce5e 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -41,13 +41,18 @@ where import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON)) import Data.Aeson qualified as JSON +import Data.ByteString (toStrict) import Data.ByteString.Builder qualified as BB import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Conversion -import Data.Range (Range, fromRange) +import Data.OpenApi qualified as S +import Data.Range (Range, fromRange, rangedSchema) import Data.Range qualified as Range +import Data.Schema import Data.Set qualified as Set +import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding qualified as Text +import Data.Text.Encoding.Error (lenientDecode) import Data.Type.Ord import GHC.TypeLits (KnownNat, Nat) import Imports @@ -173,6 +178,16 @@ instance FromJSON ServiceTag where JSON.withText "ServiceTag" $ either fail pure . runParser parser . Text.encodeUtf8 +instance ToSchema ServiceTag where + schema = enum @Text "" . mconcat $ (\a -> element (decodeUtf8With lenientDecode $ toStrict $ toByteString a) a) <$> [minBound ..] + +instance S.ToParamSchema ServiceTag where + toParamSchema _ = + mempty + { S._schemaType = Just S.OpenApiString, + S._schemaEnum = Just (toJSON <$> [(minBound :: ServiceTag) ..]) + } + -------------------------------------------------------------------------------- -- Bounded ServiceTag Queries @@ -181,6 +196,19 @@ newtype QueryAnyTags (m :: Nat) (n :: Nat) = QueryAnyTags {queryAnyTagsRange :: Range m n (Set (QueryAllTags m n))} deriving stock (Eq, Show, Ord) +instance (m <= n) => S.ToParamSchema (QueryAnyTags m n) where + toParamSchema _ = + mempty + { S._schemaType = Just S.OpenApiString, + S._schemaEnum = Just (toJSON <$> [(minBound :: ServiceTag) ..]) + } + +instance (KnownNat n, KnownNat m, m <= n) => ToSchema (QueryAnyTags m n) where + schema = + let sch :: ValueSchema NamedSwaggerDoc (Range m n (Set (QueryAllTags m n))) + sch = fromRange .= rangedSchema (named "QueryAnyTags" $ set schema) + in queryAnyTagsRange .= (QueryAnyTags <$> sch) + instance (KnownNat m, KnownNat n, m <= n) => Arbitrary (QueryAnyTags m n) where arbitrary = QueryAnyTags <$> arbitrary @@ -236,6 +264,12 @@ instance (KnownNat m, KnownNat n, m <= n) => FromByteString (QueryAllTags m n) w rs <- either fail pure (Range.checkedEither (Set.fromList ts)) pure $! QueryAllTags rs +instance (KnownNat m, KnownNat n, m <= n) => ToSchema (QueryAllTags m n) where + schema = + let sch :: ValueSchema NamedSwaggerDoc (Range m n (Set ServiceTag)) + sch = fromRange .= rangedSchema (named "QueryAllTags" $ set schema) + in queryAllTagsRange .= fmap QueryAllTags sch + -------------------------------------------------------------------------------- -- ServiceTag Matchers diff --git a/libs/wire-api/src/Wire/API/Push/V2/Token.hs b/libs/wire-api/src/Wire/API/Push/V2/Token.hs index ee8e828670d..0cf7b292af4 100644 --- a/libs/wire-api/src/Wire/API/Push/V2/Token.hs +++ b/libs/wire-api/src/Wire/API/Push/V2/Token.hs @@ -47,10 +47,10 @@ import Data.Aeson qualified as A import Data.Attoparsec.ByteString (takeByteString) import Data.ByteString.Conversion import Data.Id +import Data.OpenApi (ToParamSchema) +import Data.OpenApi qualified as S import Data.SOP import Data.Schema -import Data.Swagger (ToParamSchema) -import Data.Swagger qualified as S import Generics.SOP qualified as GSOP import Imports import Servant diff --git a/libs/wire-api/src/Wire/API/RawJson.hs b/libs/wire-api/src/Wire/API/RawJson.hs index fd0517ea289..08529ded900 100644 --- a/libs/wire-api/src/Wire/API/RawJson.hs +++ b/libs/wire-api/src/Wire/API/RawJson.hs @@ -20,7 +20,7 @@ module Wire.API.RawJson where import Control.Lens -import Data.Swagger qualified as Swagger +import Data.OpenApi qualified as Swagger import Imports import Servant import Test.QuickCheck @@ -43,6 +43,6 @@ instance Swagger.ToSchema RawJson where declareNamedSchema _ = pure . Swagger.NamedSchema (Just "RawJson") $ mempty - & Swagger.type_ ?~ Swagger.SwaggerObject + & Swagger.type_ ?~ Swagger.OpenApiObject & Swagger.description ?~ "Any JSON as plain string. The object structure is not specified in this schema." diff --git a/libs/wire-api/src/Wire/API/Routes/API.hs b/libs/wire-api/src/Wire/API/Routes/API.hs index b43569bf76f..23ac38e6fed 100644 --- a/libs/wire-api/src/Wire/API/Routes/API.hs +++ b/libs/wire-api/src/Wire/API/Routes/API.hs @@ -31,14 +31,14 @@ where import Data.Domain import Data.Kind +import Data.OpenApi qualified as S import Data.Proxy -import Data.Swagger qualified as S import Imports import Polysemy import Polysemy.Error import Polysemy.Internal import Servant hiding (Union) -import Servant.Swagger +import Servant.OpenApi import Wire.API.Error import Wire.API.Routes.Named import Wire.API.Routes.Version @@ -47,8 +47,8 @@ class ServiceAPI service (v :: Version) where type ServiceAPIRoutes service type SpecialisedAPIRoutes v service :: Type type SpecialisedAPIRoutes v service = SpecialiseToVersion v (ServiceAPIRoutes service) - serviceSwagger :: HasSwagger (SpecialisedAPIRoutes v service) => S.Swagger - serviceSwagger = toSwagger (Proxy @(SpecialisedAPIRoutes v service)) + serviceSwagger :: HasOpenApi (SpecialisedAPIRoutes v service) => S.OpenApi + serviceSwagger = toOpenApi (Proxy @(SpecialisedAPIRoutes v service)) instance ServiceAPI VersionAPITag v where type ServiceAPIRoutes VersionAPITag = VersionAPI diff --git a/libs/wire-api/src/Wire/API/Routes/AssetBody.hs b/libs/wire-api/src/Wire/API/Routes/AssetBody.hs index 2b6989d308b..4998c10f538 100644 --- a/libs/wire-api/src/Wire/API/Routes/AssetBody.hs +++ b/libs/wire-api/src/Wire/API/Routes/AssetBody.hs @@ -25,13 +25,13 @@ where import Conduit import Data.ByteString.Lazy qualified as LBS -import Data.Swagger -import Data.Swagger.Internal.Schema +import Data.OpenApi +import Data.OpenApi.Internal.Schema import Imports import Network.HTTP.Media ((//)) import Servant import Servant.Conduit () -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () data MultipartMixed diff --git a/libs/wire-api/src/Wire/API/Routes/Bearer.hs b/libs/wire-api/src/Wire/API/Routes/Bearer.hs index 545db5254df..64a1baed79f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Bearer.hs +++ b/libs/wire-api/src/Wire/API/Routes/Bearer.hs @@ -21,11 +21,11 @@ import Control.Lens ((<>~)) import Data.ByteString qualified as BS import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Metrics.Servant -import Data.Swagger hiding (Header) +import Data.OpenApi hiding (HasServer, Header) import Data.Text.Encoding qualified as T import Imports import Servant -import Servant.Swagger +import Servant.OpenApi import Wire.API.Routes.Version newtype Bearer a = Bearer {unBearer :: a} @@ -47,9 +47,9 @@ type instance SpecialiseToVersion v (Bearer a :> api) = Bearer a :> SpecialiseToVersion v api -instance HasSwagger api => HasSwagger (Bearer a :> api) where - toSwagger _ = - toSwagger (Proxy @api) +instance HasOpenApi api => HasOpenApi (Bearer a :> api) where + toOpenApi _ = + toOpenApi (Proxy @api) & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "ZAuth" []] instance RoutesToPaths api => RoutesToPaths (Bearer a :> api) where diff --git a/libs/wire-api/src/Wire/API/Routes/CSV.hs b/libs/wire-api/src/Wire/API/Routes/CSV.hs index 0d09941545c..0345336c378 100644 --- a/libs/wire-api/src/Wire/API/Routes/CSV.hs +++ b/libs/wire-api/src/Wire/API/Routes/CSV.hs @@ -17,6 +17,10 @@ module Wire.API.Routes.CSV where +import Control.Lens +import Data.OpenApi qualified as O +import Data.OpenApi.Internal.Schema +import Imports import Network.HTTP.Media.MediaType import Servant.API @@ -24,3 +28,11 @@ data CSV instance Accept CSV where contentType _ = "text" // "csv" + +instance ToSchema CSV where + declareNamedSchema _ = + plain $ + mempty + & O.title ?~ "CSV" + & O.type_ ?~ O.OpenApiString + & O.format ?~ "text/csv" diff --git a/libs/wire-api/src/Wire/API/Routes/Cookies.hs b/libs/wire-api/src/Wire/API/Routes/Cookies.hs index 10383904ccb..2449f074c76 100644 --- a/libs/wire-api/src/Wire/API/Routes/Cookies.hs +++ b/libs/wire-api/src/Wire/API/Routes/Cookies.hs @@ -27,7 +27,7 @@ import Data.Text.Encoding qualified as T import GHC.TypeLits import Imports import Servant -import Servant.Swagger +import Servant.OpenApi import Web.Cookie (parseCookies) import Wire.API.Routes.Version @@ -63,8 +63,8 @@ type instance SpecialiseToVersion v (Cookies cs :> api) = Cookies cs :> SpecialiseToVersion v api -instance HasSwagger api => HasSwagger (Cookies cs :> api) where - toSwagger _ = toSwagger (Proxy @api) +instance HasOpenApi api => HasOpenApi (Cookies cs :> api) where + toOpenApi _ = toOpenApi (Proxy @api) class CookieArgs (cs :: [Type]) where -- example: AddArgs ["foo" :: Foo, "bar" :: Bar] a = Foo -> Bar -> a diff --git a/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs b/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs index 5ce5e7ca871..8530f78275a 100644 --- a/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs +++ b/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs @@ -26,8 +26,8 @@ where import Control.Lens ((?~)) import Data.Aeson (FromJSON, ToJSON) import Data.Domain (Domain) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import GHC.Generics import Imports import Wire.API.User.Search (FederatedUserSearchPolicy) 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 8a343a285b3..10308c8a58e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -46,14 +46,14 @@ import Data.CommaSeparatedList import Data.Domain (Domain) import Data.Handle (Handle) import Data.Id as Id +import Data.OpenApi (HasInfo (info), HasTitle (title), OpenApi) +import Data.OpenApi qualified as S import Data.Qualified (Qualified) import Data.Schema hiding (swaggerDoc) -import Data.Swagger (HasInfo (info), HasTitle (title), Swagger) -import Data.Swagger qualified as S import Imports hiding (head) import Servant hiding (Handler, WithStatus, addHeader, respond) -import Servant.Swagger (HasSwagger (toSwagger)) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi (HasOpenApi (toOpenApi)) +import Servant.OpenApi.Internal.Orphans () import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig @@ -768,7 +768,7 @@ type FederationRemotesAPI = type FederationRemotesAPIDescription = "See https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections for background. " -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @API) + toOpenApi (Proxy @API) & info . title .~ "Wire-Server internal brig API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/Connection.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/Connection.hs index f9226607259..7f3d76810cf 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/Connection.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/Connection.hs @@ -21,9 +21,9 @@ module Wire.API.Routes.Internal.Brig.Connection where import Data.Aeson (FromJSON, ToJSON) import Data.Id +import Data.OpenApi qualified as S import Data.Qualified import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.Connection diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs index efd26df2ee0..93db38b2974 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs @@ -28,7 +28,7 @@ where import Data.Aeson hiding (json) import Data.Handle (Handle) import Data.Id (TeamId, UserId) -import Data.Swagger (ToSchema) +import Data.OpenApi (ToSchema) import Deriving.Swagger (CamelToSnake, CustomSwagger (..), FieldLabelModifier, StripSuffix) import Imports hiding (head) import Test.QuickCheck (Arbitrary) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs index a8a2747af7d..8974da4c27c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs @@ -20,7 +20,7 @@ module Wire.API.Routes.Internal.Brig.OAuth where import Data.Id (OAuthClientId) import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.OAuth import Wire.API.Routes.Named (Named (..)) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs index 6b45b977e68..0cca4948901 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs @@ -19,7 +19,7 @@ module Wire.API.Routes.Internal.Brig.SearchIndex where import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Routes.Named (Named (..)) type ISearchIndexAPI = diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs index ff0fe916a1a..b8f1652bc7a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs @@ -2,10 +2,10 @@ module Wire.API.Routes.Internal.Cannon where import Control.Lens ((.~)) import Data.Id -import Data.Swagger (HasInfo (info), HasTitle (title), Swagger) +import Data.OpenApi (HasInfo (info), HasTitle (title), OpenApi) import Imports import Servant -import Servant.Swagger (HasSwagger (toSwagger)) +import Servant.OpenApi (HasOpenApi (toOpenApi)) import Wire.API.Error import Wire.API.Error.Cannon import Wire.API.Internal.BulkPush @@ -59,7 +59,7 @@ type API = ) ) -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @API) + toOpenApi (Proxy @API) & info . title .~ "Wire-Server internal cannon API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs index 825623ac9c6..cb9599b441e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs @@ -18,10 +18,10 @@ module Wire.API.Routes.Internal.Cargohold where import Control.Lens -import Data.Swagger +import Data.OpenApi import Imports import Servant -import Servant.Swagger +import Servant.OpenApi import Wire.API.Routes.MultiVerb type InternalAPI = @@ -29,7 +29,7 @@ type InternalAPI = :> "status" :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] () -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @InternalAPI) + toOpenApi (Proxy @InternalAPI) & info . title .~ "Wire-Server internal cargohold API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index e9d4e7e834f..ccfcf69a617 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -19,13 +19,13 @@ module Wire.API.Routes.Internal.Galley where import Control.Lens ((.~)) import Data.Id as Id +import Data.OpenApi (OpenApi, info, title) import Data.Range -import Data.Swagger (Swagger, info, title) import GHC.TypeLits (AppendSymbol) import Imports hiding (head) import Servant hiding (JSON, WithStatus) import Servant qualified hiding (WithStatus) -import Servant.Swagger +import Servant.OpenApi import Wire.API.ApplyMods import Wire.API.Conversation.Role import Wire.API.Error @@ -426,7 +426,7 @@ type IFederationAPI = :> Get '[Servant.JSON] FederationStatus ) -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @InternalAPI) + toOpenApi (Proxy @InternalAPI) & info . title .~ "Wire-Server internal galley API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs index cd81ed7473e..b644906cd95 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs @@ -26,9 +26,9 @@ where import Data.Aeson qualified as A import Data.Aeson.Types (FromJSON, ToJSON) import Data.Id (ConvId, UserId) +import Data.OpenApi qualified as Swagger import Data.Qualified import Data.Schema -import Data.Swagger qualified as Swagger import Imports data DesiredMembership = Included | Excluded diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs index fdb40b05aec..9f96c0b024c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs @@ -24,8 +24,8 @@ where import Data.Aeson qualified as A import Data.Id +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.Team.Feature qualified as Public diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs index 09432560ea5..0bc3ae5a593 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs @@ -31,8 +31,8 @@ import Control.Lens ((?~)) import Data.Aeson import Data.Currency qualified as Currency import Data.Json.Util +import Data.OpenApi qualified as Swagger import Data.Schema qualified as S -import Data.Swagger qualified as Swagger import Data.Time (UTCTime) import Imports import Test.QuickCheck.Arbitrary (Arbitrary) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs index 69d114dca82..ffde2e561c3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs @@ -19,11 +19,12 @@ module Wire.API.Routes.Internal.LegalHold where import Control.Lens import Data.Id +import Data.OpenApi (OpenApi) +import Data.OpenApi.Lens import Data.Proxy -import Data.Swagger import Imports import Servant.API hiding (Header, WithStatus) -import Servant.Swagger +import Servant.OpenApi import Wire.API.Team.Feature type InternalLegalHoldAPI = @@ -38,7 +39,7 @@ type InternalLegalHoldAPI = :> Put '[] NoContent ) -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @InternalLegalHoldAPI) + toOpenApi (Proxy @InternalLegalHoldAPI) & info . title .~ "Wire-Server internal legalhold API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs index 63f2358f5e1..8cc2207031c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs @@ -19,10 +19,10 @@ module Wire.API.Routes.Internal.Spar where import Control.Lens import Data.Id -import Data.Swagger +import Data.OpenApi import Imports import Servant -import Servant.Swagger +import Servant.OpenApi import Wire.API.User import Wire.API.User.Saml @@ -34,7 +34,7 @@ type InternalAPI = :<|> "scim" :> "userinfos" :> ReqBody '[JSON] UserSet :> Post '[JSON] ScimUserInfos ) -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @InternalAPI) + toOpenApi (Proxy @InternalAPI) & info . title .~ "Wire-Server internal spar API" diff --git a/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs b/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs index d9287bd5fa9..f39080b54f7 100644 --- a/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs +++ b/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs @@ -17,13 +17,13 @@ module Wire.API.Routes.LowLevelStream where -import Control.Lens (at, (.~), (?~)) +import Control.Lens (at, (.~), (?~), _Just) import Data.ByteString.Char8 as B8 import Data.CaseInsensitive qualified as CI import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Metrics.Servant +import Data.OpenApi qualified as S import Data.Proxy -import Data.Swagger qualified as S import Data.Text qualified as Text import GHC.TypeLits import Imports @@ -33,10 +33,10 @@ import Network.Wai import Servant.API import Servant.API.ContentTypes import Servant.API.Status +import Servant.OpenApi as S +import Servant.OpenApi.Internal as S import Servant.Server hiding (respond) import Servant.Server.Internal -import Servant.Swagger as S -import Servant.Swagger.Internal as S import Wire.API.Routes.Version -- FUTUREWORK: make it possible to generate headers at runtime @@ -90,27 +90,30 @@ type instance LowLevelStream m s h d t instance - (Accept ctype, KnownNat status, KnownSymbol desc, SwaggerMethod method) => - HasSwagger (LowLevelStream method status headers desc ctype) + (S.ToSchema ctype, Accept ctype, KnownNat status, KnownSymbol desc, OpenApiMethod method) => + HasOpenApi (LowLevelStream method status headers desc ctype) where - toSwagger _ = + toOpenApi _ = mempty & S.paths . at "/" ?~ ( mempty & method ?~ ( mempty - & S.produces ?~ S.MimeList [contentType (Proxy @ctype)] & S.responses . S.responses .~ fmap S.Inline responses ) ) where - method = S.swaggerMethod (Proxy @method) + method = S.openApiMethod (Proxy @method) responses = InsOrdHashMap.singleton (fromIntegral (natVal (Proxy @status))) $ mempty & S.description .~ Text.pack (symbolVal (Proxy @desc)) + & S.content + .~ InsOrdHashMap.singleton + (contentType $ Proxy @ctype) + (mempty & S.schema . _Just . S._Inline .~ S.toSchema (Proxy @ctype)) instance RoutesToPaths (LowLevelStream method status headers desc ctype) where getRoutes = [] diff --git a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs index e0438210f77..0fc48cdaf06 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs @@ -26,10 +26,10 @@ where import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Kind +import Data.OpenApi qualified as S import Data.Proxy import Data.Range import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import GHC.TypeLits import Imports @@ -77,7 +77,10 @@ deriving via deriving via Schema (GetMultiTablePageRequest name tables max def) instance - RequestSchemaConstraint name tables max def => S.ToSchema (GetMultiTablePageRequest name tables max def) + ( Typeable tables, + RequestSchemaConstraint name tables max def + ) => + S.ToSchema (GetMultiTablePageRequest name tables max def) instance RequestSchemaConstraint name tables max def => ToSchema (GetMultiTablePageRequest name tables max def) where schema = @@ -126,7 +129,7 @@ deriving via deriving via (Schema (MultiTablePage name resultsKey tables a)) instance - PageSchemaConstraints name resultsKey tables a => + (Typeable tables, Typeable a, PageSchemaConstraints name resultsKey tables a) => S.ToSchema (MultiTablePage name resultsKey tables a) instance diff --git a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs index 197e44a959b..7d43b3009be 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs @@ -26,9 +26,9 @@ import Data.Attoparsec.ByteString qualified as AB import Data.ByteString qualified as BS import Data.ByteString.Base64.URL qualified as Base64Url import Data.Either.Combinators (mapLeft) +import Data.OpenApi qualified as S import Data.Proxy import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import GHC.TypeLits diff --git a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs index db16fb8fc01..ed24bbfdbe5 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs @@ -41,6 +41,7 @@ module Wire.API.Routes.MultiVerb ResponseType, IsResponse (..), IsSwaggerResponse (..), + IsSwaggerResponseList (..), simpleResponseSwagger, combineResponseSwagger, ResponseTypes, @@ -54,18 +55,18 @@ import Control.Lens hiding (Context, (<|)) import Data.ByteString.Builder import Data.ByteString.Lazy qualified as LBS import Data.CaseInsensitive qualified as CI -import Data.Containers.ListUtils import Data.Either.Combinators (leftToMaybe) -import Data.HashMap.Strict.InsOrd (InsOrdHashMap) +import Data.HashMap.Strict.InsOrd (InsOrdHashMap, unionWith) import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Kind import Data.Metrics.Servant +import Data.OpenApi hiding (HasServer, Response, contentType) +import Data.OpenApi qualified as S +import Data.OpenApi.Declare qualified as S import Data.Proxy import Data.SOP import Data.Sequence (Seq, (<|), pattern (:<|)) import Data.Sequence qualified as Seq -import Data.Swagger qualified as S -import Data.Swagger.Declare qualified as S import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Data.Typeable @@ -82,10 +83,10 @@ import Servant.API.ContentTypes import Servant.API.Status (KnownStatus (..)) import Servant.Client import Servant.Client.Core hiding (addHeader) +import Servant.OpenApi as S +import Servant.OpenApi.Internal as S import Servant.Server import Servant.Server.Internal -import Servant.Swagger as S -import Servant.Swagger.Internal as S import Servant.Types.SourceT type Declare = S.Declare (S.Definitions S.Schema) @@ -191,19 +192,25 @@ instance (AllMimeRender cs a, AllMimeUnrender cs a, KnownStatus s) => IsResponse Nothing -> empty Just f -> either UnrenderError UnrenderSuccess (f (responseBody output)) -simpleResponseSwagger :: forall a desc. (S.ToSchema a, KnownSymbol desc) => Declare S.Response +simpleResponseSwagger :: forall a cs desc. (S.ToSchema a, KnownSymbol desc, AllMime cs) => Declare S.Response simpleResponseSwagger = do ref <- S.declareSchemaRef (Proxy @a) + let resps :: InsOrdHashMap M.MediaType MediaTypeObject + resps = InsOrdHashMap.fromList $ (,MediaTypeObject (pure ref) Nothing mempty mempty) <$> cs pure $ mempty & S.description .~ Text.pack (symbolVal (Proxy @desc)) - & S.schema ?~ ref + & S.content .~ resps + where + cs :: [M.MediaType] + cs = allMime $ Proxy @cs instance (KnownSymbol desc, S.ToSchema a) => IsSwaggerResponse (Respond s desc a) where - responseSwagger = simpleResponseSwagger @a @desc + -- Defaulting this to JSON, as openapi3 needs something to map a schema against. + responseSwagger = simpleResponseSwagger @a @'[JSON] @desc type instance ResponseType (RespondAs ct s desc a) = a @@ -248,10 +255,10 @@ instance KnownStatus s => IsResponse cs (RespondAs '() s desc ()) where guard (responseStatusCode output == statusVal (Proxy @s)) instance - (KnownSymbol desc, S.ToSchema a) => + (KnownSymbol desc, S.ToSchema a, Accept ct) => IsSwaggerResponse (RespondAs (ct :: Type) s desc a) where - responseSwagger = simpleResponseSwagger @a @desc + responseSwagger = simpleResponseSwagger @a @'[ct] @desc instance (KnownSymbol desc) => @@ -348,8 +355,8 @@ instance -- FUTUREWORK: should we concatenate all the matching headers instead of just -- taking the first one? extractHeaders hs = do - let name = headerName @name - (hs0, hs1) = Seq.partition (\(h, _) -> h == name) hs + let name' = headerName @name + (hs0, hs1) = Seq.partition (\(h, _) -> h == name') hs x <- case hs0 of Seq.Empty -> empty ((_, h) :<| _) -> either (const empty) pure (parseHeader h) @@ -378,11 +385,11 @@ instance (KnownSymbol name, KnownSymbol desc, S.ToParamSchema a) => ToResponseHeader (DescHeader name desc a) where - toResponseHeader _ = (name, S.Header (Just desc) sch) + toResponseHeader _ = (name', S.Header (Just desc) Nothing Nothing Nothing Nothing Nothing mempty sch) where - name = Text.pack (symbolVal (Proxy @name)) + name' = Text.pack (symbolVal (Proxy @name)) desc = Text.pack (symbolVal (Proxy @desc)) - sch = S.toParamSchema (Proxy @a) + sch = pure $ Inline $ S.toParamSchema (Proxy @a) instance ToResponseHeader h => ToResponseHeader (OptHeader h) where toResponseHeader _ = toResponseHeader (Proxy @h) @@ -419,7 +426,7 @@ instance where responseSwagger = fmap - (S.headers .~ toAllResponseHeaders (Proxy @hs)) + (S.headers .~ fmap S.Inline (toAllResponseHeaders (Proxy @hs))) (responseSwagger @r) class IsSwaggerResponseList as where @@ -477,7 +484,17 @@ combineResponseSwagger :: S.Response -> S.Response -> S.Response combineResponseSwagger r1 r2 = r1 & S.description <>~ ("\n\n" <> r2 ^. S.description) - & S.schema . _Just . S._Inline %~ flip combineSwaggerSchema (r2 ^. S.schema . _Just . S._Inline) + & S.content %~ flip (unionWith combineMediaTypeObject) (r2 ^. S.content) + +combineMediaTypeObject :: S.MediaTypeObject -> S.MediaTypeObject -> S.MediaTypeObject +combineMediaTypeObject m1 m2 = + m1 & S.schema .~ merge (m1 ^. S.schema) (m2 ^. S.schema) + where + merge Nothing a = a + merge a Nothing = a + merge (Just (Inline a)) (Just (Inline b)) = pure $ Inline $ combineSwaggerSchema a b + merge a@(Just (Ref _)) _ = a + merge _ a@(Just (Ref _)) = a combineSwaggerSchema :: S.Schema -> S.Schema -> S.Schema combineSwaggerSchema s1 s2 @@ -698,44 +715,61 @@ instance fromUnion (S (S x)) = case x of {} instance - (SwaggerMethod method, IsSwaggerResponseList as) => - S.HasSwagger (MultiVerb method '() as r) + (OpenApiMethod method, IsSwaggerResponseList as) => + S.HasOpenApi (MultiVerb method '() as r) where - toSwagger _ = + toOpenApi _ = mempty - & S.definitions <>~ defs + & S.components . S.schemas <>~ defs & S.paths . at "/" ?~ ( mempty & method ?~ ( mempty - & S.responses . S.responses .~ fmap S.Inline responses + & S.responses . S.responses .~ refResps ) ) where - method = S.swaggerMethod (Proxy @method) - (defs, responses) = S.runDeclare (responseListSwagger @as) mempty + method = S.openApiMethod (Proxy @method) + (defs, resps) = S.runDeclare (responseListSwagger @as) mempty + refResps = S.Inline <$> resps instance - (SwaggerMethod method, IsSwaggerResponseList as, AllMime cs) => - S.HasSwagger (MultiVerb method (cs :: [Type]) as r) + (OpenApiMethod method, IsSwaggerResponseList as, AllMime cs) => + S.HasOpenApi (MultiVerb method (cs :: [Type]) as r) where - toSwagger _ = + toOpenApi _ = mempty - & S.definitions <>~ defs + & S.components . S.schemas <>~ defs & S.paths . at "/" ?~ ( mempty & method ?~ ( mempty - & S.produces ?~ S.MimeList (nubOrd cs) - & S.responses . S.responses .~ fmap S.Inline responses + & S.responses . S.responses .~ refResps ) ) where - method = S.swaggerMethod (Proxy @method) + method = S.openApiMethod (Proxy @method) + -- This has our content types. cs = allMime (Proxy @cs) - (defs, responses) = S.runDeclare (responseListSwagger @as) mempty + -- This has our schemas + (defs, resps) = S.runDeclare (responseListSwagger @as) mempty + -- We need to zip them together, and stick it all back into the contentMap + -- Since we have a single schema per type, and are only changing the content-types, + -- we should be able to pick a schema out of the resps' map, and then use it for + -- all of the values of cs + addMime :: S.Response -> S.Response + addMime resp = + resp + & S.content + %~ + -- pick out an element from the map, if any exist. + -- These will all have the same schemas, and we are reapplying the content types. + foldMap (\c -> InsOrdHashMap.fromList $ (,c) <$> cs) + . listToMaybe + . toList + refResps = S.Inline . addMime <$> resps class Typeable a => IsWaiBody a where responseToWai :: ResponseF a -> Wai.Response diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index 79136daebfa..f76ada19664 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -22,14 +22,15 @@ module Wire.API.Routes.Named where import Control.Lens ((%~)) import Data.Kind import Data.Metrics.Servant +import Data.OpenApi.Lens hiding (HasServer) +import Data.OpenApi.Operation import Data.Proxy -import Data.Swagger import GHC.TypeLits import Imports import Servant import Servant.Client import Servant.Client.Core (clientIn) -import Servant.Swagger +import Servant.OpenApi -- | See http://docs.wire.com/developer/developer/servant.html#named-and-internal-route-ids-in-swagger newtype Named name x = Named {unnamed :: x} @@ -46,9 +47,9 @@ instance {-# OVERLAPPABLE #-} KnownSymbol a => RenderableSymbol a where instance {-# OVERLAPPING #-} (RenderableSymbol a, RenderableSymbol b) => RenderableSymbol '(a, b) where renderSymbol = "(" <> (renderSymbol @a) <> ", " <> (renderSymbol @b) <> ")" -instance (HasSwagger api, RenderableSymbol name) => HasSwagger (Named name api) where - toSwagger _ = - toSwagger (Proxy @api) +instance (HasOpenApi api, RenderableSymbol name) => HasOpenApi (Named name api) where + toOpenApi _ = + toOpenApi (Proxy @api) & allOperations . description %~ (Just (dscr <> "\n\n") <>) where dscr :: Text diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index a9d5ab6646b..68886e65407 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -44,18 +44,19 @@ import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Id as Id import Data.Kind import Data.Metrics.Servant +import Data.OpenApi hiding (HasServer, Header, Server) +import Data.OpenApi qualified as S import Data.Qualified -import Data.Swagger hiding (Header) import GHC.Base (Symbol) import GHC.TypeLits (KnownSymbol) import Imports hiding (All, head) import Network.Wai qualified as Wai import Servant hiding (Handler, JSON, addHeader, respond) import Servant.API.Modifiers +import Servant.OpenApi (HasOpenApi (toOpenApi)) import Servant.Server.Internal.Delayed import Servant.Server.Internal.DelayedIO import Servant.Server.Internal.Router (Router) -import Servant.Swagger (HasSwagger (toSwagger)) import Wire.API.OAuth qualified as OAuth import Wire.API.Routes.Version @@ -223,15 +224,15 @@ type ZHostValue = Text type ZOptHostHeader = Header' '[Servant.Optional, Strict] "Z-Host" ZHostValue -instance HasSwagger api => HasSwagger (ZHostOpt :> api) where - toSwagger _ = toSwagger (Proxy @api) +instance HasOpenApi api => HasOpenApi (ZHostOpt :> api) where + toOpenApi _ = toOpenApi (Proxy @api) type instance SpecialiseToVersion v (ZHostOpt :> api) = ZHostOpt :> SpecialiseToVersion v api -addZAuthSwagger :: Swagger -> Swagger +addZAuthSwagger :: OpenApi -> OpenApi addZAuthSwagger s = s - & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) + & S.components . S.securitySchemes <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "ZAuth" []] where secScheme = @@ -244,11 +245,11 @@ type instance SpecialiseToVersion v (ZAuthServant t opts :> api) = ZAuthServant t opts :> SpecialiseToVersion v api -instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts :> api) where - toSwagger _ = addZAuthSwagger (toSwagger (Proxy @api)) +instance HasOpenApi api => HasOpenApi (ZAuthServant 'ZAuthUser _opts :> api) where + toOpenApi _ = addZAuthSwagger (toOpenApi (Proxy @api)) -instance HasSwagger api => HasSwagger (ZAuthServant 'ZLocalAuthUser opts :> api) where - toSwagger _ = addZAuthSwagger (toSwagger (Proxy @api)) +instance HasOpenApi api => HasOpenApi (ZAuthServant 'ZLocalAuthUser opts :> api) where + toOpenApi _ = addZAuthSwagger (toOpenApi (Proxy @api)) instance HasLink endpoint => HasLink (ZAuthServant usr opts :> endpoint) where type MkLink (ZAuthServant _ _ :> endpoint) a = MkLink endpoint a @@ -256,10 +257,10 @@ instance HasLink endpoint => HasLink (ZAuthServant usr opts :> endpoint) where instance {-# OVERLAPPABLE #-} - HasSwagger api => - HasSwagger (ZAuthServant ztype _opts :> api) + HasOpenApi api => + HasOpenApi (ZAuthServant ztype _opts :> api) where - toSwagger _ = toSwagger (Proxy @api) + toOpenApi _ = toOpenApi (Proxy @api) instance ( HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, @@ -301,8 +302,8 @@ instance checkType :: Maybe ByteString -> Wai.Request -> DelayedIO () checkType token req = case (token, lookup "Z-Type" (Wai.requestHeaders req)) of - (Just t, value) - | value /= Just t -> + (Just t, v) + | v /= Just t -> delayedFail ServerError { errHTTPCode = 403, @@ -321,7 +322,7 @@ instance RoutesToPaths api => RoutesToPaths (ZHostOpt :> api) where getRoutes = getRoutes @api -- FUTUREWORK: Make a PR to the servant-swagger package with this instance -instance ToSchema a => ToSchema (Headers ls a) where +instance (Typeable ls, ToSchema a) => ToSchema (Headers ls a) where declareNamedSchema _ = declareNamedSchema (Proxy @a) data DescriptionOAuthScope (scope :: OAuth.OAuthScope) @@ -331,12 +332,12 @@ type instance DescriptionOAuthScope scope :> SpecialiseToVersion v api instance - (HasSwagger api, OAuth.IsOAuthScope scope) => - HasSwagger (DescriptionOAuthScope scope :> api) + (HasOpenApi api, OAuth.IsOAuthScope scope) => + HasOpenApi (DescriptionOAuthScope scope :> api) where - toSwagger _ = addScopeDescription @scope (toSwagger (Proxy @api)) + toOpenApi _ = addScopeDescription @scope (toOpenApi (Proxy @api)) -addScopeDescription :: forall scope. OAuth.IsOAuthScope scope => Swagger -> Swagger +addScopeDescription :: forall scope. OAuth.IsOAuthScope scope => OpenApi -> OpenApi addScopeDescription = allOperations . description %~ Just . (<> "\nOAuth scope: `" <> cs (toByteString (OAuth.toOAuthScope @scope)) <> "`") . fold instance (HasServer api ctx) => HasServer (DescriptionOAuthScope scope :> api) ctx where diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 48e8c36fca4..b8b22b88a33 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -19,6 +19,7 @@ module Wire.API.Routes.Public.Brig where +import Control.Lens ((?~)) import Data.Aeson qualified as A (FromJSON, ToJSON, Value) import Data.ByteString.Conversion import Data.Code (Timeout) @@ -28,20 +29,21 @@ import Data.Handle import Data.Id as Id import Data.Misc (IpAddr) import Data.Nonce (Nonce) +import Data.OpenApi hiding (Contact, Header, Schema, ToSchema) +import Data.OpenApi qualified as S import Data.Qualified (Qualified (..)) import Data.Range import Data.SOP import Data.Schema as Schema -import Data.Swagger hiding (Contact, Header, Schema, ToSchema) -import Data.Swagger qualified as S import Generics.SOP qualified as GSOP import Imports hiding (head) import Network.Wai.Utilities import Servant (JSON) import Servant hiding (Handler, JSON, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Call.Config (RTCConfiguration) import Wire.API.Connection hiding (MissingLegalholdConsent) +import Wire.API.Deprecated import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Error.Empty @@ -564,6 +566,7 @@ type AccountAPI = :<|> Named "post-password-reset-key-deprecated" ( Summary "Complete a password reset." + :> Deprecated :> CanThrow 'PasswordResetInProgress :> CanThrow 'InvalidPasswordResetKey :> CanThrow 'InvalidPasswordResetCode @@ -577,6 +580,7 @@ type AccountAPI = :<|> Named "onboarding" ( Summary "Upload contacts and invoke matching." + :> Deprecated :> Description "DEPRECATED: the feature has been turned off, the end-point does \ \nothing and always returns '{\"results\":[],\"auto-connects\":[]}'." @@ -598,8 +602,9 @@ data DeprecatedMatchingResult = DeprecatedMatchingResult instance ToSchema DeprecatedMatchingResult where schema = - object + objectWithDocModifier "DeprecatedMatchingResult" + (S.deprecated ?~ True) $ DeprecatedMatchingResult <$ const [] .= field "results" (array (null_ @SwaggerDoc)) @@ -1344,8 +1349,9 @@ type CallingAPI = Named "get-calls-config" ( Summary - "[deprecated] Retrieve TURN server addresses and credentials for \ - \ IP addresses, scheme `turn` and transport `udp` only" + "Retrieve TURN server addresses and credentials for \ + \ IP addresses, scheme `turn` and transport `udp` only (deprecated)" + :> Deprecated :> ZUser :> ZConn :> "calls" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs index 285202fb993..7e3259d8fca 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs @@ -22,7 +22,7 @@ import Data.Id as Id import Imports import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Bot import Wire.API.Error (CanThrow, ErrorResponse) import Wire.API.Error.Brig (BrigError (..)) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs index 0a4adf52401..a096c78d975 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs @@ -22,7 +22,7 @@ import Data.SOP import Imports hiding (exp, head) import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.OAuth import Wire.API.Routes.API diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs index 1ce9dd600cc..a1dc8001504 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs @@ -24,7 +24,7 @@ import Data.Qualified import Data.SOP import Imports import Servant -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import URI.ByteString import Wire.API.Asset import Wire.API.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 d24c473738e..52ec0ee5022 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -21,7 +21,7 @@ module Wire.API.Routes.Public.Galley where import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Bot import Wire.API.Routes.Public.Galley.Conversation diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs index 2c4752fda43..3eb711a96c8 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs @@ -18,7 +18,7 @@ module Wire.API.Routes.Public.Galley.Bot where import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley import Wire.API.MakesFederatedCall diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 58b42a4e4a1..79a5db7c59f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -24,11 +24,12 @@ import Data.Range import Data.SOP (I (..), NS (..)) import Imports hiding (head) import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation import Wire.API.Conversation.Code import Wire.API.Conversation.Role import Wire.API.Conversation.Typing +import Wire.API.Deprecated import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation @@ -785,6 +786,7 @@ type ConversationAPI = :<|> Named "update-other-member-unqualified" ( Summary "Update membership of the specified user (deprecated)" + :> Deprecated :> Description "Use `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" @@ -835,6 +837,7 @@ type ConversationAPI = :<|> Named "update-conversation-name-deprecated" ( Summary "Update conversation name (deprecated)" + :> Deprecated :> Description "Use `/conversations/:domain/:conv/name` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" @@ -855,6 +858,7 @@ type ConversationAPI = :<|> Named "update-conversation-name-unqualified" ( Summary "Update conversation name (deprecated)" + :> Deprecated :> Description "Use `/conversations/:domain/:conv/name` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" @@ -898,6 +902,7 @@ type ConversationAPI = :<|> Named "update-conversation-message-timer-unqualified" ( Summary "Update the message timer for a conversation (deprecated)" + :> Deprecated :> Description "Use `/conversations/:domain/:cnv/message-timer` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" @@ -943,6 +948,7 @@ type ConversationAPI = :<|> Named "update-conversation-receipt-mode-unqualified" ( Summary "Update receipt mode for a conversation (deprecated)" + :> Deprecated :> Description "Use `PUT /conversations/:domain/:cnv/receipt-mode` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" @@ -1064,6 +1070,7 @@ type ConversationAPI = :<|> Named "get-conversation-self-unqualified" ( Summary "Get self membership properties (deprecated)" + :> Deprecated :> ZLocalUser :> "conversations" :> Capture' '[Description "Conversation ID"] "cnv" ConvId @@ -1073,6 +1080,7 @@ type ConversationAPI = :<|> Named "update-conversation-self-unqualified" ( Summary "Update self membership properties (deprecated)" + :> Deprecated :> Description "Use `/conversations/:domain/:conv/self` instead." :> CanThrow 'ConvNotFound :> ZLocalUser diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs index 079858baa0e..607a6e62573 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs @@ -19,7 +19,7 @@ module Wire.API.Routes.Public.Galley.CustomBackend where import Data.Domain import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.CustomBackend import Wire.API.Error import Wire.API.Error.Galley diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index eb8e6cd3075..7d460614080 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -20,7 +20,7 @@ module Wire.API.Routes.Public.Galley.Feature where import Data.Id import GHC.TypeLits import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.ApplyMods import Wire.API.Conversation.Role import Wire.API.Error diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs index 24848c46fd2..8a8b576f2c3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs @@ -21,7 +21,7 @@ import Data.Id import GHC.Generics import Generics.SOP qualified as GSOP import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index 0ae7d3feb10..116c1dc11c3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -17,6 +17,7 @@ module Wire.API.Routes.Public.Galley.MLS where -import Servant +import Servant hiding (WithStatus) +import Servant.OpenApi.Internal.Orphans () type MLSAPI = EmptyAPI diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs index b5f07834e7f..72aa70f4125 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs @@ -22,7 +22,7 @@ import Data.SOP import Generics.SOP qualified as GSOP import Imports import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Brig qualified as BrigError import Wire.API.Error.Galley diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs index 3d3571dd23c..fd3fd392a4a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs @@ -20,7 +20,7 @@ module Wire.API.Routes.Public.Galley.Team where import Data.Id import Imports import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Routes.MultiVerb diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs index ce5fed146d5..a6a2c8aceb4 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs @@ -19,7 +19,7 @@ module Wire.API.Routes.Public.Galley.TeamConversation where import Data.Id import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs index 6d14ebc1483..4c71df03e49 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs @@ -23,7 +23,7 @@ import Data.Range import GHC.Generics import Generics.SOP qualified as GSOP import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Routes.CSV diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs index 59b98ddc9eb..107ed1de9a5 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -25,11 +25,12 @@ import SAML2.WebSSO qualified as SAML import Servant import Servant.API.Extended import Servant.Multipart -import Servant.Swagger +import Servant.OpenApi import URI.ByteString qualified as URI import Web.Scim.Capabilities.MetaSchema as Scim.Meta import Web.Scim.Class.Auth as Scim.Auth import Web.Scim.Class.User as Scim.User +import Wire.API.Deprecated (Deprecated) import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Routes.API @@ -57,7 +58,7 @@ type DeprecateSSOAPIV1 = \Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams" type APISSO = - DeprecateSSOAPIV1 :> "metadata" :> SAML.APIMeta + DeprecateSSOAPIV1 :> Deprecated :> "metadata" :> SAML.APIMeta :<|> "metadata" :> Capture "team" TeamId :> SAML.APIMeta :<|> "initiate-login" :> APIAuthReqPrecheck :<|> "initiate-login" :> APIAuthReq @@ -82,6 +83,7 @@ type APIAuthReq = type APIAuthRespLegacy = DeprecateSSOAPIV1 + :> Deprecated :> "finalize-login" -- (SAML.APIAuthResp from here on, except for response) :> MultipartForm Mem SAML.AuthnResponseBody @@ -191,4 +193,4 @@ data SparAPITag instance ServiceAPI SparAPITag v where type ServiceAPIRoutes SparAPITag = SparAPI type SpecialisedAPIRoutes v SparAPITag = SparAPI - serviceSwagger = toSwagger (Proxy @SparAPI) + serviceSwagger = toOpenApi (Proxy @SparAPI) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Util.hs b/libs/wire-api/src/Wire/API/Routes/Public/Util.hs index 694230f7574..ab34186bb12 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Util.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Util.hs @@ -23,7 +23,7 @@ module Wire.API.Routes.Public.Util where import Control.Comonad import Data.SOP (I (..), NS (..)) import Servant -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Routes.MultiVerb instance diff --git a/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs b/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs index f54cccf4f36..9147e008fda 100644 --- a/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs +++ b/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs @@ -24,16 +24,16 @@ where import Data.Domain import Data.Kind import Data.Metrics.Servant +import Data.OpenApi hiding (HasServer, value) import Data.Qualified -import Data.Swagger import GHC.TypeLits import Imports import Servant import Servant.API.Description import Servant.API.Modifiers import Servant.Client.Core.HasClient +import Servant.OpenApi import Servant.Server.Internal.ErrorFormatter -import Servant.Swagger import Wire.API.Routes.Version -- | Capture a value qualified by a domain, with modifiers. @@ -56,16 +56,15 @@ type instance QualifiedCapture' mods capture a :> SpecialiseToVersion v api instance - ( Typeable a, - ToParamSchema a, - HasSwagger api, + ( ToParamSchema a, + HasOpenApi api, KnownSymbol capture, KnownSymbol (AppendSymbol capture "_domain"), KnownSymbol (FoldDescription mods) ) => - HasSwagger (QualifiedCapture' mods capture a :> api) + HasOpenApi (QualifiedCapture' mods capture a :> api) where - toSwagger _ = toSwagger (Proxy @(WithDomain mods capture a api)) + toOpenApi _ = toOpenApi (Proxy @(WithDomain mods capture a api)) instance ( KnownSymbol capture, diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index ea8bfc26f8b..a2a337b61df 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -52,15 +52,16 @@ import Data.Binary.Builder qualified as Builder import Data.ByteString.Conversion (ToByteString (builder), toByteString') import Data.ByteString.Lazy qualified as LBS import Data.Domain +import Data.OpenApi qualified as S import Data.Schema import Data.Singletons.Base.TH -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding as Text import GHC.TypeLits import Imports import Servant import Servant.API.Extended.RawM +import Wire.API.Deprecated import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.VersionInfo @@ -230,6 +231,10 @@ type instance SpecialiseToVersion v (Summary s :> api) = Summary s :> SpecialiseToVersion v api +type instance + SpecialiseToVersion v (Deprecated :> api) = + Deprecated :> SpecialiseToVersion v api + type instance SpecialiseToVersion v (Verb m s t r) = Verb m s t r diff --git a/libs/wire-api/src/Wire/API/Routes/Versioned.hs b/libs/wire-api/src/Wire/API/Routes/Versioned.hs index 1ca7bac0587..7707e3441e6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Versioned.hs +++ b/libs/wire-api/src/Wire/API/Routes/Versioned.hs @@ -20,15 +20,15 @@ module Wire.API.Routes.Versioned where import Data.Aeson (FromJSON, ToJSON) import Data.Kind import Data.Metrics.Servant +import Data.OpenApi qualified as S import Data.Schema import Data.Singletons -import Data.Swagger qualified as S import GHC.TypeLits import Imports import Servant import Servant.API.ContentTypes -import Servant.Swagger -import Servant.Swagger.Internal +import Servant.OpenApi +import Servant.OpenApi.Internal import Wire.API.Routes.MultiVerb import Wire.API.Routes.Version @@ -63,12 +63,12 @@ type instance instance ( S.ToSchema (Versioned v a), - HasSwagger api, + HasOpenApi api, AllAccept cts ) => - HasSwagger (VersionedReqBody v cts a :> api) + HasOpenApi (VersionedReqBody v cts a :> api) where - toSwagger _ = toSwagger (Proxy @(ReqBody cts (Versioned v a) :> api)) + toOpenApi _ = toOpenApi (Proxy @(ReqBody cts (Versioned v a) :> api)) -------------------------------------------------------------------------------- -- Versioned responses @@ -92,7 +92,7 @@ instance (KnownSymbol desc, S.ToSchema a) => IsSwaggerResponse (VersionedRespond v s desc a) where - responseSwagger = simpleResponseSwagger @a @desc + responseSwagger = simpleResponseSwagger @a @'[JSON] @desc ------------------------------------------------------------------------------- -- Versioned newtype wrapper @@ -111,7 +111,7 @@ deriving via Schema (Versioned v a) instance ToSchema (Versioned v a) => FromJSO deriving via Schema (Versioned v a) instance ToSchema (Versioned v a) => ToJSON (Versioned v a) -- add version suffix to swagger schema to prevent collisions -instance (SingI v, ToSchema (Versioned v a)) => S.ToSchema (Versioned v a) where +instance (SingI v, ToSchema (Versioned v a), Typeable a, Typeable v) => S.ToSchema (Versioned v a) where declareNamedSchema _ = do S.NamedSchema n s <- schemaToSwagger (Proxy @(Versioned v a)) pure $ S.NamedSchema (fmap (<> toUrlPiece (demote @v)) n) s diff --git a/libs/wire-api/src/Wire/API/Routes/WebSocket.hs b/libs/wire-api/src/Wire/API/Routes/WebSocket.hs index 72354d95bc1..0405b58d094 100644 --- a/libs/wire-api/src/Wire/API/Routes/WebSocket.hs +++ b/libs/wire-api/src/Wire/API/Routes/WebSocket.hs @@ -21,16 +21,16 @@ import Control.Lens import Control.Monad.Trans.Resource import Data.HashMap.Strict.InsOrd import Data.Metrics.Servant +import Data.OpenApi hiding (HasServer) import Data.Proxy -import Data.Swagger import Imports import Network.Wai.Handler.WebSockets import Network.WebSockets +import Servant.OpenApi import Servant.Server hiding (respond) import Servant.Server.Internal.Delayed import Servant.Server.Internal.RouteResult import Servant.Server.Internal.Router -import Servant.Swagger import Wire.API.Routes.Version -- | A websocket that relates to a 'PendingConnection' @@ -65,8 +65,8 @@ instance HasServer WebSocketPending ctx where type instance SpecialiseToVersion v WebSocketPending = WebSocketPending -instance HasSwagger WebSocketPending where - toSwagger _ = +instance HasOpenApi WebSocketPending where + toOpenApi _ = mempty & paths . at "/" @@ -82,7 +82,7 @@ instance HasSwagger WebSocketPending where ) ) where - resps :: InsOrdHashMap HttpStatusCode (Referenced Data.Swagger.Response) + resps :: InsOrdHashMap HttpStatusCode (Referenced Data.OpenApi.Response) resps = mempty & at 101 ?~ Inline (mempty & description .~ "Connection upgraded.") diff --git a/libs/wire-api/src/Wire/API/ServantProto.hs b/libs/wire-api/src/Wire/API/ServantProto.hs index 3eb06458fab..6e2dbd6140b 100644 --- a/libs/wire-api/src/Wire/API/ServantProto.hs +++ b/libs/wire-api/src/Wire/API/ServantProto.hs @@ -19,7 +19,7 @@ module Wire.API.ServantProto where import Data.ByteString.Lazy qualified as LBS import Data.List.NonEmpty (NonEmpty (..)) -import Data.Swagger +import Data.OpenApi import Imports import Network.HTTP.Media ((//)) import Servant diff --git a/libs/wire-api/src/Wire/API/SwaggerHelper.hs b/libs/wire-api/src/Wire/API/SwaggerHelper.hs index 9e1927156c1..3e882b8ab5c 100644 --- a/libs/wire-api/src/Wire/API/SwaggerHelper.hs +++ b/libs/wire-api/src/Wire/API/SwaggerHelper.hs @@ -19,23 +19,33 @@ module Wire.API.SwaggerHelper where import Control.Lens import Data.Containers.ListUtils (nubOrd) -import Data.Swagger hiding (Contact, Header, Schema, ToSchema) -import Data.Swagger qualified as S +import Data.HashMap.Strict.InsOrd +import Data.OpenApi hiding (Contact, Header, Schema, ToSchema) +import Data.OpenApi qualified as S +import Data.Text qualified as T import Imports hiding (head) -cleanupSwagger :: Swagger -> Swagger +cleanupSwagger :: OpenApi -> OpenApi cleanupSwagger = (S.security %~ nub) -- sanitise definitions - . (S.definitions . traverse %~ sanitise) + . (S.components . S.schemas . traverse %~ sanitise) + -- strip the default errors + . ( S.allOperations + . S.responses + . S.responses + %~ foldrWithKey stripDefaultErrors mempty + ) -- sanitise general responses - . (S.responses . traverse . S.schema . _Just . S._Inline %~ sanitise) + . (S.components . S.responses . traverse . S.content . traverse . S.schema . _Just . S._Inline %~ sanitise) -- sanitise all responses of all paths . ( S.allOperations . S.responses . S.responses . traverse . S._Inline + . S.content + . traverse . S.schema . _Just . S._Inline @@ -47,3 +57,49 @@ cleanupSwagger = (S.properties . traverse . S._Inline %~ sanitise) . (S.required %~ nubOrd) . (S.enum_ . _Just %~ nub) + -- servant-openapi and servant-swagger both insert default responses with codes 404 and 400. + -- They have a simple structure that we can match against, and remove from the final structure. + stripDefaultErrors :: HttpStatusCode -> Referenced Response -> Responses' -> Responses' + stripDefaultErrors code resp resps = + case code of + 400 -> case resp ^? _Inline . S.description of + (Just desc) -> + if "Invalid " + `T.isPrefixOf` desc + && resp + ^? _Inline + . links + == pure mempty + && resp + ^? _Inline + . content + == pure mempty + && resp + ^? _Inline + . headers + == pure mempty + then resps + else insert code resp resps + Nothing -> insert code resp resps + 404 -> case resp ^? _Inline . S.description of + (Just desc) -> + if " not found" + `T.isSuffixOf` desc + && resp + ^? _Inline + . links + == pure mempty + && resp + ^? _Inline + . content + == pure mempty + && resp + ^? _Inline + . headers + == pure mempty + then resps + else insert code resp resps + Nothing -> insert code resp resps + _ -> insert code resp resps + +type Responses' = InsOrdHashMap HttpStatusCode (Referenced Response) diff --git a/libs/wire-api/src/Wire/API/SwaggerServant.hs b/libs/wire-api/src/Wire/API/SwaggerServant.hs index 89973fb59ae..5c3918cf39c 100644 --- a/libs/wire-api/src/Wire/API/SwaggerServant.hs +++ b/libs/wire-api/src/Wire/API/SwaggerServant.hs @@ -25,7 +25,7 @@ import Data.Metrics.Servant import Data.Proxy import Imports hiding (head) import Servant -import Servant.Swagger (HasSwagger (toSwagger)) +import Servant.OpenApi (HasOpenApi (toOpenApi)) -- | A type-level tag that lets us omit any branch from Swagger docs. -- @@ -34,8 +34,8 @@ import Servant.Swagger (HasSwagger (toSwagger)) -- it's only justification is laziness. data OmitDocs -instance HasSwagger (OmitDocs :> a) where - toSwagger _ = mempty +instance HasOpenApi (OmitDocs :> a) where + toOpenApi _ = mempty instance HasServer api ctx => HasServer (OmitDocs :> api) ctx where type ServerT (OmitDocs :> api) m = ServerT api m diff --git a/libs/wire-api/src/Wire/API/SystemSettings.hs b/libs/wire-api/src/Wire/API/SystemSettings.hs index d6098ac4ec5..d07d7152a44 100644 --- a/libs/wire-api/src/Wire/API/SystemSettings.hs +++ b/libs/wire-api/src/Wire/API/SystemSettings.hs @@ -19,10 +19,10 @@ module Wire.API.SystemSettings where import Control.Lens hiding ((.=)) import Data.Aeson qualified as A +import Data.OpenApi qualified as S import Data.Schema as Schema -import Data.Swagger qualified as S import Imports -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Test.QuickCheck import Wire.Arbitrary diff --git a/libs/wire-api/src/Wire/API/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index fe3ed5e596c..13c09ab567b 100644 --- a/libs/wire-api/src/Wire/API/Team.hs +++ b/libs/wire-api/src/Wire/API/Team.hs @@ -67,7 +67,7 @@ module Wire.API.Team ) where -import Control.Lens (makeLenses, (?~)) +import Control.Lens (makeLenses, over, (?~)) import Data.Aeson (FromJSON, ToJSON, Value (..)) import Data.Aeson.Types (Parser) import Data.Attoparsec.ByteString qualified as Atto (Parser, string) @@ -76,9 +76,10 @@ import Data.ByteString.Conversion import Data.Code qualified as Code import Data.Id (TeamId, UserId) import Data.Misc (PlainTextPassword6) +import Data.OpenApi (HasDeprecated (deprecated)) +import Data.OpenApi qualified as S import Data.Range import Data.Schema -import Data.Swagger qualified as S import Data.Text.Encoding qualified as T import Imports import Test.QuickCheck.Gen (suchThat) @@ -118,7 +119,10 @@ instance ToSchema Team where <*> _teamSplashScreen .= (fromMaybe DefaultIcon <$> optField "splash_screen" schema) where desc = description ?~ "`binding` is deprecated, and should be ignored. The non-binding teams API is not used (and will not be supported from API version V4 onwards), and `binding` will always be `true`." - bindingDesc = description ?~ "Deprecated, please ignore." + bindingDesc v = + v + & description ?~ "Deprecated, please ignore." + & deprecated ?~ True -- | How a team "binds" its members (users) -- @@ -145,8 +149,9 @@ data TeamBinding instance ToSchema TeamBinding where schema = - enum @Bool "TeamBinding" $ - mconcat [element True Binding, element False NonBinding] + over doc (deprecated ?~ True) $ + enum @Bool "TeamBinding" $ + mconcat [element True Binding, element False NonBinding] -------------------------------------------------------------------------------- -- TeamList diff --git a/libs/wire-api/src/Wire/API/Team/Conversation.hs b/libs/wire-api/src/Wire/API/Team/Conversation.hs index ae020086104..3822a614923 100644 --- a/libs/wire-api/src/Wire/API/Team/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Team/Conversation.hs @@ -35,8 +35,8 @@ where import Control.Lens (makeLenses, (?~)) import Data.Aeson qualified as A import Data.Id (ConvId) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 5e3a3bf9f39..899f7491573 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -98,10 +98,10 @@ import Data.Either.Extra (maybeToEither) import Data.Id import Data.Kind import Data.Misc (HttpsUrl) +import Data.OpenApi qualified as S import Data.Proxy import Data.Schema import Data.Scientific (toBoundedInteger) -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Text.Lazy qualified as TL @@ -266,7 +266,7 @@ deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => T deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => FromJSON (WithStatus cfg) -deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => S.ToSchema (WithStatus cfg) +deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg), Typeable cfg) => S.ToSchema (WithStatus cfg) instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (WithStatus cfg) where schema = @@ -296,7 +296,7 @@ deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg)) => FromJSON (WithStatusPatch cfg) -deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg)) => S.ToSchema (WithStatusPatch cfg) +deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg), Typeable cfg) => S.ToSchema (WithStatusPatch cfg) wsPatch :: Maybe FeatureStatus -> Maybe LockStatus -> Maybe cfg -> Maybe FeatureTTL -> WithStatusPatch cfg wsPatch = WithStatusBase @@ -1043,8 +1043,8 @@ data FeatureStatus instance S.ToParamSchema FeatureStatus where toParamSchema _ = mempty - { S._paramSchemaType = Just S.SwaggerString, - S._paramSchemaEnum = Just (A.String . toQueryParam <$> [(minBound :: FeatureStatus) ..]) + { S._schemaType = Just S.OpenApiString, + S._schemaEnum = Just (A.String . toQueryParam <$> [(minBound :: FeatureStatus) ..]) } instance FromHttpApiData FeatureStatus where diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index 8593c67ce97..44cc508ab69 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -32,9 +32,9 @@ import Data.Aeson qualified as A import Data.ByteString.Conversion import Data.Id import Data.Json.Util +import Data.OpenApi qualified as S import Data.SOP import Data.Schema -import Data.Swagger qualified as S import Data.Text.Encoding qualified as TE import Imports import Servant (FromHttpApiData (..), ToHttpApiData (..)) @@ -130,7 +130,7 @@ newtype InvitationLocation = InvitationLocation instance S.ToParamSchema InvitationLocation where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.format ?~ "url" instance FromHttpApiData InvitationLocation where diff --git a/libs/wire-api/src/Wire/API/Team/LegalHold.hs b/libs/wire-api/src/Wire/API/Team/LegalHold.hs index d72dafb5da8..40fbb9a7af0 100644 --- a/libs/wire-api/src/Wire/API/Team/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Team/LegalHold.hs @@ -35,9 +35,9 @@ import Data.Aeson.Types qualified as A import Data.Id import Data.LegalHold import Data.Misc +import Data.OpenApi qualified as S hiding (info) import Data.Proxy import Data.Schema -import Data.Swagger qualified as S hiding (info) import Deriving.Aeson import Imports import Wire.API.Provider @@ -240,11 +240,11 @@ instance ToSchema LegalholdProtectee where pure $ S.NamedSchema (Just "LegalholdProtectee") $ mempty - & S.type_ ?~ S.SwaggerObject + & S.type_ ?~ S.OpenApiObject & S.properties . at "tag" ?~ S.Inline ( mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.enum_ ?~ [ A.toJSON ("ProtectedUser" :: String), A.toJSON ("UnprotectedBot" :: String), diff --git a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs b/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs index ea892087bfe..8dc5fd14366 100644 --- a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs +++ b/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs @@ -34,7 +34,7 @@ where import Data.Aeson hiding (fieldLabelModifier) import Data.Id import Data.Json.Util ((#)) -import Data.Swagger +import Data.OpenApi import Imports import Wire.API.User.Client.Prekey import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Team/LegalHold/Internal.hs b/libs/wire-api/src/Wire/API/Team/LegalHold/Internal.hs index cb3915f4a38..e706f472fc6 100644 --- a/libs/wire-api/src/Wire/API/Team/LegalHold/Internal.hs +++ b/libs/wire-api/src/Wire/API/Team/LegalHold/Internal.hs @@ -29,8 +29,8 @@ import Data.Aeson import Data.Id import Data.Json.Util import Data.Misc +import Data.OpenApi qualified as Swagger import Data.Schema qualified as Schema -import Data.Swagger qualified as Swagger import Imports import Wire.API.Provider import Wire.API.Provider.Service diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index d47061389b5..91e790aa66e 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -74,10 +74,10 @@ import Data.Json.Util import Data.Kind import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) import Data.Misc (PlainTextPassword6) +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi.Schema qualified as S import Data.Proxy import Data.Schema -import Data.Swagger (ToParamSchema (..)) -import Data.Swagger.Schema qualified as S import GHC.TypeLits import Imports import Wire.API.Routes.MultiTablePaging (MultiTablePage (..)) @@ -132,7 +132,7 @@ deriving via deriving via (Schema (TeamMember' tag)) instance - (ToSchema (TeamMember' tag)) => + (ToSchema (TeamMember' tag), Typeable tag) => S.ToSchema (TeamMember' tag) mkTeamMember :: @@ -256,7 +256,7 @@ deriving via deriving via (Schema (TeamMemberList' tag)) instance - ToSchema (TeamMemberList' tag) => + (ToSchema (TeamMemberList' tag), Typeable tag) => S.ToSchema (TeamMemberList' tag) newTeamMemberList :: [TeamMember] -> ListType -> TeamMemberList @@ -348,7 +348,7 @@ deriving via deriving via (Schema (NewTeamMember' tag)) instance - (ToSchema (NewTeamMember' tag)) => + (ToSchema (NewTeamMember' tag), Typeable tag) => S.ToSchema (NewTeamMember' tag) deriving via (GenericUniform NewTeamMember) instance Arbitrary NewTeamMember diff --git a/libs/wire-api/src/Wire/API/Team/Permission.hs b/libs/wire-api/src/Wire/API/Team/Permission.hs index be29d6b46d1..49a9893b370 100644 --- a/libs/wire-api/src/Wire/API/Team/Permission.hs +++ b/libs/wire-api/src/Wire/API/Team/Permission.hs @@ -48,10 +48,10 @@ import Control.Error.Util qualified as Err import Control.Lens (makeLenses, (?~), (^.)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bits (testBit, (.|.)) +import Data.OpenApi qualified as S import Data.Schema import Data.Set qualified as Set import Data.Singletons.Base.TH -import Data.Swagger qualified as S import Imports import Wire.API.Util.Aeson (CustomEncoded (..)) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Team/Role.hs b/libs/wire-api/src/Wire/API/Team/Role.hs index 424065e66c0..d4602394750 100644 --- a/libs/wire-api/src/Wire/API/Team/Role.hs +++ b/libs/wire-api/src/Wire/API/Team/Role.hs @@ -29,8 +29,8 @@ import Control.Lens ((?~)) import Data.Aeson import Data.Attoparsec.ByteString.Char8 (string) import Data.ByteString.Conversion (FromByteString (..), ToByteString (..)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as T import Imports import Servant.API (FromHttpApiData, parseQueryParam) @@ -93,7 +93,7 @@ instance ToSchema Role where instance S.ToParamSchema Role where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.enum_ ?~ fmap roleName [minBound .. maxBound] instance FromHttpApiData Role where diff --git a/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs b/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs index b41300a8b7a..76d530f6f15 100644 --- a/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs +++ b/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs @@ -24,8 +24,8 @@ module Wire.API.Team.SearchVisibility where import Control.Lens ((?~)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Deriving.Aeson import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Team/Size.hs b/libs/wire-api/src/Wire/API/Team/Size.hs index 811a7a094e6..ce0d8fe6468 100644 --- a/libs/wire-api/src/Wire/API/Team/Size.hs +++ b/libs/wire-api/src/Wire/API/Team/Size.hs @@ -22,8 +22,8 @@ where import Control.Lens ((?~)) import Data.Aeson qualified as A +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Numeric.Natural diff --git a/libs/wire-api/src/Wire/API/Unreachable.hs b/libs/wire-api/src/Wire/API/Unreachable.hs index baf37558eff..54055ae6359 100644 --- a/libs/wire-api/src/Wire/API/Unreachable.hs +++ b/libs/wire-api/src/Wire/API/Unreachable.hs @@ -28,9 +28,9 @@ import Data.Aeson qualified as A import Data.Id import Data.List.NonEmpty import Data.List.NonEmpty qualified as NE +import Data.OpenApi qualified as S import Data.Qualified import Data.Schema -import Data.Swagger qualified as S import Imports newtype UnreachableUsers = UnreachableUsers {unreachableUsers :: NonEmpty (Qualified UserId)} diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 9404502ca68..16be7999255 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -177,13 +177,13 @@ import Data.Json.Util (UTCTimeMillis, (#)) import Data.LegalHold (UserLegalHoldStatus) import Data.List.NonEmpty (NonEmpty (..)) import Data.Misc (PlainTextPassword6, PlainTextPassword8) +import Data.OpenApi qualified as S import Data.Qualified import Data.Range import Data.SOP import Data.Schema import Data.Schema qualified as Schema import Data.Set qualified as Set -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Ascii import Data.Text.Encoding qualified as T @@ -1819,7 +1819,7 @@ instance S.ToSchema ListUsersQuery where pure $ S.NamedSchema (Just "ListUsersQuery") $ mempty - & S.type_ ?~ S.SwaggerObject + & S.type_ ?~ S.OpenApiObject & S.description ?~ "exactly one of qualified_ids or qualified_handles must be provided." & S.properties .~ InsOrdHashMap.fromList [("qualified_ids", uids), ("qualified_handles", handles)] & S.example ?~ toJSON (ListUsersByIds [Qualified (Id UUID.nil) (Domain "example.com")]) @@ -1954,8 +1954,8 @@ instance FromByteString VerificationAction where instance S.ToParamSchema VerificationAction where toParamSchema _ = mempty - { S._paramSchemaType = Just S.SwaggerString, - S._paramSchemaEnum = Just (A.String . toQueryParam <$> [(minBound :: VerificationAction) ..]) + { S._schemaType = Just S.OpenApiString, + S._schemaEnum = Just (A.String . toQueryParam <$> [(minBound :: VerificationAction) ..]) } instance FromHttpApiData VerificationAction where diff --git a/libs/wire-api/src/Wire/API/User/Activation.hs b/libs/wire-api/src/Wire/API/User/Activation.hs index 7777b2c25b8..e14b30bc326 100644 --- a/libs/wire-api/src/Wire/API/User/Activation.hs +++ b/libs/wire-api/src/Wire/API/User/Activation.hs @@ -40,9 +40,9 @@ import Data.Aeson qualified as A import Data.Aeson.Types (Parser) import Data.ByteString.Conversion import Data.Data (Proxy (Proxy)) +import Data.OpenApi (ToParamSchema) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger (ToParamSchema) -import Data.Swagger qualified as S import Data.Text.Ascii import Data.Tuple.Extra (fst3, snd3, thd3) import Imports diff --git a/libs/wire-api/src/Wire/API/User/Auth.hs b/libs/wire-api/src/Wire/API/User/Auth.hs index 8670a4bc20e..df15827e2e2 100644 --- a/libs/wire-api/src/Wire/API/User/Auth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth.hs @@ -71,9 +71,9 @@ import Data.Handle (Handle) import Data.Id import Data.Json.Util import Data.Misc (PlainTextPassword6) +import Data.OpenApi qualified as S import Data.SOP import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Text.Lazy.Encoding qualified as LT @@ -554,7 +554,7 @@ utcToSetCookie c = } instance S.ToParamSchema UserTokenCookie where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData UserTokenCookie where parseHeader = utcFromSetCookie . parseSetCookie diff --git a/libs/wire-api/src/Wire/API/User/Auth/LegalHold.hs b/libs/wire-api/src/Wire/API/User/Auth/LegalHold.hs index 951b2c19ab2..b1f20c416a8 100644 --- a/libs/wire-api/src/Wire/API/User/Auth/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/User/Auth/LegalHold.hs @@ -21,8 +21,8 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Id import Data.Misc +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.User.Auth diff --git a/libs/wire-api/src/Wire/API/User/Auth/ReAuth.hs b/libs/wire-api/src/Wire/API/User/Auth/ReAuth.hs index 040698e848a..0892089a90d 100644 --- a/libs/wire-api/src/Wire/API/User/Auth/ReAuth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth/ReAuth.hs @@ -25,8 +25,8 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Code import Data.Misc +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.User diff --git a/libs/wire-api/src/Wire/API/User/Auth/Sso.hs b/libs/wire-api/src/Wire/API/User/Auth/Sso.hs index 6e061536e01..0c9daa86859 100644 --- a/libs/wire-api/src/Wire/API/User/Auth/Sso.hs +++ b/libs/wire-api/src/Wire/API/User/Auth/Sso.hs @@ -20,8 +20,8 @@ module Wire.API.User.Auth.Sso where import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Id +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.User.Auth diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index 761d8b7337e..198167cde03 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -83,11 +83,11 @@ import Data.Id import Data.Json.Util import Data.Map.Strict qualified as Map import Data.Misc (Latitude (..), Location, Longitude (..), PlainTextPassword6, latitude, location, longitude) +import Data.OpenApi hiding (Schema, ToSchema, nullable, schema) +import Data.OpenApi qualified as Swagger hiding (nullable) import Data.Qualified import Data.Schema import Data.Set qualified as Set -import Data.Swagger hiding (Schema, ToSchema, schema) -import Data.Swagger qualified as Swagger import Data.Text.Encoding qualified as Text.E import Data.Time.Clock import Data.UUID (toASCIIBytes) @@ -368,7 +368,7 @@ instance Swagger.ToSchema UserClientsFull where pure $ NamedSchema (Just "UserClientsFull") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & description ?~ "Dictionary object of `Client` objects indexed by `UserId`." & example ?~ "{\"1355c55a-0ac8-11ee-97ee-db1a6351f093\": , ...}" diff --git a/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs b/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs index df719886f37..99ed6e13d92 100644 --- a/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs +++ b/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs @@ -22,10 +22,10 @@ module Wire.API.User.Client.DPoPAccessToken where import Data.Aeson (FromJSON, ToJSON) import Data.ByteString.Conversion (FromByteString (..), ToByteString (..), fromByteString', toByteString') +import Data.OpenApi qualified as S +import Data.OpenApi.ParamSchema (ToParamSchema (..)) import Data.SOP import Data.Schema -import Data.Swagger qualified as S -import Data.Swagger.ParamSchema (ToParamSchema (..)) import Data.Text as T import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Imports diff --git a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs index 4f03328465a..f58eaa000ed 100644 --- a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs +++ b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs @@ -35,8 +35,8 @@ where import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Hashable (hash) import Data.Id +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/User/Handle.hs b/libs/wire-api/src/Wire/API/User/Handle.hs index 08242a6dfe9..3db27ef8c12 100644 --- a/libs/wire-api/src/Wire/API/User/Handle.hs +++ b/libs/wire-api/src/Wire/API/User/Handle.hs @@ -28,10 +28,10 @@ import Control.Applicative import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Id (UserId) +import Data.OpenApi qualified as S import Data.Qualified (Qualified (..), deprecatedSchema) import Data.Range import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index c71bec44864..2b88c1d3bd8 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -61,9 +61,9 @@ import Data.Attoparsec.Text import Data.Bifunctor (first) import Data.ByteString.Conversion import Data.CaseInsensitive qualified as CI +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger (ToParamSchema (..)) -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding (decodeUtf8', encodeUtf8) import Data.Time.Clock @@ -326,7 +326,7 @@ instance S.ToSchema UserSSOId where pure $ S.NamedSchema (Just "UserSSOId") $ mempty - & S.type_ ?~ S.SwaggerObject + & S.type_ ?~ S.OpenApiObject & S.properties .~ [ ("tenant", tenantSchema), ("subject", subjectSchema), diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs index f45a6f991ea..e954f15c2e6 100644 --- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs +++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs @@ -31,8 +31,8 @@ import Data.ByteString.Conversion qualified as BSC import Data.HashMap.Strict.InsOrd (InsOrdHashMap) import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Id (TeamId) +import Data.OpenApi import Data.Proxy (Proxy (Proxy)) -import Data.Swagger import Imports import Network.HTTP.Media ((//)) import SAML2.WebSSO (IdPConfig) @@ -108,9 +108,9 @@ instance ToHttpApiData WireIdPAPIVersion where instance ToParamSchema WireIdPAPIVersion where toParamSchema Proxy = mempty - { _paramSchemaDefault = Just "v2", - _paramSchemaType = Just SwaggerString, - _paramSchemaEnum = Just (String . toQueryParam <$> [(minBound :: WireIdPAPIVersion) ..]) + { _schemaDefault = Just "v2", + _schemaType = Just OpenApiString, + _schemaEnum = Just (String . toQueryParam <$> [(minBound :: WireIdPAPIVersion) ..]) } instance Cql.Cql WireIdPAPIVersion where @@ -205,7 +205,7 @@ instance ToSchema IdPMetadataInfo where & properties .~ properties_ & minProperties ?~ 1 & maxProperties ?~ 1 - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject where properties_ :: InsOrdHashMap Text (Referenced Schema) properties_ = diff --git a/libs/wire-api/src/Wire/API/User/Orphans.hs b/libs/wire-api/src/Wire/API/User/Orphans.hs index 05f49534e4f..10ec177a3fe 100644 --- a/libs/wire-api/src/Wire/API/User/Orphans.hs +++ b/libs/wire-api/src/Wire/API/User/Orphans.hs @@ -26,8 +26,8 @@ import Data.Char import Data.Currency qualified as Currency import Data.ISO3166_CountryCodes import Data.LanguageCodes +import Data.OpenApi import Data.Proxy -import Data.Swagger import Data.UUID import Data.X509 as X509 import Imports @@ -35,7 +35,7 @@ import SAML2.WebSSO qualified as SAML import SAML2.WebSSO.Types.TH (deriveJSONOptions) import Servant.API ((:>)) import Servant.Multipart qualified as SM -import Servant.Swagger +import Servant.OpenApi import URI.ByteString deriving instance Generic ISO639_1 @@ -94,7 +94,7 @@ instance ToSchema (SAML.FormRedirect SAML.AuthnRequest) where pure $ NamedSchema (Just "FormRedirect") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties . at "uri" ?~ Inline (toSchema (Proxy @Text)) & properties . at "xml" ?~ authnReqSchema @@ -110,8 +110,8 @@ instance ToSchema SAML.SPMetadata where instance ToSchema Void where declareNamedSchema _ = declareNamedSchema (Proxy @String) -instance HasSwagger route => HasSwagger (SM.MultipartForm SM.Mem resp :> route) where - toSwagger _proxy = toSwagger (Proxy @route) +instance HasOpenApi route => HasOpenApi (SM.MultipartForm SM.Mem resp :> route) where + toOpenApi _proxy = toOpenApi (Proxy @route) instance ToSchema SAML.IdPId where declareNamedSchema _ = declareNamedSchema (Proxy @UUID) diff --git a/libs/wire-api/src/Wire/API/User/Password.hs b/libs/wire-api/src/Wire/API/User/Password.hs index 2a6b9bf20ed..4f14e4ca7c6 100644 --- a/libs/wire-api/src/Wire/API/User/Password.hs +++ b/libs/wire-api/src/Wire/API/User/Password.hs @@ -36,11 +36,11 @@ import Data.Aeson qualified as A import Data.Aeson.Types (Parser) import Data.ByteString.Conversion import Data.Misc (PlainTextPassword8) +import Data.OpenApi qualified as S +import Data.OpenApi.ParamSchema import Data.Proxy (Proxy (Proxy)) import Data.Range (Ranged (..)) import Data.Schema as Schema -import Data.Swagger qualified as S -import Data.Swagger.ParamSchema import Data.Text.Ascii import Data.Tuple.Extra (fst3, snd3, thd3) import Imports diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index 8f03b39b375..ae018f20b75 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -58,9 +58,9 @@ import Data.Attoparsec.Text import Data.ByteString.Conversion import Data.ISO3166_CountryCodes import Data.LanguageCodes +import Data.OpenApi qualified as S import Data.Range import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Imports import Wire.API.Asset (AssetKey (..)) diff --git a/libs/wire-api/src/Wire/API/User/RichInfo.hs b/libs/wire-api/src/Wire/API/User/RichInfo.hs index ef0eac713e8..32a3db8fa19 100644 --- a/libs/wire-api/src/Wire/API/User/RichInfo.hs +++ b/libs/wire-api/src/Wire/API/User/RichInfo.hs @@ -52,8 +52,8 @@ import Data.CaseInsensitive (CI) import Data.CaseInsensitive qualified as CI import Data.List.Extra (nubOrdOn) import Data.Map qualified as Map +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Imports import Test.QuickCheck qualified as QC diff --git a/libs/wire-api/src/Wire/API/User/Saml.hs b/libs/wire-api/src/Wire/API/User/Saml.hs index 8ff2e27c954..09ad0d24367 100644 --- a/libs/wire-api/src/Wire/API/User/Saml.hs +++ b/libs/wire-api/src/Wire/API/User/Saml.hs @@ -30,8 +30,8 @@ import Data.Aeson hiding (fieldLabelModifier) import Data.Aeson.TH hiding (fieldLabelModifier) import Data.ByteString.Builder qualified as Builder import Data.Id (UserId) +import Data.OpenApi import Data.Proxy (Proxy (Proxy)) -import Data.Swagger import Data.Text qualified as T import Data.Time import GHC.TypeLits (KnownSymbol, symbolVal) diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index f3440beea7b..752c608bd85 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -59,8 +59,8 @@ import Data.Id (ScimTokenId, TeamId, UserId) import Data.Json.Util ((#)) import Data.Map qualified as Map import Data.Misc (PlainTextPassword6) +import Data.OpenApi hiding (Operation) import Data.Proxy -import Data.Swagger hiding (Operation) import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock (UTCTime) import Imports @@ -462,7 +462,7 @@ instance ToSchema ScimTokenInfo where pure $ NamedSchema (Just "ScimTokenInfo") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties .~ [ ("team", teamSchema), ("id", idSchema), @@ -478,7 +478,7 @@ instance ToSchema CreateScimToken where pure $ NamedSchema (Just "CreateScimToken") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties .~ [ ("description", textSchema), ("password", textSchema), @@ -493,7 +493,7 @@ instance ToSchema CreateScimTokenResponse where pure $ NamedSchema (Just "CreateScimTokenResponse") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties .~ [ ("token", tokenSchema), ("info", infoSchema) @@ -506,7 +506,7 @@ instance ToSchema ScimTokenList where pure $ NamedSchema (Just "ScimTokenList") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties .~ [ ("tokens", infoListSchema) ] diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index 819f0111ab0..deaf7c08f4d 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -43,11 +43,11 @@ import Data.ByteString.Conversion (FromByteString (..), ToByteString (..)) import Data.Either.Combinators (mapLeft) import Data.Id (TeamId, UserId) import Data.Json.Util (UTCTimeMillis) +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi qualified as S import Data.Proxy import Data.Qualified import Data.Schema -import Data.Swagger (ToParamSchema (..)) -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Ascii (AsciiBase64Url, toText, validateBase64Url) import Imports @@ -228,7 +228,7 @@ data TeamUserSearchSortBy instance S.ToParamSchema TeamUserSearchSortBy where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.enum_ ?~ fmap teamUserSearchSortByName [minBound .. maxBound] instance ToByteString TeamUserSearchSortBy where @@ -264,7 +264,7 @@ data TeamUserSearchSortOrder instance S.ToParamSchema TeamUserSearchSortOrder where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.enum_ ?~ fmap teamUserSearchSortOrderName [minBound .. maxBound] instance ToByteString TeamUserSearchSortOrder where diff --git a/libs/wire-api/src/Wire/API/UserMap.hs b/libs/wire-api/src/Wire/API/UserMap.hs index bcf41da1559..31f81392195 100644 --- a/libs/wire-api/src/Wire/API/UserMap.hs +++ b/libs/wire-api/src/Wire/API/UserMap.hs @@ -24,9 +24,9 @@ import Data.Aeson (FromJSON, ToJSON (toJSON)) import Data.Domain (Domain) import Data.Id (UserId) import Data.Map qualified as Map +import Data.OpenApi (HasDescription (description), HasExample (example), NamedSchema (..), ToSchema (..), declareSchema, toSchema) import Data.Proxy (Proxy (..)) import Data.Set qualified as Set -import Data.Swagger (HasDescription (description), HasExample (example), NamedSchema (..), ToSchema (..), declareSchema, toSchema) import Data.Text qualified as Text import Data.Typeable (typeRep) import Imports @@ -56,7 +56,7 @@ instance Functor QualifiedUserMap where instance Arbitrary a => Arbitrary (QualifiedUserMap a) where arbitrary = QualifiedUserMap <$> mapOf' arbitrary arbitrary -instance (Typeable a, ToSchema a, ToJSON a, Arbitrary a) => ToSchema (UserMap (Set a)) where +instance (ToSchema a, ToJSON a, Arbitrary a) => ToSchema (UserMap (Set a)) where declareNamedSchema _ = do mapSch <- declareSchema (Proxy @(Map UserId (Set a))) let valueTypeName = Text.pack $ show $ typeRep $ Proxy @a diff --git a/libs/wire-api/src/Wire/API/Wrapped.hs b/libs/wire-api/src/Wire/API/Wrapped.hs index f6d71142a5f..44c41bbddc2 100644 --- a/libs/wire-api/src/Wire/API/Wrapped.hs +++ b/libs/wire-api/src/Wire/API/Wrapped.hs @@ -21,8 +21,8 @@ import Control.Lens ((.~), (?~)) import Data.Aeson import Data.Aeson.Key qualified as Key import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap +import Data.OpenApi import Data.Proxy (Proxy (..)) -import Data.Swagger import Data.Text qualified as Text import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) import Imports @@ -48,7 +48,7 @@ instance (ToSchema a, KnownSymbol name) => ToSchema (Wrapped name a) where pure $ NamedSchema Nothing $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties .~ InsOrdHashMap.singleton (Text.pack (symbolVal (Proxy @name))) wrappedSchema instance (Arbitrary a, KnownSymbol name) => Arbitrary (Wrapped name a) where diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index 79433dbd2d7..aefaa6cb8cd 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs @@ -20,7 +20,7 @@ module Test.Wire.API.Roundtrip.Aeson (tests) where import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) import Data.Aeson.Types (parseEither) import Data.Id (ConvId) -import Data.Swagger (ToSchema, validatePrettyToJSON) +import Data.OpenApi (ToSchema, validatePrettyToJSON) import Imports import Test.Tasty qualified as T import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (.&&.), (===)) @@ -355,7 +355,7 @@ testRoundTrip = testProperty msg trip testRoundTripWithSwagger :: forall a. - (Arbitrary a, Typeable a, ToJSON a, FromJSON a, ToSchema a, Eq a, Show a) => + (Arbitrary a, ToJSON a, FromJSON a, ToSchema a, Eq a, Show a) => T.TestTree testRoundTripWithSwagger = testProperty msg (trip .&&. scm) where diff --git a/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs b/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs index 8de2cb6ad16..bbb37e6e2a4 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs @@ -18,7 +18,7 @@ module Test.Wire.API.Swagger (tests) where import Data.Aeson (ToJSON) -import Data.Swagger (ToSchema, validatePrettyToJSON) +import Data.OpenApi (ToSchema, validatePrettyToJSON) import Imports import Test.Tasty qualified as T import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty) @@ -56,7 +56,7 @@ tests = testToJSON @(Wrapped.Wrapped "some_user" User.User) ] -testToJSON :: forall a. (Arbitrary a, Typeable a, ToJSON a, ToSchema a, Show a) => T.TestTree +testToJSON :: forall a. (Arbitrary a, ToJSON a, ToSchema a, Show a) => T.TestTree testToJSON = testProperty msg trip where msg = show (typeRep @a) diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index e488149d735..b7d3af68205 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -28,6 +28,7 @@ library Wire.API.Conversation.Role Wire.API.Conversation.Typing Wire.API.CustomBackend + Wire.API.Deprecated Wire.API.Error Wire.API.Error.Brig Wire.API.Error.Cannon @@ -270,6 +271,7 @@ library , metrics-wai , mime >=0.4 , mtl + , openapi3 , pem >=0.2 , polysemy , proto-lens @@ -288,13 +290,12 @@ library , servant-client-core , servant-conduit , servant-multipart + , servant-openapi3 , servant-server - , servant-swagger , singletons , singletons-base , singletons-th , sop-core - , swagger2 , tagged , text >=0.11 , time >=1.4 @@ -748,12 +749,12 @@ test-suite wire-api-tests , imports , memory , metrics-wai + , openapi3 , process , QuickCheck , schema-profunctor , servant , servant-server - , swagger2 , tasty , tasty-hspec , tasty-hunit diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 291ea06526f..cd56258ac99 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -133,13 +133,6 @@ let sha256 = "sha256-g2lbKt3+hToVFQvaHOa9dg4HqAL7YgReo8fy7wQavmY="; }; }; - swagger2 = { - src = fetchgit { - url = "https://github.com/GetShopTV/swagger2"; - rev = "d79deca03b714cdd4531217831a8305068b2e8f9"; - sha256 = "sha256-R3p0L0TgM0Bspe5z6vauwdPq9TmEWpMC53DBkMtCEoE="; - }; - }; # MR: https://gitlab.com/twittner/cql-io/-/merge_requests/20 cql-io = { src = fetchgit { @@ -180,6 +173,15 @@ let sha256 = "sha256-SKEE9ZqhjBxHYUKQaoB4IpN4/Ui3tS4S98FgZqj7WlY="; }; }; + servant-openapi3 = { + src = fetchgit { + # This is a patched version of the library that sets the required flag for HTTP request bodies. + # A PR for these changes has been made for the upstream library. biocad/servant-openapi3#49 + url = "https://github.com/lepsa/servant-openapi3"; + rev = "5cdb2783f15058f753c41b800415d4ba1149a78b"; + sha256 = "sha256-8FM3IAA3ewCuv9Mar8aWmzbyfKK9eLXIJPMHzmYb1zE="; + }; + }; # This can be removed once postie 0.6.0.3 (or later) is in nixpkgs postie = { src = fetchgit { diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 31b6b7ce91a..4e4f70ab116 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -5,6 +5,7 @@ hself: hsuper: { aeson = (hlib.doJailbreak hsuper.aeson_2_1_2_1); binary-parsers = hlib.markUnbroken (hlib.doJailbreak hsuper.binary-parsers); bytestring-arbitrary = hlib.markUnbroken (hlib.doJailbreak hsuper.bytestring-arbitrary); + openapi3 = hlib.markUnbroken (hlib.dontCheck hsuper.openapi3); cql = hlib.appendPatch (hlib.markUnbroken hsuper.cql) (fetchpatch { url = "https://gitlab.com/twittner/cql/-/merge_requests/11.patch"; sha256 = "sha256-qfcCRkKjSS1TEqPRVBU9Ox2DjsdGsYG/F3DrZ5JGoEI="; @@ -23,7 +24,6 @@ hself: hsuper: { servant-swagger-ui = hlib.doJailbreak hsuper.servant-swagger-ui; servant-swagger-ui-core = hlib.doJailbreak hsuper.servant-swagger-ui-core; sodium-crypto-sign = hlib.addPkgconfigDepend hsuper.sodium-crypto-sign libsodium.dev; - swagger2 = hlib.doJailbreak hsuper.swagger2; text-icu-translit = hlib.markUnbroken (hlib.dontCheck hsuper.text-icu-translit); text-short = hlib.dontCheck hsuper.text-short; type-errors = hlib.dontCheck hsuper.type-errors; diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index c7d3826c41b..1a1b9814a16 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -260,6 +260,7 @@ library , mwc-random , network >=2.4 , network-conduit-tls + , openapi3 , optparse-applicative >=0.11 , polysemy , polysemy-plugin @@ -276,15 +277,14 @@ library , schema-profunctor , scientific >=0.3.4 , servant + , servant-openapi3 , servant-server - , servant-swagger , servant-swagger-ui , sodium-crypto-sign >=0.1 , split >=0.2 , ssl-util , statistics >=0.13 , stomp-queue >=0.3 - , swagger2 , template >=0.2 , template-haskell , text >=0.11 diff --git a/services/brig/default.nix b/services/brig/default.nix index c6b3867fcaa..6e7fa95539e 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -85,6 +85,7 @@ , network , network-conduit-tls , network-uri +, openapi3 , optparse-applicative , pem , pipes @@ -110,8 +111,8 @@ , servant , servant-client , servant-client-core +, servant-openapi3 , servant-server -, servant-swagger , servant-swagger-ui , sodium-crypto-sign , spar @@ -120,7 +121,6 @@ , statistics , stomp-queue , streaming-commons -, swagger2 , tasty , tasty-cannon , tasty-hunit @@ -235,6 +235,7 @@ mkDerivation { mwc-random network network-conduit-tls + openapi3 optparse-applicative polysemy polysemy-plugin @@ -251,15 +252,14 @@ mkDerivation { schema-profunctor scientific servant + servant-openapi3 servant-server - servant-swagger servant-swagger-ui sodium-crypto-sign split ssl-util statistics stomp-queue - swagger2 template template-haskell text diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index cf13f9d3386..75f90bd606c 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -76,7 +76,7 @@ import Network.Wai.Routing hiding (toList) import Network.Wai.Utilities as Utilities import Polysemy import Servant hiding (Handler, JSON, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import System.Logger.Class qualified as Log import System.Random (randomRIO) import UnliftIO.Async diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 3343b551185..24996bde99a 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -87,9 +87,10 @@ import Data.List.NonEmpty (nonEmpty) import Data.Map.Strict qualified as Map import Data.Misc (IpAddr (..)) import Data.Nonce (Nonce, randomNonce) +import Data.OpenApi qualified as S import Data.Qualified import Data.Range -import Data.Swagger qualified as S +import Data.Schema () import Data.Text qualified as Text import Data.Text.Ascii qualified as Ascii import Data.Text.Lazy (pack) @@ -103,7 +104,7 @@ import Network.Wai.Utilities as Utilities import Polysemy import Servant hiding (Handler, JSON, addHeader, respond) import Servant qualified -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Servant.Swagger.UI import System.Logger.Class qualified as Log import Util.Logging (logFunction, logHandle, logTeam, logUser) @@ -220,7 +221,7 @@ versionedSwaggerDocsAPI Nothing = allroutes (throwError listAllVersionsResp) internalEndpointsSwaggerDocsAPI :: String -> PortNumber -> - S.Swagger -> + S.OpenApi -> Servant.Server (VersionedSwaggerDocsAPIBase service) internalEndpointsSwaggerDocsAPI service examplePort swagger (Just (VersionNumber V5)) = swaggerSchemaUIServer $ diff --git a/services/brig/src/Brig/API/Public/Swagger.hs b/services/brig/src/Brig/API/Public/Swagger.hs index 0f81a74a4d4..e17607cf8f7 100644 --- a/services/brig/src/Brig/API/Public/Swagger.hs +++ b/services/brig/src/Brig/API/Public/Swagger.hs @@ -18,8 +18,8 @@ import Data.Aeson qualified as A import Data.FileEmbed import Data.HashMap.Strict.InsOrd qualified as HM import Data.HashSet.InsOrd qualified as InsOrdSet -import Data.Swagger qualified as S -import Data.Swagger.Declare qualified as S +import Data.OpenApi qualified as S +import Data.OpenApi.Declare qualified as S import Data.Text qualified as T import FileEmbedLzma import GHC.TypeLits @@ -27,7 +27,7 @@ import Imports hiding (head) import Language.Haskell.TH import Network.Socket import Servant -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Servant.Swagger.UI import Wire.API.Event.Conversation qualified import Wire.API.Event.FeatureConfig qualified @@ -68,16 +68,14 @@ swaggerPregenUIServer = . fromMaybe A.Null . A.decode -adjustSwaggerForInternalEndpoint :: String -> PortNumber -> S.Swagger -> S.Swagger +adjustSwaggerForInternalEndpoint :: String -> PortNumber -> S.OpenApi -> S.OpenApi adjustSwaggerForInternalEndpoint service examplePort swagger = swagger & S.info . S.title .~ T.pack ("Wire-Server internal API (" ++ service ++ ")") & S.info . S.description ?~ renderedDescription - & S.host ?~ S.Host "localhost" (Just examplePort) & S.allOperations . S.tags <>~ tag -- Enforce HTTP as the services themselves don't understand HTTPS - & S.schemes ?~ [S.Http] - & S.allOperations . S.schemes ?~ [S.Http] + & S.servers .~ [S.Server ("http://localhost:" <> T.pack (show examplePort)) Nothing mempty] where tag :: InsOrdSet.InsOrdHashSet S.TagName tag = InsOrdSet.singleton @S.TagName (T.pack service) @@ -102,7 +100,7 @@ adjustSwaggerForInternalEndpoint service examplePort swagger = emptySwagger :: Servant.Server (ServiceSwaggerDocsAPIBase a) emptySwagger = swaggerSchemaUIServer $ - mempty @S.Swagger + mempty @S.OpenApi & S.info . S.description ?~ "There is no Swagger documentation for this version. Please refer to v3 or later." diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index 07116e81207..fea8e51a37a 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -36,7 +36,7 @@ import Data.Id (UserId) import Data.Set qualified as Set import Imports hiding (head) import Polysemy (Member) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Connection (Relation, RelationWithHistory (..), relationDropHistory) import Wire.API.Push.Token qualified as PushTok import Wire.API.Routes.Internal.Brig.EJPD (EJPDRequestBody (EJPDRequestBody), EJPDResponseBody (EJPDResponseBody), EJPDResponseItem (EJPDResponseItem)) diff --git a/services/spar/default.nix b/services/spar/default.nix index 4bd791dfcfe..e5b1d9bd105 100644 --- a/services/spar/default.nix +++ b/services/spar/default.nix @@ -41,6 +41,7 @@ , MonadRandom , mtl , network-uri +, openapi3 , optparse-applicative , polysemy , polysemy-check @@ -53,10 +54,9 @@ , saml2-web-sso , servant , servant-multipart +, servant-openapi3 , servant-server -, servant-swagger , silently -, swagger2 , tasty-hunit , text , text-latin1 @@ -211,14 +211,14 @@ mkDerivation { metrics-wai mtl network-uri + openapi3 polysemy polysemy-plugin polysemy-wire-zoo QuickCheck saml2-web-sso servant - servant-swagger - swagger2 + servant-openapi3 time tinylog types-common diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 568034cedde..124cbb93a99 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -618,15 +618,15 @@ test-suite spec , metrics-wai , mtl , network-uri + , openapi3 , polysemy , polysemy-plugin , polysemy-wire-zoo , QuickCheck , saml2-web-sso >=0.19 , servant - , servant-swagger + , servant-openapi3 , spar - , swagger2 , time , tinylog , types-common diff --git a/services/spar/test/Arbitrary.hs b/services/spar/test/Arbitrary.hs index d8b2daf6839..44d8f38ddac 100644 --- a/services/spar/test/Arbitrary.hs +++ b/services/spar/test/Arbitrary.hs @@ -24,8 +24,8 @@ module Arbitrary where import Data.Aeson import Data.Id (TeamId, UserId) +import Data.OpenApi hiding (Header (..)) import Data.Proxy -import Data.Swagger hiding (Header (..)) import Imports import SAML2.WebSSO.Test.Arbitrary () import SAML2.WebSSO.Types diff --git a/services/spar/test/Test/Spar/APISpec.hs b/services/spar/test/Test/Spar/APISpec.hs index 07bfdb8fde3..a82d00c40f6 100644 --- a/services/spar/test/Test/Spar/APISpec.hs +++ b/services/spar/test/Test/Spar/APISpec.hs @@ -27,7 +27,7 @@ import Data.Metrics.Servant (routesToPaths) import Data.Metrics.Test (pathsConsistencyCheck) import Data.Proxy (Proxy (Proxy)) import Imports -import Servant.Swagger (validateEveryToJSON) +import Servant.OpenApi (validateEveryToJSON) import Spar.API as API import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy) import Test.QuickCheck (property) diff --git a/tools/fedcalls/default.nix b/tools/fedcalls/default.nix index 133e6e886bd..2d9d10e326d 100644 --- a/tools/fedcalls/default.nix +++ b/tools/fedcalls/default.nix @@ -3,15 +3,16 @@ # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. { mkDerivation -, aeson , base , containers , gitignoreSource , imports , insert-ordered-containers , language-dot +, lens , lib -, swagger2 +, openapi3 +, text , wire-api }: mkDerivation { @@ -21,13 +22,14 @@ mkDerivation { isLibrary = false; isExecutable = true; executableHaskellDepends = [ - aeson base containers imports insert-ordered-containers language-dot - swagger2 + lens + openapi3 + text wire-api ]; description = "Generate a dot file from swagger docs representing calls to federated instances"; diff --git a/tools/fedcalls/fedcalls.cabal b/tools/fedcalls/fedcalls.cabal index a7bf9ac1981..615a8bbd151 100644 --- a/tools/fedcalls/fedcalls.cabal +++ b/tools/fedcalls/fedcalls.cabal @@ -63,13 +63,14 @@ executable fedcalls -rtsopts -Wredundant-constraints -Wunused-packages build-depends: - aeson - , base + base , containers , imports , insert-ordered-containers , language-dot - , swagger2 + , lens + , openapi3 + , text , wire-api default-language: GHC2021 diff --git a/tools/fedcalls/src/Main.hs b/tools/fedcalls/src/Main.hs index c1b4471da9f..387424fde9c 100644 --- a/tools/fedcalls/src/Main.hs +++ b/tools/fedcalls/src/Main.hs @@ -23,23 +23,13 @@ module Main where import Control.Exception (assert) -import Data.Aeson as A -import Data.Aeson.Types qualified as A +import Control.Lens import Data.HashMap.Strict.InsOrd qualified as HM +import Data.HashSet.InsOrd (InsOrdHashSet) import Data.Map qualified as M -import Data.Swagger - ( PathItem, - Swagger, - _operationExtensions, - _pathItemDelete, - _pathItemGet, - _pathItemHead, - _pathItemOptions, - _pathItemPatch, - _pathItemPost, - _pathItemPut, - _swaggerPaths, - ) +import Data.OpenApi +import Data.OpenApi.Lens qualified as S +import Data.Text qualified as T import Imports import Language.Dot as D import Wire.API.Routes.API @@ -65,7 +55,7 @@ calls = assert (calls' == nub calls') calls' where calls' = mconcat $ parse <$> swaggers -swaggers :: [Swagger] +swaggers :: [OpenApi] swaggers = [ -- TODO: introduce allSwaggerDocs in wire-api that collects these for all -- services, use that in /services/brig/src/Brig/API/Public.hs instead of @@ -101,37 +91,63 @@ data MakesCallTo = MakesCallTo ------------------------------ -parse :: Swagger -> [MakesCallTo] -parse = +parse :: OpenApi -> [MakesCallTo] +parse oapi = mconcat - . fmap parseOperationExtensions + . fmap (parseOperationExtensions allTags) . mconcat . fmap flattenPathItems . HM.toList - . _swaggerPaths + $ oapi ^. S.paths + where + allTags = oapi ^. S.tags + +-- Simple aliases to help track which field is what +type RPC = String + +type Component = String -- | extract path, method, and operation extensions -flattenPathItems :: (FilePath, PathItem) -> [((FilePath, String), HM.InsOrdHashMap Text Value)] +flattenPathItems :: (FilePath, PathItem) -> [((FilePath, String), InsOrdHashSet TagName)] flattenPathItems (path, item) = filter ((/= mempty) . snd) $ catMaybes - [ ((path, "get"),) . _operationExtensions <$> _pathItemGet item, - ((path, "put"),) . _operationExtensions <$> _pathItemPut item, - ((path, "post"),) . _operationExtensions <$> _pathItemPost item, - ((path, "delete"),) . _operationExtensions <$> _pathItemDelete item, - ((path, "options"),) . _operationExtensions <$> _pathItemOptions item, - ((path, "head"),) . _operationExtensions <$> _pathItemHead item, - ((path, "patch"),) . _operationExtensions <$> _pathItemPatch item + [ ((path, "get"),) . view S.tags <$> _pathItemGet item, + ((path, "put"),) . view S.tags <$> _pathItemPut item, + ((path, "post"),) . view S.tags <$> _pathItemPost item, + ((path, "delete"),) . view S.tags <$> _pathItemDelete item, + ((path, "options"),) . view S.tags <$> _pathItemOptions item, + ((path, "head"),) . view S.tags <$> _pathItemHead item, + ((path, "patch"),) . view S.tags <$> _pathItemPatch item ] -parseOperationExtensions :: ((FilePath, String), HM.InsOrdHashMap Text Value) -> [MakesCallTo] -parseOperationExtensions ((path, method), hm) = uncurry (MakesCallTo path method) <$> findCallsFedInfo hm +parseOperationExtensions :: InsOrdHashSet Tag -> ((FilePath, String), InsOrdHashSet TagName) -> [MakesCallTo] +parseOperationExtensions allTags ((path, method), hm) = + uncurry (MakesCallTo path method) <$> findCallsFedInfo allTags hm -findCallsFedInfo :: HM.InsOrdHashMap Text Value -> [(String, String)] -findCallsFedInfo hm = case A.parse parseJSON <$> HM.lookup "wire-makes-federated-call-to" hm of - Just (A.Success (fedcalls :: [(String, String)])) -> fedcalls - Just bad -> error $ "invalid extension `wire-makes-federated-call-to`: expected `[(comp, name), ...]`, got " <> show bad - Nothing -> [] +-- Given a set of tags, and a set of tag names for an operation, +-- parse out the RPC calls and their components +findCallsFedInfo :: InsOrdHashSet Tag -> InsOrdHashSet TagName -> [(Component, RPC)] +findCallsFedInfo allTags = mapMaybe extractStrings . toList + where + magicPrefix :: Text + magicPrefix = "wire-makes-federated-call-to-" + extractStrings :: TagName -> Maybe (Component, RPC) + extractStrings tagName = + tag >>= \t -> + (,) + -- Extract the name and description, and drop everything that is empty + -- This gives us the component name, and as a route may call the same component + -- multiple times, it has to go into the description so it isn't dropped by the set. + <$> fmap T.unpack t._tagDescription + -- Strip off the magic string from the tag names, and drop empty results + -- This also implicitly filters for results that start with the prefix. + -- This gives us the RPC name, as that will be unique for each route, and it + -- doesn't matter if it is set multiple times and dropped in the set, as it + -- still describes that Fed call is made. + <*> fmap T.unpack (T.stripPrefix magicPrefix t._tagName) + where + tag = find (\t -> t._tagName == tagName) allTags ------------------------------ @@ -158,7 +174,7 @@ mkDotGraph inbound = Graph StrictGraph DirectedGraph Nothing (mods <> nodes <> e itemSourceNode (MakesCallTo path method _ _) = method <> " " <> path itemTargetNode :: MakesCallTo -> String - itemTargetNode (MakesCallTo _ _ comp name) = "[" <> comp <> "]:" <> name + itemTargetNode (MakesCallTo _ _ comp rpcName) = "[" <> comp <> "]:" <> rpcName callingNodes :: Map String Integer callingNodes = diff --git a/tools/stern/default.nix b/tools/stern/default.nix index c8c64c0d784..3a5afeaa844 100644 --- a/tools/stern/default.nix +++ b/tools/stern/default.nix @@ -28,16 +28,16 @@ , lib , metrics-wai , mtl +, openapi3 , optparse-applicative , random , retry , schema-profunctor , servant +, servant-openapi3 , servant-server -, servant-swagger , servant-swagger-ui , split -, swagger2 , tagged , tasty , tasty-hunit @@ -78,13 +78,13 @@ mkDerivation { lens metrics-wai mtl + openapi3 schema-profunctor servant + servant-openapi3 servant-server - servant-swagger servant-swagger-ui split - swagger2 text tinylog transformers diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index f3e7116d514..aae30de2805 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -32,14 +32,14 @@ import Data.Aeson qualified as A import Data.Handle import Data.Id import Data.Kind +import Data.OpenApi qualified as S import Data.Schema qualified as Schema -import Data.Swagger qualified as S import Imports hiding (head) import Network.HTTP.Types.Status import Network.Wai.Utilities import Servant hiding (Handler, WithStatus (..), addHeader, respond) -import Servant.Swagger (HasSwagger (toSwagger)) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi (HasOpenApi (toOpenApi)) +import Servant.OpenApi.Internal.Orphans () import Servant.Swagger.UI import Stern.Types import Wire.API.CustomBackend @@ -455,7 +455,7 @@ type SwaggerDocsAPI = SwaggerSchemaUI "swagger-ui" "swagger.json" swaggerDocs :: Servant.Server SwaggerDocsAPI swaggerDocs = swaggerSchemaUIServer $ - toSwagger (Proxy @SternAPI) + toOpenApi (Proxy @SternAPI) & S.info . S.title .~ "Stern API" & cleanupSwagger diff --git a/tools/stern/src/Stern/Types.hs b/tools/stern/src/Stern/Types.hs index b62994c943d..f8ed807492a 100644 --- a/tools/stern/src/Stern/Types.hs +++ b/tools/stern/src/Stern/Types.hs @@ -30,10 +30,10 @@ import Data.Aeson import Data.Aeson.TH import Data.ByteString.Conversion import Data.Json.Util +import Data.OpenApi qualified as Swagger import Data.Proxy import Data.Range import Data.Schema qualified as S -import Data.Swagger qualified as Swagger import Galley.Types.Teams import Imports import Servant.API @@ -127,7 +127,7 @@ instance Swagger.ToSchema ConsentLog where declareNamedSchema _ = pure . Swagger.NamedSchema (Just "ConsentLog") $ mempty - & Swagger.type_ ?~ Swagger.SwaggerObject + & Swagger.type_ ?~ Swagger.OpenApiObject & Swagger.description ?~ "(object structure is not specified in this schema)" newtype ConsentValue = ConsentValue @@ -152,7 +152,7 @@ instance Swagger.ToSchema ConsentLogAndMarketo where declareNamedSchema _ = pure . Swagger.NamedSchema (Just "ConsentLogAndMarketo") $ mempty - & Swagger.type_ ?~ Swagger.SwaggerObject + & Swagger.type_ ?~ Swagger.OpenApiObject & Swagger.description ?~ "(object structure is not specified in this schema)" newtype UserMetaInfo = UserMetaInfo @@ -164,7 +164,7 @@ instance Swagger.ToSchema UserMetaInfo where declareNamedSchema _ = pure . Swagger.NamedSchema (Just "UserMetaInfo") $ mempty - & Swagger.type_ ?~ Swagger.SwaggerObject + & Swagger.type_ ?~ Swagger.OpenApiObject & Swagger.description ?~ "(object structure is not specified in this schema)" newtype InvoiceId = InvoiceId {unInvoiceId :: Text} diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index 0a4be042c59..4cb3eff82ea 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -91,13 +91,13 @@ library , lens >=4.4 , metrics-wai >=0.3 , mtl >=2.1 + , openapi3 , schema-profunctor , servant + , servant-openapi3 , servant-server - , servant-swagger , servant-swagger-ui , split >=0.2 - , swagger2 , text >=1.1 , tinylog >=0.10 , transformers >=0.3 From 611a28090b7fb3dc2198dd6fd59eed8ed79312b9 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Thu, 21 Sep 2023 13:20:11 +0200 Subject: [PATCH 134/225] [WPB-701] Deprecated joining unqualified conversation (#3593) --- .../wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs | 3 ++- services/galley/test/integration/API/Util.hs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 79a5db7c59f..0913a076b97 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -568,7 +568,8 @@ type ConversationAPI = -- - MemberJoin event to members :<|> Named "join-conversation-by-id-unqualified" - ( Summary "Join a conversation by its ID (if link access enabled)" + ( Summary "Join a conversation by its ID (if link access enabled) (deprecated)" + :> Until 'V5 :> MakesFederatedCall 'Galley "on-conversation-updated" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 2bb2f8b40ef..3723c005e49 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -1372,7 +1372,8 @@ getJoinCodeConv u k v = do postJoinConv :: UserId -> ConvId -> TestM ResponseLBS postJoinConv u c = do - g <- viewGalley + -- This endpoint is removed from version v5 onwards + g <- fmap (addPrefixAtVersion V4 .) (view tsUnversionedGalley) post $ g . paths ["/conversations", toByteString' c, "join"] From 5be6b37741605826f02cca6fb1886d8db6067538 Mon Sep 17 00:00:00 2001 From: fisx Date: Thu, 21 Sep 2023 13:32:50 +0200 Subject: [PATCH 135/225] [WPB-4556] document internal user creation better (#3596) --- changelog.d/4-docs/WPB-4556-internal-user-creation | 1 + docs/src/understand/block-user-creation.md | 4 +--- hack/bin/create_test_team_admins.sh | 3 +++ 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelog.d/4-docs/WPB-4556-internal-user-creation diff --git a/changelog.d/4-docs/WPB-4556-internal-user-creation b/changelog.d/4-docs/WPB-4556-internal-user-creation new file mode 100644 index 00000000000..399ec6b8b86 --- /dev/null +++ b/changelog.d/4-docs/WPB-4556-internal-user-creation @@ -0,0 +1 @@ +Elaborate on internal user creation in prod \ No newline at end of file diff --git a/docs/src/understand/block-user-creation.md b/docs/src/understand/block-user-creation.md index 5c1e563aaba..a2657014da3 100644 --- a/docs/src/understand/block-user-creation.md +++ b/docs/src/understand/block-user-creation.md @@ -13,7 +13,7 @@ optSettings: If `setRestrictUserCreation` is `true`, creating new personal users or new teams on your instance from outside your backend installation is impossible. (If you want to be more technical: requests to `/register` that create a new personal account or a new team are answered with `403 forbidden`.) -On instances with restricted user creation, the site operator with access to the internal REST API can still circumvent the restriction: just log into a brig service pod via ssh and follow the steps in `hack/bin/create_test_team_admins.sh.` +On instances with restricted user creation, the site operator with access to the internal REST API can still circumvent the restriction: just log into a brig service pod and run the curl commands like `hack/bin/create_test_team_admins.sh` does it. (Running the script is also an option: this will give you a team with a random admin account, and you can use that account to give yourself access under the desired credentials.) ```{note} Once the creation of new users and teams has been disabled, it will still be possible to use the [team creation process](https://support.wire.com/hc/en-us/articles/115003858905-Create-a-team) (enter the new team name, email, password, etc), but it will fail/refuse creation late in the creation process (after the «Create team» button is clicked). @@ -30,5 +30,3 @@ FEATURE_ENABLE_ACCOUNT_REGISTRATION: "false" ```{note} If you only disable the creation of users in the webapp, but do not do so in Brig/the backend, a malicious user would be able to use the API to create users, so make sure to disable both. ``` - - diff --git a/hack/bin/create_test_team_admins.sh b/hack/bin/create_test_team_admins.sh index e6af4951314..625da458b1b 100755 --- a/hack/bin/create_test_team_admins.sh +++ b/hack/bin/create_test_team_admins.sh @@ -12,6 +12,9 @@ USAGE=" This bash script can be used to create active team admin users and their teams. +This is the way to create teams if you have set +'setRestrictUserCreation' to 'true' in your 'values.yaml'. + Note that this uses an internal brig endpoint. It is not exposed over nginz and can only be used if you have direct access to brig. From 7d3c0fd74ba21ffc78528edbaadb371bd145b176 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 21 Sep 2023 14:48:53 +0200 Subject: [PATCH 136/225] federator: Allow setting TCP connection timeout for HTTP2 requests (#3595) * http2-manager: Add timeout for creating a TCP connection Defaults to 30s. * federator: Allow setting TCP connection timeout for HTTP2 requests The helm chart defaults it to 5s which should be best for most installations. --- changelog.d/6-federation/tcp-timeout | 3 ++ charts/federator/templates/configmap.yaml | 1 + charts/federator/values.yaml | 2 ++ .../http2-manager/src/HTTP2/Client/Manager.hs | 1 + .../src/HTTP2/Client/Manager/Internal.hs | 34 +++++++++++++++++-- services/federator/federator.integration.yaml | 1 + services/federator/src/Federator/Env.hs | 8 +++-- services/federator/src/Federator/Options.hs | 3 ++ services/federator/src/Federator/Run.hs | 2 +- .../test/unit/Test/Federator/Options.hs | 10 ++++++ .../test/unit/Test/Federator/Remote.hs | 2 +- 11 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 changelog.d/6-federation/tcp-timeout diff --git a/changelog.d/6-federation/tcp-timeout b/changelog.d/6-federation/tcp-timeout new file mode 100644 index 00000000000..db622b73022 --- /dev/null +++ b/changelog.d/6-federation/tcp-timeout @@ -0,0 +1,3 @@ +federator: Allow setting TCP connection timeout for HTTP2 requests + +The helm chart defaults it to 5s which should be best for most installations. \ No newline at end of file diff --git a/charts/federator/templates/configmap.yaml b/charts/federator/templates/configmap.yaml index 44de6271071..eb9700b9727 100644 --- a/charts/federator/templates/configmap.yaml +++ b/charts/federator/templates/configmap.yaml @@ -51,5 +51,6 @@ data: clientCertificate: "/etc/wire/federator/secrets/tls.crt" clientPrivateKey: "/etc/wire/federator/secrets/tls.key" useSystemCAStore: {{ .useSystemCAStore }} + tcpConnectionTimeout: {{ .tcpConnectionTimeout }} {{- end }} {{- end }} diff --git a/charts/federator/values.yaml b/charts/federator/values.yaml index 406bb3b17c6..5d70b8e3e41 100644 --- a/charts/federator/values.yaml +++ b/charts/federator/values.yaml @@ -41,6 +41,8 @@ config: # A client certificate and corresponding private key can be specified # similarly to a custom CA store. useSystemCAStore: true + # In microseconds, default is 5s. + tcpConnectionTimeout: 5000000 podSecurityContext: allowPrivilegeEscalation: false diff --git a/libs/http2-manager/src/HTTP2/Client/Manager.hs b/libs/http2-manager/src/HTTP2/Client/Manager.hs index f3818835835..89a96517211 100644 --- a/libs/http2-manager/src/HTTP2/Client/Manager.hs +++ b/libs/http2-manager/src/HTTP2/Client/Manager.hs @@ -3,6 +3,7 @@ module HTTP2.Client.Manager setCacheLimit, setSSLContext, setSSLRemoveTrailingDot, + setTCPConnectionTimeout, TLSEnabled, HostName, Port, diff --git a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs index 96e9838d22c..884bc46959a 100644 --- a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs +++ b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs @@ -24,10 +24,13 @@ import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import Data.Unique import Foreign.Marshal.Alloc (mallocBytes) +import GHC.IO.Exception import qualified Network.HTTP2.Client as HTTP2 import qualified Network.Socket as NS import qualified OpenSSL.Session as SSL +import System.IO.Error import qualified System.TimeManager +import System.Timeout import Prelude data HTTP2Conn = HTTP2Conn @@ -74,6 +77,8 @@ data Request = Request data Http2Manager = Http2Manager { connections :: TVar (Map Target HTTP2Conn), cacheLimit :: Int, + -- | In microseconds, defaults to 30s + tcpConnectionTimeout :: Int, sslContext :: SSL.SSLContext, sslRemoveTrailingDot :: Bool } @@ -95,6 +100,7 @@ http2ManagerWithSSLCtx :: SSL.SSLContext -> IO Http2Manager http2ManagerWithSSLCtx sslContext = do connections <- newTVarIO mempty let cacheLimit = 20 + tcpConnectionTimeout = 30_000_000 sslRemoveTrailingDot = False pure $ Http2Manager {..} @@ -122,6 +128,10 @@ setSSLContext ctx mgr = mgr {sslContext = ctx} setSSLRemoveTrailingDot :: Bool -> Http2Manager -> Http2Manager setSSLRemoveTrailingDot b mgr = mgr {sslRemoveTrailingDot = b} +-- | In microseconds +setTCPConnectionTimeout :: Int -> Http2Manager -> Http2Manager +setTCPConnectionTimeout n mgr = mgr {tcpConnectionTimeout = n} + -- | Does not check whether connection is actually running. Users should use -- 'withHTTP2Request'. This function is good for testing. sendRequestWithConnection :: HTTP2Conn -> HTTP2.Request -> (HTTP2.Response -> IO r) -> IO r @@ -182,7 +192,7 @@ getOrMakeConnection mgr@Http2Manager {..} target = do connect :: IO HTTP2Conn connect = do sendReqMVar <- newEmptyMVar - thread <- liftIO . async $ startPersistentHTTP2Connection sslContext target cacheLimit sslRemoveTrailingDot sendReqMVar + thread <- liftIO . async $ startPersistentHTTP2Connection sslContext target cacheLimit sslRemoveTrailingDot tcpConnectionTimeout sendReqMVar let newConn = HTTP2Conn thread (putMVar sendReqMVar CloseConnection) sendReqMVar (inserted, finalConn) <- atomically $ insertNewConn newConn unless inserted $ do @@ -262,12 +272,14 @@ startPersistentHTTP2Connection :: Int -> -- sslRemoveTrailingDot Bool -> + -- | TCP connect timeout in microseconds + Int -> -- MVar used to communicate requests or the need to close the connection. (We could use a -- queue here to queue several requests, but since the requestor has to wait for the -- response, it might as well block before sending off the request.) MVar ConnectionAction -> IO () -startPersistentHTTP2Connection ctx (tlsEnabled, hostname, port) cl removeTrailingDot sendReqMVar = do +startPersistentHTTP2Connection ctx (tlsEnabled, hostname, port) cl removeTrailingDot tcpConnectTimeout sendReqMVar = do liveReqs <- newIORef mempty let clientConfig = HTTP2.ClientConfig @@ -309,7 +321,7 @@ startPersistentHTTP2Connection ctx (tlsEnabled, hostname, port) cl removeTrailin | otherwise = Nothing handle cleanupThreadsWith $ - bracket (fst <$> getSocketTCP hostname port) NS.close $ \sock -> do + bracket connectTCPWithTimeout NS.close $ \sock -> do bracket (mkTransport sock transportConfig) cleanupTransport $ \transport -> bracket (allocHTTP2Config transport) HTTP2.freeSimpleConfig $ \http2Cfg -> do let runAction = HTTP2.run clientConfig http2Cfg $ \sendReq -> do @@ -354,6 +366,22 @@ startPersistentHTTP2Connection ctx (tlsEnabled, hostname, port) cl removeTrailin generalHandler threadKilled e = putMVar threadKilled e exceptionHandlers threadKilled = [Handler $ tooLateHandler threadKilled, Handler $ generalHandler threadKilled] + connectTCPWithTimeout :: IO NS.Socket + connectTCPWithTimeout = do + mSock <- timeout tcpConnectTimeout $ fst <$> getSocketTCP hostname port + case mSock of + Just sock -> pure sock + Nothing -> do + let errStr = + "TCP connection with " + <> Text.unpack (Text.decodeUtf8 hostname) + <> ":" + <> show port + <> " took longer than " + <> show tcpConnectTimeout + <> " microseconds" + throwIO $ mkIOError TimeExpired errStr Nothing Nothing + type LiveReqs = Map Unique (Async (), MVar SomeException) type SendReqFn = HTTP2.Request -> (HTTP2.Response -> IO ()) -> IO () diff --git a/services/federator/federator.integration.yaml b/services/federator/federator.integration.yaml index e0d8ec2355f..9f9d2fa2001 100644 --- a/services/federator/federator.integration.yaml +++ b/services/federator/federator.integration.yaml @@ -24,5 +24,6 @@ optSettings: useSystemCAStore: false clientCertificate: "test/resources/integration-leaf.pem" clientPrivateKey: "test/resources/integration-leaf-key.pem" + tcpConnectionTimeout: 5000000 dnsHost: "127.0.0.1" dnsPort: 9053 diff --git a/services/federator/src/Federator/Env.hs b/services/federator/src/Federator/Env.hs index 90e8b1c21cf..07d75c19add 100644 --- a/services/federator/src/Federator/Env.hs +++ b/services/federator/src/Federator/Env.hs @@ -62,6 +62,8 @@ onNewSSLContext :: Env -> SSLContext -> IO () onNewSSLContext env ctx = atomicModifyIORef' (_http2Manager env) $ \mgr -> (setSSLContext ctx mgr, ()) -mkHttp2Manager :: SSLContext -> IO Http2Manager -mkHttp2Manager sslContext = - setSSLRemoveTrailingDot True <$> http2ManagerWithSSLCtx sslContext +mkHttp2Manager :: Int -> SSLContext -> IO Http2Manager +mkHttp2Manager tcpConnectionTimeout sslContext = + setTCPConnectionTimeout tcpConnectionTimeout + . setSSLRemoveTrailingDot True + <$> http2ManagerWithSSLCtx sslContext diff --git a/services/federator/src/Federator/Options.hs b/services/federator/src/Federator/Options.hs index d5bc71da53d..a4052d31652 100644 --- a/services/federator/src/Federator/Options.hs +++ b/services/federator/src/Federator/Options.hs @@ -31,6 +31,9 @@ data RunSettings = RunSettings remoteCAStore :: Maybe FilePath, clientCertificate :: FilePath, clientPrivateKey :: FilePath, + -- | Timeout for making TCP connections (for http2) with remote federators + -- and local components. In microseconds. + tcpConnectionTimeout :: Int, dnsHost :: Maybe String, dnsPort :: Maybe Word16 } diff --git a/services/federator/src/Federator/Run.hs b/services/federator/src/Federator/Run.hs index 676d5e233c3..3b73ab7051e 100644 --- a/services/federator/src/Federator/Run.hs +++ b/services/federator/src/Federator/Run.hs @@ -104,7 +104,7 @@ newEnv o _dnsResolver _applog _domainConfigs = do _internalPort = o.federatorInternal._port _httpManager <- initHttpManager sslContext <- mkTLSSettingsOrThrow _runSettings - _http2Manager <- newIORef =<< mkHttp2Manager sslContext + _http2Manager <- newIORef =<< mkHttp2Manager o.optSettings.tcpConnectionTimeout sslContext _federatorMetrics <- mkFederatorMetrics pure Env {..} diff --git a/services/federator/test/unit/Test/Federator/Options.hs b/services/federator/test/unit/Test/Federator/Options.hs index 973d410fa0b..1530489e0f5 100644 --- a/services/federator/test/unit/Test/Federator/Options.hs +++ b/services/federator/test/unit/Test/Federator/Options.hs @@ -39,6 +39,7 @@ defRunSettings client key = remoteCAStore = Nothing, clientCertificate = client, clientPrivateKey = key, + tcpConnectionTimeout = 1000, dnsHost = Nothing, dnsPort = Nothing } @@ -66,6 +67,7 @@ testSettings = allowAll: clientCertificate: client.pem clientPrivateKey: client-key.pem + tcpConnectionTimeout: 1000 useSystemCAStore: true|] ), testCase "parse configuration example (closed federation)" $ do @@ -79,6 +81,7 @@ testSettings = allowedDomains: - server2.example.com useSystemCAStore: false + tcpConnectionTimeout: 1000 clientCertificate: client.pem clientPrivateKey: client-key.pem|], testCase "succefully read client credentials" $ do @@ -89,6 +92,7 @@ testSettings = assertParsesAs settings . B8.pack $ [QQ.i| useSystemCAStore: true + tcpConnectionTimeout: 1000 federationStrategy: allowAll: null clientCertificate: test/resources/unit/localhost.pem @@ -98,12 +102,14 @@ testSettings = assertParseFailure @RunSettings . B8.pack $ [QQ.i| useSystemCAStore: true + tcpConnectionTimeout: 1000 federationStrategy: allowAll: null|], testCase "fail on missing client private key" $ do assertParseFailure @RunSettings . B8.pack $ [QQ.i| useSystemCAStore: true + tcpConnectionTimeout: 1000 federationStrategy: allowAll: null clientCertificate: test/resources/unit/localhost.pem|], @@ -111,6 +117,7 @@ testSettings = assertParseFailure @RunSettings . B8.pack $ [QQ.i| useSystemCAStore: true + tcpConnectionTimeout: 1000 federationStrategy: allowAll: null clientPrivateKey: test/resources/unit/localhost-key.pem|], @@ -119,6 +126,7 @@ testSettings = assertParsesAs settings . B8.pack $ [QQ.i| useSystemCAStore: true + tcpConnectionTimeout: 1000 federationStrategy: allowAll: null clientCertificate: non-existent @@ -140,6 +148,7 @@ testSettings = assertParsesAs settings . B8.pack $ [QQ.i| useSystemCAStore: true + tcpConnectionTimeout: 1000 federationStrategy: allowAll: null clientCertificate: test/resources/unit/invalid.pem @@ -161,6 +170,7 @@ testSettings = assertParsesAs settings . B8.pack $ [QQ.i| useSystemCAStore: true + tcpConnectionTimeout: 1000 federationStrategy: allowAll: null clientCertificate: test/resources/unit/localhost.pem diff --git a/services/federator/test/unit/Test/Federator/Remote.hs b/services/federator/test/unit/Test/Federator/Remote.hs index af13e26f1d9..5f82cea2753 100644 --- a/services/federator/test/unit/Test/Federator/Remote.hs +++ b/services/federator/test/unit/Test/Federator/Remote.hs @@ -80,7 +80,7 @@ assertNoRemoteError = \case mkTestCall :: SSLContext -> ByteString -> Int -> Codensity IO (Either RemoteError ()) mkTestCall sslCtx hostname port = do - mgr <- liftIO $ mkHttp2Manager sslCtx + mgr <- liftIO $ mkHttp2Manager 1_000_000 sslCtx runM . runEmbedded @IO @(Codensity IO) liftIO . runError @RemoteError From 769f51aa0560ce5b11cfc807c6a1dd69df9949c4 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 22 Sep 2023 16:03:23 +0200 Subject: [PATCH 137/225] federator-client: Use a new http2 connection on every request (#3602) --- changelog.d/3-bug-fixes/WPB-4787 | 1 + .../http2-manager/src/HTTP2/Client/Manager.hs | 4 ++++ libs/wire-api-federation/default.nix | 2 ++ .../src/Wire/API/Federation/Client.hs | 21 ++++++++++++++++--- .../wire-api-federation.cabal | 1 + 5 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-4787 diff --git a/changelog.d/3-bug-fixes/WPB-4787 b/changelog.d/3-bug-fixes/WPB-4787 new file mode 100644 index 00000000000..97cb562182e --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-4787 @@ -0,0 +1 @@ +Create a new http2 connection in every federator client request instead of using a shared connection. diff --git a/libs/http2-manager/src/HTTP2/Client/Manager.hs b/libs/http2-manager/src/HTTP2/Client/Manager.hs index 89a96517211..2cf1278061b 100644 --- a/libs/http2-manager/src/HTTP2/Client/Manager.hs +++ b/libs/http2-manager/src/HTTP2/Client/Manager.hs @@ -15,6 +15,10 @@ module HTTP2.Client.Manager ConnectionAlreadyClosed (..), disconnectTarget, disconnectTargetWithTimeout, + startPersistentHTTP2Connection, + sendRequestWithConnection, + HTTP2Conn (..), + ConnectionAction (..), ) where diff --git a/libs/wire-api-federation/default.nix b/libs/wire-api-federation/default.nix index 0275616b33e..48b8b6ae525 100644 --- a/libs/wire-api-federation/default.nix +++ b/libs/wire-api-federation/default.nix @@ -6,6 +6,7 @@ , aeson , aeson-pretty , amqp +, async , base , bytestring , bytestring-conversion @@ -51,6 +52,7 @@ mkDerivation { libraryHaskellDepends = [ aeson amqp + async base bytestring bytestring-conversion diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs index f9f0473306f..4104c41d92d 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs @@ -32,6 +32,7 @@ module Wire.API.Federation.Client ) where +import Control.Concurrent.Async import Control.Exception qualified as E import Control.Monad.Catch import Control.Monad.Codensity @@ -58,6 +59,7 @@ import Network.HTTP.Media qualified as HTTP import Network.HTTP.Types qualified as HTTP import Network.HTTP2.Client qualified as HTTP2 import Network.Wai.Utilities.Error qualified as Wai +import OpenSSL.Session qualified as SSL import Servant.Client import Servant.Client.Core import Servant.Types.SourceT @@ -113,13 +115,26 @@ liftCodensity = FederatorClient . lift . lift . lift headersFromTable :: HTTP2.HeaderTable -> [HTTP.Header] headersFromTable (headerList, _) = flip map headerList $ first HTTP2.tokenKey +-- This opens a new http2 connection. Using a http2-manager leads to this problem https://wearezeta.atlassian.net/browse/WPB-4787 +-- FUTUREWORK: Replace with H2Manager.withHTTP2Request once the bugs are solved. +withNewHttpRequest :: H2Manager.Target -> HTTP2.Request -> (HTTP2.Response -> IO a) -> IO a +withNewHttpRequest target req k = do + ctx <- SSL.context + let cacheLimit = 20 + sslRemoveTrailingDot = False + tcpConnectionTimeout = 30_000_000 + sendReqMVar <- newEmptyMVar + thread <- liftIO . async $ H2Manager.startPersistentHTTP2Connection ctx target cacheLimit sslRemoveTrailingDot tcpConnectionTimeout sendReqMVar + let newConn = H2Manager.HTTP2Conn thread (putMVar sendReqMVar H2Manager.CloseConnection) sendReqMVar + H2Manager.sendRequestWithConnection newConn req k + performHTTP2Request :: Http2Manager -> H2Manager.Target -> HTTP2.Request -> IO (Either FederatorClientHTTP2Error (ResponseF Builder)) -performHTTP2Request mgr target req = try $ do - H2Manager.withHTTP2Request mgr target req $ consumeStreamingResponseWith $ \resp -> do +performHTTP2Request _mgr target req = try $ do + withNewHttpRequest target req $ consumeStreamingResponseWith $ \resp -> do b <- fmap (fromRight mempty) . runExceptT @@ -210,7 +225,7 @@ withHTTP2StreamingRequest successfulStatus req handleResponse = do either throwError pure <=< liftCodensity $ Codensity $ \k -> E.catches - (H2Manager.withHTTP2Request (ceHttp2Manager env) (False, hostname, port) req' (consumeStreamingResponseWith (k . Right))) + (withNewHttpRequest (False, hostname, port) req' (consumeStreamingResponseWith (k . Right))) [ E.Handler $ k . Left . FederatorClientHTTP2Error, E.Handler $ k . Left . FederatorClientHTTP2Error . FederatorClientConnectionError, E.Handler $ k . Left . FederatorClientHTTP2Error . FederatorClientHTTP2Exception, diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index b19ff51c99d..d6e73abc4eb 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -82,6 +82,7 @@ library build-depends: aeson >=2.0.1.0 , amqp + , async , base >=4.6 && <5.0 , bytestring , bytestring-conversion From f2aa5486e2e9f824f63639f45f31e62d6da57cbc Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 25 Sep 2023 09:28:32 +0200 Subject: [PATCH 138/225] chat.py - test message sending without a client (#3603) --- hack/bin/chat.py | 329 ++++++++++++++++++++++++++++++++++++ hack/python/wire/otr_pb2.py | 54 ++++++ nix/wire-server.nix | 1 + 3 files changed, 384 insertions(+) create mode 100755 hack/bin/chat.py create mode 100644 hack/python/wire/otr_pb2.py diff --git a/hack/bin/chat.py b/hack/bin/chat.py new file mode 100755 index 00000000000..d770506b267 --- /dev/null +++ b/hack/bin/chat.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 +# +# With this tool you can send and receive (unencrypted) messages in conversations. +# It exists to test basic message sending and monitoring of events without relying on using a client. +# +# Create a config file (see example.yaml). +# +# 1. generate a shell script that sets up the port-forwarding for domain col1 via kubectl +# chat.py --config example.yaml port-forward --domain col1 +# +# 2. open websockets and listen on the clients for user u1 and u2 +# chat.py --config example.yaml listen --user u1 --user u2 +# +# 3. send a message to conv 1+2 with as user u1 +# the message will be unencrypted plaintext, so normal clients won't be able to display it +# chat.py --config example.yaml send --user u1 --conv 1+2 +# +# # example.yaml +# +# users: +# # pick any short name for user name +# u1: +# id: 13cfb002-6f07-434a-90fa-1422e8141a30 +# domain_idx: col1 +# client: 139da7a7e0034030 +# u2: +# id: f0e07e83-b573-4689-b366-5efa4a859a72 +# domain_idx: col2 +# client: 1BD4B2DCE638BD9E +# comment: User en7ump0q@wire.com +# off: +# id: 8673c02b-651d-4f4a-96d8-4dbd51fa3e1b +# client: b51351d821a734a3 +# domain_idx: offline-web +# convs: +# # pick any short name for conversation names +# 1+2: +# id: eabb40cc-bf99-5a50-bd56-60c120830235 +# domain_idx: col2 +# domains: +# # pick any short name for the domain +# col1: +# domain: bund-next-column-1.wire.link +# cannon_port: 6086 +# galley_port: 6085 +# namespace: wire +# col2: +# domain: bund-next-column-2.wire.link +# cannon_port: 7086 +# galley_port: 7085 +# namespace: wire +# offline-web: +# domain: bund-next-column-offline-web.wire.link +# cannon_port: 11086 +# galley_port: 11085 +# namespace: column-offline-web + +import websockets +import asyncio +import argparse +import requests +import base64 +from urllib.parse import urljoin, urlparse, urlencode, urlunparse +import uuid +import sys +import subprocess +import random +import json +import datetime +import yaml +import itertools +import tempfile +import wire.otr_pb2 as otr + +port_forward_script = ''' +#!/usr/bin/env bash + +set -eo pipefail +domain="{domain}" +namespace="{namespace}" +galley_port="{galley_port}" +cannon_port="{cannon_port}" + +actual_domain=$(kubectl -n wire get configmap brig -o yaml | sed -n 's/.*setFederationDomain: \(.*\)/\\1/p') +if [ ! "$actual_domain" = "$domain" ]; then echo "Error: backend is $actual_domain, but expected $domain" ; exit 1; fi + +set -x +kubectl -n wire port-forward $(kubectl -n wire get pods -lapp=galley -o=custom-columns=name:.metadata.name --no-headers) $galley_port:8080 & +pid1="$!" +kubectl -n wire port-forward $(kubectl -n wire get pods -lapp=cannon -o=custom-columns=name:.metadata.name --no-headers) $cannon_port:8080 & +pid2="$!" +set +x + +sleep 1 +read -n 1 -p "Press ENTER to kill port-forwarding processes $pid1 and $pid2:"; +kill "$pid1" +kill "$pid2" +''' + +def random_string(): + hiragana = [ "a", "i", "u", "e", "o", "ka", "ki", "ku", "ke", "ko", "sa", "shi", "su", "se", "so",\ + "ta", "chi", "tsu", "te", "to", "na", "ni", "nu", "ne", "no", "ha", "hi", "fu", "he",\ + "ho", "ma", "mi", "mu", "me", "mo", "ya", "yu", "yo", "ra", "ri", "ru", "re", "ro", "wa", "wo" ] + s = '' + n = random.choice([2,3,4]) + words = [] + for _ in range(n): + l = random.choice([2,3]) + word = '' + for i in range(l): + word += random.choice(hiragana) + words.append(word) + return '_'.join(words) + +def get_human_time(): + t = datetime.datetime.now() + return t.strftime('%H:%M:%S') + +def client_identities_from_missing(missing): + cids = [] + for domain, users in missing.items(): + for user_id, client_ids in users.items(): + for client_id in client_ids: + cids.append({'user': user_id, 'domain': domain, 'client': client_id}) + return cids + +class App: + def __init__(self, cfg): + self.cfg = cfg + for k, v in self.cfg['users'].items(): + v['idx'] = k + for k, v in self.cfg['domains'].items(): + v['idx'] = k + for k, v in self.cfg['convs'].items(): + v['idx'] = k + + def user(self, idx): + return self.cfg['users'][idx] + + def domain(self, idx): + return self.cfg['domains'][idx] + + def conv(self, name): + return self.cfg['convs'][name] + + def user_idx(self, user_id): + for k, v in self.cfg['users'].items(): + if v['id'] == user_id: + return v + return f'No user found for <{user_id}>' + + def conv_idx(self, conv_id): + for k, v in self.cfg['convs'].items(): + if v['id'] == conv_id: + return v + return f'No conv found for <{conv_id}>' + + def send_msg(self, user_from_idx, conv_name): + msg = get_human_time() + ' ' + random_string() + payload = msg.encode('utf8') + conv = self.conv(conv_name) + user_from = self.user(user_from_idx) + domain_conv = self.domain(conv['domain_idx']) + domain_from = self.domain(user_from['domain_idx']) + url = f'http://localhost:{domain_from["galley_port"]}/v4/conversations/{domain_conv["domain"]}/{conv["id"]}/proteus/messages' + + data = mk_otr(user_from['client'], [], payload) + response = requests.post(url, headers={'content-type': 'application/x-protobuf', 'z-user': user_from['id'], 'z-connection': 'con'}, data=data) + if response.status_code != 412: + print('got not 412') + print(response.status_code, response.text) + print(':(') + sys.exit(1) + b = response.json() + + client_identities = client_identities_from_missing(b['missing']) + data = mk_otr(user_from['client'], client_identities, payload) + + response = requests.post(url, headers={'content-type': 'application/x-protobuf', 'z-user': user_from['id'], 'z-connection': 'con'}, data=data) + if response.status_code != 201: + print('got not 201') + print(response.status_code, response.text) + print(':(') + sys.exit(1) + + else: + print(f'{user_from_idx} sent: {msg}') + + async def open_websocket(self, user_idx): + user = self.cfg['users'][user_idx] + domain = self.cfg['domains'][user['domain_idx']] + + url = f'ws://127.0.0.1:{domain["cannon_port"]}/await' + # add client param + urlparts = list(urlparse(url)) + params = {"client": user["client"]} + urlparts[4] = urlencode(params) + url = urlunparse(urlparts) + + headers = {"Z-User": user["id"], "Z-Connection": random_string()} + async with websockets.connect(url, extra_headers=headers, open_timeout=4 * 60) as ws: + print(f'{user_idx} opened a websocket') + while True: + message_raw = await ws.recv() + n = json.loads(message_raw.decode('utf8')) + payload = n['payload'][0] + type_ = payload['type'] + if type_ == 'conversation.otr-message-add': + + conv = self.conv_idx(payload['conversation']) + sender_user_id = payload['qualified_from']['id'] + sender = self.user_idx(sender_user_id) + msg = base64.b64decode(payload['data']['data']).decode('utf8') + print(f'{get_human_time()} {user_idx} receives in conv {conv["idx"]} from {sender["idx"]}: {msg}') + else: + print(f'{get_human_time()} {user_idx} receives event fo type {type_}') + + async def open_websockets(self, users): + await asyncio.gather(*[self.open_websocket(u) for u in users]) + + def print_port_forward(self, domain_idx): + d = self.domain(domain_idx) + domain = d['domain'] + namespace = d['namespace'] + galley_port = d['galley_port'] + cannon_port = d['cannon_port'] + script = port_forward_script.format(domain=domain, namespace=namespace, galley_port=galley_port, cannon_port=cannon_port) + with tempfile.NamedTemporaryFile(prefix=f'{domain}-port-forward-', suffix='.sh', delete=False, mode='w') as f: + print(f'Wrote port-forward script to {f.name}') + f.write(script) + +async def main_test_websocket(): + await open_websocket(3) + +def client_id_to_int(client_id): + return int("0x" + client_id, 16) + +def hex_to_bytes(hex): + return bytes(bytearray.fromhex(hex)) + +def uuid_to_bytes(uuid_string): + u = uuid.UUID(uuid_string) + return u.bytes + +def mk_client_id(client_hex): + return otr.ClientId(client=client_id_to_int(client_hex)) + +def mk_client_entry(client_hex): + client_id = mk_client_id(client_hex) + return otr.ClientEntry(client=client_id, text=hex_to_bytes(client_hex)) + +def mk_user_id(uuid_string): + uuid_bytes = uuid_to_bytes(uuid_string) + return otr.UserId(uuid=uuid_bytes) + +def mk_user_entry(user, clients): + user_id = mk_user_id(user) + clients = [mk_client_entry(c) for c in clients] + return otr.UserEntry(user=user_id, clients = clients ) + +def mk_qualified_user_entry(domain, users): + entries = [mk_user_entry(u, users[u]) for u in users] + return otr.QualifiedUserEntry(domain=domain, entries=entries) + +def mk_otr(sender_client_id_hex, client_identities, payload=b'foobar'): + sender = mk_client_id(sender_client_id_hex) + + gdomain = lambda c: c['domain'] + guser = lambda c: c['user'] + recipients = [] + for domain, users_flat in itertools.groupby(sorted(client_identities, key=gdomain), key=gdomain): + users = {} + for user_id, clients_flat in itertools.groupby(sorted(users_flat, key=guser), key=guser): + users[user_id] = [c['client'] for c in clients_flat] + recipients.append(mk_qualified_user_entry(domain, users)) + + report_all = otr.ClientMismatchStrategy.ReportAll() + m = otr.QualifiedNewOtrMessage(sender=sender, recipients=recipients, blob=payload, report_all=report_all) + return m.SerializeToString() + +def main_port_forward(cfg, domain): + app = App(cfg) + app.print_port_forward(domain) + +def main_send(cfg, user, conv): + app = App(cfg) + app.send_msg(user, conv) + +def main_listen(cfg, users): + app = App(cfg) + asyncio.run(app.open_websockets(users)) + +def main(): + parser = argparse.ArgumentParser( + prog=sys.argv[0], description="Send and receive proteus messages across backends" + ) + + subparsers = parser.add_subparsers( + title="subcommand", description="valid subcommands", dest="subparser_name" + ) + + parser.add_argument("--config", type=str, required=True) + + sp = subparsers.add_parser("send") + sp.add_argument("--user", type=str, required=True) + sp.add_argument("--conv", type=str, required=True) + + lp = subparsers.add_parser("listen") + lp.add_argument('--user', action='append', help='can be provided multiple times') + + pf = subparsers.add_parser("port-forward") + pf.add_argument("--domain", type=str, required=True) + + args = parser.parse_args() + + with open(args.config, 'r') as f: + cfg = yaml.safe_load(f) + + if args.subparser_name == "send": + main_send(cfg, args.user, args.conv) + + elif args.subparser_name == "port-forward": + main_port_forward(cfg, args.domain) + + elif args.subparser_name == "listen": + main_listen(cfg, args.user) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/hack/python/wire/otr_pb2.py b/hack/python/wire/otr_pb2.py new file mode 100644 index 00000000000..2596a0749e4 --- /dev/null +++ b/hack/python/wire/otr_pb2.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: libs/wire-message-proto-lens/generic-message-proto/proto/otr.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\nBlibs/wire-message-proto-lens/generic-message-proto/proto/otr.proto\x12\x07proteus\"\x16\n\x06UserId\x12\x0c\n\x04uuid\x18\x01 \x02(\x0c\"-\n\x0fQualifiedUserId\x12\n\n\x02id\x18\x01 \x02(\t\x12\x0e\n\x06\x64omain\x18\x02 \x02(\t\"\x1a\n\x08\x43lientId\x12\x0e\n\x06\x63lient\x18\x01 \x02(\x04\">\n\x0b\x43lientEntry\x12!\n\x06\x63lient\x18\x01 \x02(\x0b\x32\x11.proteus.ClientId\x12\x0c\n\x04text\x18\x02 \x02(\x0c\"Q\n\tUserEntry\x12\x1d\n\x04user\x18\x01 \x02(\x0b\x32\x0f.proteus.UserId\x12%\n\x07\x63lients\x18\x02 \x03(\x0b\x32\x14.proteus.ClientEntry\"I\n\x12QualifiedUserEntry\x12\x0e\n\x06\x64omain\x18\x01 \x02(\t\x12#\n\x07\x65ntries\x18\x02 \x03(\x0b\x32\x12.proteus.UserEntry\"\xeb\x01\n\rNewOtrMessage\x12!\n\x06sender\x18\x01 \x02(\x0b\x32\x11.proteus.ClientId\x12&\n\nrecipients\x18\x02 \x03(\x0b\x32\x12.proteus.UserEntry\x12\x19\n\x0bnative_push\x18\x03 \x01(\x08:\x04true\x12\x0c\n\x04\x62lob\x18\x04 \x01(\x0c\x12*\n\x0fnative_priority\x18\x05 \x01(\x0e\x32\x11.proteus.Priority\x12\x11\n\ttransient\x18\x06 \x01(\x08\x12\'\n\x0ereport_missing\x18\x07 \x03(\x0b\x32\x0f.proteus.UserId\"\xf8\x03\n\x16QualifiedNewOtrMessage\x12!\n\x06sender\x18\x01 \x02(\x0b\x32\x11.proteus.ClientId\x12/\n\nrecipients\x18\x02 \x03(\x0b\x32\x1b.proteus.QualifiedUserEntry\x12\x19\n\x0bnative_push\x18\x03 \x01(\x08:\x04true\x12\x0c\n\x04\x62lob\x18\x04 \x01(\x0c\x12*\n\x0fnative_priority\x18\x05 \x01(\x0e\x32\x11.proteus.Priority\x12\x11\n\ttransient\x18\x06 \x01(\x08\x12?\n\nreport_all\x18\x07 \x01(\x0b\x32).proteus.ClientMismatchStrategy.ReportAllH\x00\x12?\n\nignore_all\x18\x08 \x01(\x0b\x32).proteus.ClientMismatchStrategy.IgnoreAllH\x00\x12\x41\n\x0breport_only\x18\t \x01(\x0b\x32*.proteus.ClientMismatchStrategy.ReportOnlyH\x00\x12\x41\n\x0bignore_only\x18\n \x01(\x0b\x32*.proteus.ClientMismatchStrategy.IgnoreOnlyH\x00\x42\x1a\n\x18\x63lient_mismatch_strategy\"\xa6\x01\n\x16\x43lientMismatchStrategy\x1a\x0b\n\tReportAll\x1a\x0b\n\tIgnoreAll\x1a\x38\n\nReportOnly\x12*\n\x08user_ids\x18\x01 \x03(\x0b\x32\x18.proteus.QualifiedUserId\x1a\x38\n\nIgnoreOnly\x12*\n\x08user_ids\x18\x01 \x03(\x0b\x32\x18.proteus.QualifiedUserId\"\x8d\x01\n\x0cOtrAssetMeta\x12!\n\x06sender\x18\x01 \x02(\x0b\x32\x11.proteus.ClientId\x12&\n\nrecipients\x18\x02 \x03(\x0b\x32\x12.proteus.UserEntry\x12\x17\n\x08isInline\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x19\n\x0bnative_push\x18\x04 \x01(\x08:\x04true*/\n\x08Priority\x12\x10\n\x0cLOW_PRIORITY\x10\x01\x12\x11\n\rHIGH_PRIORITY\x10\x02\x42\x1a\n\x11\x63om.wire.messagesB\x03OtrH\x03') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'libs.wire_message_proto_lens.generic_message_proto.proto.otr_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\021com.wire.messagesB\003OtrH\003' + _PRIORITY._serialized_start=1458 + _PRIORITY._serialized_end=1505 + _USERID._serialized_start=79 + _USERID._serialized_end=101 + _QUALIFIEDUSERID._serialized_start=103 + _QUALIFIEDUSERID._serialized_end=148 + _CLIENTID._serialized_start=150 + _CLIENTID._serialized_end=176 + _CLIENTENTRY._serialized_start=178 + _CLIENTENTRY._serialized_end=240 + _USERENTRY._serialized_start=242 + _USERENTRY._serialized_end=323 + _QUALIFIEDUSERENTRY._serialized_start=325 + _QUALIFIEDUSERENTRY._serialized_end=398 + _NEWOTRMESSAGE._serialized_start=401 + _NEWOTRMESSAGE._serialized_end=636 + _QUALIFIEDNEWOTRMESSAGE._serialized_start=639 + _QUALIFIEDNEWOTRMESSAGE._serialized_end=1143 + _CLIENTMISMATCHSTRATEGY._serialized_start=1146 + _CLIENTMISMATCHSTRATEGY._serialized_end=1312 + _CLIENTMISMATCHSTRATEGY_REPORTALL._serialized_start=1172 + _CLIENTMISMATCHSTRATEGY_REPORTALL._serialized_end=1183 + _CLIENTMISMATCHSTRATEGY_IGNOREALL._serialized_start=1185 + _CLIENTMISMATCHSTRATEGY_IGNOREALL._serialized_end=1196 + _CLIENTMISMATCHSTRATEGY_REPORTONLY._serialized_start=1198 + _CLIENTMISMATCHSTRATEGY_REPORTONLY._serialized_end=1254 + _CLIENTMISMATCHSTRATEGY_IGNOREONLY._serialized_start=1256 + _CLIENTMISMATCHSTRATEGY_IGNOREONLY._serialized_end=1312 + _OTRASSETMETA._serialized_start=1315 + _OTRASSETMETA._serialized_end=1456 +# @@protoc_insertion_point(module_scope) diff --git a/nix/wire-server.nix b/nix/wire-server.nix index ddaa3689533..14b45676516 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -462,6 +462,7 @@ in flake8 ipdb ipython + protobuf pylint pyyaml requests From 7651200a6dbb37f3443410597f93e8c74eb3f25e Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Mon, 25 Sep 2023 10:53:38 +0300 Subject: [PATCH 139/225] federator: fix ServiceMonitor name and app label copy-paste error from another component. --- charts/federator/templates/servicemonitor.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/federator/templates/servicemonitor.yaml b/charts/federator/templates/servicemonitor.yaml index cb98cd0368a..9738af2420f 100644 --- a/charts/federator/templates/servicemonitor.yaml +++ b/charts/federator/templates/servicemonitor.yaml @@ -2,9 +2,9 @@ apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: - name: gundeck + name: federator labels: - app: gundeck + app: federator chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} From bb7c7c883e65dffaad6595037ee8c4f7a6a74578 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 25 Sep 2023 12:21:46 +0200 Subject: [PATCH 140/225] integration-tests: Allow generating tests results in ant xml format (#3568) --- changelog.d/5-internal/xml-reports | 9 ++++ integration/default.nix | 6 +++ integration/integration.cabal | 4 ++ integration/test/Testlib/Options.hs | 18 +++++-- integration/test/Testlib/Run.hs | 42 ++++++--------- integration/test/Testlib/Types.hs | 15 ++++++ integration/test/Testlib/XML.hs | 60 +++++++++++++++++++++ nix/haskell-pins.nix | 16 ++++++ nix/manual-overrides.nix | 3 ++ services/brig/brig.cabal | 1 + services/brig/default.nix | 2 + services/brig/test/integration/Main.hs | 5 +- services/cargohold/cargohold.cabal | 1 + services/cargohold/default.nix | 2 + services/cargohold/test/integration/Main.hs | 5 ++ services/federator/default.nix | 5 ++ services/federator/federator.cabal | 3 ++ services/federator/test/integration/Main.hs | 26 ++++++++- services/galley/default.nix | 2 + services/galley/galley.cabal | 1 + services/galley/test/integration/Main.hs | 5 ++ services/gundeck/default.nix | 2 + services/gundeck/gundeck.cabal | 1 + services/gundeck/test/integration/Main.hs | 5 ++ services/spar/default.nix | 6 +++ services/spar/spar.cabal | 3 ++ services/spar/test-integration/Main.hs | 26 ++++++++- tools/stern/default.nix | 2 + tools/stern/stern.cabal | 1 + tools/stern/test/integration/Main.hs | 5 ++ 30 files changed, 251 insertions(+), 31 deletions(-) create mode 100644 changelog.d/5-internal/xml-reports create mode 100644 integration/test/Testlib/XML.hs diff --git a/changelog.d/5-internal/xml-reports b/changelog.d/5-internal/xml-reports new file mode 100644 index 00000000000..5ab1e589deb --- /dev/null +++ b/changelog.d/5-internal/xml-reports @@ -0,0 +1,9 @@ +All integration tests can generate XML reports. + +To generate the report in brig-integration, galley-integration, +cargohold-integration, gundeck-integration, stern-integration and the new +integration suite pass `--xml=` to generate the XML file. + +For spar-integration and federator-integration pass `-f junit` and set +`JUNIT_OUTPUT_DIRECTORY` and `JUNIT_SUITE_NAME` environment variables. The XML +report will be generated at `$JUNIT_OUTPUT_DIRECTORY/junit.xml`. diff --git a/integration/default.nix b/integration/default.nix index b42304134c1..1955f0037ba 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -45,6 +45,8 @@ , proto-lens , random , raw-strings-qq +, regex-base +, regex-tdfa , retry , scientific , split @@ -62,6 +64,7 @@ , vector , websockets , wire-message-proto-lens +, xml , yaml }: mkDerivation { @@ -111,6 +114,8 @@ mkDerivation { proto-lens random raw-strings-qq + regex-base + regex-tdfa retry scientific split @@ -128,6 +133,7 @@ mkDerivation { vector websockets wire-message-proto-lens + xml yaml ]; license = lib.licenses.agpl3Only; diff --git a/integration/integration.cabal b/integration/integration.cabal index 5ff99f89941..4edcf08b67c 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -132,6 +132,7 @@ library Testlib.Run Testlib.RunServices Testlib.Types + Testlib.XML build-depends: , aeson @@ -173,6 +174,8 @@ library , proto-lens , random , raw-strings-qq + , regex-base + , regex-tdfa , retry , scientific , split @@ -190,4 +193,5 @@ library , vector , websockets , wire-message-proto-lens + , xml , yaml diff --git a/integration/test/Testlib/Options.hs b/integration/test/Testlib/Options.hs index 2ba8fdafd9a..094c5951543 100644 --- a/integration/test/Testlib/Options.hs +++ b/integration/test/Testlib/Options.hs @@ -9,6 +9,7 @@ data TestOptions = TestOptions { includeTests :: [String], excludeTests :: [String], listTests :: Bool, + xmlReport :: Maybe FilePath, configFile :: String } @@ -32,6 +33,13 @@ parser = ) ) <*> switch (long "list" <> short 'l' <> help "Only list tests.") + <*> optional + ( strOption + ( long "xml" + <> metavar "FILE" + <> help "Generate XML report for the tests" + ) + ) <*> strOption ( long "config" <> short 'c' @@ -53,12 +61,16 @@ getOptions :: IO TestOptions getOptions = do defaultsInclude <- maybe [] (splitOn ",") <$> lookupEnv "TEST_INCLUDE" defaultsExclude <- maybe [] (splitOn ",") <$> lookupEnv "TEST_EXCLUDE" + defaultsXMLReport <- lookupEnv "TEST_XML" opts <- execParser optInfo pure opts { includeTests = includeTests opts `orFromEnv` defaultsInclude, - excludeTests = excludeTests opts `orFromEnv` defaultsExclude + excludeTests = excludeTests opts `orFromEnv` defaultsExclude, + xmlReport = xmlReport opts `orFromEnv` defaultsXMLReport } where - orFromEnv [] fromEnv = fromEnv - orFromEnv patterns _ = patterns + orFromEnv fromArgs fromEnv = + if null fromArgs + then fromEnv + else fromArgs diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 777ad6ebcc6..82c1b1eaaa0 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -24,22 +24,11 @@ import Testlib.JSON import Testlib.Options import Testlib.Printing import Testlib.Types +import Testlib.XML import Text.Printf import UnliftIO.Async import Prelude -data TestReport = TestReport - { count :: Int, - failures :: [String] - } - deriving (Eq, Show) - -instance Semigroup TestReport where - TestReport s1 f1 <> TestReport s2 f2 = TestReport (s1 + s2) (f1 <> f2) - -instance Monoid TestReport where - mempty = TestReport 0 mempty - runTest :: GlobalEnv -> App a -> IO (Either String a) runTest ge action = lowerCodensity $ do env <- mkEnv ge @@ -55,16 +44,18 @@ pluralise :: Int -> String -> String pluralise 1 x = x pluralise _ x = x <> "s" -printReport :: TestReport -> IO () +printReport :: TestSuiteReport -> IO () printReport report = do - unless (null report.failures) $ putStrLn $ "----------" - putStrLn $ show report.count <> " " <> pluralise report.count "test" <> " run." - unless (null report.failures) $ do + let numTests = length report.cases + failures = filter (\testCase -> testCase.result /= TestSuccess) report.cases + numFailures = length failures + when (numFailures > 0) $ putStrLn $ "----------" + putStrLn $ show numTests <> " " <> pluralise numTests "test" <> " run." + when (numFailures > 0) $ do putStrLn "" - let numFailures = length report.failures putStrLn $ colored red (show numFailures <> " failed " <> pluralise numFailures "test" <> ": ") - for_ report.failures $ \name -> - putStrLn $ " - " <> name + for_ failures $ \testCase -> + putStrLn $ " - " <> testCase.name testFilter :: TestOptions -> String -> Bool testFilter opts n = included n && not (excluded n) @@ -105,7 +96,7 @@ main = do qualifiedName = module0 <> "." <> name in (qualifiedName, summary, full, action) - if opts.listTests then doListTests tests else runTests tests cfg + if opts.listTests then doListTests tests else runTests tests opts.xmlReport cfg createGlobalEnv :: FilePath -> IO GlobalEnv createGlobalEnv cfg = do @@ -123,8 +114,8 @@ createGlobalEnv cfg = do Just dir -> dir "galley" relPath pure genv0 {gRemovalKeyPath = path} -runTests :: [(String, x, y, App ())] -> FilePath -> IO () -runTests tests cfg = do +runTests :: [(String, x, y, App ())] -> Maybe FilePath -> FilePath -> IO () +runTests tests mXMLOutput cfg = do output <- newChan let displayOutput = readChan output >>= \case @@ -149,14 +140,15 @@ runTests tests cfg = do <> ") -----\n" <> err <> "\n" - pure (TestReport 1 [qname]) + pure (TestSuiteReport [TestCaseReport qname (TestFailure err) tm]) Right _ -> do writeOutput $ qname <> colored green " OK" <> " (" <> printTime tm <> ")" <> "\n" - pure (TestReport 1 []) + pure (TestSuiteReport [TestCaseReport qname TestSuccess tm]) writeChan output Nothing wait displayThread printReport report - unless (null report.failures) $ + mapM_ (saveXMLReport report) mXMLOutput + when (any (\testCase -> testCase.result /= TestSuccess) report.cases) $ exitFailure doListTests :: [(String, String, String, x)] -> IO () diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 12403339816..268cc14f82a 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -26,6 +26,7 @@ import Data.Set qualified as Set import Data.String import Data.Text qualified as T import Data.Text.Encoding qualified as T +import Data.Time import Data.Word import GHC.Generics (Generic) import GHC.Records @@ -442,3 +443,17 @@ data BackendName allServices :: [Service] allServices = [minBound .. maxBound] + +newtype TestSuiteReport = TestSuiteReport {cases :: [TestCaseReport]} + deriving (Eq, Show) + deriving newtype (Semigroup, Monoid) + +data TestCaseReport = TestCaseReport + { name :: String, + result :: TestResult, + time :: NominalDiffTime + } + deriving (Eq, Show) + +data TestResult = TestSuccess | TestFailure String + deriving (Eq, Show) diff --git a/integration/test/Testlib/XML.hs b/integration/test/Testlib/XML.hs new file mode 100644 index 00000000000..35235dec4d6 --- /dev/null +++ b/integration/test/Testlib/XML.hs @@ -0,0 +1,60 @@ +module Testlib.XML where + +import Data.Array qualified as Array +import Data.Fixed +import Data.Time +import Testlib.Types +import Text.Regex.Base qualified as Regex +import Text.Regex.TDFA.String qualified as Regex +import Text.XML.Light +import Prelude + +saveXMLReport :: TestSuiteReport -> FilePath -> IO () +saveXMLReport report output = + writeFile output $ showTopElement $ xmlReport report + +xmlReport :: TestSuiteReport -> Element +xmlReport report = + unode + "testsuites" + ( Attr (unqual "name") "wire-server", + testSuiteElements + ) + where + testSuiteElements = + unode + "testsuite" + ( attrs, + map encodeTestCase report.cases + ) + attrs = + [ Attr (unqual "name") "integration", + Attr (unqual "tests") $ show $ length report.cases, + Attr (unqual "failures") $ show $ length $ filter (\testCase -> testCase.result /= TestSuccess) report.cases, + Attr (unqual "time") $ showFixed True $ nominalDiffTimeToSeconds $ sum $ map (.time) report.cases + ] + +encodeTestCase :: TestCaseReport -> Element +encodeTestCase TestCaseReport {..} = + unode "testcase" (attrs, content) + where + attrs = + [ Attr (unqual "name") name, + Attr (unqual "time") (showFixed True (nominalDiffTimeToSeconds time)) + ] + content = case result of + TestSuccess -> [] + TestFailure msg -> [failure msg] + failure msg = unode "failure" (blank_cdata {cdData = dropConsoleFormatting msg}) + + -- Drops ANSI control characters which might be used to set colors. + -- Including these breaks XML, there is not much point encoding them. + dropConsoleFormatting input = + let regex = Regex.makeRegex "\x1b\\[[0-9;]*[mGKHF]" :: Regex.Regex + matches = Regex.matchAll regex input + dropMatch (offset, len) input' = + let (begining, rest) = splitAt offset input' + (_, end) = splitAt len rest + in begining <> end + matchTuples = map (Array.! 0) matches + in foldr dropMatch input matchTuples diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index cd56258ac99..edc5b3a4496 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -206,6 +206,22 @@ let sha256 = "sha256-htEIJY+LmIMACVZrflU60+X42/g14NxUyFM7VJs4E6w="; }; }; + # PR: https://github.com/ocharles/tasty-ant-xml/pull/32 + tasty-ant-xml = { + src = fetchgit { + url = "https://github.com/akshaymankar/tasty-ant-xml"; + rev = "34ff294d805e62e73678dccc0be9d3da13540fbe"; + sha256 = "sha256-+rHcS+BwEFsXqPAHX/KZDIgv9zfk1dZl0LlZJ57Com4="; + }; + }; + # PR: https://github.com/freckle/hspec-junit-formatter/pull/24 + hspec-junit-formatter = { + src = fetchgit { + url = "https://github.com/akshaymankar/hspec-junit-formatter"; + rev = "acec31822cc4f90489d9940bad23b3fd6d1d7c75"; + sha256 = "sha256-4xGW3KHQKbTL+6+Q/gzfaMBP+J0npUe7tP5ZCQCB5+s="; + }; + }; }; hackagePins = { # Major re-write upstream, we should get rid of this dependency rather than diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 4e4f70ab116..90022986207 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -30,6 +30,9 @@ hself: hsuper: { wai-middleware-prometheus = hlib.doJailbreak hsuper.wai-middleware-prometheus; wai-predicates = hlib.markUnbroken hsuper.wai-predicates; + # PR with fix: https://github.com/freckle/hspec-junit-formatter/pull/23 + hspec-junit-formatter = hlib.markUnbroken (hlib.dontCheck hsuper.hspec-junit-formatter); + # Some test seems to be broken hsaml2 = hlib.dontCheck hsuper.hsaml2; saml2-web-sso = hlib.dontCheck hsuper.saml2-web-sso; diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 1a1b9814a16..1bdbecf240a 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -594,6 +594,7 @@ executable brig-integration , spar , streaming-commons , tasty >=1.0 + , tasty-ant-xml , tasty-cannon >=0.3.4 , tasty-hunit >=0.2 , temporary >=1.2.1 diff --git a/services/brig/default.nix b/services/brig/default.nix index 6e7fa95539e..bce76636bee 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -122,6 +122,7 @@ , stomp-queue , streaming-commons , tasty +, tasty-ant-xml , tasty-cannon , tasty-hunit , tasty-quickcheck @@ -353,6 +354,7 @@ mkDerivation { spar streaming-commons tasty + tasty-ant-xml tasty-cannon tasty-hunit temporary diff --git a/services/brig/test/integration/Main.hs b/services/brig/test/integration/Main.hs index dcdef16cd5a..13cd48c7489 100644 --- a/services/brig/test/integration/Main.hs +++ b/services/brig/test/integration/Main.hs @@ -64,6 +64,9 @@ import System.Environment (withArgs) import System.Logger qualified as Logger import Test.Tasty import Test.Tasty.HUnit +import Test.Tasty.Ingredients +import Test.Tasty.Runners +import Test.Tasty.Runners.AntXML import Util import Util.Options import Util.Test @@ -165,7 +168,7 @@ runTests iConf brigOpts otherArgs = do mlsApi = MLS.tests mg b brigOpts oauthAPI = API.OAuth.tests mg db b n brigOpts - withArgs otherArgs . defaultMain + withArgs otherArgs . defaultMainWithIngredients (listingTests : (composeReporters antXMLRunner consoleTestReporter) : defaultIngredients) $ testGroup "Brig API Integration" $ [ testCase "sitemap" $ diff --git a/services/cargohold/cargohold.cabal b/services/cargohold/cargohold.cabal index 725699a95c9..cbe5965a748 100644 --- a/services/cargohold/cargohold.cabal +++ b/services/cargohold/cargohold.cabal @@ -289,6 +289,7 @@ executable cargohold-integration , servant-client , tagged >=0.8 , tasty >=1.0 + , tasty-ant-xml , tasty-hunit >=0.9 , text >=1.1 , time >=1.5 diff --git a/services/cargohold/default.nix b/services/cargohold/default.nix index 38aed9f0757..144a55f1943 100644 --- a/services/cargohold/default.nix +++ b/services/cargohold/default.nix @@ -50,6 +50,7 @@ , servant-server , tagged , tasty +, tasty-ant-xml , tasty-hunit , text , time @@ -154,6 +155,7 @@ mkDerivation { servant-client tagged tasty + tasty-ant-xml tasty-hunit text time diff --git a/services/cargohold/test/integration/Main.hs b/services/cargohold/test/integration/Main.hs index 86da6b26449..4615fa52bfd 100644 --- a/services/cargohold/test/integration/Main.hs +++ b/services/cargohold/test/integration/Main.hs @@ -30,7 +30,10 @@ import Imports hiding (local) import qualified Metrics import Options.Applicative import Test.Tasty +import Test.Tasty.Ingredients import Test.Tasty.Options +import Test.Tasty.Runners +import Test.Tasty.Runners.AntXML import TestSetup import Util.Test @@ -75,4 +78,6 @@ main = do [ Option (Proxy :: Proxy ServiceConfigFile), Option (Proxy :: Proxy IntegrationConfigFile) ] + : listingTests + : composeReporters antXMLRunner consoleTestReporter : defaultIngredients diff --git a/services/federator/default.nix b/services/federator/default.nix index 77517559a75..7aafe2dc588 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -23,6 +23,8 @@ , hinotify , HsOpenSSL , hspec +, hspec-core +, hspec-junit-formatter , http-client , http-client-tls , http-media @@ -125,6 +127,7 @@ mkDerivation { ]; executableHaskellDepends = [ aeson + async base bilge binary @@ -136,6 +139,8 @@ mkDerivation { exceptions HsOpenSSL hspec + hspec-core + hspec-junit-formatter http-client-tls http-types http2-manager diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index 53895622a41..4a5228e30c9 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -275,6 +275,7 @@ executable federator-integration build-depends: aeson + , async , base , bilge , binary @@ -287,6 +288,8 @@ executable federator-integration , federator , HsOpenSSL , hspec + , hspec-core + , hspec-junit-formatter , http-client-tls , http-types , http2-manager diff --git a/services/federator/test/integration/Main.hs b/services/federator/test/integration/Main.hs index fb1183a5926..d63572adf78 100644 --- a/services/federator/test/integration/Main.hs +++ b/services/federator/test/integration/Main.hs @@ -20,6 +20,7 @@ module Main ) where +import Control.Concurrent.Async import Imports import OpenSSL (withOpenSSL) import System.Environment (withArgs) @@ -27,6 +28,10 @@ import Test.Federator.IngressSpec qualified import Test.Federator.InwardSpec qualified import Test.Federator.Util (TestEnv, mkEnvFromOptions) import Test.Hspec +import Test.Hspec.Core.Format +import Test.Hspec.JUnit +import Test.Hspec.JUnit.Config.Env +import Test.Hspec.Runner main :: IO () main = withOpenSSL $ do @@ -34,7 +39,26 @@ main = withOpenSSL $ do env <- withArgs wireArgs mkEnvFromOptions -- withArgs hspecArgs . hspec $ do -- beforeAll (pure env) . afterAll destroyEnv $ Hspec.mkspec - withArgs hspecArgs . hspec $ mkspec env + cfg <- hspecConfig + withArgs hspecArgs . hspecWith cfg $ mkspec env + +hspecConfig :: IO Config +hspecConfig = do + junitConfig <- envJUnitConfig + pure $ + defaultConfig + { configAvailableFormatters = + ("junit", checksAndJUnitFormatter junitConfig) + : configAvailableFormatters defaultConfig + } + where + checksAndJUnitFormatter :: JUnitConfig -> FormatConfig -> IO Format + checksAndJUnitFormatter junitConfig config = do + junit <- junitFormat junitConfig config + let checksFormatter = fromJust (lookup "checks" $ configAvailableFormatters defaultConfig) + checks <- checksFormatter config + pure $ \event -> do + concurrently_ (junit event) (checks event) partitionArgs :: [String] -> ([String], [String]) partitionArgs = go [] [] diff --git a/services/galley/default.nix b/services/galley/default.nix index 6f114b76486..cc56a5b353a 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -90,6 +90,7 @@ , streaming-commons , tagged , tasty +, tasty-ant-xml , tasty-cannon , tasty-hunit , tasty-quickcheck @@ -282,6 +283,7 @@ mkDerivation { streaming-commons tagged tasty + tasty-ant-xml tasty-cannon tasty-hunit temporary diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 47306eedb3d..eb2416dce64 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -488,6 +488,7 @@ executable galley-integration , streaming-commons , tagged , tasty >=0.8 + , tasty-ant-xml , tasty-cannon >=0.3.2 , tasty-hunit >=0.9 , temporary diff --git a/services/galley/test/integration/Main.hs b/services/galley/test/integration/Main.hs index c578808998d..6b3d59928b1 100644 --- a/services/galley/test/integration/Main.hs +++ b/services/galley/test/integration/Main.hs @@ -49,7 +49,10 @@ import Options.Applicative import System.Logger.Class qualified as Logger import Test.Tasty import Test.Tasty.HUnit +import Test.Tasty.Ingredients +import Test.Tasty.Ingredients.Basic import Test.Tasty.Options +import Test.Tasty.Runners.AntXML import TestHelpers (test) import TestSetup import Util.Options @@ -84,6 +87,8 @@ runTests run = defaultMainWithIngredients ings $ [ Option (Proxy :: Proxy ServiceConfigFile), Option (Proxy :: Proxy IntegrationConfigFile) ] + : listingTests + : composeReporters antXMLRunner consoleTestReporter : defaultIngredients main :: IO () diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index 624a143c7ed..18410fe9988 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -59,6 +59,7 @@ , servant-server , tagged , tasty +, tasty-ant-xml , tasty-hunit , tasty-quickcheck , text @@ -171,6 +172,7 @@ mkDerivation { safe tagged tasty + tasty-ant-xml tasty-hunit text tinylog diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index 1327735f990..7a116433a4e 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -303,6 +303,7 @@ executable gundeck-integration , safe , tagged , tasty >=1.0 + , tasty-ant-xml , tasty-hunit >=0.9 , text , tinylog diff --git a/services/gundeck/test/integration/Main.hs b/services/gundeck/test/integration/Main.hs index 6a9502c9c95..9ab372ede39 100644 --- a/services/gundeck/test/integration/Main.hs +++ b/services/gundeck/test/integration/Main.hs @@ -39,7 +39,10 @@ import OpenSSL (withOpenSSL) import Options.Applicative import System.Logger qualified as Logger import Test.Tasty +import Test.Tasty.Ingredients import Test.Tasty.Options +import Test.Tasty.Runners +import Test.Tasty.Runners.AntXML import TestSetup import Util.Options import Util.Test @@ -83,6 +86,8 @@ runTests run = defaultMainWithIngredients ings $ [ Option (Proxy :: Proxy ServiceConfigFile), Option (Proxy :: Proxy IntegrationConfigFile) ] + : listingTests + : composeReporters antXMLRunner consoleTestReporter : defaultIngredients main :: IO () diff --git a/services/spar/default.nix b/services/spar/default.nix index e5b1d9bd105..ffc27016e3c 100644 --- a/services/spar/default.nix +++ b/services/spar/default.nix @@ -5,6 +5,7 @@ { mkDerivation , aeson , aeson-qq +, async , base , base64-bytestring , bilge @@ -27,7 +28,9 @@ , hscim , HsOpenSSL , hspec +, hspec-core , hspec-discover +, hspec-junit-formatter , hspec-wai , http-api-data , http-client @@ -136,6 +139,7 @@ mkDerivation { executableHaskellDepends = [ aeson aeson-qq + async base base64-bytestring bilge @@ -156,6 +160,8 @@ mkDerivation { hscim HsOpenSSL hspec + hspec-core + hspec-junit-formatter hspec-wai http-api-data http-client diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 124cbb93a99..428b2c8e6f1 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -313,6 +313,7 @@ executable spar-integration build-depends: aeson , aeson-qq + , async , base , base64-bytestring , bilge @@ -331,6 +332,8 @@ executable spar-integration , hscim , HsOpenSSL , hspec + , hspec-core + , hspec-junit-formatter , hspec-wai , http-api-data , http-client diff --git a/services/spar/test-integration/Main.hs b/services/spar/test-integration/Main.hs index b42bb8c66b2..3eefa283be5 100644 --- a/services/spar/test-integration/Main.hs +++ b/services/spar/test-integration/Main.hs @@ -29,6 +29,7 @@ -- the solution: https://github.com/hspec/hspec/pull/397. module Main where +import Control.Concurrent.Async import Control.Lens ((.~), (^.)) import Data.Text (pack) import Imports @@ -37,6 +38,10 @@ import Spar.Run (mkApp) import System.Environment (withArgs) import System.Random (randomRIO) import Test.Hspec +import Test.Hspec.Core.Format +import Test.Hspec.Core.Runner +import Test.Hspec.JUnit +import Test.Hspec.JUnit.Config.Env import qualified Test.LoggingSpec import qualified Test.MetricsSpec import qualified Test.Spar.APISpec @@ -53,7 +58,8 @@ main :: IO () main = do (wireArgs, hspecArgs) <- partitionArgs <$> getArgs let env = withArgs wireArgs mkEnvFromOptions - withArgs hspecArgs . hspec $ do + cfg <- hspecConfig + withArgs hspecArgs . hspecWith cfg $ do for_ [minBound ..] $ \idpApiVersion -> do describe (show idpApiVersion) . beforeAll (env <&> teWireIdPAPIVersion .~ idpApiVersion) . afterAll destroyEnv $ do mkspecMisc @@ -61,6 +67,24 @@ main = do mkspecScim mkspecHscimAcceptance env destroyEnv +hspecConfig :: IO Config +hspecConfig = do + junitConfig <- envJUnitConfig + pure $ + defaultConfig + { configAvailableFormatters = + ("junit", checksAndJUnitFormatter junitConfig) + : configAvailableFormatters defaultConfig + } + where + checksAndJUnitFormatter :: JUnitConfig -> FormatConfig -> IO Format + checksAndJUnitFormatter junitConfig config = do + junit <- junitFormat junitConfig config + let checksFormatter = fromJust (lookup "checks" $ configAvailableFormatters defaultConfig) + checks <- checksFormatter config + pure $ \event -> do + concurrently_ (junit event) (checks event) + partitionArgs :: [String] -> ([String], [String]) partitionArgs = go [] [] where diff --git a/tools/stern/default.nix b/tools/stern/default.nix index 3a5afeaa844..2c5867d329a 100644 --- a/tools/stern/default.nix +++ b/tools/stern/default.nix @@ -40,6 +40,7 @@ , split , tagged , tasty +, tasty-ant-xml , tasty-hunit , text , tinylog @@ -119,6 +120,7 @@ mkDerivation { schema-profunctor tagged tasty + tasty-ant-xml tasty-hunit text tinylog diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index 4cb3eff82ea..aedd9ca5883 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -270,6 +270,7 @@ executable stern-integration , stern , tagged , tasty >=0.8 + , tasty-ant-xml , tasty-hunit >=0.9 , text , tinylog diff --git a/tools/stern/test/integration/Main.hs b/tools/stern/test/integration/Main.hs index 6c95115b870..3acef76603c 100644 --- a/tools/stern/test/integration/Main.hs +++ b/tools/stern/test/integration/Main.hs @@ -34,7 +34,10 @@ import OpenSSL (withOpenSSL) import Options.Applicative import System.Logger qualified as Logger import Test.Tasty +import Test.Tasty.Ingredients import Test.Tasty.Options +import Test.Tasty.Runners +import Test.Tasty.Runners.AntXML import TestSetup import Util.Options (Endpoint (Endpoint)) import Util.Test @@ -74,6 +77,8 @@ runTests run = defaultMainWithIngredients ings $ [ Option (Proxy :: Proxy ServiceConfigFile), Option (Proxy :: Proxy IntegrationConfigFile) ] + : listingTests + : composeReporters antXMLRunner consoleTestReporter : defaultIngredients main :: IO () From 043aa0f123c31735846b9e6233195af8433a351e Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 26 Sep 2023 09:21:12 +0200 Subject: [PATCH 141/225] cabal.project: Add -Werror for integration tests (#3609) The CI runs this build with -Werror, local compilation should also follow that as we do for all the other packages. --- cabal.project | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cabal.project b/cabal.project index 60b607ea01b..96d20a6f060 100644 --- a/cabal.project +++ b/cabal.project @@ -96,6 +96,8 @@ package hscim ghc-options: -Werror package http2-manager ghc-options: -Werror +package integration + ghc-options: -Werror package imports ghc-options: -Werror package jwt-tools From c692a8949d43b536a7b2215fcf6d03bdbc50b792 Mon Sep 17 00:00:00 2001 From: fisx Date: Tue, 26 Sep 2023 09:53:53 +0200 Subject: [PATCH 142/225] Fix link (#3613) * Revert "Fix broken "we are hiring" link (#3549)" This reverts commit 5f66f8af4ac1cf39c4f597673d029e9b3fcd435d. * Update careers link. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92c34edcab3..ada3b2d2cc5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Wire™ -[![Wire logo](https://github.com/wireapp/wire/blob/master/assets/header-small.png?raw=true)](https://start.wire.com/careers-en) +[![Wire logo](https://github.com/wireapp/wire/blob/master/assets/header-small.png?raw=true)](https://wire.bamboohr.com/careers) This repository is part of the source code of Wire. You can find more information at [wire.com](https://wire.com) or by contacting opensource@wire.com. From 1c6f7463d9eacfe68cdc402952e41213869267c0 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:44:22 +0200 Subject: [PATCH 143/225] [WPB-700] Pass along server errors between Galley and Stern (#3608) * Stop swallowing error messages. * Update services/brig/src/Brig/API/Internal.hs Co-authored-by: fisx --------- Co-authored-by: fisx --- .../src/Wire/API/Routes/Internal/Brig.hs | 2 +- tools/stern/src/Stern/API/Routes.hs | 2 +- tools/stern/src/Stern/Intra.hs | 26 +++++++++++-------- tools/stern/test/integration/API.hs | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) 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 10308c8a58e..acf06f3b8a6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -283,7 +283,7 @@ type AccountAPI = :> QueryParam' [Optional, Strict] "email" Email :> QueryParam' [Optional, Strict] "phone" Phone :> MultiVerb - 'HEAD + 'GET '[Servant.JSON] '[ Respond 404 "Not blacklisted" (), Respond 200 "Yes blacklisted" () diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index aae30de2805..e54540adb94 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -233,7 +233,7 @@ type SternAPI = :> "blacklist" :> QueryParam' [Optional, Strict, Description "A verified email address"] "email" Email :> QueryParam' [Optional, Strict, Description "A verified phone number (E.164 format)."] "phone" Phone - :> Verb 'HEAD 200 '[JSON] NoContent + :> Verb 'GET 200 '[JSON] NoContent ) :<|> Named "post-user-blacklist" diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 8ab4c1e3a86..b68821eba8c 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -90,11 +90,12 @@ import Data.Qualified (qUnqualified) import Data.Text (strip) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.Lazy (pack) +import Data.Text.Lazy.Encoding qualified as TL import GHC.TypeLits (KnownSymbol) import Imports import Network.HTTP.Types (urlEncode) import Network.HTTP.Types.Method -import Network.HTTP.Types.Status hiding (statusCode) +import Network.HTTP.Types.Status hiding (statusCode, statusMessage) import Network.Wai.Utilities (Error (..), mkError) import Servant.API (toUrlPiece) import Stern.App @@ -454,7 +455,7 @@ getTeamBillingInfo :: TeamId -> Handler (Maybe TeamBillingInfo) getTeamBillingInfo tid = do info $ msg "Getting team billing info" i <- view ibis - r <- + resp <- catchRpcErrors $ rpc' "ibis" @@ -462,10 +463,10 @@ getTeamBillingInfo tid = do ( method GET . Bilge.paths ["i", "team", toByteString' tid, "billing"] ) - case Bilge.statusCode r of - 200 -> Just <$> parseResponse (mkError status502 "bad-upstream") r + case Bilge.statusCode resp of + 200 -> Just <$> parseResponse (mkError status502 "bad-upstream") resp 404 -> pure Nothing - _ -> throwE (mkError status502 "bad-upstream" "bad response") + _ -> throwE (mkError status502 "bad-upstream" (errorMessage resp)) setTeamBillingInfo :: TeamId -> TeamBillingInfo -> Handler () setTeamBillingInfo tid tbu = do @@ -486,19 +487,19 @@ isBlacklisted :: Either Email Phone -> Handler Bool isBlacklisted emailOrPhone = do info $ msg "Checking blacklist" b <- view brig - r <- + resp <- catchRpcErrors $ rpc' "brig" b - ( method HEAD + ( method GET . Bilge.path "i/users/blacklist" . userKeyToParam emailOrPhone ) - case Bilge.statusCode r of + case Bilge.statusCode resp of 200 -> pure True 404 -> pure False - _ -> throwE (mkError status502 "bad-upstream" "bad response") + _ -> throwE (mkError status502 "bad-upstream" (errorMessage resp)) setBlacklistStatus :: Bool -> Either Email Phone -> Handler () setBlacklistStatus status emailOrPhone = do @@ -535,7 +536,7 @@ getTeamFeatureFlag tid = do case Bilge.statusCode resp of 200 -> pure $ responseJsonUnsafe @(Public.WithStatus cfg) resp 404 -> throwE (mkError status404 "bad-upstream" "team doesnt exist") - _ -> throwE (mkError status502 "bad-upstream" "bad response") + _ -> throwE (mkError status502 "bad-upstream" (errorMessage resp)) setTeamFeatureFlag :: forall cfg. @@ -559,7 +560,7 @@ setTeamFeatureFlag tid status = do 200 -> pure () 404 -> throwE (mkError status404 "bad-upstream" "team doesnt exist") 403 -> throwE (mkError status403 "bad-upstream" "legal hold config cannot be changed") - _ -> throwE (mkError status502 "bad-upstream" "bad response") + _ -> throwE (mkError status502 "bad-upstream" (errorMessage resp)) where checkDaysLimit :: FeatureTTL -> Handler () checkDaysLimit = \case @@ -620,6 +621,9 @@ userKeyToParam :: Either Email Phone -> Request -> Request userKeyToParam (Left e) = queryItem "email" (stripBS $ toByteString' e) userKeyToParam (Right p) = queryItem "phone" (stripBS $ toByteString' p) +errorMessage :: Response (Maybe LByteString) -> LText +errorMessage = maybe "" TL.decodeUtf8 . responseBody + -- | Run an App and catch any RPCException's which may occur, lifting them to ExceptT -- This isn't an ideal set-up; but is required in certain cases because 'ExceptT' isn't -- an instance of 'MonadUnliftIO' diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index 5edaf92362c..744481eda6f 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -563,7 +563,7 @@ ejpdInfo includeContacts handles = do userBlacklistHead :: Either Email Phone -> TestM ResponseLBS userBlacklistHead emailOrPhone = do s <- view tsStern - Bilge.head (s . paths ["users", "blacklist"] . mkQueryParam emailOrPhone) + Bilge.get (s . paths ["users", "blacklist"] . mkQueryParam emailOrPhone) postUserBlacklist :: Either Email Phone -> TestM () postUserBlacklist emailOrPhone = do From d835b971b5182cb52d6d8827aa49423c5c7968d0 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 26 Sep 2023 12:06:31 +0200 Subject: [PATCH 144/225] Fix federationStrategy 'allowAll' (#3588) * hack/{helmfile,helm_vars}: Remove unnecessary hacks * backend-notification-pusher: Watch RabbitMQ queues to push notifications Instead of watching brig's federation configs internal endpoint. * background-notification-pusher: Better function name * Move federated search tests to new integration suite The tests require search policy to be set, it is easier to test with dynamic backends. * Simplify integration tests Now that allowAll is fixed and is the default for all test backends, we can write tests in a simpler way. * background-worker: Do not run federation domains config sync thread * background-worker: Make remotesRefreshInterval configurable Defaults to 5 min in the helm chart. For integration tests the value is set to 1s. --- .../5-internal/background-worker-nosync | 1 + .../templates/configmap.yaml | 8 - charts/background-worker/values.yaml | 1 + hack/helm_vars/wire-server/values.yaml.gotmpl | 16 +- hack/helmfile.yaml | 16 -- integration/test/API/Brig.hs | 14 ++ integration/test/API/Common.hs | 8 + integration/test/SetupHelpers.hs | 7 - integration/test/Test/Brig.hs | 34 ++- integration/test/Test/Conversation.hs | 203 +++++++----------- integration/test/Test/Demo.hs | 23 +- integration/test/Test/Federation.hs | 13 -- .../background-worker/background-worker.cabal | 1 - .../background-worker.integration.yaml | 13 +- services/background-worker/default.nix | 2 - .../src/Wire/BackendNotificationPusher.hs | 35 +-- .../src/Wire/BackgroundWorker.hs | 8 +- .../src/Wire/BackgroundWorker/Env.hs | 17 +- .../src/Wire/BackgroundWorker/Options.hs | 7 +- .../Wire/BackendNotificationPusherSpec.hs | 11 +- .../background-worker/test/Test/Wire/Util.hs | 9 +- .../test/integration/Federation/End2end.hs | 39 +--- 22 files changed, 156 insertions(+), 330 deletions(-) create mode 100644 changelog.d/5-internal/background-worker-nosync diff --git a/changelog.d/5-internal/background-worker-nosync b/changelog.d/5-internal/background-worker-nosync new file mode 100644 index 00000000000..b9eda2712c5 --- /dev/null +++ b/changelog.d/5-internal/background-worker-nosync @@ -0,0 +1 @@ +background-worker: Get list of domains from RabbitMQ instead of brig for pushing backend notifications \ No newline at end of file diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index 7e3612252d8..1a03ad0d5e4 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -21,14 +21,6 @@ data: host: federator port: 8080 - galley: - host: galley - port: 8080 - - brig: - host: brig - port: 8080 - rabbitmq: {{toYaml .rabbitmq | indent 6 }} backendNotificationPusher: diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml index fcae0115bfc..a7a552a4536 100644 --- a/charts/background-worker/values.yaml +++ b/charts/background-worker/values.yaml @@ -26,6 +26,7 @@ config: backendNotificationPusher: pushBackoffMinWait: 10000 # in microseconds, so 10ms pushBackoffMaxWait: 300000000 # microseconds, so 300s + remotesRefreshInterval: 300000000 # microseconds, so 300s serviceAccount: # When setting this to 'false', either make sure that a service account named diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 4bd06d953fe..7185c30b4f7 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -77,21 +77,6 @@ brig: setMaxConvSize: 16 # See helmfile for the real value setFederationDomain: integration.example.com - setFederationDomainConfigs: - # 'setFederationDomainConfigs' is deprecated as of https://github.com/wireapp/wire-server/pull/3260. See - # https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections - # for details. - - domain: integration.example.com - search_policy: full_search - - domain: federation-test-helper.{{ .Release.Namespace }}.svc.cluster.local - search_policy: full_search - # Remove these after fixing https://wearezeta.atlassian.net/browse/WPB-3796 - - domain: dyn-backend-1 - search_policy: full_search - - domain: dyn-backend-2 - search_policy: full_search - - domain: dyn-backend-3 - search_policy: full_search setFederationStrategy: allowAll setFederationDomainConfigsUpdateFreq: 10 set2FACodeGenerationDelaySecs: 5 @@ -321,6 +306,7 @@ background-worker: backendNotificationPusher: pushBackoffMinWait: 1000 # 1ms pushBackoffMaxWait: 500000 # 0.5s + remotesRefreshInterval: 1000000 # 1s secrets: rabbitmq: username: {{ .Values.rabbitmqUsername }} diff --git a/hack/helmfile.yaml b/hack/helmfile.yaml index b40cd73d623..878cb016f50 100644 --- a/hack/helmfile.yaml +++ b/hack/helmfile.yaml @@ -127,14 +127,6 @@ releases: value: {{ .Values.federationDomain1 }} - name: cargohold.config.settings.federationDomain value: {{ .Values.federationDomain1 }} - - name: brig.config.optSettings.setFederationDomainConfigs[0].domain - value: {{ .Values.federationDomain2 }} - - name: brig.config.optSettings.setFederationDomainConfigs[2].domain - value: {{ .Values.dynBackendDomain1 }} - - name: brig.config.optSettings.setFederationDomainConfigs[3].domain - value: {{ .Values.dynBackendDomain2 }} - - name: brig.config.optSettings.setFederationDomainConfigs[4].domain - value: {{ .Values.dynBackendDomain3 }} needs: - 'databases-ephemeral' @@ -151,13 +143,5 @@ releases: value: {{ .Values.federationDomain2 }} - name: cargohold.config.settings.federationDomain value: {{ .Values.federationDomain2 }} - - name: brig.config.optSettings.setFederationDomainConfigs[0].domain - value: {{ .Values.federationDomain1 }} - - name: brig.config.optSettings.setFederationDomainConfigs[2].domain - value: {{ .Values.dynBackendDomain1 }} - - name: brig.config.optSettings.setFederationDomainConfigs[3].domain - value: {{ .Values.dynBackendDomain2 }} - - name: brig.config.optSettings.setFederationDomainConfigs[4].domain - value: {{ .Values.dynBackendDomain3 }} needs: - 'databases-ephemeral' diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 82abfb5a6bb..abad2be9a11 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -21,6 +21,14 @@ getUser user target = do joinHttpPath ["users", domain, uid] submit "GET" req +getUserByHandle :: (HasCallStack, MakesValue user, MakesValue domain) => user -> domain -> String -> App Response +getUserByHandle user domain handle = do + domainStr <- asString domain + req <- + baseRequest user Brig Versioned $ + joinHttpPath ["users", "by-handle", domainStr, handle] + submit "GET" req + getClient :: (HasCallStack, MakesValue user, MakesValue client) => user -> @@ -39,6 +47,12 @@ deleteUser user = do submit "DELETE" $ req & addJSONObject ["password" .= defPassword] +putHandle :: (HasCallStack, MakesValue user) => user -> String -> App Response +putHandle user handle = do + req <- baseRequest user Brig Versioned "/self/handle" + submit "PUT" $ + req & addJSONObject ["handle" .= handle] + data AddClient = AddClient { ctype :: String, internal :: Bool, diff --git a/integration/test/API/Common.hs b/integration/test/API/Common.hs index 85c978cb7a3..125c6150bf1 100644 --- a/integration/test/API/Common.hs +++ b/integration/test/API/Common.hs @@ -26,6 +26,14 @@ randomEmail = liftIO $ do chars = mkArray $ ['A' .. 'Z'] <> ['a' .. 'z'] <> ['0' .. '9'] pick = (chars !) <$> randomRIO (Array.bounds chars) +randomHandle :: App String +randomHandle = liftIO $ do + n <- randomRIO (50, 256) + replicateM n pick + where + chars = mkArray $ ['a' .. 'z'] <> ['0' .. '9'] <> "_-." + pick = (chars !) <$> randomRIO (Array.bounds chars) + randomHex :: Int -> App String randomHex n = liftIO $ replicateM n pick where diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index b3c6b2dd5b5..3e2f2313894 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -6,7 +6,6 @@ import API.Brig qualified as Brig import API.BrigInternal qualified as Internal import API.Common import API.Galley -import Control.Concurrent (threadDelay) import Control.Monad.Reader import Data.Aeson hiding ((.=)) import Data.Aeson.Types qualified as Aeson @@ -17,12 +16,6 @@ import Data.UUID.V4 (nextRandom) import GHC.Stack import Testlib.Prelude --- | `n` should be 2 x `setFederationDomainConfigsUpdateFreq` in the config -connectAllDomainsAndWaitToSync :: HasCallStack => Int -> [String] -> App () -connectAllDomainsAndWaitToSync n domains = do - sequence_ [Internal.createFedConn x (Internal.FedConn y "full_search") | x <- domains, y <- domains, x /= y] - liftIO $ threadDelay (n * 1000 * 1000) -- wait for federation status to be updated - randomUser :: (HasCallStack, MakesValue domain) => domain -> Internal.CreateUser -> App Value randomUser domain cu = bindResponse (Internal.createUser domain cu) $ \resp -> do resp.status `shouldMatchInt` 201 diff --git a/integration/test/Test/Brig.hs b/integration/test/Test/Brig.hs index 4b5623a3ecc..8179641174b 100644 --- a/integration/test/Test/Brig.hs +++ b/integration/test/Test/Brig.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + module Test.Brig where import API.Brig qualified as Public @@ -5,7 +7,6 @@ import API.BrigInternal qualified as Internal import API.Common qualified as API import API.GalleyInternal qualified as Internal import Control.Concurrent (threadDelay) -import Data.Aeson qualified as Aeson import Data.Aeson.Types hiding ((.=)) import Data.Set qualified as Set import Data.String.Conversions @@ -143,18 +144,35 @@ testSwagger = do testRemoteUserSearch :: HasCallStack => App () testRemoteUserSearch = do - let overrides = - setField "optSettings.setFederationStrategy" "allowDynamic" - >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - startDynamicBackends [def {brigCfg = overrides}, def {brigCfg = overrides}] $ \dynDomains -> do - domains@[d1, d2] <- pure dynDomains - connectAllDomainsAndWaitToSync 1 domains - [u1, u2] <- createAndConnectUsers [d1, d2] + startDynamicBackends [def, def] $ \[d1, d2] -> do + void $ Internal.createFedConn d2 (Internal.FedConn d1 "full_search") + + u1 <- randomUser d1 def + u2 <- randomUser d2 def Internal.refreshIndex d2 uidD2 <- objId u2 + bindResponse (Public.searchContacts u1 (u2 %. "name") d2) $ \resp -> do resp.status `shouldMatchInt` 200 docs <- resp.json %. "documents" >>= asList case docs of [] -> assertFailure "Expected a non empty result, but got an empty one" doc : _ -> doc %. "id" `shouldMatch` uidD2 + +testRemoteUserSearchExactHandle :: HasCallStack => App () +testRemoteUserSearchExactHandle = do + startDynamicBackends [def, def] $ \[d1, d2] -> do + void $ Internal.createFedConn d2 (Internal.FedConn d1 "exact_handle_search") + + u1 <- randomUser d1 def + u2 <- randomUser d2 def + u2Handle <- API.randomHandle + bindResponse (Public.putHandle u2 u2Handle) $ assertSuccess + Internal.refreshIndex d2 + + bindResponse (Public.searchContacts u1 u2Handle d2) $ \resp -> do + resp.status `shouldMatchInt` 200 + docs <- resp.json %. "documents" >>= asList + case docs of + [] -> assertFailure "Expected a non empty result, but got an empty one" + doc : _ -> objQid doc `shouldMatch` objQid u2 diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index afd2e07aaad..9a42bf0493d 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -39,9 +39,8 @@ import Testlib.ResourcePool testDynamicBackendsFullyConnectedWhenAllowAll :: HasCallStack => App () testDynamicBackendsFullyConnectedWhenAllowAll = do - let overrides = - def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - startDynamicBackends [overrides, overrides, overrides] $ \dynDomains -> do + -- The default setting is 'allowAll' + startDynamicBackends [def, def, def] $ \dynDomains -> do [domainA, domainB, domainC] <- pure dynDomains uidA <- randomUser domainA def {team = True} uidB <- randomUser domainA def {team = True} @@ -65,66 +64,55 @@ testDynamicBackendsNotFederating = do { brigCfg = setField "optSettings.setFederationStrategy" "allowNone" } - startDynamicBackends [overrides, overrides, overrides] $ - \dynDomains -> do - [domainA, domainB, domainC] <- pure dynDomains - uidA <- randomUser domainA def {team = True} - retryT - $ bindResponse - (getFederationStatus uidA [domainB, domainC]) - $ \resp -> do - resp.status `shouldMatchInt` 533 - resp.json %. "unreachable_backends" `shouldMatchSet` [domainB, domainC] + startDynamicBackends [overrides, overrides, overrides] $ \[domainA, domainB, domainC] -> do + uidA <- randomUser domainA def {team = True} + retryT + $ bindResponse + (getFederationStatus uidA [domainB, domainC]) + $ \resp -> do + resp.status `shouldMatchInt` 533 + resp.json %. "unreachable_backends" `shouldMatchSet` [domainB, domainC] testDynamicBackendsFullyConnectedWhenAllowDynamic :: HasCallStack => App () testDynamicBackendsFullyConnectedWhenAllowDynamic = do - let overrides = - setField "optSettings.setFederationStrategy" "allowDynamic" - >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - startDynamicBackends - [ def {brigCfg = overrides}, - def {brigCfg = overrides}, - def {brigCfg = overrides} - ] - $ \dynDomains -> do - domains@[domainA, domainB, domainC] <- pure dynDomains - sequence_ [createFedConn x (FedConn y "full_search") | x <- domains, y <- domains, x /= y] - uidA <- randomUser domainA def {team = True} - uidB <- randomUser domainB def {team = True} - uidC <- randomUser domainC def {team = True} - let assertConnected u d d' = - bindResponse - (getFederationStatus u [d, d']) - $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "status" `shouldMatch` "fully-connected" - retryT $ assertConnected uidA domainB domainC - retryT $ assertConnected uidB domainA domainC - retryT $ assertConnected uidC domainA domainB + withFederatingBackendsAllowDynamic $ \(domainA, domainB, domainC) -> do + -- Allowing 'full_search' or any type of search is how we enable federation + -- between backends when the federation strategy is 'allowDynamic'. + sequence_ + [ createFedConn x (FedConn y "full_search") + | x <- [domainA, domainB, domainC], + y <- [domainA, domainB, domainC], + x /= y + ] + uidA <- randomUser domainA def {team = True} + uidB <- randomUser domainB def {team = True} + uidC <- randomUser domainC def {team = True} + let assertConnected u d d' = + bindResponse + (getFederationStatus u [d, d']) + $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "fully-connected" + retryT $ assertConnected uidA domainB domainC + retryT $ assertConnected uidB domainA domainC + retryT $ assertConnected uidC domainA domainB testDynamicBackendsNotFullyConnected :: HasCallStack => App () testDynamicBackendsNotFullyConnected = do - let overrides = - def - { brigCfg = - setField "optSettings.setFederationStrategy" "allowDynamic" - >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - } - startDynamicBackends [overrides, overrides, overrides] $ - \[domainA, domainB, domainC] -> do - -- A is connected to B and C, but B and C are not connected to each other - void $ createFedConn domainA $ FedConn domainB "full_search" - void $ createFedConn domainB $ FedConn domainA "full_search" - void $ createFedConn domainA $ FedConn domainC "full_search" - void $ createFedConn domainC $ FedConn domainA "full_search" - uidA <- randomUser domainA def {team = True} - retryT - $ bindResponse - (getFederationStatus uidA [domainB, domainC]) - $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "status" `shouldMatch` "non-fully-connected" - resp.json %. "not_connected" `shouldMatchSet` [domainB, domainC] + withFederatingBackendsAllowDynamic $ \(domainA, domainB, domainC) -> do + -- A is connected to B and C, but B and C are not connected to each other + void $ createFedConn domainA $ FedConn domainB "full_search" + void $ createFedConn domainB $ FedConn domainA "full_search" + void $ createFedConn domainA $ FedConn domainC "full_search" + void $ createFedConn domainC $ FedConn domainA "full_search" + uidA <- randomUser domainA def {team = True} + retryT + $ bindResponse + (getFederationStatus uidA [domainB, domainC]) + $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "non-fully-connected" + resp.json %. "not_connected" `shouldMatchSet` [domainB, domainC] testFederationStatus :: HasCallStack => App () testFederationStatus = do @@ -151,55 +139,34 @@ testFederationStatus = do testCreateConversationFullyConnected :: HasCallStack => App () testCreateConversationFullyConnected = do - let setFederationConfig = - setField "optSettings.setFederationStrategy" "allowDynamic" - >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - startDynamicBackends - [ def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig} - ] - $ \dynDomains -> do - domains@[domainA, domainB, domainC] <- pure dynDomains - connectAllDomainsAndWaitToSync 1 domains - [u1, u2, u3] <- createAndConnectUsers [domainA, domainB, domainC] - bindResponse (postConversation u1 (defProteus {qualifiedUsers = [u2, u3]})) $ \resp -> do - resp.status `shouldMatchInt` 201 + startDynamicBackends [def, def, def] $ \[domainA, domainB, domainC] -> do + [u1, u2, u3] <- createAndConnectUsers [domainA, domainB, domainC] + bindResponse (postConversation u1 (defProteus {qualifiedUsers = [u2, u3]})) $ \resp -> do + resp.status `shouldMatchInt` 201 testCreateConversationNonFullyConnected :: HasCallStack => App () testCreateConversationNonFullyConnected = do - let setFederationConfig = - setField "optSettings.setFederationStrategy" "allowDynamic" - >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - startDynamicBackends - [ def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig} - ] - $ \dynDomains -> do - [domainA, domainB, domainC] <- pure dynDomains - - -- A is connected to B and C, but B and C are not connected to each other - void $ createFedConn domainA $ FedConn domainB "full_search" - void $ createFedConn domainB $ FedConn domainA "full_search" - void $ createFedConn domainA $ FedConn domainC "full_search" - void $ createFedConn domainC $ FedConn domainA "full_search" - liftIO $ threadDelay (2 * 1000 * 1000) - - u1 <- randomUser domainA def - u2 <- randomUser domainB def - u3 <- randomUser domainC def - connectUsers u1 u2 - connectUsers u1 u3 - - bindResponse (postConversation u1 (defProteus {qualifiedUsers = [u2, u3]})) $ \resp -> do - resp.status `shouldMatchInt` 409 - resp.json %. "non_federating_backends" `shouldMatchSet` [domainB, domainC] + withFederatingBackendsAllowDynamic $ \(domainA, domainB, domainC) -> do + -- A is connected to B and C, but B and C are not connected to each other + void $ createFedConn domainA $ FedConn domainB "full_search" + void $ createFedConn domainB $ FedConn domainA "full_search" + void $ createFedConn domainA $ FedConn domainC "full_search" + void $ createFedConn domainC $ FedConn domainA "full_search" + liftIO $ threadDelay (2 * 1000 * 1000) + + u1 <- randomUser domainA def + u2 <- randomUser domainB def + u3 <- randomUser domainC def + connectUsers u1 u2 + connectUsers u1 u3 + + bindResponse (postConversation u1 (defProteus {qualifiedUsers = [u2, u3]})) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "non_federating_backends" `shouldMatchSet` [domainB, domainC] testAddMembersFullyConnectedProteus :: HasCallStack => App () testAddMembersFullyConnectedProteus = do - withFederatingBackendsAllowDynamic $ \(domainA, domainB, domainC) -> do - connectAllDomainsAndWaitToSync 2 [domainA, domainB, domainC] + startDynamicBackends [def, def, def] $ \[domainA, domainB, domainC] -> do [u1, u2, u3] <- createAndConnectUsers [domainA, domainB, domainC] -- create conversation with no users cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 @@ -293,10 +260,8 @@ testAddMemberV1 domain = do testConvWithUnreachableRemoteUsers :: HasCallStack => App () testConvWithUnreachableRemoteUsers = do - let overrides = - def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} ([alice, alex, bob, charlie, dylan], domains) <- - startDynamicBackends [overrides, overrides] $ \domains -> do + startDynamicBackends [def, def] $ \domains -> do own <- make OwnDomain & asString other <- make OtherDomain & asString users <- createAndConnectUsers $ [own, own, other] <> domains @@ -313,10 +278,8 @@ testConvWithUnreachableRemoteUsers = do testAddReachableWithUnreachableRemoteUsers :: HasCallStack => App () testAddReachableWithUnreachableRemoteUsers = do - let overrides = - def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} ([alex, bob], conv, domains) <- - startDynamicBackends [overrides, overrides] $ \domains -> do + startDynamicBackends [def, def] $ \domains -> do own <- make OwnDomain & asString other <- make OtherDomain & asString [alice, alex, bob, charlie, dylan] <- @@ -338,10 +301,8 @@ testAddReachableWithUnreachableRemoteUsers = do testAddUnreachable :: HasCallStack => App () testAddUnreachable = do - let overrides = - def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} ([alex, charlie], [charlieDomain, dylanDomain], conv) <- - startDynamicBackends [overrides, overrides] $ \domains -> do + startDynamicBackends [def, def] $ \domains -> do own <- make OwnDomain & asString [alice, alex, charlie, dylan] <- createAndConnectUsers $ [own, own] <> domains @@ -474,10 +435,8 @@ testMultiIngressGuestLinks = do testAddUserWhenOtherBackendOffline :: HasCallStack => App () testAddUserWhenOtherBackendOffline = do - let overrides = - def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} ([alice, alex], conv) <- - startDynamicBackends [overrides] $ \domains -> do + startDynamicBackends [def] $ \domains -> do own <- make OwnDomain & asString [alice, alex, charlie] <- createAndConnectUsers $ [own, own] <> domains @@ -644,18 +603,6 @@ testDeleteTeamConversationWithUnreachableRemoteMembers = do runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> do (bob, bobClient) <- runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do - -- FUTUREWORK: get rid of this once the background worker is able to listen to all queues - do - ownDomain <- make OwnDomain & asString - otherDomain <- make OtherDomain & asString - let domains = [ownDomain, otherDomain, dynBackend.berDomain] - sequence_ - [ createFedConn x (FedConn y "full_search") - | x <- domains, - y <- domains, - x /= y - ] - bob <- randomUser dynBackend.berDomain def bobClient <- objId $ bindResponse (addClient bob def) $ getJSON 201 connectUsers alice bob @@ -676,9 +623,7 @@ testLeaveConversationSuccess = do createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain, OtherDomain] [aClient, bClient] <- forM [alice, bob] $ \user -> objId $ bindResponse (addClient user def) $ getJSON 201 - let overrides = - def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - startDynamicBackends [overrides] $ \[dynDomain] -> do + startDynamicBackends [def] $ \[dynDomain] -> do eve <- randomUser dynDomain def eClient <- objId $ bindResponse (addClient eve def) $ getJSON 201 connectUsers alice eve @@ -697,9 +642,7 @@ testLeaveConversationSuccess = do testOnUserDeletedConversations :: HasCallStack => App () testOnUserDeletedConversations = do - let overrides = - def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - startDynamicBackends [overrides] $ \[dynDomain] -> do + startDynamicBackends [def] $ \[dynDomain] -> do [ownDomain, otherDomain] <- forM [OwnDomain, OtherDomain] asString [alice, alex, bob, bart, chad] <- createAndConnectUsers [ownDomain, ownDomain, otherDomain, otherDomain, dynDomain] diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 547c6598220..36d58cbab57 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + -- | This module is meant to show how Testlib can be used module Test.Demo where @@ -9,7 +11,6 @@ import Control.Monad.Cont import GHC.Stack import SetupHelpers import Testlib.Prelude -import UnliftIO.Concurrent (threadDelay) -- | Legalhold clients cannot be deleted. testCantDeleteLHClient :: HasCallStack => App () @@ -174,22 +175,10 @@ testIndependentESIndices = do testDynamicBackendsFederation :: HasCallStack => App () testDynamicBackendsFederation = do - startDynamicBackends [def, def] $ \dynDomains -> do - [aDynDomain, anotherDynDomain] <- pure dynDomains - _ <- Internal.createFedConn anotherDynDomain (Internal.FedConn aDynDomain "full_search") - threadDelay 2_000_000 - - u1 <- randomUser aDynDomain def - u2 <- randomUser anotherDynDomain def - uid2 <- objId u2 - Internal.refreshIndex anotherDynDomain - - bindResponse (Public.searchContacts u1 (u2 %. "name") anotherDynDomain) $ \resp -> do - resp.status `shouldMatchInt` 200 - docs <- resp.json %. "documents" >>= asList - case docs of - [] -> assertFailure "Expected a non empty result, but got an empty one" - doc : _ -> doc %. "id" `shouldMatch` uid2 + startDynamicBackends [def, def] $ \[aDynDomain, anotherDynDomain] -> do + [u1, u2] <- createAndConnectUsers [aDynDomain, anotherDynDomain] + bindResponse (Public.getConnection u1 u2) assertSuccess + bindResponse (Public.getConnection u2 u1) assertSuccess testWebSockets :: HasCallStack => App () testWebSockets = do diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs index 97ec011f028..b690430146c 100644 --- a/integration/test/Test/Federation.hs +++ b/integration/test/Test/Federation.hs @@ -4,7 +4,6 @@ module Test.Federation where import API.Brig qualified as API -import API.BrigInternal qualified as API import API.Galley import Control.Lens import Control.Monad.Codensity @@ -32,18 +31,6 @@ testNotificationsForOfflineBackends = do -- except for setup and assertions. Perhaps there is a better name. runCodensity (acquireResources 1 resourcePool) $ \[downBackend] -> do (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) <- runCodensity (startDynamicBackend downBackend mempty) $ \_ -> do - -- FUTUREWORK: get rid of this once the background worker is able to listen to all queues - do - ownDomain <- make OwnDomain & asString - otherDomain <- make OtherDomain & asString - let domains = [ownDomain, otherDomain, downBackend.berDomain] - sequence_ - [ API.createFedConn x (API.FedConn y "full_search") - | x <- domains, - y <- domains, - x /= y - ] - downUser1 <- randomUser downBackend.berDomain def downUser2 <- randomUser downBackend.berDomain def downClient1 <- objId $ bindResponse (API.addClient downUser1 def) $ getJSON 201 diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index f0c7083d066..44eaec6d2d7 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -29,7 +29,6 @@ library build-depends: aeson , amqp - , async , base , containers , exceptions diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml index 9762cc70825..02a2a69851a 100644 --- a/services/background-worker/background-worker.integration.yaml +++ b/services/background-worker/background-worker.integration.yaml @@ -8,14 +8,6 @@ federatorInternal: host: 127.0.0.1 port: 8097 -galley: - host: 127.0.0.1 - port: 8085 - -brig: - host: 127.0.0.1 - port: 8082 - rabbitmq: host: 127.0.0.1 port: 5672 @@ -23,5 +15,6 @@ rabbitmq: adminPort: 15672 backendNotificationPusher: - pushBackoffMinWait: 1000 - pushBackoffMaxWait: 1000000 + pushBackoffMinWait: 1000 # 1ms + pushBackoffMaxWait: 1000000 # 1s + remotesRefreshInterval: 10000 # 10ms diff --git a/services/background-worker/default.nix b/services/background-worker/default.nix index ce67f35095a..4a6288d5097 100644 --- a/services/background-worker/default.nix +++ b/services/background-worker/default.nix @@ -5,7 +5,6 @@ { mkDerivation , aeson , amqp -, async , base , bytestring , containers @@ -51,7 +50,6 @@ mkDerivation { libraryHaskellDepends = [ aeson amqp - async base containers exceptions diff --git a/services/background-worker/src/Wire/BackendNotificationPusher.hs b/services/background-worker/src/Wire/BackendNotificationPusher.hs index f52f165dbbd..793bafd418d 100644 --- a/services/background-worker/src/Wire/BackendNotificationPusher.hs +++ b/services/background-worker/src/Wire/BackendNotificationPusher.hs @@ -11,7 +11,6 @@ import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as Text import Imports -import Network.AMQP (cancelConsumer) import Network.AMQP qualified as Q import Network.AMQP.Extended import Network.AMQP.Lifted qualified as QL @@ -21,7 +20,6 @@ import System.Logger.Class qualified as Log import UnliftIO import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Client -import Wire.API.Routes.FederationDomainConfig import Wire.BackgroundWorker.Env import Wire.BackgroundWorker.Options import Wire.BackgroundWorker.Util @@ -114,14 +112,14 @@ startPusher consumersRef chan = do -- delivered in order. markAsWorking BackendNotificationPusher lift $ Q.qos chan 0 1 False - env <- ask -- Make sure threads aren't dangling if/when this async thread is killed let cleanup :: (Exception e, MonadThrow m, MonadIO m) => e -> m () cleanup e = do consumers <- liftIO $ readIORef consumersRef - traverse_ (liftIO . cancelConsumer chan . fst) $ Map.elems consumers + traverse_ (liftIO . Q.cancelConsumer chan . fst) $ Map.elems consumers throwM e + timeBeforeNextRefresh <- asks (.backendNotificationsConfig.remotesRefreshInterval) -- If this thread is cancelled, catch the exception, kill the consumers, and carry on. -- FUTUREWORK?: -- If this throws an exception on the Chan / in the forever loop, the exception will @@ -131,26 +129,11 @@ startPusher consumersRef chan = do [ Handler $ cleanup @SomeException, Handler $ cleanup @SomeAsyncException ] + $ forever $ do - -- Get an initial set of domains from the sync thread - -- The Chan that we will be waiting on isn't initialised with a - -- value until the domain update loop runs the callback for the - -- first time. - initRemotes <- liftIO $ readIORef env.remoteDomains - -- Get an initial set of consumers for the domains pulled from the IORef - -- so that we aren't just sitting around not doing anything for a bit at - -- the start. - ensureConsumers consumersRef chan $ domain <$> initRemotes.remotes - -- Wait for updates to the domains, this is where the bulk of the action - -- is going to take place - forever $ do - -- Wait for a new set of domains. This is a blocking action - -- so we will only move past here when we get a new set of domains. - -- It is a bit nicer than having another timeout value, as Brig is - -- already providing one in the domain update message. - chanRemotes <- liftIO $ readChan env.remoteDomainsChan - -- Make new consumers for the new domains, clean up old ones from the consumer map. - ensureConsumers consumersRef chan $ domain <$> chanRemotes.remotes + remotes <- getRemoteDomains + ensureConsumers consumersRef chan remotes + threadDelay timeBeforeNextRefresh ensureConsumers :: IORef (Map Domain (Q.ConsumerTag, MVar ())) -> Q.Channel -> [Domain] -> AppT IO () ensureConsumers consumers chan domains = do @@ -161,10 +144,10 @@ ensureConsumers consumers chan domains = do traverse_ (ensureConsumer consumers chan) domains -- Loop over all of the dropped domains. These need to be cancelled as they are no longer -- on the domain list. - traverse_ (cancelConsumer' consumers chan) droppedDomains + traverse_ (cancelConsumer consumers chan) droppedDomains -cancelConsumer' :: IORef (Map Domain (Q.ConsumerTag, MVar ())) -> Q.Channel -> Domain -> AppT IO () -cancelConsumer' consumers chan domain = do +cancelConsumer :: IORef (Map Domain (Q.ConsumerTag, MVar ())) -> Q.Channel -> Domain -> AppT IO () +cancelConsumer consumers chan domain = do Log.info $ Log.msg (Log.val "Stopping consumer") . Log.field "domain" (domainText domain) -- The ' version of atomicModifyIORef is strict in the function update and is useful -- for not leaking memory. diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index 7709cb8bd52..31a9c769034 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -2,7 +2,6 @@ module Wire.BackgroundWorker where -import Control.Concurrent.Async (cancel) import Data.Domain import Data.Map.Strict qualified as Map import Data.Metrics.Servant qualified as Metrics @@ -20,17 +19,16 @@ import Wire.BackgroundWorker.Options run :: Opts -> IO () run opts = do - (env, syncThread) <- mkEnv opts + env <- mkEnv opts (notifChanRef, notifConsumersRef) <- runAppT env $ BackendNotificationPusher.startWorker opts.rabbitmq let -- cleanup will run in a new thread when the signal is caught, so we need to use IORefs and -- specific exception types to message threads to clean up l = logger env cleanup = do - cancel syncThread -- Notification pusher thread - Log.info (logger env) $ Log.msg (Log.val "Cancelling the notification pusher thread") + Log.info l $ Log.msg (Log.val "Cancelling the notification pusher thread") readIORef notifChanRef >>= traverse_ \chan -> do - Log.info (logger env) $ Log.msg (Log.val "Got channel") + Log.info l $ Log.msg (Log.val "Got channel") readIORef notifConsumersRef >>= \m -> for_ (Map.assocs m) \(domain, (consumer, runningFlag)) -> do Log.info l $ Log.msg (Log.val "Cancelling consumer") . Log.field "Domain" domain._domainText -- Remove the consumer from the channel so it isn't called again diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index 37bbaffad01..ef99676c49a 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -3,7 +3,6 @@ module Wire.BackgroundWorker.Env where -import Control.Concurrent.Async import Control.Concurrent.Chan import Control.Monad.Base import Control.Monad.Catch @@ -23,7 +22,6 @@ import System.Logger qualified as Log import System.Logger.Class (Logger, MonadLogger (..)) import System.Logger.Extended qualified as Log import Util.Options -import Wire.API.FederationUpdate import Wire.API.Routes.FederationDomainConfig import Wire.BackgroundWorker.Options @@ -42,10 +40,7 @@ data Env = Env metrics :: Metrics.Metrics, federatorInternal :: Endpoint, httpManager :: Manager, - galley :: Endpoint, - brig :: Endpoint, defederationTimeout :: ResponseTimeout, - remoteDomains :: IORef FederationDomainConfigs, remoteDomainsChan :: Chan FederationDomainConfigs, backendNotificationMetrics :: BackendNotificationMetrics, backendNotificationsConfig :: BackendNotificationsConfig, @@ -65,27 +60,19 @@ mkBackendNotificationMetrics = <*> register (vector "targetDomain" $ counter $ Prometheus.Info "wire_backend_notifications_errors" "Number of errors that occurred while pushing notifications") <*> register (vector "targetDomain" $ gauge $ Prometheus.Info "wire_backend_notifications_stuck_queues" "Set to 1 when pushing notifications is stuck") -mkEnv :: Opts -> IO (Env, Async ()) +mkEnv :: Opts -> IO Env mkEnv opts = do http2Manager <- initHttp2Manager logger <- Log.mkLogger opts.logLevel Nothing opts.logFormat httpManager <- newManager defaultManagerSettings remoteDomainsChan <- newChan let federatorInternal = opts.federatorInternal - galley = opts.galley defederationTimeout = maybe responseTimeoutNone (\t -> responseTimeoutMicro $ 1000000 * t) -- seconds to microseconds opts.defederationTimeout - brig = opts.brig rabbitmqVHost = opts.rabbitmq.vHost - callback = - SyncFedDomainConfigsCallback - { fromFedUpdateCallback = \_old new -> do - writeChan remoteDomainsChan new - } - (remoteDomains, syncThread) <- syncFedDomainConfigs brig logger callback rabbitmqAdminClient <- mkRabbitMqAdminClientEnv opts.rabbitmq statuses <- newIORef $ @@ -95,7 +82,7 @@ mkEnv opts = do metrics <- Metrics.metrics backendNotificationMetrics <- mkBackendNotificationMetrics let backendNotificationsConfig = opts.backendNotificationPusher - pure (Env {..}, syncThread) + pure Env {..} initHttp2Manager :: IO Http2Manager initHttp2Manager = do diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index 7cac93318db..da31c41255a 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -12,8 +12,6 @@ data Opts = Opts backgroundWorker :: !Endpoint, federatorInternal :: !Endpoint, rabbitmq :: !RabbitMqAdminOpts, - galley :: !Endpoint, - brig :: !Endpoint, -- | Seconds, Nothing for no timeout defederationTimeout :: Maybe Int, backendNotificationPusher :: BackendNotificationsConfig @@ -31,7 +29,10 @@ data BackendNotificationsConfig = BackendNotificationsConfig -- | Upper limit on amount of time (in microseconds) to wait before retrying -- any notification. This exists to ensure that exponential back-off doesn't -- cause wait times to be very big. - pushBackoffMaxWait :: Int + pushBackoffMaxWait :: Int, + -- | The list of remotes is refreshed at an interval. This value in + -- microseconds decides the interval for polling. + remotesRefreshInterval :: Int } deriving (Show, Generic) diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index bb37c87fce5..0aa74d531f4 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -42,7 +42,6 @@ import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Common import Wire.API.Federation.BackendNotifications import Wire.API.RawJson -import Wire.API.Routes.FederationDomainConfig import Wire.BackendNotificationPusher import Wire.BackgroundWorker.Env import Wire.BackgroundWorker.Options @@ -182,7 +181,6 @@ spec = do ] logger <- Logger.new Logger.defSettings httpManager <- newManager defaultManagerSettings - remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined @@ -191,9 +189,7 @@ spec = do rabbitmqAdminClient = mockRabbitMqAdminClient mockAdmin rabbitmqVHost = "test-vhost" defederationTimeout = responseTimeoutNone - galley = Endpoint "localhost" 8085 - brig = Endpoint "localhost" 8082 - backendNotificationsConfig = BackendNotificationsConfig 1000 500000 + backendNotificationsConfig = BackendNotificationsConfig 1000 500000 1000 backendNotificationMetrics <- mkBackendNotificationMetrics domains <- runAppT Env {..} getRemoteDomains @@ -204,7 +200,6 @@ spec = do mockAdmin <- newMockRabbitMqAdmin True ["backend-notifications.foo.example"] logger <- Logger.new Logger.defSettings httpManager <- newManager defaultManagerSettings - remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined @@ -213,9 +208,7 @@ spec = do rabbitmqAdminClient = mockRabbitMqAdminClient mockAdmin rabbitmqVHost = "test-vhost" defederationTimeout = responseTimeoutNone - galley = Endpoint "localhost" 8085 - brig = Endpoint "localhost" 8082 - backendNotificationsConfig = BackendNotificationsConfig 1000 500000 + backendNotificationsConfig = BackendNotificationsConfig 1000 500000 1000 backendNotificationMetrics <- mkBackendNotificationMetrics domainsThread <- async $ runAppT Env {..} getRemoteDomains diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index 58454c9582c..22a7c38dcef 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -7,9 +7,7 @@ import Imports import Network.HTTP.Client import System.Logger.Class qualified as Logger import Util.Options (Endpoint (..)) -import Wire.API.Routes.FederationDomainConfig -import Wire.BackgroundWorker.Env hiding (federatorInternal, galley) -import Wire.BackgroundWorker.Env qualified as E +import Wire.BackgroundWorker.Env import Wire.BackgroundWorker.Options import Wire.BackgroundWorker.Util @@ -20,16 +18,13 @@ testEnv = do statuses <- newIORef mempty backendNotificationMetrics <- mkBackendNotificationMetrics httpManager <- newManager defaultManagerSettings - remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan let federatorInternal = Endpoint "localhost" 0 rabbitmqAdminClient = undefined rabbitmqVHost = undefined metrics = undefined - galley = Endpoint "localhost" 8085 - brig = Endpoint "localhost" 8082 defederationTimeout = responseTimeoutNone - backendNotificationsConfig = BackendNotificationsConfig 1000 500000 + backendNotificationsConfig = BackendNotificationsConfig 1000 500000 1000 pure Env {..} runTestAppT :: AppT IO a -> Int -> IO a diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index cad9c16082d..ee744b29fca 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -17,7 +17,6 @@ module Federation.End2end where -import API.Search.Util import API.User.Util import Bilge import Bilge.Assert ((!!!), (+ +------->+ +--------->+ | -- +------+ +-+-------+ +---------+ +------+ -testHandleLookup :: Brig -> Brig -> Http () -testHandleLookup brig brigTwo = do - -- Create a user on the "other side" using an internal brig endpoint from a - -- second brig instance in backendTwo (in another namespace in kubernetes) - (handle, userBrigTwo) <- createUserWithHandle brigTwo - -- Get result from brig two for comparison - let domain = qDomain $ userQualifiedId userBrigTwo - resultViaBrigTwo <- getUserInfoFromHandle brigTwo domain handle - - -- query the local-namespace brig for a user sitting on the other backend - -- (which will exercise the network traffic via two federators to the remote brig) - resultViaBrigOne <- getUserInfoFromHandle brig domain handle - - liftIO $ assertEqual "remote handle lookup via federator should work in the happy case" (profileQualifiedId resultViaBrigOne) (userQualifiedId userBrigTwo) - liftIO $ assertEqual "querying brig1 or brig2 about the same user should give same result" resultViaBrigTwo resultViaBrigOne - -testSearchUsers :: Brig -> Brig -> Http () -testSearchUsers brig brigTwo = do - -- Create a user on the "other side" using an internal brig endpoint from a - -- second brig instance in backendTwo (in another namespace in kubernetes) - (handle, userBrigTwo) <- createUserWithHandle brigTwo - - searcher <- userId <$> randomUser brig - let expectedUserId = userQualifiedId userBrigTwo - searchTerm = fromHandle handle - domain = qDomain expectedUserId - liftIO $ putStrLn "search for user on brigTwo (directly)..." - assertCanFindWithDomain brigTwo searcher expectedUserId searchTerm domain - - -- exercises multi-backend network traffic - liftIO $ putStrLn "search for user on brigOne via federators to remote brig..." - assertCanFindWithDomain brig searcher expectedUserId searchTerm domain - testGetUsersById :: Brig -> Brig -> Http () testGetUsersById brig1 brig2 = do users <- traverse randomUser [brig1, brig2] From cfba3175624bd78507cd72f71eed660af739d02a Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 26 Sep 2023 14:12:24 +0200 Subject: [PATCH 145/225] WPB-4835 list-clients endpoint returns with partial success even if one of the remote backends is unreachable (#3611) --- changelog.d/3-bug-fixes/WPB-4835 | 1 + integration/test/API/Brig.hs | 6 +++ integration/test/Test/Client.hs | 40 ++++++++++++++++++- .../src/Wire/API/Routes/Public/Brig.hs | 1 + services/brig/src/Brig/API/Client.hs | 20 +++++++--- 5 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-4835 diff --git a/changelog.d/3-bug-fixes/WPB-4835 b/changelog.d/3-bug-fixes/WPB-4835 new file mode 100644 index 00000000000..148ded40f27 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-4835 @@ -0,0 +1 @@ +list-clients returns with partial success even if one of the remote backends is unreachable diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index abad2be9a11..310a44c7c66 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -167,6 +167,12 @@ getClientsQualified user domain otherUser = do <> "/clients" submit "GET" req +listUsersClients :: (HasCallStack, MakesValue user, MakesValue qualifiedUserIds) => user -> [qualifiedUserIds] -> App Response +listUsersClients usr qualifiedUserIds = do + qUsers <- mapM objQidObject qualifiedUserIds + req <- baseRequest usr Brig Versioned $ joinHttpPath ["users", "list-clients"] + submit "POST" (req & addJSONObject ["qualified_users" .= qUsers]) + searchContacts :: ( MakesValue user, MakesValue searchTerm, diff --git a/integration/test/Test/Client.hs b/integration/test/Test/Client.hs index a14de01a24a..4ee88901419 100644 --- a/integration/test/Test/Client.hs +++ b/integration/test/Test/Client.hs @@ -1,13 +1,21 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + module Test.Client where import API.Brig +import API.Brig qualified as API import API.Gundeck -import Data.Aeson +import Control.Lens hiding ((.=)) +import Control.Monad.Codensity +import Control.Monad.Reader +import Data.Aeson hiding ((.=)) +import Data.ProtoLens.Labels () import Data.Time.Clock.POSIX import Data.Time.Clock.System import Data.Time.Format import SetupHelpers import Testlib.Prelude +import Testlib.ResourcePool testClientLastActive :: HasCallStack => App () testClientLastActive = do @@ -32,3 +40,33 @@ testClientLastActive = do . utcTimeToPOSIXSeconds <$> parseTimeM False defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" tm1 assertBool "last_active is earlier than expected" $ ts1 >= now + +testListClientsIfBackendIsOffline :: HasCallStack => App () +testListClientsIfBackendIsOffline = do + resourcePool <- asks (.resourcePool) + ownDomain <- asString OwnDomain + otherDomain <- asString OtherDomain + [ownUser1, ownUser2] <- createAndConnectUsers [OwnDomain, OtherDomain] + ownClient1 <- objId $ bindResponse (API.addClient ownUser1 def) $ getJSON 201 + ownClient2 <- objId $ bindResponse (API.addClient ownUser2 def) $ getJSON 201 + ownUser1Id <- objId ownUser1 + ownUser2Id <- objId ownUser2 + + let expectedResponse = + object + [ ownDomain .= object [ownUser1Id .= [object ["id" .= ownClient1]]], + otherDomain .= object [ownUser2Id .= [object ["id" .= ownClient2]]] + ] + + bindResponse (listUsersClients ownUser1 [ownUser1, ownUser2]) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "qualified_user_map" `shouldMatch` expectedResponse + + -- we don't even have to start the backend, but we have to take the resource so that it doesn't get started by another test + runCodensity (acquireResources 1 resourcePool) $ \[downBackend] -> do + rndUsrId <- randomId + let downUser = (object ["domain" .= downBackend.berDomain, "id" .= rndUsrId]) + + bindResponse (listUsersClients ownUser1 [ownUser1, ownUser2, downUser]) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "qualified_user_map" `shouldMatch` expectedResponse diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index b8b22b88a33..42080cbd4b9 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -920,6 +920,7 @@ type ClientAPI = :<|> Named "list-clients-bulk@v2" ( Summary "List all clients for a set of user ids" + :> Description "If a backend is unreachable, the clients from that backend will be omitted from the response" :> From 'V2 :> MakesFederatedCall 'Brig "get-user-clients" :> ZUser diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 0d91a678f08..a3317223af0 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -78,10 +78,10 @@ import Control.Lens (view) import Data.ByteString.Conversion import Data.Code as Code import Data.Domain +import Data.Either.Extra (mapLeft) import Data.IP (IP) import Data.Id (ClientId, ConnId, UserId) import Data.List.Split (chunksOf) -import Data.Map.Strict (traverseWithKey) import Data.Map.Strict qualified as Map import Data.Misc (PlainTextPassword6) import Data.Qualified @@ -133,13 +133,21 @@ lookupPubClientsBulk :: [Qualified UserId] -> ExceptT ClientError (AppT r) (Qual lookupPubClientsBulk qualifiedUids = do loc <- qualifyLocal () let (localUsers, remoteUsers) = partitionQualified loc qualifiedUids - remoteUserClientMap <- - traverseWithKey - (\domain' uids -> getUserClients domain' (GetUserClients uids)) - (indexQualified (fmap tUntagged remoteUsers)) - !>> ClientFederationError + remoteUserClientMap <- lift $ getRemoteClients $ indexQualified (fmap tUntagged remoteUsers) localUserClientMap <- Map.singleton (tDomain loc) <$> lookupLocalPubClientsBulk localUsers pure $ QualifiedUserMap (Map.union localUserClientMap remoteUserClientMap) + where + getRemoteClients :: Map Domain [UserId] -> AppT r (Map Domain (UserMap (Set PubClient))) + getRemoteClients uids = do + results <- + traverse + (\(d, ids) -> mapLeft (const d) . fmap (d,) <$> runExceptT (getUserClients d (GetUserClients ids))) + (Map.toList uids) + forM_ (lefts results) $ \d -> + Log.warn $ + field "remote_domain" (domainText d) + ~~ msg (val "Failed to fetch clients for domain") + pure $ Map.fromList (rights results) lookupLocalPubClientsBulk :: [UserId] -> ExceptT ClientError (AppT r) (UserMap (Set PubClient)) lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk From dbfc6c1dc76c94c97e6514024d243815c58c4879 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 26 Sep 2023 18:33:47 +0200 Subject: [PATCH 146/225] Remove flaky-tests.yaml and script (#3615) --- flaky-tests.yaml | 71 ------------- hack/bin/flaky_tests.py | 224 ---------------------------------------- 2 files changed, 295 deletions(-) delete mode 100644 flaky-tests.yaml delete mode 100755 hack/bin/flaky_tests.py diff --git a/flaky-tests.yaml b/flaky-tests.yaml deleted file mode 100644 index 6c461ec71ea..00000000000 --- a/flaky-tests.yaml +++ /dev/null @@ -1,71 +0,0 @@ -- - test_name: no extra results.new-index - comments: | - I think this is a known flake (Stefan) - -- - test_name: "team tests around truncation limits - no events, too large team" - comments: | - Exception: Timeout: No matching notification received. - Match failure: expected: Just "user.delete" - but got: Just "conversation.delete" - - 2023-03-24: The test has multiple "user.delete" assertions, but since there - is a "conversation.delete" event I think it must be the one - generated by the team deletion. This would also explain why it - might take longer for the event to happen than in other cases. - I've increased the timeout from 4 to 7 seconds for that one - assertion. Let's see if this helps. - -- test_name: "GET /mls/key-packages/claim/local/:user - self claim" - comments: | - /bin/sh: createProcess: posix_spawnp: failed (Bad address) - This is probably to resource exhaustion. - Can we try increasing limits? - -- - test_name: "Brig API Integration.MLS.GET /mls/key-packages/claim/local/:user" - comments: | - Same error as for teset "GET /mls/key-packages/claim/local/:user - self claim" - - Error executing request: /bin/sh: createProcess: posix_spawnp: failed (Bad address) - -- - test_name: "max active tokens" - comments: | - CallStack (from HasCallStack): - assertFailure, called at ./Test/Tasty/HUnit/Orig.hs:71:30 in tasty-hunit-0.10.0.3-BA9Dg64ujOjHrKq3kYOvGI:Test.Tasty.HUnit.Orig - assertBool, called at test/integration/API/OAuth.hs:473:16 in main:API.OAuth - Use -p '(!/turn/&&!/user.auth.cookies.limit/)&&/max active tokens/' to rerun this test only. - -- - test_name: "delete team conversation" - comments: | - Exception: unexpected notification received - Match failure: expected: no notification - but got: "conversation.create" - - 2023-03-24: The test is not waiting for notifications triggered by creating - conversations. When later asserting that no notification is sent - for deleting conversations, the test fails under load when the - create conversation notification happens to come in late. This - is fixed by waiting for all previous notifications before - asserting the absence of (new) notifications. - -- - test_name: "POST /federation/on-user-deleted-conversations : Remove deleted remote user from local conversations" - comments: | - Exception: unexpected notification received - Match failure: expected: no notification - but got: "conversation.create" - - 2023-03-24: The test is not waiting for notifications triggered by creating - conversations. When later asserting that no notification is sent - for deleting user, the test fails under load when the create - conversation notification happens to come in late. This is fixed - by waiting for all previous notifications before asserting the - absence of (new) notifications. -- - test_name: "POST /register - can add team members above fanout limit when whitelisting is enabled" - comments: | - 2023-03-27: Hopefully fix by increasing timeout from 10 to 15 seconds for this test. diff --git a/hack/bin/flaky_tests.py b/hack/bin/flaky_tests.py deleted file mode 100755 index 84bc8ad861e..00000000000 --- a/hack/bin/flaky_tests.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python3 - -import datetime -import requests -import json -import os -import yaml -import argparse -from pydoc import pager - -BUCKET_BASEURL = 'https://s3.eu-west-1.amazonaws.com/public.wire.com/ci/failing-tests' -CONCOURSE_BASEURL = 'https://concourse.ops.zinfra.io/teams/main' -CONCOURSE_LOG_RETENTION_DAYS = 7 - -class Colors: - GREEN = "\x1b[38;5;10m" - YELLOW = "\x1b[38;5;11m" - BLUE = "\x1b[38;5;6m" - PURPLEISH = "\x1b[38;5;13m" - ORANGE = "\x1b[38;5;3m" - RED = "\x1b[38;5;1m" - RESET = "\x1b[0m" - -def read_flaky_tests(): - project_root = os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir) - result = {} - with open(os.path.join(project_root, 'flaky-tests.yaml'), 'r') as f: - d = yaml.safe_load(f) - for item in d: - result[item['test_name']] = item['comments'] - return result - -def current_week_start(now): - k = now.weekday() - return (now - datetime.timedelta(days=k)).replace(hour=0, minute=0, second=0, microsecond=0) - -def format_date(dt): - return dt.strftime('%Y-%m-%d') - -def failing_tests_fn(week_start): - return format_date(week_start) + '_failing_tests.json' - -def fetch_week(week_start): - url = f'{BUCKET_BASEURL}/{failing_tests_fn(week_start)}' - r = requests.get(url) - result = [] - if r.status_code == 200: - for line in r.content.split(b'\n'): - if len(line) > 0: - item = json.loads(line.decode('utf8')) - result.append(item) - return result - -def fetch(today, n_weeks): - ws_start = current_week_start(today) - data = [] - for i in range(n_weeks): - ws = ws_start + datetime.timedelta(days=-7*i) - print(f'\rFetching {i+1}/{n_weeks} {format_date(ws)}') - data += fetch_week(ws) - print() - return data - -def tests_match(s1, s2): - return s1 in s2 or s2 in s1 - -def longer(s1, s2): - if len(s1) > len(s2): - return s1 - else: - return s2 - -def is_flake(item): - b = item['build'] - return ((b['pipeline_name'] == 'mls' and b['job_name'] == 'test') \ - or (b['pipeline_name'] == 'staging' and b['job_name'] == 'test') \ - or (b['pipeline_name'] == 'prod' and b['job_name'] == 'test')) - - -def add_flake(flake_set, test_name): - for k in flake_set: - if tests_match(k, test_name): - k_ = longer(k, test_name) - if k_ != k: - flake_set.remove(k) - flake_set.add(k_) - return - flake_set.add(test_name) - -def discover_flakes(data): - flake_set = set() - for item in data: - if is_flake(item): - add_flake(flake_set, item['test_name']) - return flake_set - -def search_matching_flake(test_names, test_name): - for flake in test_names: - if tests_match(flake, test_name): - return flake - -def associate_fails(test_names, data, default_comment=''): - d = {k: [] for k in test_names} - unassociated = [] - for item in data: - flake = search_matching_flake(test_names, item['test_name']) - if flake: - d[flake].append(item) - else: - unassociated.append(item) - - return [{'test_name': k, 'fails': v, 'comments': default_comment} for k, v in d.items()], unassociated - -def associate_comments(flakes, comments, default_comments=''): - for flake in flakes: - flake['comments'] = comments.get(flake['test_name'], default_comments) - -def sort_flakes(flakes): - flakes.sort(key=lambda f: (-len(f['fails']), f['test_name']), reverse=False) - for flake in flakes: - flake['fails'].sort(key=lambda f: f['build']['end_time'], reverse=True) - -def humanize_days(n): - if n < 7: - if n == 0: - return 'today' - elif n == 1: - return 'yesterday' - else: - return f'{n} days ago' - else: - weeks = n // 7 - if weeks < 4: - return f'{weeks} week{"s" if weeks >= 2 else ""} ago' - else: - return f'{weeks // 4} month{"s" if weeks >= 8 else ""} ago' - -def human_format_date(dt, today): - days = (today - dt).days - return Colors.BLUE + f'· {format_date(dt)} ({humanize_days(days)})' + Colors.RESET - -def create_url(build): - return CONCOURSE_BASEURL + f"/pipelines/{build['pipeline_name']}/jobs/{build['job_name']}/builds/{build['name']}" - -def pretty_flake(flake, today, logs=False): - lines = [] - lines.append(Colors.YELLOW + f"❄ \"{flake['test_name']}\"" + Colors.RESET) - - if not logs: - lines.append(f' Run with --logs "{flake["test_name"]}" to see error logs') - - comments = flake['comments'] - if comments: - for l in comments.splitlines(): - lines.append(' ' + Colors.PURPLEISH + l + Colors.RESET) - lines.append('') - for fail in flake['fails']: - b = fail['build'] - - end_time = datetime.datetime.fromtimestamp(b['end_time']) - s = human_format_date(end_time, today) - if (today - end_time) < datetime.timedelta(days=CONCOURSE_LOG_RETENTION_DAYS): - url = create_url(b) - s = s + ' ' + url - lines.append(' ' + s) - if logs: - lines.append('') - for l in fail['context'].splitlines(): - lines.append(' ' + l) - lines.append('') - - return "\n".join(lines) + '\n' - -def pretty_flakes(flakes, today, logs=False): - lines = [] - for flake in flakes: - lines.append(pretty_flake(flake, today, logs)) - return '\n'.join(lines) - -explain = '''Tips: - Run with --discover to manually discover new flaky tests - -''' - -def main(): - parser = argparse.ArgumentParser(prog='flaky_test.py', description='Shows flaky tests') - parser.add_argument('-d', '--discover', action='store_true', help='Show failing tests that are not marked/discovered as being flaky. Use this to manually discover flaky test.') - parser.add_argument('-l', '--logs', help='Show surrounding logs for given test') - args = parser.parse_args() - - today = datetime.datetime.now() - data = fetch(today=today, n_weeks=4*4) - if args.logs: - flakes, unassociated = associate_fails([args.logs], data) - sort_flakes(flakes) - pager(explain + pretty_flakes(flakes, today, logs=True)) - return - - test_names = discover_flakes(data) - flaky_tests_comments = read_flaky_tests() - test_names = test_names.union(flaky_tests_comments.keys()) - flakes, unassociated = associate_fails(test_names, data) - - - if args.discover: - - test_names = set([i['test_name'] for i in unassociated]) - flake_candidates, _ = associate_fails(test_names, unassociated, '(if this is a flaky test, please add it to flaky-tests.yaml)') - sort_flakes(flake_candidates) - pager(explain + pretty_flakes(flake_candidates, today)) - - else: - associate_comments(flakes, flaky_tests_comments, '(discovered flake, please check and add it to flaky-tests.yaml)') - sort_flakes(flakes) - pager(explain + pretty_flakes(flakes, today)) - - -def test(): - data = fetch(1) - return data - - -if __name__ == '__main__': - main() From eee936ee74cc210247151966fde951760b5e7b29 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 27 Sep 2023 08:57:42 +0200 Subject: [PATCH 147/225] Cleanup error logs on shutdown (#3592) * Do not log normal RabbitMQ exceptions * Do not log async cancelled exception in gundeck * Fix nesting of codensity actions * Add CHANGELOG entry --- changelog.d/5-internal/shutdown-cleanup | 1 + integration/test/Testlib/RunServices.hs | 18 +++++++++--------- libs/extended/src/Network/AMQP/Extended.hs | 5 +++-- services/gundeck/src/Gundeck/Monad.hs | 12 ++++++++---- 4 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 changelog.d/5-internal/shutdown-cleanup diff --git a/changelog.d/5-internal/shutdown-cleanup b/changelog.d/5-internal/shutdown-cleanup new file mode 100644 index 00000000000..86579b0906b --- /dev/null +++ b/changelog.d/5-internal/shutdown-cleanup @@ -0,0 +1 @@ +Avoid unnecessary error logs on service shutdown diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index 9e07814aa53..a2b774f5c5e 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -3,7 +3,7 @@ module Testlib.RunServices where import Control.Concurrent -import Control.Monad.Codensity (lowerCodensity) +import Control.Monad.Codensity import System.Directory import System.Environment (getArgs) import System.Exit (exitWith) @@ -44,7 +44,6 @@ main = do pure $ joinPath [projectRoot, "services/integration.yaml"] genv <- createGlobalEnv cfg - env <- lowerCodensity $ mkEnv genv args <- getArgs @@ -57,10 +56,11 @@ main = do (_, _, _, ph) <- createProcess cp exitWith =<< waitForProcess ph - runAppWithEnv env $ do - lowerCodensity $ do - _modifyEnv <- - traverseConcurrentlyCodensity - (`startDynamicBackend` def) - [backendA, backendB] - liftIO run + runCodensity (mkEnv genv) $ \env -> + runAppWithEnv env $ + lowerCodensity $ do + _modifyEnv <- + traverseConcurrentlyCodensity + (`startDynamicBackend` def) + [backendA, backendB] + liftIO run diff --git a/libs/extended/src/Network/AMQP/Extended.hs b/libs/extended/src/Network/AMQP/Extended.hs index f2c98a84a03..502cdb95a77 100644 --- a/libs/extended/src/Network/AMQP/Extended.hs +++ b/libs/extended/src/Network/AMQP/Extended.hs @@ -144,7 +144,6 @@ openConnectionWithRetries l RabbitMqOpts {..} hooks = do chanExceptionHandler :: Q.Connection -> SomeException -> m () chanExceptionHandler conn e = do - logException l "RabbitMQ channel closed" e hooks.onChannelException e `catch` logException l "onChannelException hook threw an exception" case (Q.isNormalChannelClose e, fromException e) of (True, _) -> @@ -153,7 +152,9 @@ openConnectionWithRetries l RabbitMqOpts {..} hooks = do (_, Just (Q.ConnectionClosedException {})) -> Log.info l $ Log.msg (Log.val "RabbitMQ connection is closed, not attempting to reopen channel") - _ -> openChan conn + _ -> do + logException l "RabbitMQ channel closed" e + openChan conn logException :: (MonadIO m) => Logger -> String -> SomeException -> m () logException l m (SomeException e) = do diff --git a/services/gundeck/src/Gundeck/Monad.hs b/services/gundeck/src/Gundeck/Monad.hs index f3a3a9512b3..7995b71b17f 100644 --- a/services/gundeck/src/Gundeck/Monad.hs +++ b/services/gundeck/src/Gundeck/Monad.hs @@ -45,6 +45,7 @@ where import Bilge hiding (Request, header, options, statusCode) import Bilge.RPC import Cassandra +import Control.Concurrent.Async (AsyncCancelled) import Control.Error import Control.Exception (throwIO) import Control.Lens @@ -170,10 +171,13 @@ runDirect :: Env -> Gundeck a -> IO a runDirect e m = runClient (e ^. cstate) (runReaderT (unGundeck m) e) `catch` ( \(exception :: SomeException) -> do - Log.err (e ^. applog) $ - Log.msg ("IO Exception occurred" :: ByteString) - . Log.field "message" (displayException exception) - . Log.field "request" (unRequestId (e ^. reqId)) + case fromException exception of + Nothing -> + Log.err (e ^. applog) $ + Log.msg ("IO Exception occurred" :: ByteString) + . Log.field "message" (displayException exception) + . Log.field "request" (unRequestId (e ^. reqId)) + Just (_ :: AsyncCancelled) -> pure () throwIO exception ) From a4c92807ff5bc058f993e2918b18313e1ea4dfbe Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 24 Aug 2023 13:40:42 +0200 Subject: [PATCH 148/225] Support post-quantum MLS ciphersuite (#3454) * Add hybrid Kyber ciphersuite * Fix ciphersuite tag parser * Change conversation ciphersuite on first commit * Save MLS keystore as part of a client's state * Move invalid epoch test to new suite * Rewrite unsupported proposal test Instead of crafting an unsupported proposal in the haskell code, we now use mls-test-cli to create a `ReInit` proposal. * Forbid bare proposals at epoch 0 * Do not crash on invalid subconversation entries * Index key packages by ciphersuite * Add ciphersuite parameter to some tests * Test key packages with multiple ciphersuites * Test key package upload with unsupported CS * Test commit with unsupported ciphersuite * Update mls-test-cli to pq branch * Add CHANGELOG entries * Remove unused dependency * Use `ToHttpApiData` instance of CipherSuite When constructing the query parameter corresponding to a ciphersuite in an internal API, use the `ToHttpApiData` instance instead of converting the ciphersuite into a bytestring manually. * Linter fixes --- cassandra-schema.cql | 3 +- .../mls-key-package-ciphersuites | 1 + changelog.d/2-features/mls-ciphersuites | 1 + integration/test/API/Brig.hs | 18 +- integration/test/MLS/Util.hs | 100 ++++++---- integration/test/Test/MLS.hs | 168 +++++++++++++++-- integration/test/Test/MLS/KeyPackage.hs | 39 +++- integration/test/Testlib/Env.hs | 27 ++- integration/test/Testlib/PTest.hs | 8 + .../src/Wire/API/Federation/API/Brig.hs | 6 +- libs/wire-api/src/Wire/API/MLS/CipherSuite.hs | 58 +++++- libs/wire-api/src/Wire/API/MLS/Validation.hs | 9 +- .../src/Wire/API/Routes/Internal/Brig.hs | 6 +- .../src/Wire/API/Routes/Public/Brig.hs | 31 +-- nix/pkgs/mls-test-cli/default.nix | 10 +- services/brig/brig.cabal | 2 + services/brig/schema/src/Run.hs | 4 +- .../schema/src/V80_KeyPackageCiphersuite.hs | 50 +++++ services/brig/src/Brig/API/Federation.hs | 6 +- services/brig/src/Brig/API/Internal.hs | 13 +- services/brig/src/Brig/API/MLS/CipherSuite.hs | 29 +++ services/brig/src/Brig/API/MLS/KeyPackages.hs | 38 ++-- .../Brig/API/MLS/KeyPackages/Validation.hs | 4 +- services/brig/src/Brig/API/Public.hs | 2 +- services/brig/src/Brig/Data/Instances.hs | 11 ++ services/brig/src/Brig/Data/MLS/KeyPackage.hs | 51 ++--- .../brig/test/integration/API/Federation.hs | 11 +- .../brig/test/integration/API/Internal.hs | 27 ++- services/brig/test/integration/API/MLS.hs | 2 +- services/galley/default.nix | 1 - services/galley/galley.cabal | 1 - .../galley/src/Galley/API/MLS/Commit/Core.hs | 8 +- .../Galley/API/MLS/Commit/InternalCommit.hs | 5 +- .../src/Galley/API/MLS/IncomingMessage.hs | 4 +- services/galley/src/Galley/API/MLS/Message.hs | 25 ++- .../galley/src/Galley/API/MLS/Proposal.hs | 31 +-- services/galley/src/Galley/API/MLS/Types.hs | 14 +- .../src/Galley/Cassandra/Conversation.hs | 8 + .../galley/src/Galley/Cassandra/Queries.hs | 8 +- .../src/Galley/Cassandra/SubConversation.hs | 48 +++-- .../galley/src/Galley/Effects/BrigAccess.hs | 2 +- .../src/Galley/Effects/ConversationStore.hs | 2 + .../Galley/Effects/SubConversationStore.hs | 1 + services/galley/src/Galley/Intra/Client.hs | 11 +- services/galley/test/integration/API/MLS.hs | 178 +----------------- .../galley/test/integration/API/MLS/Util.hs | 1 + 46 files changed, 704 insertions(+), 379 deletions(-) create mode 100644 changelog.d/1-api-changes/mls-key-package-ciphersuites create mode 100644 changelog.d/2-features/mls-ciphersuites create mode 100644 services/brig/schema/src/V80_KeyPackageCiphersuite.hs create mode 100644 services/brig/src/Brig/API/MLS/CipherSuite.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index bd93b6c0abf..d300556af70 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -222,9 +222,10 @@ CREATE TABLE brig_test.user_cookies ( CREATE TABLE brig_test.mls_key_packages ( user uuid, client text, + cipher_suite int, ref blob, data blob, - PRIMARY KEY ((user, client), ref) + PRIMARY KEY ((user, client, cipher_suite), ref) ) WITH CLUSTERING ORDER BY (ref ASC) AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} diff --git a/changelog.d/1-api-changes/mls-key-package-ciphersuites b/changelog.d/1-api-changes/mls-key-package-ciphersuites new file mode 100644 index 00000000000..9ed10cbd19f --- /dev/null +++ b/changelog.d/1-api-changes/mls-key-package-ciphersuites @@ -0,0 +1 @@ +The key package API has gained a `ciphersuite` query parameter, which should be the hexadecimal value of an MLS ciphersuite, defaulting to `0x0001`. The `ciphersuite` parameter is used by the claim and count endpoints. For uploads, the API is unchanged, and the ciphersuite is taken directly from the uploaded key package. diff --git a/changelog.d/2-features/mls-ciphersuites b/changelog.d/2-features/mls-ciphersuites new file mode 100644 index 00000000000..7886487e8bb --- /dev/null +++ b/changelog.d/2-features/mls-ciphersuites @@ -0,0 +1 @@ +Added support for post-quantum ciphersuite 0xf031. Correspondingly, MLS groups with a non-default ciphersuite are now supported. The first commit in a group determines the group ciphersuite. diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 91206de95e5..6a780e19ff4 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -254,18 +254,22 @@ uploadKeyPackage cid kp = do & addJSONObject ["key_packages" .= [T.decodeUtf8 (Base64.encode kp)]] ) -claimKeyPackages :: (MakesValue u, MakesValue v) => u -> v -> App Response -claimKeyPackages u v = do +claimKeyPackages :: (MakesValue u, MakesValue v) => Ciphersuite -> u -> v -> App Response +claimKeyPackages suite u v = do (targetDom, targetUid) <- objQid v req <- baseRequest u Brig Versioned $ "/mls/key-packages/claim/" <> targetDom <> "/" <> targetUid - submit "POST" req + submit "POST" $ + req + & addQueryParams [("ciphersuite", suite.code)] -countKeyPackages :: ClientIdentity -> App Response -countKeyPackages cid = do - baseRequest cid Brig Versioned ("/mls/key-packages/self/" <> cid.client <> "/count") - >>= submit "GET" +countKeyPackages :: Ciphersuite -> ClientIdentity -> App Response +countKeyPackages suite cid = do + req <- baseRequest cid Brig Versioned ("/mls/key-packages/self/" <> cid.client <> "/count") + submit "GET" $ + req + & addQueryParams [("ciphersuite", suite.code)] deleteKeyPackages :: ClientIdentity -> [String] -> App Response deleteKeyPackages cid kps = do diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 9d25380041d..969e954f6ed 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -76,20 +76,17 @@ randomFileName = do mlscli :: HasCallStack => ClientIdentity -> [String] -> Maybe ByteString -> App ByteString mlscli cid args mbstdin = do - bd <- getBaseDir - let cdir = bd cid2Str cid - groupOut <- randomFileName let substOut = argSubst "" groupOut - hasState <- hasClientGroupState cid - substIn <- - if hasState - then do - gs <- getClientGroupState cid - fn <- toRandomFile gs - pure (argSubst "" fn) - else pure id + gs <- getClientGroupState cid + + substIn <- case gs.group of + Nothing -> pure id + Just groupData -> do + fn <- toRandomFile groupData + pure (argSubst "" fn) + store <- maybe randomFileName toRandomFile gs.keystore let args' = map (substIn . substOut) args for_ args' $ \arg -> @@ -100,16 +97,25 @@ mlscli cid args mbstdin = do spawn ( proc "mls-test-cli" - ( ["--store", cdir "store"] + ( ["--store", store] <> args' ) ) mbstdin - groupOutWritten <- liftIO $ doesFileExist groupOut - when groupOutWritten $ do - gs <- liftIO (BS.readFile groupOut) - setClientGroupState cid gs + setGroup <- do + groupOutWritten <- liftIO $ doesFileExist groupOut + if groupOutWritten + then do + groupData <- liftIO (BS.readFile groupOut) + pure $ \x -> x {group = Just groupData} + else pure id + setStore <- do + storeData <- liftIO (BS.readFile store) + pure $ \x -> x {keystore = Just storeData} + + setClientGroupState cid ((setGroup . setStore) gs) + pure out argSubst :: String -> String -> String -> String @@ -125,8 +131,9 @@ createWireClient u = do initMLSClient :: HasCallStack => ClientIdentity -> App () initMLSClient cid = do bd <- getBaseDir + mls <- getMLSState liftIO $ createDirectory (bd cid2Str cid) - void $ mlscli cid ["init", cid2Str cid] Nothing + void $ mlscli cid ["init", "--ciphersuite", mls.ciphersuite.code, cid2Str cid] Nothing -- | Create new mls client and register with backend. createMLSClient :: (MakesValue u, HasCallStack) => u -> App ClientIdentity @@ -160,7 +167,8 @@ uploadNewKeyPackage cid = do generateKeyPackage :: HasCallStack => ClientIdentity -> App (ByteString, String) generateKeyPackage cid = do - kp <- mlscli cid ["key-package", "create"] Nothing + mls <- getMLSState + kp <- mlscli cid ["key-package", "create", "--ciphersuite", mls.ciphersuite.code] Nothing ref <- B8.unpack . Base64.encode <$> mlscli cid ["key-package", "ref", "-"] (Just kp) fp <- keyPackageFile cid ref liftIO $ BS.writeFile fp kp @@ -217,17 +225,21 @@ resetGroup cid conv = do resetClientGroup :: ClientIdentity -> String -> App () resetClientGroup cid gid = do removalKeyPath <- asks (.removalKeyPath) - groupJSON <- + mls <- getMLSState + void $ mlscli cid [ "group", "create", "--removal-key", removalKeyPath, + "--group-out", + "", + "--ciphersuite", + mls.ciphersuite.code, gid ] Nothing - setClientGroupState cid groupJSON keyPackageFile :: HasCallStack => ClientIdentity -> String -> App FilePath keyPackageFile cid ref = do @@ -260,8 +272,9 @@ unbundleKeyPackages bundle = do -- group to the previous state by using an older version of the group file. createAddCommit :: HasCallStack => ClientIdentity -> [Value] -> App MessagePackage createAddCommit cid users = do + mls <- getMLSState kps <- fmap concat . for users $ \user -> do - bundle <- claimKeyPackages cid user >>= getJSON 200 + bundle <- claimKeyPackages mls.ciphersuite cid user >>= getJSON 200 unbundleKeyPackages bundle createAddCommitWithKeyPackages cid kps @@ -325,7 +338,10 @@ createRemoveCommit cid targets = do welcomeFile <- liftIO $ emptyTempFile bd "welcome" giFile <- liftIO $ emptyTempFile bd "gi" - groupStateMap <- Map.fromList <$> (getClientGroupState cid >>= readGroupState) + groupStateMap <- do + gs <- getClientGroupState cid + groupData <- assertJust "Group state not initialised" gs.group + Map.fromList <$> readGroupState groupData let indices = map (fromMaybe (error "could not find target") . flip Map.lookup groupStateMap) targets commit <- @@ -359,10 +375,26 @@ createRemoveCommit cid targets = do createAddProposals :: HasCallStack => ClientIdentity -> [Value] -> App [MessagePackage] createAddProposals cid users = do - bundles <- for users $ (claimKeyPackages cid >=> getJSON 200) + mls <- getMLSState + bundles <- for users $ (claimKeyPackages mls.ciphersuite cid >=> getJSON 200) kps <- concat <$> traverse unbundleKeyPackages bundles traverse (createAddProposalWithKeyPackage cid) kps +createReInitProposal :: HasCallStack => ClientIdentity -> App MessagePackage +createReInitProposal cid = do + prop <- + mlscli + cid + ["proposal", "--group-in", "", "--group-out", "", "re-init"] + Nothing + pure + MessagePackage + { sender = cid, + message = prop, + welcome = Nothing, + groupInfo = Nothing + } + createAddProposalWithKeyPackage :: ClientIdentity -> (ClientIdentity, ByteString) -> @@ -503,8 +535,10 @@ consumeWelcome :: HasCallStack => ByteString -> App () consumeWelcome welcome = do mls <- getMLSState for_ mls.newMembers $ \cid -> do - hasState <- hasClientGroupState cid - assertBool "Existing clients in a conversation should not consume welcomes" (not hasState) + gs <- getClientGroupState cid + assertBool + "Existing clients in a conversation should not consume welcomes" + (isNothing gs.group) void $ mlscli cid @@ -546,19 +580,12 @@ spawn cp minput = do (Just out, ExitSuccess) -> pure out _ -> assertFailure "Failed spawning process" -hasClientGroupState :: HasCallStack => ClientIdentity -> App Bool -hasClientGroupState cid = do - mls <- getMLSState - pure $ Map.member cid mls.clientGroupState - -getClientGroupState :: HasCallStack => ClientIdentity -> App ByteString +getClientGroupState :: HasCallStack => ClientIdentity -> App ClientGroupState getClientGroupState cid = do mls <- getMLSState - case Map.lookup cid mls.clientGroupState of - Nothing -> assertFailure ("Attempted to get non-existing group state for client " <> cid2Str cid) - Just g -> pure g + pure $ Map.findWithDefault emptyClientGroupState cid mls.clientGroupState -setClientGroupState :: HasCallStack => ClientIdentity -> ByteString -> App () +setClientGroupState :: HasCallStack => ClientIdentity -> ClientGroupState -> App () setClientGroupState cid g = modifyMLSState $ \s -> s {clientGroupState = Map.insert cid g (clientGroupState s)} @@ -607,3 +634,6 @@ createApplicationMessage cid messageContent = do welcome = Nothing, groupInfo = Nothing } + +setMLSCiphersuite :: Ciphersuite -> App () +setMLSCiphersuite suite = modifyMLSState $ \mls -> mls {ciphersuite = suite} diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 2d333f40876..bbab5479dcd 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -203,7 +203,7 @@ testMixedProtocolAddPartialClients secondDomain = do -- create add commit for only one of bob's two clients do - bundle <- claimKeyPackages alice1 bob >>= getJSON 200 + bundle <- claimKeyPackages def alice1 bob >>= getJSON 200 kps <- unbundleKeyPackages bundle kp1 <- assertOne (filter ((== bob1) . fst) kps) mp <- createAddCommitWithKeyPackages alice1 [kp1] @@ -212,7 +212,7 @@ testMixedProtocolAddPartialClients secondDomain = do -- this tests that bob's backend has a mapping of group id to the remote conv -- this test is only interesting when bob is on OtherDomain do - bundle <- claimKeyPackages bob1 bob >>= getJSON 200 + bundle <- claimKeyPackages def bob1 bob >>= getJSON 200 kps <- unbundleKeyPackages bundle kp2 <- assertOne (filter ((== bob2) . fst) kps) mp <- createAddCommitWithKeyPackages bob1 [kp2] @@ -311,14 +311,13 @@ testMLSProtocolUpgrade secondDomain = do resp.status `shouldMatchInt` 200 resp.json %. "protocol" `shouldMatch` "mls" -testAddUser :: HasCallStack => App () -testAddUser = do - [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] +testAddUserSimple :: HasCallStack => Ciphersuite -> App () +testAddUserSimple suite = do + setMLSCiphersuite suite + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] - traverse_ uploadNewKeyPackage [bob1, bob2] - (_, qcnv) <- createNewGroup alice1 resp <- createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle @@ -378,8 +377,9 @@ testRemoteRemoveClient = do msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob msg %. "message.content.sender.External" `shouldMatchInt` 0 -testCreateSubConv :: HasCallStack => App () -testCreateSubConv = do +testCreateSubConv :: HasCallStack => Ciphersuite -> App () +testCreateSubConv suite = do + setMLSCiphersuite suite alice <- randomUser OwnDomain def alice1 <- createMLSClient alice (_, conv) <- createNewGroup alice1 @@ -499,7 +499,7 @@ testFirstCommitAllowsPartialAdds = do (_, _qcnv) <- createNewGroup alice1 - bundle <- claimKeyPackages alice1 alice >>= getJSON 200 + bundle <- claimKeyPackages def alice1 alice >>= getJSON 200 kps <- unbundleKeyPackages bundle -- first commit only adds kp for alice2 (not alice2 and alice3) @@ -525,7 +525,7 @@ testAddUserPartial = do -- alice sends a commit now, and should get a conflict error kps <- fmap concat . for [bob, charlie] $ \user -> do - bundle <- claimKeyPackages alice1 user >>= getJSON 200 + bundle <- claimKeyPackages def alice1 user >>= getJSON 200 unbundleKeyPackages bundle mp <- createAddCommitWithKeyPackages alice1 kps @@ -607,3 +607,149 @@ testLocalWelcome = do event %. "conversation" `shouldMatch` objId qcnv addedUser <- (event %. "data.users") >>= asList >>= assertOne objQid addedUser `shouldMatch` objQid bob + +testStaleCommit :: HasCallStack => App () +testStaleCommit = do + (alice : users) <- createAndConnectUsers (replicate 5 OwnDomain) + let (users1, users2) = splitAt 2 users + + (alice1 : clients) <- traverse createMLSClient (alice : users) + traverse_ uploadNewKeyPackage clients + void $ createNewGroup alice1 + + gsBackup <- getClientGroupState alice1 + + -- add the first batch of users to the conversation + void $ createAddCommit alice1 users1 >>= sendAndConsumeCommitBundle + + -- now roll back alice1 and try to add the second batch of users + setClientGroupState alice1 gsBackup + + mp <- createAddCommit alice1 users2 + bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "mls-stale-message" + +testPropInvalidEpoch :: HasCallStack => App () +testPropInvalidEpoch = do + users@[_alice, bob, charlie, dee] <- createAndConnectUsers (replicate 4 OwnDomain) + [alice1, bob1, charlie1, dee1] <- traverse createMLSClient users + void $ createNewGroup alice1 + + -- Add bob -> epoch 1 + void $ uploadNewKeyPackage bob1 + gsBackup <- getClientGroupState alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + gsBackup2 <- getClientGroupState alice1 + + -- try to send a proposal from an old epoch (0) + do + setClientGroupState alice1 gsBackup + void $ uploadNewKeyPackage dee1 + [prop] <- createAddProposals alice1 [dee] + bindResponse (postMLSMessage alice1 prop.message) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "mls-stale-message" + + -- try to send a proposal from a newer epoch (2) + do + void $ uploadNewKeyPackage dee1 + void $ uploadNewKeyPackage charlie1 + setClientGroupState alice1 gsBackup2 + void $ createAddCommit alice1 [charlie] -- --> epoch 2 + [prop] <- createAddProposals alice1 [dee] + bindResponse (postMLSMessage alice1 prop.message) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "mls-stale-message" + -- remove charlie from users expected to get a welcome message + modifyMLSState $ \mls -> mls {newMembers = mempty} + + -- alice send a well-formed proposal and commits it + void $ uploadNewKeyPackage dee1 + setClientGroupState alice1 gsBackup2 + createAddProposals alice1 [dee] >>= traverse_ sendAndConsumeMessage + void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + +--- | This test submits a ReInit proposal, which is currently ignored by the +-- backend, in order to check that unsupported proposal types are accepted. +testPropUnsupported :: HasCallStack => App () +testPropUnsupported = do + users@[_alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) + [alice1, bob1] <- traverse createMLSClient users + void $ uploadNewKeyPackage bob1 + void $ createNewGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + mp <- createReInitProposal alice1 + + -- we cannot consume this message, because the membership tag is fake + void $ postMLSMessage mp.sender mp.message >>= getJSON 201 + +testAddUserBareProposalCommit :: HasCallStack => App () +testAddUserBareProposalCommit = do + [alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) + [alice1, bob1] <- traverse createMLSClient [alice, bob] + (_, qcnv) <- createNewGroup alice1 + void $ uploadNewKeyPackage bob1 + void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle + + createAddProposals alice1 [bob] + >>= traverse_ sendAndConsumeMessage + commit <- createPendingProposalCommit alice1 + void $ assertJust "Expected welcome" commit.welcome + void $ sendAndConsumeCommitBundle commit + + -- check that bob can now see the conversation + convs <- getAllConvs bob + convIds <- traverse (%. "qualified_id") convs + void $ + assertBool + "Users added to an MLS group should find it when listing conversations" + (qcnv `elem` convIds) + +testPropExistingConv :: HasCallStack => App () +testPropExistingConv = do + [alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ uploadNewKeyPackage bob1 + void $ createNewGroup alice1 + void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle + res <- createAddProposals alice1 [bob] >>= traverse sendAndConsumeMessage >>= assertOne + shouldBeEmpty (res %. "events") + +testCommitNotReferencingAllProposals :: HasCallStack => App () +testCommitNotReferencingAllProposals = do + users@[_alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) + + [alice1, bob1, charlie1] <- traverse createMLSClient users + void $ createNewGroup alice1 + traverse_ uploadNewKeyPackage [bob1, charlie1] + void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle + + gsBackup <- getClientGroupState alice1 + + -- create proposals for bob and charlie + createAddProposals alice1 [bob, charlie] + >>= traverse_ sendAndConsumeMessage + + -- now create a commit referencing only the first proposal + setClientGroupState alice1 gsBackup + commit <- createPendingProposalCommit alice1 + + -- send commit and expect and error + bindResponse (postMLSCommitBundle alice1 (mkBundle commit)) $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-commit-missing-references" + +testUnsupportedCiphersuite :: HasCallStack => App () +testUnsupportedCiphersuite = do + setMLSCiphersuite (Ciphersuite "0x0002") + alice <- randomUser OwnDomain def + alice1 <- createMLSClient alice + void $ createNewGroup alice1 + + mp <- createPendingProposalCommit alice1 + + bindResponse (postMLSCommitBundle alice1 (mkBundle mp)) $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-protocol-error" diff --git a/integration/test/Test/MLS/KeyPackage.hs b/integration/test/Test/MLS/KeyPackage.hs index 5ec25410f62..40760527d1c 100644 --- a/integration/test/Test/MLS/KeyPackage.hs +++ b/integration/test/Test/MLS/KeyPackage.hs @@ -16,6 +16,43 @@ testDeleteKeyPackages = do bindResponse (deleteKeyPackages alice1 kps') $ \resp -> do resp.status `shouldMatchInt` 201 - bindResponse (countKeyPackages alice1) $ \resp -> do + + bindResponse (countKeyPackages def alice1) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "count" `shouldMatchInt` 0 + +testKeyPackageMultipleCiphersuites :: App () +testKeyPackageMultipleCiphersuites = do + alice <- randomUser OwnDomain def + [alice1, alice2] <- replicateM 2 (createMLSClient alice) + + kp <- uploadNewKeyPackage alice2 + + let suite = Ciphersuite "0xf031" + setMLSCiphersuite suite + void $ uploadNewKeyPackage alice2 + + -- count key packages with default ciphersuite + bindResponse (countKeyPackages def alice2) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "count" `shouldMatchInt` 1 + + -- claim key packages with default ciphersuite + bindResponse (claimKeyPackages def alice1 alice) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "key_packages.0.key_package_ref" `shouldMatch` kp + + -- count key package with the other ciphersuite + bindResponse (countKeyPackages suite alice2) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "count" `shouldMatchInt` 1 + +testUnsupportedCiphersuite :: HasCallStack => App () +testUnsupportedCiphersuite = do + setMLSCiphersuite (Ciphersuite "0x0002") + bob <- randomUser OwnDomain def + bob1 <- createMLSClient bob + (kp, _) <- generateKeyPackage bob1 + bindResponse (uploadKeyPackage bob1 kp) $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-protocol-error" diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index d0a6a541850..7df880fca06 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -6,6 +6,7 @@ import Control.Monad.Codensity import Control.Monad.IO.Class import Data.Aeson hiding ((.=)) import Data.ByteString (ByteString) +import Data.Default import Data.Functor import Data.IORef import Data.Map (Map) @@ -224,6 +225,24 @@ create ioRef = Nothing -> error "No resources available" Just (r, s') -> (s', r) +data ClientGroupState = ClientGroupState + { group :: Maybe ByteString, + keystore :: Maybe ByteString + } + deriving (Show) + +emptyClientGroupState :: ClientGroupState +emptyClientGroupState = ClientGroupState Nothing Nothing + +newtype Ciphersuite = Ciphersuite {code :: String} + deriving (Eq, Ord, Show) + +instance Default Ciphersuite where + def = Ciphersuite "0x0001" + +allCiphersuites :: [Ciphersuite] +allCiphersuites = map Ciphersuite ["0x0001", "0xf031"] + data MLSState = MLSState { baseDir :: FilePath, members :: Set ClientIdentity, @@ -231,8 +250,9 @@ data MLSState = MLSState newMembers :: Set ClientIdentity, groupId :: Maybe String, convId :: Maybe Value, - clientGroupState :: Map ClientIdentity ByteString, - epoch :: Word64 + clientGroupState :: Map ClientIdentity ClientGroupState, + epoch :: Word64, + ciphersuite :: Ciphersuite } deriving (Show) @@ -247,7 +267,8 @@ mkMLSState = Codensity $ \k -> groupId = Nothing, convId = Nothing, clientGroupState = mempty, - epoch = 0 + epoch = 0, + ciphersuite = def } data ClientIdentity = ClientIdentity diff --git a/integration/test/Testlib/PTest.hs b/integration/test/Testlib/PTest.hs index d2613fa214e..1aa478b720f 100644 --- a/integration/test/Testlib/PTest.hs +++ b/integration/test/Testlib/PTest.hs @@ -1,6 +1,7 @@ module Testlib.PTest where import Testlib.App +import Testlib.Env import Testlib.Types import Prelude @@ -16,3 +17,10 @@ instance HasTests x => HasTests (Domain -> x) where mkTests m n s f x = mkTests m (n <> "[domain=own]") s f (x OwnDomain) <> mkTests m (n <> "[domain=other]") s f (x OtherDomain) + +instance HasTests x => HasTests (Ciphersuite -> x) where + mkTests m n s f x = + mconcat + [ mkTests m (n <> "[suite=" <> suite.code <> "]") s f (x suite) + | suite <- allCiphersuites + ] diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs index f663444419a..c2a25af65bd 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs @@ -97,7 +97,7 @@ newtype GetUserClients = GetUserClients data MLSClientsRequest = MLSClientsRequest { userId :: UserId, -- implicitly qualified by the local domain - signatureScheme :: SignatureSchemeTag + cipherSuite :: CipherSuite } deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded MLSClientsRequest) @@ -160,7 +160,9 @@ data ClaimKeyPackageRequest = ClaimKeyPackageRequest claimant :: UserId, -- | The user whose key packages are being claimed, implictly qualified by -- the target domain. - target :: UserId + target :: UserId, + -- | The ciphersuite of the key packages being claimed. + cipherSuite :: CipherSuite } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ClaimKeyPackageRequest) diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index f6da30927ec..339f00f0122 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -20,6 +20,7 @@ module Wire.API.MLS.CipherSuite ( -- * MLS ciphersuites CipherSuite (..), + defCipherSuite, CipherSuiteTag (..), cipherSuiteTag, tagCipherSuite, @@ -50,6 +51,7 @@ import Crypto.PubKey.Ed25519 qualified as Ed25519 import Data.Aeson qualified as Aeson import Data.Aeson.Types (FromJSON (..), FromJSONKey (..), ToJSON (..), ToJSONKey (..)) import Data.Aeson.Types qualified as Aeson +import Data.Bifunctor import Data.ByteArray hiding (index) import Data.ByteArray qualified as BA import Data.Proxy @@ -57,25 +59,55 @@ import Data.Schema import Data.Swagger qualified as S import Data.Swagger.Internal.Schema qualified as S import Data.Text qualified as T +import Data.Text.Lazy qualified as LT +import Data.Text.Lazy.Builder qualified as LT +import Data.Text.Lazy.Builder.Int qualified as LT +import Data.Text.Read qualified as T import Data.Word import Imports hiding (cs) -import Servant (FromHttpApiData (parseQueryParam)) +import Web.HttpApiData import Wire.API.MLS.Serialisation import Wire.Arbitrary newtype CipherSuite = CipherSuite {cipherSuiteNumber :: Word16} deriving stock (Eq, Show) deriving newtype (ParseMLS, SerialiseMLS, Arbitrary) + deriving (FromJSON, ToJSON) via Schema CipherSuite instance ToSchema CipherSuite where schema = named "CipherSuite" $ cipherSuiteNumber .= fmap CipherSuite (unnamed schema) -data CipherSuiteTag = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 +instance S.ToParamSchema CipherSuite where + toParamSchema _ = + mempty + & S.type_ ?~ S.SwaggerNumber + +instance FromHttpApiData CipherSuite where + parseUrlPiece t = do + (x, rest) <- first T.pack $ T.hexadecimal t + unless (T.null rest) $ + Left "Trailing characters after ciphersuite number" + pure (CipherSuite x) + +instance ToHttpApiData CipherSuite where + toUrlPiece = + LT.toStrict + . LT.toLazyText + . ("0x" <>) + . LT.hexadecimal + . cipherSuiteNumber + +data CipherSuiteTag + = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + | MLS_128_X25519Kyber768Draft00_AES128GCM_SHA256_Ed25519 deriving stock (Bounded, Enum, Eq, Show, Generic, Ord) deriving (Arbitrary) via (GenericUniform CipherSuiteTag) +defCipherSuite :: CipherSuiteTag +defCipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + instance S.ToSchema CipherSuiteTag where declareNamedSchema _ = pure . S.named "CipherSuiteTag" $ @@ -99,20 +131,29 @@ instance ToSchema CipherSuiteTag where -- | See https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#table-5. cipherSuiteTag :: CipherSuite -> Maybe CipherSuiteTag -cipherSuiteTag (CipherSuite n) = case n of - 1 -> pure MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - _ -> Nothing +cipherSuiteTag cs = listToMaybe $ do + t <- [minBound .. maxBound] + guard (tagCipherSuite t == cs) + pure t -- | Inverse of 'cipherSuiteTag' tagCipherSuite :: CipherSuiteTag -> CipherSuite tagCipherSuite MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = CipherSuite 1 +tagCipherSuite MLS_128_X25519Kyber768Draft00_AES128GCM_SHA256_Ed25519 = CipherSuite 0xf031 csHash :: CipherSuiteTag -> ByteString -> RawMLS a -> ByteString -csHash MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 ctx value = - convert . hashWith SHA256 . encodeMLS' $ RefHashInput ctx value +csHash MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = sha256Hash +csHash MLS_128_X25519Kyber768Draft00_AES128GCM_SHA256_Ed25519 = sha256Hash + +sha256Hash :: ByteString -> RawMLS a -> ByteString +sha256Hash ctx value = convert . hashWith SHA256 . encodeMLS' $ RefHashInput ctx value csVerifySignature :: CipherSuiteTag -> ByteString -> RawMLS a -> ByteString -> Bool -csVerifySignature MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 pub x sig = +csVerifySignature MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = ed25519VerifySignature +csVerifySignature MLS_128_X25519Kyber768Draft00_AES128GCM_SHA256_Ed25519 = ed25519VerifySignature + +ed25519VerifySignature :: ByteString -> RawMLS a -> ByteString -> Bool +ed25519VerifySignature pub x sig = fromMaybe False . maybeCryptoError $ do pub' <- Ed25519.publicKey pub sig' <- Ed25519.signature sig @@ -158,6 +199,7 @@ signWithLabel sigLabel priv pub x = BA.convert $ Ed25519.sign priv pub (encodeML csSignatureScheme :: CipherSuiteTag -> SignatureSchemeTag csSignatureScheme MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 = Ed25519 +csSignatureScheme MLS_128_X25519Kyber768Draft00_AES128GCM_SHA256_Ed25519 = Ed25519 -- | A TLS signature scheme. -- diff --git a/libs/wire-api/src/Wire/API/MLS/Validation.hs b/libs/wire-api/src/Wire/API/MLS/Validation.hs index f4fd60c56e1..eadc3442f27 100644 --- a/libs/wire-api/src/Wire/API/MLS/Validation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Validation.hs @@ -23,6 +23,9 @@ module Wire.API.MLS.Validation where import Control.Applicative +import Data.Text.Lazy qualified as LT +import Data.Text.Lazy.Builder qualified as LT +import Data.Text.Lazy.Builder.Int qualified as LT import Imports hiding (cs) import Wire.API.MLS.Capabilities import Wire.API.MLS.CipherSuite @@ -41,7 +44,11 @@ validateKeyPackage mIdentity kp = do -- get ciphersuite cs <- maybe - (Left "Unsupported ciphersuite") + ( Left + ( "Unsupported ciphersuite 0x" + <> LT.toStrict (LT.toLazyText (LT.hexadecimal kp.cipherSuite.cipherSuiteNumber)) + ) + ) pure $ cipherSuiteTag kp.cipherSuite 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 b296cb919ad..241864cfe8c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -54,7 +54,7 @@ import Servant.Swagger.Internal.Orphans () import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig -import Wire.API.MLS.CipherSuite (SignatureSchemeTag) +import Wire.API.MLS.CipherSuite import Wire.API.MakesFederatedCall import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Internal.Brig.Connection @@ -64,7 +64,7 @@ import Wire.API.Routes.Internal.Brig.SearchIndex (ISearchIndexAPI) import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named -import Wire.API.Routes.Public (ZUser {- yes, this is a bit weird -}) +import Wire.API.Routes.Public (ZUser) import Wire.API.Team.Feature import Wire.API.Team.LegalHold.Internal import Wire.API.User @@ -500,7 +500,7 @@ type GetMLSClients = :> "clients" :> CanThrow 'UserNotFound :> Capture "user" UserId - :> QueryParam' '[Required, Strict] "sig_scheme" SignatureSchemeTag + :> QueryParam' '[Required, Strict] "ciphersuite" CipherSuite :> MultiVerb1 'GET '[Servant.JSON] diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 7550b5bd605..9dab0231075 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -45,6 +45,7 @@ import Wire.API.Connection hiding (MissingLegalholdConsent) import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Error.Empty +import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage import Wire.API.MLS.Servant import Wire.API.MakesFederatedCall @@ -1098,6 +1099,8 @@ type ConnectionAPI = :> Get '[Servant.JSON] (SearchResult Contact) ) +-- Properties API ----------------------------------------------------- + type PropertiesAPI = LiftNamed ( ZUser @@ -1158,7 +1161,16 @@ type PropertiesAPI = :> Get '[JSON] PropertyKeysAndValues ) --- Properties API ----------------------------------------------------- +-- MLS API --------------------------------------------------------------------- + +type CipherSuiteParam = + QueryParam' + [ Optional, + Strict, + Description "Ciphersuite in hex format (e.g. 0xf031) - default is 0x0001" + ] + "ciphersuite" + CipherSuite type MLSKeyPackageAPI = "key-packages" @@ -1178,25 +1190,21 @@ type MLSKeyPackageAPI = "mls-key-packages-claim" ( "claim" :> Summary "Claim one key package for each client of the given user" - :> MakesFederatedCall 'Brig "claim-key-packages" + :> Description "Only key packages for the specified ciphersuite are claimed. For backwards compatibility, the `ciphersuite` parameter is optional, defaulting to ciphersuite 0x0001 when omitted." :> ZLocalUser + :> ZOptClient :> QualifiedCaptureUserId "user" - :> QueryParam' - [ Optional, - Strict, - Description "Do not claim a key package for the given own client" - ] - "skip_own" - ClientId + :> CipherSuiteParam :> MultiVerb1 'POST '[JSON] (Respond 200 "Claimed key packages" KeyPackageBundle) ) :<|> Named "mls-key-packages-count" ( "self" + :> Summary "Return the number of unclaimed key packages for a given ciphersuite and client" :> ZLocalUser :> CaptureClientId "client" :> "count" - :> Summary "Return the number of unused key packages for the given client" + :> CipherSuiteParam :> MultiVerb1 'GET '[JSON] (Respond 200 "Number of key packages" KeyPackageCount) ) :<|> Named @@ -1204,7 +1212,8 @@ type MLSKeyPackageAPI = ( "self" :> ZLocalUser :> CaptureClientId "client" - :> Summary "Return the number of unused key packages for the given client" + :> Summary "Delete all key packages for a given ciphersuite and client" + :> CipherSuiteParam :> ReqBody '[JSON] DeleteKeyPackages :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 201 "OK") ) diff --git a/nix/pkgs/mls-test-cli/default.nix b/nix/pkgs/mls-test-cli/default.nix index e7104041ed8..1e38ca6039c 100644 --- a/nix/pkgs/mls-test-cli/default.nix +++ b/nix/pkgs/mls-test-cli/default.nix @@ -13,8 +13,8 @@ let src = fetchFromGitHub { owner = "wireapp"; repo = "mls-test-cli"; - rev = "87845faa7d5ee69652747ceaf1664baa8198c0d8"; - sha256 = "sha256-DoQ6brp1KvglVVCDp4vC5zaRx76IUywu3Rcu/TzJlvo="; + rev = "cc815d71a1d9485265b7ae158daf7b27badedee6"; + sha256 = "sha256-CJoc20pOtsxAQNCA3qhv8NtPbzZ4yCIMvuhlgcqPrds="; }; cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/Cargo.lock"); in rustPlatform.buildRustPackage rec { @@ -24,8 +24,10 @@ in rustPlatform.buildRustPackage rec { cargoLock = { lockFile = cargoLockFile; outputHashes = { - "hpke-0.10.0" = "sha256-XYkG72ZeQ3nM4JjgNU5Fe0HqNGkBGcI70rE1Kbz/6vs="; - "openmls-0.20.0" = "sha256-i5xNTYP1wPzwlnqz+yPu8apKCibRZacz4OV5VVZwY5Y="; + "hpke-0.10.0" = "sha256-6zyTb2c2DU4mXn9vRQe+lXNaeQ3JOVUz+BS15Xb2E+Y="; + "openmls-0.20.2" = "sha256-QgQb5Ts8TB2nwfxMss4qHCz096ijMXBxyq7q2ITyEGg="; + "safe_pqc_kyber-0.6.0" = "sha256-Ch1LA+by+ezf5RV0LDSQGC1o+IWKXk8IPvkwSrAos68="; + "tls_codec-0.3.0" = "sha256-IO6tenXKkC14EoUDp/+DtFNOVzDfOlLu8K1EJI7sOzs="; }; }; diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 93b775db4a7..5e067fcbba2 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -82,6 +82,7 @@ library Brig.API.Federation Brig.API.Handler Brig.API.Internal + Brig.API.MLS.CipherSuite Brig.API.MLS.KeyPackages Brig.API.MLS.KeyPackages.Validation Brig.API.MLS.Util @@ -537,6 +538,7 @@ executable brig-schema V77_FederationRemotes V78_ClientLastActive V79_ConnectionRemoteIndex + V80_KeyPackageCiphersuite V_FUTUREWORK hs-source-dirs: schema/src diff --git a/services/brig/schema/src/Run.hs b/services/brig/schema/src/Run.hs index c9423fd6531..0f1b71127b3 100644 --- a/services/brig/schema/src/Run.hs +++ b/services/brig/schema/src/Run.hs @@ -59,6 +59,7 @@ import V76_AddSupportedProtocols qualified import V77_FederationRemotes qualified import V78_ClientLastActive qualified import V79_ConnectionRemoteIndex qualified +import V80_KeyPackageCiphersuite qualified main :: IO () main = do @@ -105,7 +106,8 @@ main = do V76_AddSupportedProtocols.migration, V77_FederationRemotes.migration, V78_ClientLastActive.migration, - V79_ConnectionRemoteIndex.migration + V79_ConnectionRemoteIndex.migration, + V80_KeyPackageCiphersuite.migration -- When adding migrations here, don't forget to update -- 'schemaVersion' in Brig.App diff --git a/services/brig/schema/src/V80_KeyPackageCiphersuite.hs b/services/brig/schema/src/V80_KeyPackageCiphersuite.hs new file mode 100644 index 00000000000..7e7ec8b21d8 --- /dev/null +++ b/services/brig/schema/src/V80_KeyPackageCiphersuite.hs @@ -0,0 +1,50 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 V80_KeyPackageCiphersuite + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +-- Index key packages by ciphersuite as well as user and client. + +-- Note: this migration recreates the mls_key_packages table from scratch, and +-- therefore loses all the data it contains. That means clients will need to +-- re-upload key packages after this migration is run. + +migration :: Migration +migration = + Migration 80 "Recreate mls_key_packages table" $ do + schema' [r| DROP TABLE IF EXISTS mls_key_packages; |] + schema' + [r| + CREATE TABLE mls_key_packages + ( user uuid + , client text + , cipher_suite int + , ref blob + , data blob + , PRIMARY KEY ((user, client, cipher_suite), ref) + ) WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND gc_grace_seconds = 864000; + |] diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index c8da1a5e702..f7bdf0f387c 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -25,6 +25,7 @@ import Brig.API.Error import Brig.API.Handler (Handler) import Brig.API.Internal hiding (getMLSClients) import Brig.API.Internal qualified as Internal +import Brig.API.MLS.CipherSuite import Brig.API.MLS.KeyPackages import Brig.API.MLS.Util import Brig.API.User qualified as API @@ -166,10 +167,11 @@ fedClaimKeyPackages :: Domain -> ClaimKeyPackageRequest -> Handler r (Maybe KeyP fedClaimKeyPackages domain ckpr = isMLSEnabled >>= \case True -> do + suite <- getCipherSuite (Just ckpr.cipherSuite) ltarget <- qualifyLocal ckpr.target let rusr = toRemoteUnsafe domain ckpr.claimant lift . fmap hush . runExceptT $ - claimLocalKeyPackages (tUntagged rusr) Nothing ltarget + claimLocalKeyPackages (tUntagged rusr) Nothing suite ltarget False -> pure Nothing -- | Searching for federated users on a remote backend should @@ -220,7 +222,7 @@ getUserClients _ (GetUserClients uids) = API.lookupLocalPubClientsBulk uids !>> getMLSClients :: Domain -> MLSClientsRequest -> Handler r (Set ClientInfo) getMLSClients _domain mcr = do - Internal.getMLSClients mcr.userId mcr.signatureScheme + Internal.getMLSClients mcr.userId mcr.cipherSuite onUserDeleted :: Domain -> UserDeletedConnectionsNotification -> (Handler r) EmptyResponse onUserDeleted origDomain udcn = lift $ do diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 71a691640b5..306ea0858c9 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -28,6 +28,7 @@ import Brig.API.Client qualified as API import Brig.API.Connection qualified as API import Brig.API.Error import Brig.API.Handler +import Brig.API.MLS.KeyPackages.Validation import Brig.API.OAuth (internalOauthAPI) import Brig.API.Types import Brig.API.User qualified as API @@ -384,12 +385,12 @@ deleteAccountConferenceCallingConfig :: UserId -> (Handler r) NoContent deleteAccountConferenceCallingConfig uid = lift $ wrapClient $ Data.updateFeatureConferenceCalling uid Nothing $> NoContent -getMLSClients :: UserId -> SignatureSchemeTag -> Handler r (Set ClientInfo) -getMLSClients usr _ss = do - -- FUTUREWORK: check existence of key packages with a given ciphersuite +getMLSClients :: UserId -> CipherSuite -> Handler r (Set ClientInfo) +getMLSClients usr suite = do lusr <- qualifyLocal usr + suiteTag <- maybe (mlsProtocolError "Unknown ciphersuite") pure (cipherSuiteTag suite) allClients <- lift (wrapClient (API.lookupUsersClientIds (pure usr))) >>= getResult - clientInfo <- lift . wrapClient $ pooledMapConcurrentlyN 16 (getValidity lusr) (toList allClients) + clientInfo <- lift . wrapClient $ pooledMapConcurrentlyN 16 (\c -> getValidity lusr c suiteTag) (toList allClients) pure . Set.fromList . map (uncurry ClientInfo) $ clientInfo where getResult [] = pure mempty @@ -397,9 +398,9 @@ getMLSClients usr _ss = do | u == usr = pure cs' | otherwise = getResult rs - getValidity lusr cid = + getValidity lusr cid suiteTag = (cid,) . (> 0) - <$> Data.countKeyPackages lusr cid + <$> Data.countKeyPackages lusr cid suiteTag getVerificationCode :: UserId -> VerificationAction -> Handler r (Maybe Code.Value) getVerificationCode uid action = do diff --git a/services/brig/src/Brig/API/MLS/CipherSuite.hs b/services/brig/src/Brig/API/MLS/CipherSuite.hs new file mode 100644 index 00000000000..ec6b9756787 --- /dev/null +++ b/services/brig/src/Brig/API/MLS/CipherSuite.hs @@ -0,0 +1,29 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Brig.API.MLS.CipherSuite (getCipherSuite) where + +import Brig.API.Handler +import Brig.API.MLS.KeyPackages.Validation +import Imports +import Wire.API.MLS.CipherSuite + +getCipherSuite :: Maybe CipherSuite -> Handler r CipherSuiteTag +getCipherSuite mSuite = case mSuite of + Nothing -> pure defCipherSuite + Just x -> + maybe (mlsProtocolError "Unknown ciphersuite") pure (cipherSuiteTag x) diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index ce8b1ad7643..4a3c244b356 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -26,6 +26,7 @@ where import Brig.API.Error import Brig.API.Handler +import Brig.API.MLS.CipherSuite import Brig.API.MLS.KeyPackages.Validation import Brig.API.MLS.Util import Brig.API.Types @@ -42,6 +43,7 @@ import Data.Set qualified as Set import Imports import Wire.API.Federation.API import Wire.API.Federation.API.Brig +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation @@ -57,23 +59,26 @@ uploadKeyPackages lusr cid kps = do claimKeyPackages :: Local UserId -> - Qualified UserId -> Maybe ClientId -> + Qualified UserId -> + Maybe CipherSuite -> Handler r KeyPackageBundle -claimKeyPackages lusr target skipOwn = do +claimKeyPackages lusr mClient target mSuite = do assertMLSEnabled + suite <- getCipherSuite mSuite foldQualified lusr - (withExceptT clientError . claimLocalKeyPackages (tUntagged lusr) skipOwn) - (claimRemoteKeyPackages lusr) + (withExceptT clientError . claimLocalKeyPackages (tUntagged lusr) mClient suite) + (claimRemoteKeyPackages lusr (tagCipherSuite suite)) target claimLocalKeyPackages :: Qualified UserId -> Maybe ClientId -> + CipherSuiteTag -> Local UserId -> ExceptT ClientError (AppT r) KeyPackageBundle -claimLocalKeyPackages qusr skipOwn target = do +claimLocalKeyPackages qusr skipOwn suite target = do -- skip own client when the target is the requesting user itself let own = guard (qusr == tUntagged target) *> skipOwn clients <- map clientId <$> wrapClientE (Data.lookupClients (tUnqualified target)) @@ -94,13 +99,14 @@ claimLocalKeyPackages qusr skipOwn target = do runMaybeT $ do guard $ Just c /= own uncurry (KeyPackageBundleEntry (tUntagged target) c) - <$> wrapClientM (Data.claimKeyPackage target c) + <$> wrapClientM (Data.claimKeyPackage target c suite) claimRemoteKeyPackages :: Local UserId -> + CipherSuite -> Remote UserId -> Handler r KeyPackageBundle -claimRemoteKeyPackages lusr target = do +claimRemoteKeyPackages lusr suite target = do bundle <- withExceptT clientError . (handleFailure =<<) @@ -109,7 +115,8 @@ claimRemoteKeyPackages lusr target = do $ fedClient @'Brig @"claim-key-packages" $ ClaimKeyPackageRequest { claimant = tUnqualified lusr, - target = tUnqualified target + target = tUnqualified target, + cipherSuite = suite } -- validate all claimed key packages @@ -121,7 +128,7 @@ claimRemoteKeyPackages lusr target = do . decodeMLS' . kpData $ e.keyPackage - (refVal, _) <- validateUploadedKeyPackage cid kpRaw + (refVal, _, _) <- validateUploadedKeyPackage cid kpRaw unless (refVal == e.ref) . throwE . clientDataError @@ -132,18 +139,21 @@ claimRemoteKeyPackages lusr target = do handleFailure :: Monad m => Maybe x -> ExceptT ClientError m x handleFailure = maybe (throwE (ClientUserNotFound (tUnqualified target))) pure -countKeyPackages :: Local UserId -> ClientId -> Handler r KeyPackageCount -countKeyPackages lusr c = do +countKeyPackages :: Local UserId -> ClientId -> Maybe CipherSuite -> Handler r KeyPackageCount +countKeyPackages lusr c mSuite = do assertMLSEnabled + suite <- getCipherSuite mSuite lift $ KeyPackageCount . fromIntegral - <$> wrapClient (Data.countKeyPackages lusr c) + <$> wrapClient (Data.countKeyPackages lusr c suite) deleteKeyPackages :: Local UserId -> ClientId -> + Maybe CipherSuite -> DeleteKeyPackages -> Handler r () -deleteKeyPackages lusr c (unDeleteKeyPackages -> refs) = do +deleteKeyPackages lusr c mSuite (unDeleteKeyPackages -> refs) = do assertMLSEnabled - lift $ wrapClient (Data.deleteKeyPackages (tUnqualified lusr) c refs) + suite <- getCipherSuite mSuite + lift $ wrapClient (Data.deleteKeyPackages (tUnqualified lusr) c suite refs) diff --git a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs index d559d09e9b9..0783663e807 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs @@ -47,7 +47,7 @@ import Wire.API.MLS.Validation validateUploadedKeyPackage :: ClientIdentity -> RawMLS KeyPackage -> - Handler r (KeyPackageRef, KeyPackageData) + Handler r (KeyPackageRef, CipherSuiteTag, KeyPackageData) validateUploadedKeyPackage identity kp = do (cs, lt) <- either mlsProtocolError pure $ validateKeyPackage (Just identity) kp.value @@ -77,7 +77,7 @@ validateUploadedKeyPackage identity kp = do (cidQualifiedClient identity) let kpd = KeyPackageData kp.raw - pure (kpRef cs kpd, kpd) + pure (kpRef cs kpd, cs, kpd) validateLifetime :: Lifetime -> Handler r () validateLifetime lt = do diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index dcf59d4b0a1..467143eae61 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -384,7 +384,7 @@ servantSitemap = mlsAPI :: ServerT MLSAPI (Handler r) mlsAPI = Named @"mls-key-packages-upload" uploadKeyPackages - :<|> Named @"mls-key-packages-claim" (callsFed (exposeAnnotations claimKeyPackages)) + :<|> Named @"mls-key-packages-claim" claimKeyPackages :<|> Named @"mls-key-packages-count" countKeyPackages :<|> Named @"mls-key-packages-delete" deleteKeyPackages diff --git a/services/brig/src/Brig/Data/Instances.hs b/services/brig/src/Brig/Data/Instances.hs index dfbba99f65e..34309315771 100644 --- a/services/brig/src/Brig/Data/Instances.hs +++ b/services/brig/src/Brig/Data/Instances.hs @@ -39,6 +39,7 @@ import Data.Text.Encoding (encodeUtf8) import Imports import Wire.API.Asset (AssetKey, assetKeyToText, nilAssetKey) import Wire.API.Connection (RelationWithHistory (..)) +import Wire.API.MLS.CipherSuite import Wire.API.Properties import Wire.API.User import Wire.API.User.Activation @@ -306,3 +307,13 @@ instance Cql (Imports.Set BaseProtocolTag) where toCql = CqlInt . fromIntegral . protocolSetBits fromCql (CqlInt bits) = pure $ protocolSetFromBits (fromIntegral bits) fromCql _ = Left "Protocol set: Int expected" + +instance Cql CipherSuiteTag where + ctype = Tagged IntColumn + toCql = CqlInt . fromIntegral . cipherSuiteNumber . tagCipherSuite + + fromCql (CqlInt index) = + case cipherSuiteTag (CipherSuite (fromIntegral index)) of + Just tag -> Right tag + Nothing -> Left "CipherSuiteTag: unexpected index" + fromCql _ = Left "CipherSuiteTag: int expected" diff --git a/services/brig/src/Brig/Data/MLS/KeyPackage.hs b/services/brig/src/Brig/Data/MLS/KeyPackage.hs index fc3c4183bce..a03192f32e6 100644 --- a/services/brig/src/Brig/Data/MLS/KeyPackage.hs +++ b/services/brig/src/Brig/Data/MLS/KeyPackage.hs @@ -37,19 +37,25 @@ import Data.Qualified import Data.Time.Clock import Data.Time.Clock.POSIX import Imports +import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage import Wire.API.MLS.LeafNode import Wire.API.MLS.Serialisation -insertKeyPackages :: MonadClient m => UserId -> ClientId -> [(KeyPackageRef, KeyPackageData)] -> m () +insertKeyPackages :: + MonadClient m => + UserId -> + ClientId -> + [(KeyPackageRef, CipherSuiteTag, KeyPackageData)] -> + m () insertKeyPackages uid cid kps = retry x5 . batch $ do setType BatchLogged setConsistency LocalQuorum - for_ kps $ \(ref, kp) -> do - addPrepQuery q (uid, cid, kp, ref) + for_ kps $ \(ref, suite, kp) -> do + addPrepQuery q (uid, cid, suite, kp, ref) where - q :: PrepQuery W (UserId, ClientId, KeyPackageData, KeyPackageRef) () - q = "INSERT INTO mls_key_packages (user, client, data, ref) VALUES (?, ?, ?, ?)" + q :: PrepQuery W (UserId, ClientId, CipherSuiteTag, KeyPackageData, KeyPackageRef) () + q = "INSERT INTO mls_key_packages (user, client, cipher_suite, data, ref) VALUES (?, ?, ?, ?, ?)" claimKeyPackage :: ( MonadReader Env m, @@ -58,21 +64,22 @@ claimKeyPackage :: ) => Local UserId -> ClientId -> + CipherSuiteTag -> MaybeT m (KeyPackageRef, KeyPackageData) -claimKeyPackage u c = do +claimKeyPackage u c suite = do -- FUTUREWORK: investigate better locking strategies lock <- lift $ view keyPackageLocalLock -- get a random key package and delete it (ref, kpd) <- MaybeT . withMVar lock . const $ do - kps <- getNonClaimedKeyPackages u c + kps <- getNonClaimedKeyPackages u c suite mk <- liftIO (pick kps) for mk $ \(ref, kpd) -> do - retry x5 $ write delete1Query (params LocalQuorum (tUnqualified u, c, ref)) + retry x5 $ write delete1Query (params LocalQuorum (tUnqualified u, c, suite, ref)) pure (ref, kpd) pure (ref, kpd) where - delete1Query :: PrepQuery W (UserId, ClientId, KeyPackageRef) () - delete1Query = "DELETE FROM mls_key_packages WHERE user = ? AND client = ? AND ref = ?" + delete1Query :: PrepQuery W (UserId, ClientId, CipherSuiteTag, KeyPackageRef) () + delete1Query = "DELETE FROM mls_key_packages WHERE user = ? AND client = ? AND cipher_suite = ? AND ref = ?" -- | Fetch all unclaimed non-expired key packages for a given client and delete -- from the database those that have expired. @@ -82,9 +89,10 @@ getNonClaimedKeyPackages :: ) => Local UserId -> ClientId -> + CipherSuiteTag -> m [(KeyPackageRef, KeyPackageData)] -getNonClaimedKeyPackages u c = do - kps <- retry x1 $ query lookupQuery (params LocalQuorum (tUnqualified u, c)) +getNonClaimedKeyPackages u c suite = do + kps <- retry x1 $ query lookupQuery (params LocalQuorum (tUnqualified u, c, suite)) let decodedKps = foldMap (keepDecoded . (decodeKp &&& id)) kps now <- liftIO getPOSIXTime @@ -93,11 +101,11 @@ getNonClaimedKeyPackages u c = do let (kpsExpired, kpsNonExpired) = partition (hasExpired now mMaxLifetime) decodedKps -- delete expired key packages - deleteKeyPackages (tUnqualified u) c (map (\(_, (ref, _)) -> ref) kpsExpired) + deleteKeyPackages (tUnqualified u) c suite (map (\(_, (ref, _)) -> ref) kpsExpired) pure $ fmap snd kpsNonExpired where - lookupQuery :: PrepQuery R (UserId, ClientId) (KeyPackageRef, KeyPackageData) - lookupQuery = "SELECT ref, data FROM mls_key_packages WHERE user = ? AND client = ?" + lookupQuery :: PrepQuery R (UserId, ClientId, CipherSuiteTag) (KeyPackageRef, KeyPackageData) + lookupQuery = "SELECT ref, data FROM mls_key_packages WHERE user = ? AND client = ? AND cipher_suite = ?" decodeKp :: (a, KeyPackageData) -> Maybe KeyPackage decodeKp = hush . decodeMLS' . kpData . snd @@ -120,18 +128,19 @@ countKeyPackages :: ) => Local UserId -> ClientId -> + CipherSuiteTag -> m Int64 -countKeyPackages u c = fromIntegral . length <$> getNonClaimedKeyPackages u c +countKeyPackages u c suite = fromIntegral . length <$> getNonClaimedKeyPackages u c suite -deleteKeyPackages :: MonadClient m => UserId -> ClientId -> [KeyPackageRef] -> m () -deleteKeyPackages u c refs = +deleteKeyPackages :: MonadClient m => UserId -> ClientId -> CipherSuiteTag -> [KeyPackageRef] -> m () +deleteKeyPackages u c suite refs = retry x5 $ write deleteQuery - (params LocalQuorum (u, c, refs)) + (params LocalQuorum (u, c, suite, refs)) where - deleteQuery :: PrepQuery W (UserId, ClientId, [KeyPackageRef]) () - deleteQuery = "DELETE FROM mls_key_packages WHERE user = ? AND client = ? AND ref in ?" + deleteQuery :: PrepQuery W (UserId, ClientId, CipherSuiteTag, [KeyPackageRef]) () + deleteQuery = "DELETE FROM mls_key_packages WHERE user = ? AND client = ? AND cipher_suite = ? AND ref in ?" -------------------------------------------------------------------------------- -- Utilities diff --git a/services/brig/test/integration/API/Federation.hs b/services/brig/test/integration/API/Federation.hs index 8e0f7624b3c..47025e9a6df 100644 --- a/services/brig/test/integration/API/Federation.hs +++ b/services/brig/test/integration/API/Federation.hs @@ -53,6 +53,7 @@ import Wire.API.Federation.API.Brig qualified as FedBrig import Wire.API.Federation.API.Brig qualified as S import Wire.API.Federation.Component import Wire.API.Federation.Version +import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage import Wire.API.Routes.FederationDomainConfig as FD import Wire.API.User @@ -420,7 +421,10 @@ testClaimKeyPackages brig fedBrigClient = do Just bundle <- runFedClient @"claim-key-packages" fedBrigClient (qDomain alice) $ - ClaimKeyPackageRequest (qUnqualified alice) (qUnqualified bob) + ClaimKeyPackageRequest + (qUnqualified alice) + (qUnqualified bob) + (tagCipherSuite defCipherSuite) liftIO $ Set.map (\e -> (e.user, e.client)) bundle.entries @@ -440,6 +444,9 @@ testClaimKeyPackagesMLSDisabled opts brig = do withSettingsOverrides (opts & Opt.optionSettings . Opt.enableMLS ?~ False) $ runWaiTestFedClient (qDomain alice) $ createWaiTestFedClient @"claim-key-packages" @'Brig $ - ClaimKeyPackageRequest (qUnqualified alice) (qUnqualified bob) + ClaimKeyPackageRequest + (qUnqualified alice) + (qUnqualified bob) + (tagCipherSuite defCipherSuite) liftIO $ mbundle @?= Nothing diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index ad973eff805..3d22a98eb57 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -291,25 +291,24 @@ testGetMlsClients :: Brig -> Http () testGetMlsClients brig = do qusr <- userQualifiedId <$> randomUser brig c <- createClient brig qusr 0 - (cs0 :: Set ClientInfo) <- - responseJsonError - =<< get - ( brig - . paths ["i", "mls", "clients", toByteString' (qUnqualified qusr)] - . queryItem "sig_scheme" "ed25519" - ) + + let getClients :: Http (Set ClientInfo) + getClients = + responseJsonError + =<< get + ( brig + . paths ["i", "mls", "clients", toByteString' (qUnqualified qusr)] + . queryItem "ciphersuite" "0x0001" + ) + uploadKeyPackages brig tmp def qusr c 2 - (cs1 :: Set ClientInfo) <- - responseJsonError - =<< get - ( brig - . paths ["i", "mls", "clients", toByteString' (qUnqualified qusr)] - . queryItem "sig_scheme" "ed25519" - ) + cs1 <- getClients liftIO $ toList cs1 @?= [ClientInfo c True] getFeatureConfig :: forall cfg m. (MonadHttp m, HasCallStack, KnownSymbol (ApiFt.FeatureSymbol cfg)) => (Request -> Request) -> UserId -> m ResponseLBS diff --git a/services/brig/test/integration/API/MLS.hs b/services/brig/test/integration/API/MLS.hs index f9220e5590b..b578f424d80 100644 --- a/services/brig/test/integration/API/MLS.hs +++ b/services/brig/test/integration/API/MLS.hs @@ -149,8 +149,8 @@ testKeyPackageSelfClaim brig = do =<< post ( brig . paths ["mls", "key-packages", "claim", toByteString' (qDomain u), toByteString' (qUnqualified u)] - . queryItem "skip_own" (toByteString' c1) . zUser (qUnqualified u) + . zClient c1 ) (e.user, e.client)) bundle.entries @?= Set.fromList [(u, c2)] diff --git a/services/galley/default.nix b/services/galley/default.nix index 3acd41013f6..727579f2e19 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -233,7 +233,6 @@ mkDerivation { conduit containers cookie - cryptonite currency-codes data-default data-timeout diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index b18ebef1d39..660695de062 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -415,7 +415,6 @@ executable galley-integration , cereal , containers , cookie - , cryptonite , currency-codes , data-default , data-timeout diff --git a/services/galley/src/Galley/API/MLS/Commit/Core.hs b/services/galley/src/Galley/API/MLS/Commit/Core.hs index ce6d32d4c2a..12f2a1cf73e 100644 --- a/services/galley/src/Galley/API/MLS/Commit/Core.hs +++ b/services/galley/src/Galley/API/MLS/Commit/Core.hs @@ -135,7 +135,7 @@ getClientInfo :: ) => Local x -> Qualified UserId -> - SignatureSchemeTag -> + CipherSuiteTag -> Sem r (Either FederationError (Set ClientInfo)) getClientInfo loc = foldQualified loc (\lusr -> fmap Right . getLocalMLSClients lusr) getRemoteMLSClients @@ -144,14 +144,14 @@ getRemoteMLSClients :: ( Member FederatorAccess r ) => Remote UserId -> - SignatureSchemeTag -> + CipherSuiteTag -> Sem r (Either FederationError (Set ClientInfo)) -getRemoteMLSClients rusr ss = do +getRemoteMLSClients rusr suite = do runFederatedEither rusr $ fedClient @'Brig @"get-mls-clients" $ MLSClientsRequest { userId = tUnqualified rusr, - signatureScheme = ss + cipherSuite = tagCipherSuite suite } -------------------------------------------------------------------------------- diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index ddcd06cefa3..b7ac03592c9 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -53,7 +53,6 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.Credential import Wire.API.MLS.Proposal qualified as Proposal @@ -83,7 +82,7 @@ processInternalCommit senderIdentity con lConvOrSub epoch action commit = do let convOrSub = tUnqualified lConvOrSub qusr = cidQualifiedUser senderIdentity cm = convOrSub.members - ss = csSignatureScheme (cnvmlsCipherSuite convOrSub.mlsMeta) + suite = cnvmlsCipherSuite convOrSub.mlsMeta newUserClients = Map.assocs (paAdd action) -- check all pending proposals are referenced in the commit @@ -145,7 +144,7 @@ processInternalCommit senderIdentity con lConvOrSub epoch action commit = do -- final set of clients in the conversation let clients = Map.keysSet (newclients <> Map.findWithDefault mempty qtarget cm) -- get list of mls clients from Brig (local or remote) - getClientInfo lConvOrSub qtarget ss >>= \case + getClientInfo lConvOrSub qtarget suite >>= \case Left _e -> pure (Just qtarget) Right clientInfo -> do let allClients = Set.map ciId clientInfo diff --git a/services/galley/src/Galley/API/MLS/IncomingMessage.hs b/services/galley/src/Galley/API/MLS/IncomingMessage.hs index 96b63cc6975..b4a8b7fb206 100644 --- a/services/galley/src/Galley/API/MLS/IncomingMessage.hs +++ b/services/galley/src/Galley/API/MLS/IncomingMessage.hs @@ -69,7 +69,7 @@ data IncomingBundle = IncomingBundle commit :: RawMLS Commit, rawMessage :: RawMLS Message, welcome :: Maybe (RawMLS Welcome), - groupInfo :: GroupInfoData, + groupInfo :: RawMLS GroupInfo, serialized :: ByteString } @@ -126,6 +126,6 @@ mkIncomingBundle bundle = do commit = commit, rawMessage = bundle.value.commitMsg, welcome = bundle.value.welcome, - groupInfo = GroupInfoData bundle.value.groupInfo.raw, + groupInfo = bundle.value.groupInfo, serialized = bundle.raw } diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index fedbcaae683..6c52f59c4da 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -74,6 +74,7 @@ import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit hiding (output) import Wire.API.MLS.CommitBundle import Wire.API.MLS.Credential @@ -204,7 +205,27 @@ postMLSCommitBundleToLocalConv :: Local ConvOrSubConvId -> Sem r [LocalConversationUpdate] postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do - lConvOrSub <- fetchConvOrSub qusr bundle.groupId ctype lConvOrSubId + lConvOrSub <- do + lConvOrSub <- fetchConvOrSub qusr bundle.groupId ctype lConvOrSubId + let convOrSub = tUnqualified lConvOrSub + giCipherSuite <- + note (mlsProtocolError "Unsupported ciphersuite") $ + cipherSuiteTag bundle.groupInfo.value.groupContext.cipherSuite + let convCipherSuite = convOrSub.mlsMeta.cnvmlsCipherSuite + -- if this is the first commit of the conversation, update ciphersuite + if (giCipherSuite == convCipherSuite) + then pure lConvOrSub + else do + unless (convOrSub.mlsMeta.cnvmlsEpoch == Epoch 0) $ + throw $ + mlsProtocolError "GroupInfo ciphersuite does not match conversation" + -- save to cassandra + case convOrSub.id of + Conv cid -> setConversationCipherSuite cid giCipherSuite + SubConv cid sub -> + setSubConversationCipherSuite cid sub giCipherSuite + pure $ fmap (convOrSubConvSetCipherSuite giCipherSuite) lConvOrSub + senderIdentity <- getSenderIdentity qusr c bundle.sender lConvOrSub (events, newClients) <- case bundle.sender of @@ -236,7 +257,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do bundle.commit.value.path pure ([], []) - storeGroupInfo (tUnqualified lConvOrSub).id bundle.groupInfo + storeGroupInfo (tUnqualified lConvOrSub).id (GroupInfoData bundle.groupInfo.raw) propagateMessage qusr (Just c) lConvOrSub conn bundle.rawMessage (tUnqualified lConvOrSub).members >>= mapM_ (throw . unreachableUsersToUnreachableBackends) diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs index fb4e9273af9..0005765d191 100644 --- a/services/galley/src/Galley/API/MLS/Proposal.hs +++ b/services/galley/src/Galley/API/MLS/Proposal.hs @@ -163,20 +163,19 @@ checkProposal :: IndexMap -> Proposal -> Sem r () -checkProposal mlsMeta im p = - case p of - AddProposal kp -> do - (cs, _lifetime) <- - either - (\msg -> throw (mlsProtocolError ("Invalid key package in Add proposal: " <> msg))) - pure - $ validateKeyPackage Nothing kp.value - -- we are not checking lifetime constraints here - unless (mlsMeta.cnvmlsCipherSuite == cs) $ - throw (mlsProtocolError "Key package ciphersuite does not match conversation") - RemoveProposal idx -> do - void $ noteS @'MLSInvalidLeafNodeIndex $ imLookup im idx - _ -> pure () +checkProposal mlsMeta im p = case p of + AddProposal kp -> do + (cs, _lifetime) <- + either + (\msg -> throw (mlsProtocolError ("Invalid key package in Add proposal: " <> msg))) + pure + $ validateKeyPackage Nothing kp.value + -- we are not checking lifetime constraints here + unless (mlsMeta.cnvmlsCipherSuite == cs) $ + throw (mlsProtocolError "Key package ciphersuite does not match conversation") + RemoveProposal idx -> do + void $ noteS @'MLSInvalidLeafNodeIndex $ imLookup im idx + _ -> pure () addProposedClient :: Member (State IndexMap) r => ClientIdentity -> Sem r ProposalAction addProposedClient cid = do @@ -248,6 +247,10 @@ processProposal qusr lConvOrSub groupId epoch pub prop = do unless (groupId == cnvmlsGroupId mlsMeta) $ throwS @'ConvNotFound let suiteTag = cnvmlsCipherSuite mlsMeta + -- Reject proposals before first commit + when (mlsMeta.cnvmlsEpoch == Epoch 0) $ + throw (mlsProtocolError "Bare proposals at epoch 0 are not supported") + -- FUTUREWORK: validate the member's conversation role checkProposal mlsMeta (tUnqualified lConvOrSub).indexMap prop.value when (isExternal pub.sender) $ checkExternalProposalUser qusr prop.value diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index 58f97bc24cf..13a14d9b6a4 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -27,7 +27,7 @@ import Data.Qualified import GHC.Records (HasField (..)) import Galley.Data.Conversation.Types import Galley.Types.Conversations.Members -import Imports +import Imports hiding (cs) import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite @@ -214,3 +214,15 @@ instance HasField "id" ConvOrSubConv ConvOrSubConvId where instance HasField "migrationState" ConvOrSubConv MLSMigrationState where getField (Conv c) = c.mcMigrationState getField (SubConv _ _) = MLSMigrationMLS + +convOrSubConvSetCipherSuite :: CipherSuiteTag -> ConvOrSubConv -> ConvOrSubConv +convOrSubConvSetCipherSuite cs (Conv c) = + Conv $ + c + { mcMLSData = (mcMLSData c) {cnvmlsCipherSuite = cs} + } +convOrSubConvSetCipherSuite cs (SubConv c s) = + SubConv c $ + s + { scMLSData = (scMLSData s) {cnvmlsCipherSuite = cs} + } diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 29eba7f8522..2d24adb63b2 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -244,6 +244,13 @@ getConvEpoch cid = updateConvEpoch :: ConvId -> Epoch -> Client () updateConvEpoch cid epoch = retry x5 $ write Cql.updateConvEpoch (params LocalQuorum (epoch, cid)) +updateConvCipherSuite :: ConvId -> CipherSuiteTag -> Client () +updateConvCipherSuite cid cs = + retry x5 $ + write + Cql.updateConvCipherSuite + (params LocalQuorum (cs, cid)) + setGroupInfo :: ConvId -> GroupInfoData -> Client () setGroupInfo conv gid = write Cql.updateGroupInfo (params LocalQuorum (gid, conv)) @@ -460,6 +467,7 @@ interpretConversationStoreToCassandra = interpret $ \case SetConversationReceiptMode cid value -> embedClient $ updateConvReceiptMode cid value SetConversationMessageTimer cid value -> embedClient $ updateConvMessageTimer cid value SetConversationEpoch cid epoch -> embedClient $ updateConvEpoch cid epoch + SetConversationCipherSuite cid cs -> embedClient $ updateConvCipherSuite cid cs DeleteConversation cid -> embedClient $ deleteConversation cid SetGroupInfo cid gib -> embedClient $ setGroupInfo cid gib AcquireCommitLock gId epoch ttl -> embedClient $ acquireCommitLock gId epoch ttl diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 3ef718aa8ec..25bd766c789 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -296,6 +296,9 @@ getConvEpoch = "select epoch from conversation where conv = ?" updateConvEpoch :: PrepQuery W (Epoch, ConvId) () updateConvEpoch = {- `IF EXISTS`, but that requires benchmarking -} "update conversation set epoch = ? where conv = ?" +updateConvCipherSuite :: PrepQuery W (CipherSuiteTag, ConvId) () +updateConvCipherSuite = "update conversation set cipher_suite = ? where conv = ?" + deleteConv :: PrepQuery W (Identity ConvId) () deleteConv = "delete from conversation using timestamp 32503680000000000 where conv = ?" @@ -338,7 +341,7 @@ deleteUserConv = "delete from user where user = ? and conv = ?" -- MLS SubConversations ----------------------------------------------------- -selectSubConversation :: PrepQuery R (ConvId, SubConvId) (CipherSuiteTag, Epoch, Writetime Epoch, GroupId) +selectSubConversation :: PrepQuery R (ConvId, SubConvId) (Maybe CipherSuiteTag, Maybe Epoch, Maybe (Writetime Epoch), Maybe GroupId) selectSubConversation = "SELECT cipher_suite, epoch, WRITETIME(epoch), group_id FROM subconversation WHERE conv_id = ? and subconv_id = ?" insertSubConversation :: PrepQuery W (ConvId, SubConvId, CipherSuiteTag, Epoch, GroupId, Maybe GroupInfoData) () @@ -356,6 +359,9 @@ selectSubConvEpoch = "SELECT epoch FROM subconversation WHERE conv_id = ? AND su insertEpochForSubConversation :: PrepQuery W (Epoch, ConvId, SubConvId) () insertEpochForSubConversation = "UPDATE subconversation set epoch = ? WHERE conv_id = ? AND subconv_id = ?" +insertCipherSuiteForSubConversation :: PrepQuery W (CipherSuiteTag, ConvId, SubConvId) () +insertCipherSuiteForSubConversation = "UPDATE subconversation set cipher_suite = ? WHERE conv_id = ? AND subconv_id = ?" + listSubConversations :: PrepQuery R (Identity ConvId) (SubConvId, CipherSuiteTag, Epoch, Writetime Epoch, GroupId) listSubConversations = "SELECT subconv_id, cipher_suite, epoch, WRITETIME(epoch), group_id FROM subconversation WHERE conv_id = ?" diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index 8a9a0287c1f..5827435aaa3 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -22,6 +22,8 @@ where import Cassandra import Cassandra.Util +import Control.Error.Util +import Control.Monad.Trans.Maybe import Data.Id import Data.Map qualified as Map import Data.Time.Clock @@ -40,24 +42,29 @@ import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation selectSubConversation :: ConvId -> SubConvId -> Client (Maybe SubConversation) -selectSubConversation convId subConvId = do - m <- retry x5 (query1 Cql.selectSubConversation (params LocalQuorum (convId, subConvId))) - for m $ \(suite, epoch, epochWritetime, groupId) -> do - (cm, im) <- lookupMLSClientLeafIndices groupId - pure $ - SubConversation - { scParentConvId = convId, - scSubConvId = subConvId, - scMLSData = - ConversationMLSData - { cnvmlsGroupId = groupId, - cnvmlsEpoch = epoch, - cnvmlsEpochTimestamp = epochTimestamp epoch epochWritetime, - cnvmlsCipherSuite = suite - }, - scMembers = cm, - scIndexMap = im - } +selectSubConversation convId subConvId = runMaybeT $ do + (mSuite, mEpoch, mEpochWritetime, mGroupId) <- + MaybeT $ + retry x5 (query1 Cql.selectSubConversation (params LocalQuorum (convId, subConvId))) + suite <- hoistMaybe mSuite + epoch <- hoistMaybe mEpoch + epochWritetime <- hoistMaybe mEpochWritetime + groupId <- hoistMaybe mGroupId + (cm, im) <- lift $ lookupMLSClientLeafIndices groupId + pure $ + SubConversation + { scParentConvId = convId, + scSubConvId = subConvId, + scMLSData = + ConversationMLSData + { cnvmlsGroupId = groupId, + cnvmlsEpoch = epoch, + cnvmlsEpochTimestamp = epochTimestamp epoch epochWritetime, + cnvmlsCipherSuite = suite + }, + scMembers = cm, + scIndexMap = im + } insertSubConversation :: ConvId -> @@ -93,6 +100,10 @@ setEpochForSubConversation :: ConvId -> SubConvId -> Epoch -> Client () setEpochForSubConversation cid sconv epoch = retry x5 (write Cql.insertEpochForSubConversation (params LocalQuorum (epoch, cid, sconv))) +setCipherSuiteForSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> Client () +setCipherSuiteForSubConversation cid sconv cs = + retry x5 (write Cql.insertCipherSuiteForSubConversation (params LocalQuorum (cs, cid, sconv))) + deleteSubConversation :: ConvId -> SubConvId -> Client () deleteSubConversation cid sconv = retry x5 $ write Cql.deleteSubConversation (params LocalQuorum (cid, sconv)) @@ -124,6 +135,7 @@ interpretSubConversationStoreToCassandra = interpret $ \case GetSubConversationEpoch convId subConvId -> embedClient (selectSubConvEpoch convId subConvId) SetSubConversationGroupInfo convId subConvId mPgs -> embedClient (updateSubConvGroupInfo convId subConvId mPgs) SetSubConversationEpoch cid sconv epoch -> embedClient $ setEpochForSubConversation cid sconv epoch + SetSubConversationCipherSuite cid sconv cs -> embedClient $ setCipherSuiteForSubConversation cid sconv cs ListSubConversations cid -> embedClient $ listSubConversations cid DeleteSubConversation convId subConvId -> embedClient $ deleteSubConversation convId subConvId diff --git a/services/galley/src/Galley/Effects/BrigAccess.hs b/services/galley/src/Galley/Effects/BrigAccess.hs index 68c4d21bd71..642a3ab4c10 100644 --- a/services/galley/src/Galley/Effects/BrigAccess.hs +++ b/services/galley/src/Galley/Effects/BrigAccess.hs @@ -123,7 +123,7 @@ data BrigAccess m a where BrigAccess m (Either AuthenticationError ClientId) RemoveLegalHoldClientFromUser :: UserId -> BrigAccess m () GetAccountConferenceCallingConfigClient :: UserId -> BrigAccess m (WithStatusNoLock ConferenceCallingConfig) - GetLocalMLSClients :: Local UserId -> SignatureSchemeTag -> BrigAccess m (Set ClientInfo) + GetLocalMLSClients :: Local UserId -> CipherSuiteTag -> BrigAccess m (Set ClientInfo) UpdateSearchVisibilityInbound :: Multi.TeamStatus SearchVisibilityInboundConfig -> BrigAccess m () diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index 3bdf6808811..cd0a2e8dce7 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -43,6 +43,7 @@ module Galley.Effects.ConversationStore setConversationReceiptMode, setConversationMessageTimer, setConversationEpoch, + setConversationCipherSuite, acceptConnectConversation, setGroupInfo, updateToMixedProtocol, @@ -96,6 +97,7 @@ data ConversationStore m a where SetConversationReceiptMode :: ConvId -> ReceiptMode -> ConversationStore m () SetConversationMessageTimer :: ConvId -> Maybe Milliseconds -> ConversationStore m () SetConversationEpoch :: ConvId -> Epoch -> ConversationStore m () + SetConversationCipherSuite :: ConvId -> CipherSuiteTag -> ConversationStore m () SetGroupInfo :: ConvId -> GroupInfoData -> ConversationStore m () AcquireCommitLock :: GroupId -> Epoch -> NominalDiffTime -> ConversationStore m LockAcquired ReleaseCommitLock :: GroupId -> Epoch -> ConversationStore m () diff --git a/services/galley/src/Galley/Effects/SubConversationStore.hs b/services/galley/src/Galley/Effects/SubConversationStore.hs index b70b1167e83..2179781b134 100644 --- a/services/galley/src/Galley/Effects/SubConversationStore.hs +++ b/services/galley/src/Galley/Effects/SubConversationStore.hs @@ -36,6 +36,7 @@ data SubConversationStore m a where GetSubConversationEpoch :: ConvId -> SubConvId -> SubConversationStore m (Maybe Epoch) SetSubConversationGroupInfo :: ConvId -> SubConvId -> Maybe GroupInfoData -> SubConversationStore m () SetSubConversationEpoch :: ConvId -> SubConvId -> Epoch -> SubConversationStore m () + SetSubConversationCipherSuite :: ConvId -> SubConvId -> CipherSuiteTag -> SubConversationStore m () ListSubConversations :: ConvId -> SubConversationStore m (Map SubConvId ConversationMLSData) DeleteSubConversation :: ConvId -> SubConvId -> SubConversationStore m () diff --git a/services/galley/src/Galley/Intra/Client.hs b/services/galley/src/Galley/Intra/Client.hs index f8890c9bc11..5907498ad60 100644 --- a/services/galley/src/Galley/Intra/Client.hs +++ b/services/galley/src/Galley/Intra/Client.hs @@ -30,7 +30,7 @@ import Bilge hiding (getHeader, options, statusCode) import Bilge.RPC import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) -import Data.ByteString.Conversion (toByteString') +import Data.ByteString.Conversion import Data.Id import Data.Misc import Data.Qualified @@ -50,6 +50,7 @@ import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P +import Servant.API import System.Logger.Class qualified as Logger import Wire.API.Error.Galley import Wire.API.MLS.CipherSuite @@ -171,8 +172,8 @@ brigAddClient uid connId client = do else pure (Left ReAuthFailed) -- | Calls 'Brig.API.Internal.getMLSClients'. -getLocalMLSClients :: Local UserId -> SignatureSchemeTag -> App (Set ClientInfo) -getLocalMLSClients lusr ss = +getLocalMLSClients :: Local UserId -> CipherSuiteTag -> App (Set ClientInfo) +getLocalMLSClients lusr suite = call Brig ( method GET @@ -182,7 +183,9 @@ getLocalMLSClients lusr ss = "clients", toByteString' (tUnqualified lusr) ] - . queryItem "sig_scheme" (toByteString' (signatureSchemeName ss)) + . queryItem + "ciphersuite" + (toHeader (tagCipherSuite suite)) . expect2xx ) >>= parseResponse (mkError status502 "server-error") diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 5318a9f549c..79e6918c630 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -28,8 +28,6 @@ import Cassandra hiding (Set) import Control.Lens (view) import Control.Lens.Extras import Control.Monad.State qualified as State -import Crypto.Error -import Crypto.PubKey.Ed25519 qualified as Ed25519 import Data.Aeson qualified as Aeson import Data.Domain import Data.Id @@ -58,12 +56,9 @@ import Wire.API.Conversation.Role import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.API.Galley -import Wire.API.MLS.AuthenticatedContent import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.Keys -import Wire.API.MLS.Message -import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Message @@ -98,13 +93,10 @@ tests s = [ test s "add user (not connected)" testAddUserNotConnected, test s "add client of existing user" testAddClientPartial, test s "add user with some non-MLS clients" testAddUserWithProteusClients, - test s "send a stale commit" testStaleCommit, test s "add remote user to a conversation" testAddRemoteUser, test s "add remote users to a conversation (some unreachable)" testAddRemotesSomeUnreachable, test s "return error when commit is locked" testCommitLock, - test s "add user to a conversation with proposal + commit" testAddUserBareProposalCommit, - test s "post commit that references an unknown proposal" testUnknownProposalRefCommit, - test s "post commit that is not referencing all proposals" testCommitNotReferencingAllProposals + test s "post commit that references an unknown proposal" testUnknownProposalRefCommit ], testGroup "External commit" @@ -141,10 +133,7 @@ tests s = ], testGroup "Proposal" - [ test s "add a new client to a non-existing conversation" propNonExistingConv, - test s "add a new client to an existing conversation" propExistingConv, - test s "add a new client in an invalid epoch" propInvalidEpoch, - test s "forward an unsupported proposal" propUnsupported + [ test s "add a new client to a non-existing conversation" propNonExistingConv ], testGroup "External Add Proposal" @@ -493,32 +482,6 @@ testProteusMessage = do >= sendAndConsumeCommitBundle - - -- now roll back alice1 and try to add the second batch of users - setClientGroupState alice1 gsBackup - - commit <- createAddCommit alice1 users2 - bundle <- createBundle commit - err <- - responseJsonError - =<< localPostCommitBundle (mpSender commit) bundle - >= traverse_ sendAndConsumeMessage - commit <- createPendingProposalCommit alice1 - void $ assertJust (mpWelcome commit) - void $ sendAndConsumeCommitBundle commit - - -- check that bob can now see the conversation - liftTest $ do - convs <- getAllConvs (ciUser bob1) - liftIO $ - assertBool - "Users added to an MLS group should find it when listing conversations" - (qcnv `elem` map cnvQualifiedId convs) - testUnknownProposalRefCommit :: TestM () testUnknownProposalRefCommit = do [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) @@ -665,33 +606,6 @@ testUnknownProposalRefCommit = do >= traverse_ sendAndConsumeMessage - - -- now create a commit referencing only the first proposal - setClientGroupState alice1 gsBackup - commit <- createPendingProposalCommit alice1 - - -- send commit and expect and error - bundle <- createBundle commit - err <- - responseJsonError - =<< localPostCommitBundle alice1 bundle - res) @?= [[]] - -propInvalidEpoch :: TestM () -propInvalidEpoch = do - users@[_alice, bob, charlie, dee] <- createAndConnectUsers (replicate 4 Nothing) - runMLSTest $ do - [alice1, bob1, charlie1, dee1] <- traverse createMLSClient users - void $ setupMLSGroup alice1 - - -- Add bob -> epoch 1 - void $ uploadNewKeyPackage bob1 - gsBackup <- getClientGroupState alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - gsBackup2 <- getClientGroupState alice1 - - -- try to send a proposal from an old epoch (0) - do - setClientGroupState alice1 gsBackup - void $ uploadNewKeyPackage dee1 - [prop] <- createAddProposals alice1 [dee] - err <- - responseJsonError - =<< postMessage alice1 (mpMessage prop) - epoch 2 - [prop] <- createAddProposals alice1 [dee] - err <- - responseJsonError - =<< postMessage alice1 (mpMessage prop) - mls {mlsNewMembers = mempty} - - -- alice send a well-formed proposal and commits it - void $ uploadNewKeyPackage dee1 - setClientGroupState alice1 gsBackup2 - createAddProposals alice1 [dee] >>= traverse_ sendAndConsumeMessage - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - -- scenario: -- alice1 creates a group and adds bob1 -- bob2 joins with external proposal (alice1 commits it) @@ -1531,38 +1389,6 @@ testPublicKeys = do ) @?= [Ed25519] ---- | The test manually reads from mls-test-cli's store and extracts a private ---- key. The key is needed for signing an unsupported proposal, which is then --- forwarded by the backend without being inspected. -propUnsupported :: TestM () -propUnsupported = do - users@[_alice, bob] <- createAndConnectUsers (replicate 2 Nothing) - runMLSTest $ do - [alice1, bob1] <- traverse createMLSClient users - void $ uploadNewKeyPackage bob1 - (gid, _) <- setupMLSGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - - (priv, pub) <- clientKeyPair alice1 - pmsg <- - liftIO . throwCryptoErrorIO $ - mkSignedPublicMessage - <$> Ed25519.secretKey priv - <*> Ed25519.publicKey pub - <*> pure gid - <*> pure (Epoch 1) - <*> pure (TaggedSenderMember 0 "foo") - <*> pure - ( FramedContentProposal - (mkRawMLS (GroupContextExtensionsProposal [])) - ) - - let msg = mkMessage (MessagePublic pmsg) - let msgData = encodeMLS' msg - - -- we cannot consume this message, because the membership tag is fake - postMessage alice1 msgData !!! const 201 === statusCode - testBackendRemoveProposalRecreateClient :: TestM () testBackendRemoveProposalRecreateClient = do alice <- randomQualifiedUser diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index ebd2d6d49fd..aca41bd50d4 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -661,6 +661,7 @@ createApplicationMessage cid messageContent = do } createAddCommitWithKeyPackages :: + HasCallStack => ClientIdentity -> [(ClientIdentity, ByteString)] -> MLSTest MessagePackage From 0acaf70ae6f9fc3dc5a329b943db5d65e2e4f0f0 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 25 Aug 2023 11:18:47 +0200 Subject: [PATCH 149/225] Support x509 credentials (#3532) * Add options to createMLSClient * Add failing test with x509 key packages * Support x509 credentials * Add CHANGELOG entry * Upgrade mls-test-cli to version supporting x509 --- changelog.d/1-api-changes/mls-x509 | 1 + integration/test/MLS/Util.hs | 30 +++++++-- integration/test/Test/MLS.hs | 64 ++++++++++---------- integration/test/Test/MLS/KeyPackage.hs | 6 +- integration/test/Test/MLS/One2One.hs | 2 +- integration/test/Test/MLS/SubConversation.hs | 6 +- libs/wire-api/src/Wire/API/MLS/Credential.hs | 33 +++++++--- libs/wire-api/src/Wire/API/MLS/KeyPackage.hs | 42 ++++++++++++- libs/wire-api/src/Wire/API/MLS/Validation.hs | 13 ++-- nix/pkgs/mls-test-cli/default.nix | 8 +-- 10 files changed, 140 insertions(+), 65 deletions(-) create mode 100644 changelog.d/1-api-changes/mls-x509 diff --git a/changelog.d/1-api-changes/mls-x509 b/changelog.d/1-api-changes/mls-x509 new file mode 100644 index 00000000000..5f07ef57782 --- /dev/null +++ b/changelog.d/1-api-changes/mls-x509 @@ -0,0 +1 @@ +Key packages and leaf nodes with x509 credentials are now supported diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 969e954f6ed..bff7fbf29f1 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -128,18 +128,36 @@ createWireClient u = do c <- addClient u def {lastPrekey = Just lpk} >>= getJSON 201 mkClientIdentity u c -initMLSClient :: HasCallStack => ClientIdentity -> App () -initMLSClient cid = do +data CredentialType = BasicCredentialType | X509CredentialType + +instance MakesValue CredentialType where + make BasicCredentialType = make "basic" + make X509CredentialType = make "x509" + +instance HasTests x => HasTests (CredentialType -> x) where + mkTests m n s f x = + mkTests m (n <> "[ctype=basic]") s f (x BasicCredentialType) + <> mkTests m (n <> "[ctype=x509]") s f (x X509CredentialType) + +data InitMLSClient = InitMLSClient + {credType :: CredentialType} + +instance Default InitMLSClient where + def = InitMLSClient {credType = BasicCredentialType} + +initMLSClient :: HasCallStack => InitMLSClient -> ClientIdentity -> App () +initMLSClient opts cid = do bd <- getBaseDir mls <- getMLSState liftIO $ createDirectory (bd cid2Str cid) - void $ mlscli cid ["init", "--ciphersuite", mls.ciphersuite.code, cid2Str cid] Nothing + ctype <- make opts.credType & asString + void $ mlscli cid ["init", "--ciphersuite", mls.ciphersuite.code, "-t", ctype, cid2Str cid] Nothing -- | Create new mls client and register with backend. -createMLSClient :: (MakesValue u, HasCallStack) => u -> App ClientIdentity -createMLSClient u = do +createMLSClient :: (MakesValue u, HasCallStack) => InitMLSClient -> u -> App ClientIdentity +createMLSClient opts u = do cid <- createWireClient u - initMLSClient cid + initMLSClient opts cid -- set public key pkey <- mlscli cid ["public-key"] Nothing diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index bbab5479dcd..82de95fa190 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -14,7 +14,7 @@ import Testlib.Prelude testSendMessageNoReturnToSender :: HasCallStack => App () testSendMessageNoReturnToSender = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, alice2, bob1, bob2] <- traverse createMLSClient [alice, alice, bob, bob] + [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def) [alice, alice, bob, bob] traverse_ uploadNewKeyPackage [alice2, bob1, bob2] void $ createNewGroup alice1 void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle @@ -43,7 +43,7 @@ testStaleApplicationMessage :: HasCallStack => Domain -> App () testStaleApplicationMessage otherDomain = do [alice, bob, charlie, dave, eve] <- createAndConnectUsers [OwnDomain, otherDomain, OwnDomain, OwnDomain, OwnDomain] - [alice1, bob1, charlie1] <- traverse createMLSClient [alice, bob, charlie] + [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] traverse_ uploadNewKeyPackage [bob1, charlie1] void $ createNewGroup alice1 @@ -130,7 +130,7 @@ testMixedProtocolAddUsers secondDomain = do bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do resp.status `shouldMatchInt` 200 - [alice1, bob1] <- traverse createMLSClient [alice, bob] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] bindResponse (getConversation alice qcnv) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -158,7 +158,7 @@ testMixedProtocolUserLeaves secondDomain = do bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do resp.status `shouldMatchInt` 200 - [alice1, bob1] <- traverse createMLSClient [alice, bob] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] bindResponse (getConversation alice qcnv) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -193,7 +193,7 @@ testMixedProtocolAddPartialClients secondDomain = do bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do resp.status `shouldMatchInt` 200 - [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] bindResponse (getConversation alice qcnv) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -231,7 +231,7 @@ testMixedProtocolRemovePartialClients secondDomain = do bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do resp.status `shouldMatchInt` 200 - [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] bindResponse (getConversation alice qcnv) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -256,7 +256,7 @@ testMixedProtocolAppMessagesAreDenied secondDomain = do bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do resp.status `shouldMatchInt` 200 - [alice1, bob1] <- traverse createMLSClient [alice, bob] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [bob1] @@ -277,7 +277,7 @@ testMLSProtocolUpgrade secondDomain = do charlie <- randomUser OwnDomain def -- alice creates MLS group and bob joins - [alice1, bob1, charlie1] <- traverse createMLSClient [alice, bob, charlie] + [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] createGroup alice1 conv void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle @@ -311,12 +311,12 @@ testMLSProtocolUpgrade secondDomain = do resp.status `shouldMatchInt` 200 resp.json %. "protocol" `shouldMatch` "mls" -testAddUserSimple :: HasCallStack => Ciphersuite -> App () -testAddUserSimple suite = do +testAddUserSimple :: HasCallStack => Ciphersuite -> CredentialType -> App () +testAddUserSimple suite ctype = do setMLSCiphersuite suite - [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + [alice1, bob1, bob2] <- traverse (createMLSClient def {credType = ctype}) [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] (_, qcnv) <- createNewGroup alice1 @@ -343,7 +343,7 @@ testAddUserSimple suite = do testRemoteAddUser :: HasCallStack => App () testRemoteAddUser = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OwnDomain] - [alice1, bob1, charlie1] <- traverse createMLSClient [alice, bob, charlie] + [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] traverse_ uploadNewKeyPackage [bob1, charlie1] (_, conv) <- createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle @@ -360,7 +360,7 @@ testRemoteAddUser = do testRemoteRemoveClient :: HasCallStack => App () testRemoteRemoveClient = do [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] - [alice1, bob1] <- traverse createMLSClient [alice, bob] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] void $ uploadNewKeyPackage bob1 (_, conv) <- createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle @@ -381,7 +381,7 @@ testCreateSubConv :: HasCallStack => Ciphersuite -> App () testCreateSubConv suite = do setMLSCiphersuite suite alice <- randomUser OwnDomain def - alice1 <- createMLSClient alice + alice1 <- createMLSClient def alice (_, conv) <- createNewGroup alice1 bindResponse (getSubConversation alice conv "conference") $ \resp -> do resp.status `shouldMatchInt` 200 @@ -403,7 +403,7 @@ testCreateSubConvProteus = do testSelfConversation :: App () testSelfConversation = do alice <- randomUser OwnDomain def - creator : others <- traverse createMLSClient (replicate 3 alice) + creator : others <- traverse (createMLSClient def) (replicate 3 alice) traverse_ uploadNewKeyPackage others (_, cnv) <- createSelfGroup creator commit <- createAddCommit creator [alice] @@ -421,7 +421,7 @@ testSelfConversation = do testJoinSubConv :: App () testJoinSubConv = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] (_, qcnv) <- createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle @@ -445,7 +445,7 @@ testDeleteParentOfSubConv secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - [alice1, bob1] <- traverse createMLSClient [alice, bob] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [alice1, bob1] (_, qcnv) <- createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle @@ -494,7 +494,7 @@ testFirstCommitAllowsPartialAdds :: HasCallStack => App () testFirstCommitAllowsPartialAdds = do alice <- randomUser OwnDomain def - [alice1, alice2, alice3] <- traverse createMLSClient [alice, alice, alice] + [alice1, alice2, alice3] <- traverse (createMLSClient def) [alice, alice, alice] traverse_ uploadNewKeyPackage [alice1, alice2, alice2, alice3, alice3] (_, _qcnv) <- createNewGroup alice1 @@ -513,9 +513,9 @@ testAddUserPartial = do [alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) -- Bob has 3 clients, Charlie has 2 - alice1 <- createMLSClient alice - bobClients@[_bob1, _bob2, bob3] <- replicateM 3 (createMLSClient bob) - charlieClients <- replicateM 2 (createMLSClient charlie) + alice1 <- createMLSClient def alice + bobClients@[_bob1, _bob2, bob3] <- replicateM 3 (createMLSClient def bob) + charlieClients <- replicateM 2 (createMLSClient def charlie) -- Only the first 2 clients of Bob's have uploaded key packages traverse_ uploadNewKeyPackage (take 2 bobClients <> charlieClients) @@ -540,7 +540,7 @@ testRemoveClientsIncomplete :: HasCallStack => App () testRemoveClientsIncomplete = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] void $ createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle @@ -552,7 +552,7 @@ testRemoveClientsIncomplete = do testAdminRemovesUserFromConv :: HasCallStack => App () testAdminRemovesUserFromConv = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] void $ createWireClient bob traverse_ uploadNewKeyPackage [bob1, bob2] (gid, qcnv) <- createNewGroup alice1 @@ -582,7 +582,7 @@ testLocalWelcome :: HasCallStack => App () testLocalWelcome = do users@[alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1] <- traverse createMLSClient users + [alice1, bob1] <- traverse (createMLSClient def) users void $ uploadNewKeyPackage bob1 @@ -613,7 +613,7 @@ testStaleCommit = do (alice : users) <- createAndConnectUsers (replicate 5 OwnDomain) let (users1, users2) = splitAt 2 users - (alice1 : clients) <- traverse createMLSClient (alice : users) + (alice1 : clients) <- traverse (createMLSClient def) (alice : users) traverse_ uploadNewKeyPackage clients void $ createNewGroup alice1 @@ -633,7 +633,7 @@ testStaleCommit = do testPropInvalidEpoch :: HasCallStack => App () testPropInvalidEpoch = do users@[_alice, bob, charlie, dee] <- createAndConnectUsers (replicate 4 OwnDomain) - [alice1, bob1, charlie1, dee1] <- traverse createMLSClient users + [alice1, bob1, charlie1, dee1] <- traverse (createMLSClient def) users void $ createNewGroup alice1 -- Add bob -> epoch 1 @@ -675,7 +675,7 @@ testPropInvalidEpoch = do testPropUnsupported :: HasCallStack => App () testPropUnsupported = do users@[_alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse createMLSClient users + [alice1, bob1] <- traverse (createMLSClient def) users void $ uploadNewKeyPackage bob1 void $ createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle @@ -688,7 +688,7 @@ testPropUnsupported = do testAddUserBareProposalCommit :: HasCallStack => App () testAddUserBareProposalCommit = do [alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse createMLSClient [alice, bob] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] (_, qcnv) <- createNewGroup alice1 void $ uploadNewKeyPackage bob1 void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle @@ -710,7 +710,7 @@ testAddUserBareProposalCommit = do testPropExistingConv :: HasCallStack => App () testPropExistingConv = do [alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse createMLSClient [alice, bob] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] void $ uploadNewKeyPackage bob1 void $ createNewGroup alice1 void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle @@ -721,7 +721,7 @@ testCommitNotReferencingAllProposals :: HasCallStack => App () testCommitNotReferencingAllProposals = do users@[_alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) - [alice1, bob1, charlie1] <- traverse createMLSClient users + [alice1, bob1, charlie1] <- traverse (createMLSClient def) users void $ createNewGroup alice1 traverse_ uploadNewKeyPackage [bob1, charlie1] void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle @@ -745,7 +745,7 @@ testUnsupportedCiphersuite :: HasCallStack => App () testUnsupportedCiphersuite = do setMLSCiphersuite (Ciphersuite "0x0002") alice <- randomUser OwnDomain def - alice1 <- createMLSClient alice + alice1 <- createMLSClient def alice void $ createNewGroup alice1 mp <- createPendingProposalCommit alice1 diff --git a/integration/test/Test/MLS/KeyPackage.hs b/integration/test/Test/MLS/KeyPackage.hs index 40760527d1c..2da06e14a98 100644 --- a/integration/test/Test/MLS/KeyPackage.hs +++ b/integration/test/Test/MLS/KeyPackage.hs @@ -8,7 +8,7 @@ import Testlib.Prelude testDeleteKeyPackages :: App () testDeleteKeyPackages = do alice <- randomUser OwnDomain def - alice1 <- createMLSClient alice + alice1 <- createMLSClient def alice kps <- replicateM 3 (uploadNewKeyPackage alice1) -- add an extra non-existing key package to the delete request @@ -24,7 +24,7 @@ testDeleteKeyPackages = do testKeyPackageMultipleCiphersuites :: App () testKeyPackageMultipleCiphersuites = do alice <- randomUser OwnDomain def - [alice1, alice2] <- replicateM 2 (createMLSClient alice) + [alice1, alice2] <- replicateM 2 (createMLSClient def alice) kp <- uploadNewKeyPackage alice2 @@ -51,7 +51,7 @@ testUnsupportedCiphersuite :: HasCallStack => App () testUnsupportedCiphersuite = do setMLSCiphersuite (Ciphersuite "0x0002") bob <- randomUser OwnDomain def - bob1 <- createMLSClient bob + bob1 <- createMLSClient def bob (kp, _) <- generateKeyPackage bob1 bindResponse (uploadKeyPackage bob1 kp) $ \resp -> do resp.status `shouldMatchInt` 400 diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index a7cf895498e..a7de9fe5837 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -71,7 +71,7 @@ testMLSOne2One scenario = do let otherDomain = one2OneScenarioDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain - [alice1, bob1] <- traverse createMLSClient [alice, bob] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [bob1] conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 0524560e640..e86c01b4129 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -8,7 +8,7 @@ import Testlib.Prelude testJoinSubConv :: App () testJoinSubConv = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] (_, qcnv) <- createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle @@ -32,7 +32,7 @@ testDeleteParentOfSubConv secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - [alice1, bob1] <- traverse createMLSClient [alice, bob] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [alice1, bob1] (_, qcnv) <- createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle @@ -80,7 +80,7 @@ testDeleteSubConversation :: HasCallStack => Domain -> App () testDeleteSubConversation otherDomain = do [alice, bob] <- createAndConnectUsers [OwnDomain, otherDomain] charlie <- randomUser OwnDomain def - [alice1, bob1] <- traverse createMLSClient [alice, bob] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] void $ uploadNewKeyPackage bob1 (_, qcnv) <- createNewGroup alice1 void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle diff --git a/libs/wire-api/src/Wire/API/MLS/Credential.hs b/libs/wire-api/src/Wire/API/MLS/Credential.hs index 2a10d9516ea..ecfda8810ba 100644 --- a/libs/wire-api/src/Wire/API/MLS/Credential.hs +++ b/libs/wire-api/src/Wire/API/MLS/Credential.hs @@ -28,6 +28,8 @@ import Data.Binary.Get import Data.Binary.Parser import Data.Binary.Parser.Char8 import Data.Binary.Put +import Data.ByteString.Base64.URL qualified as B64URL +import Data.ByteString.Lazy qualified as L import Data.Domain import Data.Id import Data.Qualified @@ -36,7 +38,6 @@ import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.UUID -import GHC.Records import Imports import Web.HttpApiData import Wire.API.MLS.Serialisation @@ -44,14 +45,12 @@ import Wire.Arbitrary -- | An MLS credential. -- --- Only the @BasicCredential@ type is supported. -- https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-5.3-3 -data Credential = BasicCredential ByteString +data Credential = BasicCredential ByteString | X509Credential [ByteString] deriving stock (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform Credential -data CredentialTag where - BasicCredentialTag :: CredentialTag +data CredentialTag = BasicCredentialTag | X509CredentialTag deriving stock (Enum, Bounded, Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform CredentialTag) @@ -67,17 +66,21 @@ instance ParseMLS Credential where BasicCredentialTag -> BasicCredential <$> parseMLSBytes @VarInt + X509CredentialTag -> + X509Credential + <$> parseMLSVector @VarInt (parseMLSBytes @VarInt) instance SerialiseMLS Credential where serialiseMLS (BasicCredential i) = do serialiseMLS BasicCredentialTag serialiseMLSBytes @VarInt i + serialiseMLS (X509Credential certs) = do + serialiseMLS X509CredentialTag + serialiseMLSVector @VarInt (serialiseMLSBytes @VarInt) certs credentialTag :: Credential -> CredentialTag -credentialTag BasicCredential {} = BasicCredentialTag - -instance HasField "identityData" Credential ByteString where - getField (BasicCredential i) = i +credentialTag (BasicCredential _) = BasicCredentialTag +credentialTag (X509Credential _) = X509CredentialTag data ClientIdentity = ClientIdentity { ciDomain :: Domain, @@ -132,6 +135,18 @@ instance ParseMLS ClientIdentity where either fail pure . (mkDomain . T.pack) =<< many' anyChar pure $ ClientIdentity dom uid cid +parseX509ClientIdentity :: Get ClientIdentity +parseX509ClientIdentity = do + b64uuid <- getByteString 22 + uidBytes <- either fail pure $ B64URL.decodeUnpadded b64uuid + uid <- maybe (fail "Invalid UUID") (pure . Id) $ fromByteString (L.fromStrict uidBytes) + char '/' + cid <- newClientId <$> hexadecimal + char '@' + dom <- + either fail pure . (mkDomain . T.pack) =<< many' anyChar + pure $ ClientIdentity dom uid cid + instance SerialiseMLS ClientIdentity where serialiseMLS cid = do putByteString $ toASCIIBytes (toUUID (ciUser cid)) diff --git a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs index a0cd183109f..568943c0262 100644 --- a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs +++ b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs @@ -23,6 +23,7 @@ module Wire.API.MLS.KeyPackage KeyPackageData (..), DeleteKeyPackages (..), KeyPackage (..), + credentialIdentity, keyPackageIdentity, kpRef, kpRef', @@ -35,6 +36,7 @@ import Cassandra.CQL hiding (Set) import Control.Applicative import Control.Lens hiding (set, (.=)) import Data.Aeson (FromJSON, ToJSON) +import Data.Bifunctor import Data.ByteString.Lazy qualified as LBS import Data.Id import Data.Json.Util @@ -42,6 +44,9 @@ import Data.Qualified import Data.Range import Data.Schema hiding (HasField) import Data.Swagger qualified as S +import Data.Text qualified as T +import Data.Text.Encoding qualified as T +import Data.X509 qualified as X509 import GHC.Records import Imports hiding (cs) import Test.QuickCheck @@ -226,8 +231,43 @@ instance HasField "extensions" KeyPackage [Extension] where instance HasField "leafNode" KeyPackage LeafNode where getField = (.tbs.value.leafNode) +credentialIdentity :: Credential -> Either Text ClientIdentity +credentialIdentity (BasicCredential i) = decodeMLS' i +credentialIdentity (X509Credential certs) = do + bs <- case certs of + [] -> Left "Invalid x509 certificate chain" + (c : _) -> pure c + signed <- + first (\e -> "Failed to decode x509 certificate: " <> T.pack e) $ + X509.decodeSignedCertificate bs + -- FUTUREWORK: verify signature + let cert = X509.getCertificate signed + certificateIdentity cert + keyPackageIdentity :: KeyPackage -> Either Text ClientIdentity -keyPackageIdentity = decodeMLS' @ClientIdentity . (.leafNode.credential.identityData) +keyPackageIdentity kp = credentialIdentity kp.leafNode.credential + +certificateIdentity :: X509.Certificate -> Either Text ClientIdentity +certificateIdentity cert = + let getNames (X509.ExtSubjectAltName names) = names + getURI (X509.AltNameURI u) = Just u + getURI _ = Nothing + altNames = maybe [] getNames (X509.extensionGet (X509.certExtensions cert)) + ids = map sanIdentity (mapMaybe getURI altNames) + in case partitionEithers ids of + (_, (cid : _)) -> pure cid + ((e : _), []) -> Left e + _ -> Left "No SAN URIs found" + +sanIdentity :: String -> Either Text ClientIdentity +sanIdentity s = case break (== '=') s of + ("im:wireapp", '=' : s') -> + first (\e -> e <> " (while parsing identity string " <> T.pack (show s') <> ")") + . decodeMLSWith' parseX509ClientIdentity + . T.encodeUtf8 + . T.pack + $ s' + _ -> Left "No im:wireapp label found" rawKeyPackageSchema :: ValueSchema NamedSwaggerDoc (RawMLS KeyPackage) rawKeyPackageSchema = diff --git a/libs/wire-api/src/Wire/API/MLS/Validation.hs b/libs/wire-api/src/Wire/API/MLS/Validation.hs index eadc3442f27..f97e7fc0218 100644 --- a/libs/wire-api/src/Wire/API/MLS/Validation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Validation.hs @@ -99,13 +99,14 @@ validateLeafNode cs mIdentity extra leafNode = do validateCredential mIdentity leafNode.credential validateSource extra.tag leafNode.source - validateCapabilities leafNode.capabilities + validateCapabilities (credentialTag leafNode.credential) leafNode.capabilities validateCredential :: Maybe ClientIdentity -> Credential -> Either Text () -validateCredential mIdentity (BasicCredential cred) = do +validateCredential mIdentity cred = do + -- FUTUREWORK: check signature in the case of an x509 credential identity <- either credentialError pure $ - decodeMLS' cred + credentialIdentity cred unless (maybe True (identity ==) mIdentity) $ Left "client identity does not match credential identity" where @@ -126,7 +127,7 @@ validateSource t s = do <> t'.name <> "'" -validateCapabilities :: Capabilities -> Either Text () -validateCapabilities caps = - unless (fromMLSEnum BasicCredentialTag `elem` caps.credentials) $ +validateCapabilities :: CredentialTag -> Capabilities -> Either Text () +validateCapabilities ctag caps = + unless (fromMLSEnum ctag `elem` caps.credentials) $ Left "missing BasicCredential capability" diff --git a/nix/pkgs/mls-test-cli/default.nix b/nix/pkgs/mls-test-cli/default.nix index 1e38ca6039c..bbaab19f303 100644 --- a/nix/pkgs/mls-test-cli/default.nix +++ b/nix/pkgs/mls-test-cli/default.nix @@ -13,8 +13,8 @@ let src = fetchFromGitHub { owner = "wireapp"; repo = "mls-test-cli"; - rev = "cc815d71a1d9485265b7ae158daf7b27badedee6"; - sha256 = "sha256-CJoc20pOtsxAQNCA3qhv8NtPbzZ4yCIMvuhlgcqPrds="; + rev = "d16b4e9d4e93b731e81cd04a00620f2c6a36e696"; + sha256 = "sha256-2p5m6R80dnyJShAvjmO+ZbX8wxMtuFmvPnp9uX4eezc="; }; cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/Cargo.lock"); in rustPlatform.buildRustPackage rec { @@ -24,8 +24,8 @@ in rustPlatform.buildRustPackage rec { cargoLock = { lockFile = cargoLockFile; outputHashes = { - "hpke-0.10.0" = "sha256-6zyTb2c2DU4mXn9vRQe+lXNaeQ3JOVUz+BS15Xb2E+Y="; - "openmls-0.20.2" = "sha256-QgQb5Ts8TB2nwfxMss4qHCz096ijMXBxyq7q2ITyEGg="; + "hpke-0.10.0" = "sha256-T1+BFwX6allljNZ/8T3mrWhOejnUU27BiWQetqU+0fY="; + "openmls-1.0.0" = "sha256-s1ejM/aicFGvsKY7ajEun1Mc645/k8QVrE8YSbyD3Fg="; "safe_pqc_kyber-0.6.0" = "sha256-Ch1LA+by+ezf5RV0LDSQGC1o+IWKXk8IPvkwSrAos68="; "tls_codec-0.3.0" = "sha256-IO6tenXKkC14EoUDp/+DtFNOVzDfOlLu8K1EJI7sOzs="; }; From a1d91026ff8e7b644e94d619d902f50d31a3d56e Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 30 Aug 2023 10:21:25 +0200 Subject: [PATCH 150/225] Validate public key in an x509 credential (#3542) --- changelog.d/2-features/mls-x509-improvements | 1 + libs/wire-api/src/Wire/API/MLS/KeyPackage.hs | 18 +++++++++--------- libs/wire-api/src/Wire/API/MLS/Validation.hs | 19 ++++++++++++++----- nix/pkgs/mls-test-cli/default.nix | 4 ++-- 4 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 changelog.d/2-features/mls-x509-improvements diff --git a/changelog.d/2-features/mls-x509-improvements b/changelog.d/2-features/mls-x509-improvements new file mode 100644 index 00000000000..36e3d457df8 --- /dev/null +++ b/changelog.d/2-features/mls-x509-improvements @@ -0,0 +1 @@ +The public key in an x509 credential is now checked against that of the client diff --git a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs index 568943c0262..c3bc1855b69 100644 --- a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs +++ b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs @@ -23,7 +23,7 @@ module Wire.API.MLS.KeyPackage KeyPackageData (..), DeleteKeyPackages (..), KeyPackage (..), - credentialIdentity, + credentialIdentityAndKey, keyPackageIdentity, kpRef, kpRef', @@ -231,9 +231,9 @@ instance HasField "extensions" KeyPackage [Extension] where instance HasField "leafNode" KeyPackage LeafNode where getField = (.tbs.value.leafNode) -credentialIdentity :: Credential -> Either Text ClientIdentity -credentialIdentity (BasicCredential i) = decodeMLS' i -credentialIdentity (X509Credential certs) = do +credentialIdentityAndKey :: Credential -> Either Text (ClientIdentity, Maybe X509.PubKey) +credentialIdentityAndKey (BasicCredential i) = (,) <$> decodeMLS' i <*> pure Nothing +credentialIdentityAndKey (X509Credential certs) = do bs <- case certs of [] -> Left "Invalid x509 certificate chain" (c : _) -> pure c @@ -242,20 +242,20 @@ credentialIdentity (X509Credential certs) = do X509.decodeSignedCertificate bs -- FUTUREWORK: verify signature let cert = X509.getCertificate signed - certificateIdentity cert + certificateIdentityAndKey cert keyPackageIdentity :: KeyPackage -> Either Text ClientIdentity -keyPackageIdentity kp = credentialIdentity kp.leafNode.credential +keyPackageIdentity kp = fst <$> credentialIdentityAndKey kp.leafNode.credential -certificateIdentity :: X509.Certificate -> Either Text ClientIdentity -certificateIdentity cert = +certificateIdentityAndKey :: X509.Certificate -> Either Text (ClientIdentity, Maybe X509.PubKey) +certificateIdentityAndKey cert = let getNames (X509.ExtSubjectAltName names) = names getURI (X509.AltNameURI u) = Just u getURI _ = Nothing altNames = maybe [] getNames (X509.extensionGet (X509.certExtensions cert)) ids = map sanIdentity (mapMaybe getURI altNames) in case partitionEithers ids of - (_, (cid : _)) -> pure cid + (_, (cid : _)) -> pure (cid, Just (X509.certPubKey cert)) ((e : _), []) -> Left e _ -> Left "No SAN URIs found" diff --git a/libs/wire-api/src/Wire/API/MLS/Validation.hs b/libs/wire-api/src/Wire/API/MLS/Validation.hs index f97e7fc0218..2f98d969426 100644 --- a/libs/wire-api/src/Wire/API/MLS/Validation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Validation.hs @@ -23,9 +23,12 @@ module Wire.API.MLS.Validation where import Control.Applicative +import Control.Error.Util +import Data.ByteArray qualified as BA import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Builder qualified as LT import Data.Text.Lazy.Builder.Int qualified as LT +import Data.X509 qualified as X509 import Imports hiding (cs) import Wire.API.MLS.Capabilities import Wire.API.MLS.CipherSuite @@ -97,16 +100,17 @@ validateLeafNode cs mIdentity extra leafNode = do ) $ Left "Invalid LeafNode signature" - validateCredential mIdentity leafNode.credential + validateCredential cs leafNode.signatureKey mIdentity leafNode.credential validateSource extra.tag leafNode.source validateCapabilities (credentialTag leafNode.credential) leafNode.capabilities -validateCredential :: Maybe ClientIdentity -> Credential -> Either Text () -validateCredential mIdentity cred = do +validateCredential :: CipherSuiteTag -> ByteString -> Maybe ClientIdentity -> Credential -> Either Text () +validateCredential cs pkey mIdentity cred = do -- FUTUREWORK: check signature in the case of an x509 credential - identity <- + (identity, mkey) <- either credentialError pure $ - credentialIdentity cred + credentialIdentityAndKey cred + traverse_ (validateCredentialKey (csSignatureScheme cs) pkey) mkey unless (maybe True (identity ==) mIdentity) $ Left "client identity does not match credential identity" where @@ -114,6 +118,11 @@ validateCredential mIdentity cred = do Left $ "Failed to parse identity: " <> e +validateCredentialKey :: SignatureSchemeTag -> ByteString -> X509.PubKey -> Either Text () +validateCredentialKey Ed25519 pk1 (X509.PubKeyEd25519 pk2) = + note "Certificate public key does not match client's" $ guard (pk1 == BA.convert pk2) +validateCredentialKey _ _ _ = Left "Certificate signature scheme does not match client's public key" + validateSource :: LeafNodeSourceTag -> LeafNodeSource -> Either Text () validateSource t s = do let t' = leafNodeSourceTag s diff --git a/nix/pkgs/mls-test-cli/default.nix b/nix/pkgs/mls-test-cli/default.nix index bbaab19f303..7cd0852fd46 100644 --- a/nix/pkgs/mls-test-cli/default.nix +++ b/nix/pkgs/mls-test-cli/default.nix @@ -13,8 +13,8 @@ let src = fetchFromGitHub { owner = "wireapp"; repo = "mls-test-cli"; - rev = "d16b4e9d4e93b731e81cd04a00620f2c6a36e696"; - sha256 = "sha256-2p5m6R80dnyJShAvjmO+ZbX8wxMtuFmvPnp9uX4eezc="; + rev = "e6e6ce0c29f0e48e84b4ccef058130aca0625492"; + sha256 = "sha256-J9M8w3GJnULH3spKEuPGCL/t43zb2Wd+YfZ0LY3YITo="; }; cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/Cargo.lock"); in rustPlatform.buildRustPackage rec { From d1f026f50829fb8299f14d3e4514e981c790b5a1 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 14 Sep 2023 11:07:33 +0200 Subject: [PATCH 151/225] Remove MLS endpoints from API v4 (#3583) This commit removes some remaining MLS endpoints (only present in the mls branch) from v4 and regenerates the v4 swagger documentation. --- .../API/Routes/Public/Galley/Conversation.hs | 2 + services/brig/docs/swagger-v4.json | 21935 ++++++++++++++++ 2 files changed, 21937 insertions(+) create mode 100644 services/brig/docs/swagger-v4.json diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 2e8e6a99eee..bab416e2aeb 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -608,6 +608,7 @@ type ConversationAPI = :<|> Named "get-one-to-one-mls-conversation" ( Summary "Get an MLS 1:1 conversation" + :> From 'V5 :> ZLocalUser :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected @@ -1249,6 +1250,7 @@ type ConversationAPI = :<|> Named "update-conversation-protocol" ( Summary "Update the protocol of the conversation" + :> From 'V5 :> Description "**Note**: Only proteus->mixed upgrade is supported." :> CanThrow 'ConvNotFound :> CanThrow 'ConvInvalidProtocolTransition diff --git a/services/brig/docs/swagger-v4.json b/services/brig/docs/swagger-v4.json new file mode 100644 index 00000000000..86a7f633321 --- /dev/null +++ b/services/brig/docs/swagger-v4.json @@ -0,0 +1,21935 @@ +{ + "definitions": { + "": { + "description": "Username to use for authenticating against the given TURN servers", + "type": "string" + }, + "ASCII": { + "description": "Stable conversation identifier", + "maxLength": 20, + "minLength": 20, + "type": "string" + }, + "Access": { + "description": "How users can join conversations", + "enum": [ + "private", + "invite", + "link", + "code" + ], + "type": "string" + }, + "AccessRole": { + "description": "Which users/services can join conversations. This replaces legacy access roles and allows a more fine grained configuration of access roles, and in particular a separation of guest and services access.\n\nThis field is optional. If it is not present, the default will be `[team_member, non_team_member, service]`. Please note that an empty list is not allowed when creating a new conversation.", + "enum": [ + "team_member", + "non_team_member", + "guest", + "service" + ], + "type": "string" + }, + "AccessRoleLegacy": { + "description": "Deprecated, please use access_role_v2", + "enum": [ + "private", + "team", + "activated", + "non_activated" + ], + "type": "string" + }, + "AccessToken": { + "properties": { + "access_token": { + "description": "The opaque access token string", + "type": "string" + }, + "expires_in": { + "description": "The number of seconds this token is valid", + "type": "integer" + }, + "token_type": { + "$ref": "#/definitions/TokenType" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "access_token", + "token_type", + "expires_in" + ], + "type": "object" + }, + "AccessTokenType": { + "enum": [ + "DPoP" + ], + "type": "string" + }, + "Action": { + "enum": [ + "add_conversation_member", + "remove_conversation_member", + "modify_conversation_name", + "modify_conversation_message_timer", + "modify_conversation_receipt_mode", + "modify_conversation_access", + "modify_other_conversation_member", + "leave_conversation", + "delete_conversation" + ], + "type": "string" + }, + "Activate": { + "description": "Data for an activation request.", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "dryrun": { + "description": "At least one of key, email, or phone has to be present while key takes precedence over email, and email takes precedence over phone. Whether to perform a dryrun, i.e. to only check whether activation would succeed. Dry-runs never issue access cookies or tokens on success but failures still count towards the maximum failure count.", + "type": "boolean" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "required": [ + "code", + "dryrun" + ], + "type": "object" + }, + "ActivationResponse": { + "description": "Response body of a successful activation request", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "first": { + "description": "Whether this is the first successful activation (i.e. account activation).", + "type": "boolean" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "sso_id": { + "$ref": "#/definitions/UserSSOId" + } + }, + "type": "object" + }, + "AddBot": { + "properties": { + "locale": { + "$ref": "#/definitions/Locale" + }, + "provider": { + "$ref": "#/definitions/UUID" + }, + "service": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "provider", + "service" + ], + "type": "object" + }, + "AddBotResponse": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "client": { + "$ref": "#/definitions/ClientId" + }, + "event": { + "$ref": "#/definitions/Event" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "client", + "name", + "accent_id", + "assets", + "event" + ], + "type": "object" + }, + "AllFeatureConfigs": { + "properties": { + "appLock": { + "$ref": "#/definitions/AppLockConfig.WithStatus" + }, + "classifiedDomains": { + "$ref": "#/definitions/ClassifiedDomainsConfig.WithStatus" + }, + "conferenceCalling": { + "$ref": "#/definitions/ConferenceCallingConfig.WithStatus" + }, + "conversationGuestLinks": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + }, + "digitalSignatures": { + "$ref": "#/definitions/DigitalSignaturesConfig.WithStatus" + }, + "exposeInvitationURLsToTeamAdmin": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + }, + "fileSharing": { + "$ref": "#/definitions/FileSharingConfig.WithStatus" + }, + "legalhold": { + "$ref": "#/definitions/LegalholdConfig.WithStatus" + }, + "mls": { + "$ref": "#/definitions/MLSConfig.WithStatus" + }, + "mlsE2EId": { + "$ref": "#/definitions/MlsE2EIdConfig.WithStatus" + }, + "mlsMigration": { + "$ref": "#/definitions/MlsMigration.WithStatus" + }, + "outlookCalIntegration": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatus" + }, + "searchVisibility": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatus" + }, + "searchVisibilityInbound": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatus" + }, + "selfDeletingMessages": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatus" + }, + "sndFactorPasswordChallenge": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatus" + }, + "sso": { + "$ref": "#/definitions/SSOConfig.WithStatus" + }, + "validateSAMLemails": { + "$ref": "#/definitions/ValidateSAMLEmailsConfig.WithStatus" + } + }, + "required": [ + "legalhold", + "sso", + "searchVisibility", + "searchVisibilityInbound", + "validateSAMLemails", + "digitalSignatures", + "appLock", + "fileSharing", + "classifiedDomains", + "conferenceCalling", + "selfDeletingMessages", + "conversationGuestLinks", + "sndFactorPasswordChallenge", + "mls", + "exposeInvitationURLsToTeamAdmin", + "outlookCalIntegration", + "mlsE2EId", + "mlsMigration" + ], + "type": "object" + }, + "Alpha": { + "enum": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "UYI", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBA", + "XBB", + "XBC", + "XBD", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "XSU", + "XTS", + "XUA", + "XXX", + "YER", + "ZAR", + "ZMW", + "ZWL" + ], + "type": "string" + }, + "AppLockConfig": { + "properties": { + "enforceAppLock": { + "type": "boolean" + }, + "inactivityTimeoutSecs": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforceAppLock", + "inactivityTimeoutSecs" + ], + "type": "object" + }, + "AppLockConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/AppLockConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "AppLockConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/definitions/AppLockConfig" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "ApproveLegalHoldForUserRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "Asset": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "expires": { + "$ref": "#/definitions/UTCTime" + }, + "key": { + "$ref": "#/definitions/AssetKey" + }, + "token": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "key", + "domain" + ], + "type": "object" + }, + "AssetKey": { + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", + "type": "string" + }, + "AssetSize": { + "enum": [ + "preview", + "complete" + ], + "type": "string" + }, + "AssetSource": {}, + "AssetType": { + "enum": [ + "image" + ], + "type": "string" + }, + "AuthnRequest": { + "properties": { + "iD": { + "$ref": "#/definitions/ID" + }, + "issueInstant": { + "$ref": "#/definitions/Time" + }, + "issuer": { + "type": "string" + }, + "nameIDPolicy": { + "$ref": "#/definitions/NameIdPolicy" + } + }, + "required": [ + "iD", + "issueInstant", + "issuer" + ], + "type": "object" + }, + "Base64ByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "BaseProtocol": { + "enum": [ + "proteus", + "mls" + ], + "type": "string" + }, + "BindingNewTeamUser": { + "properties": { + "currency": { + "$ref": "#/definitions/Alpha" + }, + "icon": { + "$ref": "#/definitions/Icon" + }, + "icon_key": { + "description": "team icon asset key", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "members": { + "description": "initial team member ids (between 1 and 127)" + }, + "name": { + "description": "team name", + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "icon" + ], + "type": "object" + }, + "Body": {}, + "BotUserView": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "handle": { + "$ref": "#/definitions/Handle" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "id", + "name", + "accent_id" + ], + "type": "object" + }, + "CheckHandles": { + "properties": { + "handles": { + "items": { + "type": "string" + }, + "maxItems": 50, + "minItems": 1, + "type": "array" + }, + "return": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "handles", + "return" + ], + "type": "object" + }, + "CipherSuiteTag": { + "description": "The cipher suite of the corresponding MLS group", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "ClassifiedDomainsConfig": { + "properties": { + "domains": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "domains" + ], + "type": "object" + }, + "ClassifiedDomainsConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/ClassifiedDomainsConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "Client": { + "properties": { + "capabilities": { + "$ref": "#/definitions/ClientCapabilityList" + }, + "class": { + "$ref": "#/definitions/ClientClass" + }, + "cookie": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/ClientId" + }, + "label": { + "type": "string" + }, + "last_active": { + "$ref": "#/definitions/UTCTime" + }, + "location": { + "$ref": "#/definitions/Location" + }, + "mls_public_keys": { + "$ref": "#/definitions/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "time": { + "$ref": "#/definitions/UTCTime" + }, + "type": { + "$ref": "#/definitions/ClientType" + } + }, + "required": [ + "id", + "type", + "time" + ], + "type": "object" + }, + "ClientCapability": { + "enum": [ + "legalhold-implicit-consent" + ], + "type": "string" + }, + "ClientCapabilityList": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/definitions/ClientCapability" + }, + "type": "array" + } + }, + "required": [ + "capabilities" + ], + "type": "object" + }, + "ClientClass": { + "enum": [ + "phone", + "tablet", + "desktop", + "legalhold" + ], + "type": "string" + }, + "ClientId": { + "type": "string" + }, + "ClientMismatch": { + "properties": { + "deleted": { + "$ref": "#/definitions/UserClients" + }, + "missing": { + "$ref": "#/definitions/UserClients" + }, + "redundant": { + "$ref": "#/definitions/UserClients" + }, + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted" + ], + "type": "object" + }, + "ClientPrekey": { + "properties": { + "client": { + "$ref": "#/definitions/ClientId" + }, + "prekey": { + "$ref": "#/definitions/Prekey" + } + }, + "required": [ + "client", + "prekey" + ], + "type": "object" + }, + "ClientType": { + "enum": [ + "temporary", + "permanent", + "legalhold" + ], + "type": "string" + }, + "CodeChallengeMethod": { + "description": "The method used to encode the code challenge. Only `S256` is supported.", + "enum": [ + "S256" + ], + "type": "string" + }, + "CompletePasswordReset": { + "description": "Data to complete a password reset", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "password": { + "description": "New password (6 - 1024 characters)", + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "required": [ + "code", + "password" + ], + "type": "object" + }, + "ConferenceCallingConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Connect": { + "properties": { + "email": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "qualified_recipient": { + "$ref": "#/definitions/Qualified_UserId" + }, + "recipient": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_recipient" + ], + "type": "object" + }, + "ConnectionUpdate": { + "properties": { + "status": { + "$ref": "#/definitions/Relation" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Connections_Page": { + "properties": { + "connections": { + "items": { + "$ref": "#/definitions/UserConnection" + }, + "type": "array" + }, + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/definitions/Connections_PagingState" + } + }, + "required": [ + "connections", + "has_more", + "paging_state" + ], + "type": "object" + }, + "Connections_PagingState": { + "type": "string" + }, + "Contact": { + "description": "Contact discovered through search", + "properties": { + "accent_id": { + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "type": "string" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_id", + "name" + ], + "type": "object" + }, + "ConvMembers": { + "description": "Users of a conversation", + "properties": { + "others": { + "description": "All other current users of this conversation", + "items": { + "$ref": "#/definitions/OtherMember" + }, + "type": "array" + }, + "self": { + "$ref": "#/definitions/Member" + } + }, + "required": [ + "self", + "others" + ], + "type": "object" + }, + "ConvTeamInfo": { + "description": "Team information of this conversation", + "properties": { + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + }, + "teamid": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "teamid", + "managed" + ], + "type": "object" + }, + "ConvType": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer" + }, + "Conversation": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/definitions/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "creator": { + "$ref": "#/definitions/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/definitions/Epoch Timestamp" + }, + "group_id": { + "$ref": "#/definitions/GroupId" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/definitions/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "name": { + "type": "string" + }, + "protocol": { + "$ref": "#/definitions/Protocol" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/definitions/UUID" + }, + "type": { + "$ref": "#/definitions/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite" + ], + "type": "object" + }, + "ConversationAccessDatav2": { + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/definitions/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access" + ], + "type": "object" + }, + "ConversationAccessDatav3": { + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access", + "access_role" + ], + "type": "object" + }, + "ConversationCode": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "uri": { + "$ref": "#/definitions/HttpsUrl" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "ConversationCodeInfo": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "uri": { + "$ref": "#/definitions/HttpsUrl" + } + }, + "required": [ + "key", + "code", + "has_password" + ], + "type": "object" + }, + "ConversationCoverView": { + "description": "Limited view of Conversation.", + "properties": { + "has_password": { + "type": "boolean" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "has_password" + ], + "type": "object" + }, + "ConversationIds_Page": { + "properties": { + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/definitions/ConversationIds_PagingState" + }, + "qualified_conversations": { + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "qualified_conversations", + "has_more", + "paging_state" + ], + "type": "object" + }, + "ConversationIds_PagingState": { + "type": "string" + }, + "ConversationMessageTimerUpdate": { + "description": "Contains conversation properties to update", + "properties": { + "message_timer": { + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "type": "object" + }, + "ConversationReceiptModeUpdate": { + "description": "Contains conversation receipt mode to update to. Receipt mode tells clients whether certain types of receipts should be sent in the given conversation or not. How this value is interpreted is up to clients.", + "properties": { + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "receipt_mode" + ], + "type": "object" + }, + "ConversationRename": { + "properties": { + "name": { + "description": "The new conversation name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "ConversationRole": { + "properties": { + "actions": { + "description": "The set of actions allowed for this role", + "items": { + "$ref": "#/definitions/Action" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/definitions/RoleName" + } + } + }, + "ConversationRolesList": { + "properties": { + "conversation_roles": { + "items": { + "$ref": "#/definitions/ConversationRole" + }, + "type": "array" + } + }, + "required": [ + "conversation_roles" + ], + "type": "object" + }, + "ConversationsResponse": { + "description": "Response object for getting metadata of a list of conversations", + "properties": { + "failed": { + "description": "The server failed to fetch these conversations, most likely due to network issues while contacting a remote server", + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/definitions/Conversation" + }, + "type": "array" + }, + "not_found": { + "description": "These conversations either don't exist or are deleted.", + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "found", + "not_found", + "failed" + ], + "type": "object" + }, + "Cookie": { + "properties": { + "created": { + "$ref": "#/definitions/UTCTime" + }, + "expires": { + "$ref": "#/definitions/UTCTime" + }, + "id": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "label": { + "type": "string" + }, + "successor": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": { + "$ref": "#/definitions/CookieType" + } + }, + "required": [ + "id", + "type", + "created", + "expires" + ], + "type": "object" + }, + "CookieList": { + "description": "List of cookie information", + "properties": { + "cookies": { + "items": { + "$ref": "#/definitions/Cookie" + }, + "type": "array" + } + }, + "required": [ + "cookies" + ], + "type": "object" + }, + "CookieType": { + "enum": [ + "session", + "persistent" + ], + "type": "string" + }, + "CreateConversationCodeRequest": { + "description": "Request body for creating a conversation code", + "properties": { + "password": { + "description": "Password for accessing the conversation via guest link. Set to null or omit for no password.", + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "type": "object" + }, + "CreateGroupConversation": { + "description": "A created group-conversation object extended with a list of failed-to-add users", + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "creator": { + "$ref": "#/definitions/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/definitions/Epoch Timestamp" + }, + "failed_to_add": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "group_id": { + "$ref": "#/definitions/GroupId" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/definitions/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "name": { + "type": "string" + }, + "protocol": { + "$ref": "#/definitions/Protocol" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/definitions/UUID" + }, + "type": { + "$ref": "#/definitions/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite", + "failed_to_add" + ], + "type": "object" + }, + "CreateOAuthAuthorizationCodeRequest": { + "properties": { + "client_id": { + "$ref": "#/definitions/UUID" + }, + "code_challenge": { + "$ref": "#/definitions/OAuthCodeChallenge" + }, + "code_challenge_method": { + "$ref": "#/definitions/CodeChallengeMethod" + }, + "redirect_uri": { + "$ref": "#/definitions/RedirectUrl" + }, + "response_type": { + "$ref": "#/definitions/OAuthResponseType" + }, + "scope": { + "description": "The scopes which are requested to get authorization for, separated by a space", + "type": "string" + }, + "state": { + "description": "An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery", + "type": "string" + } + }, + "required": [ + "client_id", + "scope", + "response_type", + "redirect_uri", + "state", + "code_challenge_method", + "code_challenge" + ], + "type": "object" + }, + "CreateScimToken": { + "properties": { + "description": { + "type": "string" + }, + "password": { + "type": "string" + }, + "verification_code": { + "type": "string" + } + }, + "required": [ + "description" + ], + "type": "object" + }, + "CreateScimTokenResponse": { + "properties": { + "info": { + "$ref": "#/definitions/ScimTokenInfo" + }, + "token": { + "description": "Authentication token", + "type": "string" + } + }, + "required": [ + "token", + "info" + ], + "type": "object" + }, + "CustomBackend": { + "description": "Description of a custom backend", + "properties": { + "config_json_url": { + "$ref": "#/definitions/HttpsUrl" + }, + "webapp_welcome_url": { + "$ref": "#/definitions/HttpsUrl" + } + }, + "required": [ + "config_json_url", + "webapp_welcome_url" + ], + "type": "object" + }, + "DPoPAccessToken": { + "type": "string" + }, + "DPoPAccessTokenResponse": { + "properties": { + "expires_in": { + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "token": { + "$ref": "#/definitions/DPoPAccessToken" + }, + "type": { + "$ref": "#/definitions/AccessTokenType" + } + }, + "required": [ + "token", + "type", + "expires_in" + ], + "type": "object" + }, + "DeleteClient": { + "properties": { + "password": { + "description": "The password of the authenticated user for verification. The password is not required for deleting temporary clients.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeleteUser": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeletionCodeTimeout": { + "properties": { + "expires_in": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "expires_in" + ], + "type": "object" + }, + "DeprecatedMatchingResult": { + "properties": { + "auto-connects": { + "items": {}, + "type": "array" + }, + "results": { + "items": {}, + "type": "array" + } + }, + "required": [ + "results", + "auto-connects" + ], + "type": "object" + }, + "DigitalSignaturesConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "DisableLegalHoldForUserRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "Domain": { + "example": "example.com", + "type": "string" + }, + "Either": { + "maxProperties": 1, + "minProperties": 1, + "properties": { + "Left": { + "$ref": "#/definitions/OAuthAccessTokenRequest" + }, + "Right": { + "$ref": "#/definitions/OAuthRefreshAccessTokenRequest" + } + }, + "type": "object" + }, + "Email": { + "type": "string" + }, + "EmailUpdate": { + "properties": { + "email": { + "$ref": "#/definitions/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "Epoch Timestamp": { + "description": "The timestamp of the epoch number", + "example": "2021-05-12T10:52:02.671Z", + "format": "yyyy-mm-ddThh:MM:ss.qqq", + "type": "string" + }, + "Event": { + "properties": { + "conversation": { + "$ref": "#/definitions/UUID" + }, + "data": { + "description": "Encrypted message of a conversation", + "example": "ZXhhbXBsZQo=", + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/definitions/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "code": { + "$ref": "#/definitions/ASCII" + }, + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "creator": { + "$ref": "#/definitions/UUID" + }, + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "email": { + "type": "string" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/definitions/Epoch Timestamp" + }, + "group_id": { + "$ref": "#/definitions/GroupId" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/definitions/ConvMembers" + }, + "message": { + "type": "string" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "name": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "protocol": { + "$ref": "#/definitions/Protocol" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "qualified_recipient": { + "$ref": "#/definitions/Qualified_UserId" + }, + "qualified_target": { + "$ref": "#/definitions/Qualified_UserId" + }, + "qualified_user_ids": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "recipient": { + "$ref": "#/definitions/ClientId" + }, + "sender": { + "$ref": "#/definitions/ClientId" + }, + "status": { + "$ref": "#/definitions/TypingStatus" + }, + "target": { + "$ref": "#/definitions/UUID" + }, + "team": { + "$ref": "#/definitions/UUID" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + }, + "type": { + "$ref": "#/definitions/ConvType" + }, + "uri": { + "$ref": "#/definitions/HttpsUrl" + }, + "user_ids": { + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/definitions/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users", + "qualified_user_ids", + "user_ids", + "qualified_target", + "name", + "access", + "key", + "code", + "has_password", + "qualified_id", + "type", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite", + "qualified_recipient", + "receipt_mode", + "sender", + "recipient", + "text", + "status" + ], + "type": "object" + }, + "from": { + "$ref": "#/definitions/UUID" + }, + "qualified_conversation": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "qualified_from": { + "$ref": "#/definitions/Qualified_UserId" + }, + "subconv": { + "type": "string" + }, + "time": { + "$ref": "#/definitions/UTCTime" + }, + "type": { + "$ref": "#/definitions/EventType" + } + }, + "required": [ + "type", + "data", + "qualified_conversation", + "qualified_from", + "time" + ], + "type": "object" + }, + "EventType": { + "enum": [ + "conversation.member-join", + "conversation.member-leave", + "conversation.member-update", + "conversation.rename", + "conversation.access-update", + "conversation.receipt-mode-update", + "conversation.message-timer-update", + "conversation.code-update", + "conversation.code-delete", + "conversation.create", + "conversation.delete", + "conversation.connect-request", + "conversation.typing", + "conversation.otr-message-add", + "conversation.mls-message-add", + "conversation.mls-welcome", + "conversation.protocol-update" + ], + "type": "string" + }, + "ExposeInvitationURLsToTeamAdminConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "ExposeInvitationURLsToTeamAdminConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "FeatureStatus": { + "enum": [ + "enabled", + "disabled" + ], + "type": "string" + }, + "FederatedUserSearchPolicy": { + "description": "Search policy that was applied when searching for users", + "enum": [ + "no_search", + "exact_handle_search", + "full_search" + ], + "type": "string" + }, + "FileSharingConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "FileSharingConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Fingerprint": { + "example": "ioy3GeIjgQRsobf2EKGO3O8mq/FofFxHRqy0T4ERIZ8=", + "type": "string" + }, + "FormRedirect": { + "properties": { + "uri": { + "type": "string" + }, + "xml": { + "$ref": "#/definitions/AuthnRequest" + } + }, + "type": "object" + }, + "GetPaginated_Connections": { + "description": "A request to list some or all of a user's Connections, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/definitions/Connections_PagingState" + }, + "size": { + "description": "optional, must be <= 500, defaults to 100.", + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GetPaginated_ConversationIds": { + "description": "A request to list some or all of a user's ConversationIds, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/definitions/ConversationIds_PagingState" + }, + "size": { + "description": "optional, must be <= 1000, defaults to 1000.", + "format": "int32", + "maximum": 1000, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GroupId": { + "description": "An MLS group identifier (at most 256 bytes long)", + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "GuestLinksConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "GuestLinksConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Handle": { + "type": "string" + }, + "HandleUpdate": { + "properties": { + "handle": { + "type": "string" + } + }, + "required": [ + "handle" + ], + "type": "object" + }, + "HttpsUrl": { + "description": "Full URI (containing key/code) to join a conversation", + "example": "https://example.com", + "type": "string" + }, + "ID": { + "properties": { + "iD": { + "$ref": "#/definitions/XmlText" + } + }, + "required": [ + "iD" + ], + "type": "object" + }, + "Icon": { + "type": "string" + }, + "Id": { + "properties": { + "id": { + "$ref": "#/definitions/ClientId" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "IdPConfig": { + "properties": { + "extraInfo": { + "$ref": "#/definitions/WireIdP" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "metadata": { + "$ref": "#/definitions/IdPMetadata" + } + }, + "required": [ + "id", + "metadata", + "extraInfo" + ], + "type": "object" + }, + "IdPList": { + "properties": { + "providers": { + "items": { + "$ref": "#/definitions/IdPConfig" + }, + "type": "array" + } + }, + "required": [ + "providers" + ], + "type": "object" + }, + "IdPMetadata": { + "properties": { + "certAuthnResponse": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "issuer": { + "type": "string" + }, + "requestURI": { + "type": "string" + } + }, + "required": [ + "issuer", + "requestURI", + "certAuthnResponse" + ], + "type": "object" + }, + "IdPMetadataInfo": { + "maxProperties": 1, + "minProperties": 1, + "properties": { + "value": { + "type": "string" + } + }, + "type": "object" + }, + "Invitation": { + "description": "An invitation to join a team on Wire", + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters)", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "role": { + "$ref": "#/definitions/Role" + }, + "team": { + "$ref": "#/definitions/UUID" + }, + "url": { + "$ref": "#/definitions/URIRef Absolute" + } + }, + "required": [ + "team", + "id", + "created_at", + "email" + ], + "type": "object" + }, + "InvitationList": { + "description": "A list of sent team invitations.", + "properties": { + "has_more": { + "description": "Indicator that the server has more invitations than returned.", + "type": "boolean" + }, + "invitations": { + "items": { + "$ref": "#/definitions/Invitation" + }, + "type": "array" + } + }, + "required": [ + "invitations", + "has_more" + ], + "type": "object" + }, + "InvitationRequest": { + "description": "A request to join a team on Wire.", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters).", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "role": { + "$ref": "#/definitions/Role" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "InviteQualified": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "qualified_users": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "JoinConversationByCode": { + "description": "Request body for joining a conversation by code", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "uri": { + "$ref": "#/definitions/HttpsUrl" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "LHServiceStatus": { + "enum": [ + "configured", + "not_configured", + "disabled" + ], + "type": "string" + }, + "LegalholdConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LegalholdConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "LimitedQualifiedUserIdList": { + "properties": { + "qualified_users": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "ListConversations": { + "description": "A request to list some of a user's conversations, including remote ones. Maximum 1000 qualified conversation IDs", + "properties": { + "qualified_ids": { + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_ids" + ], + "type": "object" + }, + "ListType": { + "description": "true if 'members' doesn't contain all team members", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "ListUsersById": { + "properties": { + "failed": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "minItems": 1, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/definitions/UserProfile" + }, + "type": "array" + } + }, + "required": [ + "found" + ], + "type": "object" + }, + "ListUsersQuery": { + "description": "exactly one of qualified_ids or qualified_handles must be provided.", + "example": { + "qualified_ids": [ + { + "domain": "example.com", + "id": "00000000-0000-0000-0000-000000000000" + } + ] + }, + "properties": { + "qualified_handles": { + "items": { + "$ref": "#/definitions/Qualified_Handle" + }, + "type": "array" + }, + "qualified_ids": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + } + }, + "type": "object" + }, + "Locale": { + "type": "string" + }, + "LocaleUpdate": { + "properties": { + "locale": { + "$ref": "#/definitions/Locale" + } + }, + "required": [ + "locale" + ], + "type": "object" + }, + "Location": { + "properties": { + "lat": { + "format": "double", + "type": "number" + }, + "lon": { + "format": "double", + "type": "number" + } + }, + "required": [ + "lat", + "lon" + ], + "type": "object" + }, + "LockStatus": { + "enum": [ + "locked", + "unlocked" + ], + "type": "string" + }, + "Login": { + "properties": { + "code": { + "$ref": "#/definitions/LoginCode" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "handle": { + "$ref": "#/definitions/Handle" + }, + "label": { + "description": "This label can be used to delete all cookies matching it (cf. /cookies/remove)", + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "verification_code": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "password", + "phone", + "code" + ], + "type": "object" + }, + "LoginCode": { + "type": "string" + }, + "LoginCodeTimeout": { + "description": "A response for a successfully sent login code", + "properties": { + "expires_in": { + "description": "Number of seconds before the login code expires", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "expires_in" + ], + "type": "object" + }, + "MLSConfig": { + "properties": { + "allowedCipherSuites": { + "items": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "type": "array" + }, + "defaultCipherSuite": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "defaultProtocol": { + "$ref": "#/definitions/Protocol" + }, + "protocolToggleUsers": { + "description": "allowlist of users that may change protocols", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + }, + "supportedProtocols": { + "items": { + "$ref": "#/definitions/Protocol" + }, + "type": "array" + } + }, + "required": [ + "protocolToggleUsers", + "defaultProtocol", + "allowedCipherSuites", + "defaultCipherSuite", + "supportedProtocols" + ], + "type": "object" + }, + "MLSConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/MLSConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MLSPublicKeys": { + "additionalProperties": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "description": "Mapping from signature scheme (tags) to public key data", + "example": { + "ed25519": "ZXhhbXBsZQo=" + }, + "type": "object" + }, + "ManagedBy": { + "enum": [ + "wire", + "scim" + ], + "type": "string" + }, + "Member": { + "description": "The user ID of the requestor", + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "status": {}, + "status_ref": {}, + "status_time": {} + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "MemberUpdate": { + "properties": { + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "type": "object" + }, + "MemberUpdateData": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_target": { + "$ref": "#/definitions/Qualified_UserId" + }, + "target": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_target" + ], + "type": "object" + }, + "MessageSendingStatus": { + "description": "The Proteus message sending status. It has these fields:\n- `time`: Time of sending message.\n- `missing`: Clients that the message /should/ have been encrypted for, but wasn't.\n- `redundant`: Clients that the message /should not/ have been encrypted for, but was.\n- `deleted`: Clients that were deleted.\n- `failed_to_send`: When message sending fails for some clients but succeeds for others, e.g., because a remote backend is unreachable, this field will contain the list of clients for which the message sending failed. This list should be empty when message sending is not even tried, like when some clients are missing.", + "properties": { + "deleted": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "failed_to_confirm_clients": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "failed_to_send": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "missing": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "redundant": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted", + "failed_to_send", + "failed_to_confirm_clients" + ], + "type": "object" + }, + "MlsE2EIdConfig": { + "properties": { + "acmeDiscoveryUrl": { + "$ref": "#/definitions/HttpsUrl" + }, + "verificationExpiration": { + "description": "When a client first tries to fetch or renew a certificate, they may need to login to an identity provider (IdP) depending on their IdP domain authentication policy. The user may have a grace period during which they can “snooze” this login. The duration of this grace period (in seconds) is set in the `verificationDuration` parameter, which is enforced separately by each client. After the grace period has expired, the client will not allow the user to use the application until they have logged to refresh the certificate. The default value is 1 day (86400s). The client enrolls using the Automatic Certificate Management Environment (ACME) protocol. The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: `https://acme.example.com/acme/provisioner1/discovery`.", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "verificationExpiration" + ], + "type": "object" + }, + "MlsE2EIdConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/MlsE2EIdConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MlsMigration": { + "properties": { + "finaliseRegardlessAfter": { + "$ref": "#/definitions/UTCTime" + }, + "startTime": { + "$ref": "#/definitions/UTCTime" + } + }, + "type": "object" + }, + "MlsMigration.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/MlsMigration" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "NameIDFormat": { + "enum": [ + "NameIDFUnspecified", + "NameIDFEmail", + "NameIDFX509", + "NameIDFWindows", + "NameIDFKerberos", + "NameIDFEntity", + "NameIDFPersistent", + "NameIDFTransient" + ], + "type": "string" + }, + "NameIdPolicy": { + "properties": { + "allowCreate": { + "type": "boolean" + }, + "format": { + "$ref": "#/definitions/NameIDFormat" + }, + "spNameQualifier": { + "$ref": "#/definitions/XmlText" + } + }, + "required": [ + "format", + "allowCreate" + ], + "type": "object" + }, + "NewAssetToken": { + "properties": { + "token": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "NewClient": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/definitions/ClientCapability" + }, + "type": "array" + }, + "class": { + "$ref": "#/definitions/ClientClass" + }, + "cookie": { + "description": "The cookie label, i.e. the label used when logging in.", + "type": "string" + }, + "label": { + "type": "string" + }, + "lastkey": { + "$ref": "#/definitions/Prekey" + }, + "mls_public_keys": { + "$ref": "#/definitions/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "password": { + "description": "The password of the authenticated user for verification. Note: Required for registration of the 2nd, 3rd, ... client.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "prekeys": { + "description": "Prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/definitions/Prekey" + }, + "type": "array" + }, + "type": { + "$ref": "#/definitions/ClientType" + }, + "verification_code": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "prekeys", + "lastkey", + "type" + ], + "type": "object" + }, + "NewConv": { + "description": "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'", + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "message_timer": { + "description": "Per-conversation message timer", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "protocol": { + "$ref": "#/definitions/BaseProtocol" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/definitions/ConvTeamInfo" + }, + "users": { + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + } + }, + "type": "object" + }, + "NewLegalHoldService": { + "properties": { + "auth_token": { + "$ref": "#/definitions/ASCII" + }, + "base_url": { + "$ref": "#/definitions/HttpsUrl" + }, + "public_key": { + "$ref": "#/definitions/ServiceKeyPEM" + } + }, + "required": [ + "base_url", + "public_key", + "auth_token" + ], + "type": "object" + }, + "NewPasswordReset": { + "description": "Data to initiate a password reset", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "type": "object" + }, + "NewTeamMember": { + "description": "Required data when creating new team members", + "properties": { + "member": { + "description": "the team member to add (the legalhold_status field must be null or missing!)", + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "permissions": { + "$ref": "#/definitions/Permissions" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + } + }, + "required": [ + "member" + ], + "type": "object" + }, + "NewUser": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "email_code": { + "$ref": "#/definitions/ASCII" + }, + "expires_in": { + "maximum": 604800, + "minimum": 1, + "type": "integer" + }, + "invitation_code": { + "$ref": "#/definitions/ASCII" + }, + "label": { + "type": "string" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "managed_by": { + "$ref": "#/definitions/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "phone_code": { + "$ref": "#/definitions/ASCII" + }, + "picture": { + "$ref": "#/definitions/Pict" + }, + "sso_id": { + "$ref": "#/definitions/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/definitions/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/definitions/BindingNewTeamUser" + }, + "team_code": { + "$ref": "#/definitions/ASCII" + }, + "team_id": { + "$ref": "#/definitions/UUID" + }, + "uuid": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "OAuthAccessTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/definitions/UUID" + }, + "code": { + "$ref": "#/definitions/OAuthAuthorizationCode" + }, + "code_verifier": { + "description": "The code verifier to complete the code challenge", + "maxLength": 128, + "minLength": 43, + "type": "string" + }, + "grant_type": { + "$ref": "#/definitions/OAuthGrantType" + }, + "redirect_uri": { + "$ref": "#/definitions/RedirectUrl" + } + }, + "required": [ + "grant_type", + "client_id", + "code_verifier", + "code", + "redirect_uri" + ], + "type": "object" + }, + "OAuthAccessTokenResponse": { + "properties": { + "access_token": { + "description": "The access token, which has a relatively short lifetime", + "type": "string" + }, + "expires_in": { + "description": "The lifetime of the access token in seconds", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "refresh_token": { + "description": "The refresh token, which has a relatively long lifetime, and can be used to obtain a new access token", + "type": "string" + }, + "token_type": { + "$ref": "#/definitions/OAuthAccessTokenType" + } + }, + "required": [ + "access_token", + "token_type", + "expires_in", + "refresh_token" + ], + "type": "object" + }, + "OAuthAccessTokenType": { + "description": "The type of the access token. Currently only `Bearer` is supported.", + "enum": [ + "Bearer" + ], + "type": "string" + }, + "OAuthApplication": { + "properties": { + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "description": "The OAuth client's name", + "maxLength": 256, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "OAuthAuthorizationCode": { + "description": "The authorization code", + "type": "string" + }, + "OAuthClient": { + "properties": { + "application_name": { + "maxLength": 256, + "minLength": 6, + "type": "string" + }, + "client_id": { + "$ref": "#/definitions/UUID" + }, + "redirect_url": { + "$ref": "#/definitions/RedirectUrl" + } + }, + "required": [ + "client_id", + "application_name", + "redirect_url" + ], + "type": "object" + }, + "OAuthCodeChallenge": { + "description": "Generated by the client from the code verifier (unpadded base64url-encoded SHA256 hash of the code verifier)", + "type": "string" + }, + "OAuthGrantType": { + "description": "Indicates which authorization flow to use. Use `authorization_code` for authorization code flow.", + "enum": [ + "authorization_code", + "refresh_token" + ], + "type": "string" + }, + "OAuthRefreshAccessTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/definitions/UUID" + }, + "grant_type": { + "$ref": "#/definitions/OAuthGrantType" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "grant_type", + "client_id", + "refresh_token" + ], + "type": "object" + }, + "OAuthResponseType": { + "description": "Indicates which authorization flow to use. Use `code` for authorization code flow.", + "enum": [ + "code" + ], + "type": "string" + }, + "OAuthRevokeRefreshTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/definitions/UUID" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "client_id", + "refresh_token" + ], + "type": "object" + }, + "Object": {}, + "OtherMember": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "status": { + "description": "deprecated", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "OtherMemberUpdate": { + "description": "Update user properties of other members relative to a conversation", + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + } + }, + "type": "object" + }, + "OtrMessage": { + "description": "Encrypted message of a conversation", + "properties": { + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "recipient": { + "$ref": "#/definitions/ClientId" + }, + "sender": { + "$ref": "#/definitions/ClientId" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + } + }, + "required": [ + "sender", + "recipient", + "text" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "PagingState": { + "description": "Paging state that should be supplied to retrieve the next page of results", + "type": "string" + }, + "PasswordChange": { + "description": "Data to change a password. The old password is required if a password already exists.", + "properties": { + "new_password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "old_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "new_password" + ], + "type": "object" + }, + "PasswordReset": { + "description": "Data to complete a password reset", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "password": { + "description": "New password (6 - 1024 characters)", + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "required": [ + "code", + "password" + ], + "type": "object" + }, + "Permissions": { + "description": "This is just a complicated way of representing a team role. self and copy always have to contain the same integer, and only the following integers are allowed: 1025 (partner), 1587 (member), 5951 (admin), 8191 (owner). Unit tests of the galley-types package in wire-server contain an authoritative list.", + "properties": { + "copy": { + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "self": { + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "self", + "copy" + ], + "type": "object" + }, + "PhoneNumber": { + "description": "Phone number of the invitee, in the E.164 format", + "type": "string" + }, + "PhoneUpdate": { + "properties": { + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "required": [ + "phone" + ], + "type": "object" + }, + "Pict": { + "items": {}, + "maxItems": 10, + "minItems": 0, + "type": "array" + }, + "Prekey": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "PrekeyBundle": { + "properties": { + "clients": { + "items": { + "$ref": "#/definitions/ClientPrekey" + }, + "type": "array" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "clients" + ], + "type": "object" + }, + "Priority": { + "enum": [ + "low", + "high" + ], + "type": "string" + }, + "PropertyKeysAndValues": { + "type": "object" + }, + "PropertyValue": { + "description": "An arbitrary JSON value for a property" + }, + "Protocol": { + "enum": [ + "proteus", + "mls", + "mixed" + ], + "type": "string" + }, + "ProtocolUpdate": { + "properties": { + "protocol": { + "$ref": "#/definitions/Protocol" + } + }, + "type": "object" + }, + "PubClient": { + "properties": { + "class": { + "$ref": "#/definitions/ClientClass" + }, + "id": { + "$ref": "#/definitions/ClientId" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "PushToken": { + "description": "Native Push Token", + "properties": { + "app": { + "description": "Application", + "type": "string" + }, + "client": { + "$ref": "#/definitions/ClientId" + }, + "token": { + "description": "Access Token", + "type": "string" + }, + "transport": { + "$ref": "#/definitions/Transport" + } + }, + "required": [ + "transport", + "app", + "token", + "client" + ], + "type": "object" + }, + "PushTokenList": { + "description": "List of Native Push Tokens", + "properties": { + "tokens": { + "description": "Push tokens", + "items": { + "$ref": "#/definitions/PushToken" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "QualifiedNewOtrMessage": { + "description": "This object can only be parsed from Protobuf.\nThe specification for the protobuf types is here: \nhttps://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." + }, + "QualifiedUserClientPrekeyMapV4": { + "properties": { + "failed_to_list": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "qualified_user_client_prekeys": { + "additionalProperties": { + "$ref": "#/definitions/UserClientPrekeyMap" + }, + "type": "object" + } + }, + "required": [ + "qualified_user_client_prekeys" + ], + "type": "object" + }, + "QualifiedUserClients": { + "additionalProperties": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/ClientId" + }, + "type": "array" + }, + "type": "object" + }, + "description": "Map of Domain to UserClients", + "example": { + "domain1.example.com": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + } + }, + "type": "object" + }, + "QualifiedUserIdList": { + "properties": { + "qualified_user_ids": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "user_ids": { + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + } + }, + "required": [ + "qualified_user_ids", + "user_ids" + ], + "type": "object" + }, + "QualifiedUserMap_Set_PubClient": { + "additionalProperties": { + "$ref": "#/definitions/UserMap_Set_PubClient" + }, + "description": "Map of Domain to (UserMap (Set_PubClient)).", + "example": { + "domain1.example.com": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + { + "class": "legalhold", + "id": "d0" + } + ] + } + }, + "type": "object" + }, + "Qualified_ConvId": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "id": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "Qualified_Handle": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "handle": { + "$ref": "#/definitions/Handle" + } + }, + "required": [ + "domain", + "handle" + ], + "type": "object" + }, + "Qualified_UserId": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "id": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "QueuedNotification": { + "description": "A single notification", + "properties": { + "id": { + "$ref": "#/definitions/UUID" + }, + "payload": { + "description": "List of events", + "items": { + "$ref": "#/definitions/Object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "id", + "payload" + ], + "type": "object" + }, + "QueuedNotificationList": { + "description": "Zero or more notifications", + "properties": { + "has_more": { + "description": "Whether there are still more notifications.", + "type": "boolean" + }, + "notifications": { + "description": "Notifications", + "items": { + "$ref": "#/definitions/QueuedNotification" + }, + "type": "array" + }, + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "notifications" + ], + "type": "object" + }, + "RTCConfiguration": { + "description": "A subset of the WebRTC 'RTCConfiguration' dictionary", + "properties": { + "ice_servers": { + "description": "Array of 'RTCIceServer' objects", + "items": { + "$ref": "#/definitions/RTCIceServer" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers": { + "description": "Array of 'SFTServer' objects (optional)", + "items": { + "$ref": "#/definitions/SftServer" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers_all": { + "description": "Array of all SFT servers", + "items": { + "$ref": "#/definitions/SftServer" + }, + "type": "array" + }, + "ttl": { + "description": "Number of seconds after which the configuration should be refreshed (advisory)", + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "ice_servers", + "ttl" + ], + "type": "object" + }, + "RTCIceServer": { + "description": "A subset of the WebRTC 'RTCIceServer' object", + "properties": { + "credential": { + "$ref": "#/definitions/ASCII" + }, + "urls": { + "description": "Array of TURN server addresses of the form 'turn::'", + "items": { + "$ref": "#/definitions/TurnURI" + }, + "minItems": 1, + "type": "array" + }, + "username": { + "$ref": "#/definitions/" + } + }, + "required": [ + "urls", + "username", + "credential" + ], + "type": "object" + }, + "RedirectUrl": { + "description": "The URL must match the URL that was used to generate the authorization code.", + "type": "string" + }, + "Relation": { + "enum": [ + "accepted", + "blocked", + "pending", + "ignored", + "sent", + "cancelled", + "missing-legalhold-consent" + ], + "type": "string" + }, + "RemoveBotResponse": { + "properties": { + "event": { + "$ref": "#/definitions/Event" + } + }, + "required": [ + "event" + ], + "type": "object" + }, + "RemoveCookies": { + "description": "Data required to remove cookies", + "properties": { + "ids": { + "description": "A list of cookie IDs to revoke", + "items": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": "array" + }, + "labels": { + "description": "A list of cookie labels for which to revoke the cookies", + "items": { + "type": "string" + }, + "type": "array" + }, + "password": { + "description": "The user's password", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "RemoveLegalHoldSettingsRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "RichField": { + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "type": "object" + }, + "RichInfoAssocList": { + "description": "json object with case-insensitive fields.", + "properties": { + "fields": { + "items": { + "$ref": "#/definitions/RichField" + }, + "type": "array" + }, + "version": { + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "version", + "fields" + ], + "type": "object" + }, + "Role": { + "description": "Role of the invited user", + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "RoleName": { + "description": "Role name, between 2 and 128 chars, 'wire_' prefix is reserved for roles designed by Wire (i.e., no custom roles can have the same prefix)", + "type": "string" + }, + "SSOConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "ScimTokenInfo": { + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "description": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "idp": { + "$ref": "#/definitions/UUID" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "team", + "id", + "created_at", + "description" + ], + "type": "object" + }, + "ScimTokenList": { + "properties": { + "tokens": { + "items": { + "$ref": "#/definitions/ScimTokenInfo" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "SearchResult": { + "properties": { + "documents": { + "description": "List of contacts found", + "items": { + "$ref": "#/definitions/TeamContact" + }, + "type": "array" + }, + "found": { + "description": "Total number of hits", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "has_more": { + "description": "Indicates whether there are more results to be fetched", + "type": "boolean" + }, + "paging_state": { + "$ref": "#/definitions/PagingState" + }, + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "search_policy": { + "$ref": "#/definitions/FederatedUserSearchPolicy" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "found", + "returned", + "took", + "documents", + "search_policy" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig": { + "properties": { + "enforcedTimeoutSeconds": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforcedTimeoutSeconds" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/SelfDeletingMessagesConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/definitions/SelfDeletingMessagesConfig" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "SendActivationCode": { + "description": "Data for requesting an email or phone activation code to be sent. One of 'email' or 'phone' must be present.", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "voice_call": { + "description": "Request the code with a call instead (default is SMS).", + "type": "boolean" + } + }, + "type": "object" + }, + "SendLoginCode": { + "description": "Payload for requesting a login code to be sent", + "properties": { + "force": { + "type": "boolean" + }, + "phone": { + "description": "E.164 phone number to send the code to", + "type": "string" + }, + "voice_call": { + "description": "Request the code with a call instead (default is SMS)", + "type": "boolean" + } + }, + "required": [ + "phone" + ], + "type": "object" + }, + "SendVerificationCode": { + "properties": { + "action": { + "$ref": "#/definitions/VerificationAction" + }, + "email": { + "$ref": "#/definitions/Email" + } + }, + "required": [ + "action", + "email" + ], + "type": "object" + }, + "ServiceKeyPEM": { + "example": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0\nG06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH\nWvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV\nVPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS\nbUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8\n7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la\nnQIDAQAB\n-----END PUBLIC KEY-----\n", + "type": "string" + }, + "ServiceRef": { + "properties": { + "id": { + "$ref": "#/definitions/UUID" + }, + "provider": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "id", + "provider" + ], + "type": "object" + }, + "SftServer": { + "description": "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers", + "properties": { + "urls": { + "description": "Array containing exactly one SFT server address of the form 'https://:'", + "items": { + "$ref": "#/definitions/HttpsUrl" + }, + "type": "array" + } + }, + "required": [ + "urls" + ], + "type": "object" + }, + "SimpleMember": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "SimpleMembers": { + "properties": { + "user_ids": { + "description": "deprecated", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/definitions/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Sso": { + "properties": { + "issuer": { + "type": "string" + }, + "nameid": { + "type": "string" + } + }, + "required": [ + "issuer", + "nameid" + ], + "type": "object" + }, + "SsoSettings": { + "properties": { + "default_sso_code": { + "$ref": "#/definitions/UUID" + } + }, + "type": "object" + }, + "SystemSettings": { + "properties": { + "setEnableMls": { + "description": "Whether MLS is enabled or not", + "type": "boolean" + }, + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation", + "setEnableMls" + ], + "type": "object" + }, + "SystemSettingsPublic": { + "properties": { + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation" + ], + "type": "object" + }, + "Team": { + "description": "`binding` is deprecated, and should be ignored. The non-binding teams API is not used (and will not be supported from API version V4 onwards), and `binding` will always be `true`.", + "properties": { + "binding": { + "$ref": "#/definitions/TeamBinding" + }, + "creator": { + "$ref": "#/definitions/UUID" + }, + "icon": { + "$ref": "#/definitions/Icon" + }, + "icon_key": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "type": "string" + }, + "splash_screen": { + "$ref": "#/definitions/Icon" + } + }, + "required": [ + "id", + "creator", + "name", + "icon" + ], + "type": "object" + }, + "TeamBinding": { + "description": "Deprecated, please ignore.", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "TeamContact": { + "properties": { + "accent_id": { + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "email_unvalidated": { + "$ref": "#/definitions/Email" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "managed_by": { + "$ref": "#/definitions/ManagedBy" + }, + "name": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/Role" + }, + "saml_idp": { + "type": "string" + }, + "scim_external_id": { + "type": "string" + }, + "sso": { + "$ref": "#/definitions/Sso" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "TeamConversation": { + "description": "Team conversation data", + "properties": { + "conversation": { + "$ref": "#/definitions/UUID" + }, + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + } + }, + "required": [ + "conversation", + "managed" + ], + "type": "object" + }, + "TeamConversationList": { + "description": "Team conversation list", + "properties": { + "conversations": { + "items": { + "$ref": "#/definitions/TeamConversation" + }, + "type": "array" + } + }, + "required": [ + "conversations" + ], + "type": "object" + }, + "TeamDeleteData": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/definitions/ASCII" + } + }, + "type": "object" + }, + "TeamMember": { + "description": "team member data", + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "legalhold_status": { + "$ref": "#/definitions/UserLegalHoldStatus" + }, + "permissions": { + "$ref": "#/definitions/Permissions" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user" + ], + "type": "object" + }, + "TeamMemberDeleteData": { + "description": "Data for a team member deletion request in case of binding teams.", + "properties": { + "password": { + "description": "The account password to authorise the deletion.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "TeamMemberList": { + "description": "list of team member", + "properties": { + "hasMore": { + "$ref": "#/definitions/ListType" + }, + "members": { + "description": "the array of team members", + "items": { + "$ref": "#/definitions/TeamMember" + }, + "type": "array" + } + }, + "required": [ + "members", + "hasMore" + ], + "type": "object" + }, + "TeamMembersPage": { + "properties": { + "hasMore": { + "type": "boolean" + }, + "members": { + "items": { + "$ref": "#/definitions/TeamMember" + }, + "type": "array" + }, + "pagingState": { + "$ref": "#/definitions/TeamMembers_PagingState" + } + }, + "required": [ + "members", + "hasMore", + "pagingState" + ], + "type": "object" + }, + "TeamMembers_PagingState": { + "type": "string" + }, + "TeamSearchVisibility": { + "description": "value of visibility", + "enum": [ + "standard", + "no-name-outside-team" + ], + "type": "string" + }, + "TeamSearchVisibilityView": { + "description": "Search visibility value for the team", + "properties": { + "search_visibility": { + "$ref": "#/definitions/TeamSearchVisibility" + } + }, + "required": [ + "search_visibility" + ], + "type": "object" + }, + "TeamSize": { + "description": "A simple object with a total number of team members.", + "properties": { + "teamSize": { + "description": "Team size.", + "exclusiveMinimum": false, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "teamSize" + ], + "type": "object" + }, + "TeamUpdateData": { + "properties": { + "icon": { + "$ref": "#/definitions/Icon" + }, + "icon_key": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "splash_screen": { + "$ref": "#/definitions/Icon" + } + }, + "type": "object" + }, + "Time": { + "properties": { + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "TokenType": { + "enum": [ + "Bearer" + ], + "type": "string" + }, + "Transport": { + "description": "Transport", + "enum": [ + "GCM", + "APNS", + "APNS_SANDBOX", + "APNS_VOIP", + "APNS_VOIP_SANDBOX" + ], + "type": "string" + }, + "TurnURI": { + "type": "string" + }, + "TypingData": { + "properties": { + "status": { + "$ref": "#/definitions/TypingStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "TypingStatus": { + "enum": [ + "started", + "stopped" + ], + "type": "string" + }, + "URIRef Absolute": { + "description": "URL of the invitation link to be sent to the invitee", + "type": "string" + }, + "UTCTime": { + "example": "2021-05-12T10:52:02.671Z", + "format": "yyyy-mm-ddThh:MM:ss.qqq", + "type": "string" + }, + "UUID": { + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "Unnamed": { + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "permissions": { + "$ref": "#/definitions/Permissions" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + }, + "UpdateBotPrekeys": { + "properties": { + "prekeys": { + "items": { + "$ref": "#/definitions/Prekey" + }, + "type": "array" + } + }, + "required": [ + "prekeys" + ], + "type": "object" + }, + "UpdateClient": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/definitions/ClientCapability" + }, + "type": "array" + }, + "label": { + "description": "A new name for this client.", + "type": "string" + }, + "lastkey": { + "$ref": "#/definitions/Prekey" + }, + "mls_public_keys": { + "$ref": "#/definitions/MLSPublicKeys" + }, + "prekeys": { + "description": "New prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/definitions/Prekey" + }, + "type": "array" + } + }, + "type": "object" + }, + "User": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "expires_at": { + "$ref": "#/definitions/UTCTime" + }, + "handle": { + "$ref": "#/definitions/Handle" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "managed_by": { + "$ref": "#/definitions/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "picture": { + "$ref": "#/definitions/Pict" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "sso_id": { + "$ref": "#/definitions/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/definitions/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "id", + "qualified_id", + "name", + "accent_id", + "locale" + ], + "type": "object" + }, + "UserAsset": { + "properties": { + "key": { + "$ref": "#/definitions/AssetKey" + }, + "size": { + "$ref": "#/definitions/AssetSize" + }, + "type": { + "$ref": "#/definitions/AssetType" + } + }, + "required": [ + "key", + "type" + ], + "type": "object" + }, + "UserClientMap": { + "additionalProperties": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "object" + }, + "UserClientPrekeyMap": { + "additionalProperties": { + "additionalProperties": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "type": "object" + }, + "example": { + "000600d0-000b-9c1a-000d-a4130002c221": { + "44901fb0712e588f": { + "id": 1, + "key": "pQABAQECoQBYIOjl7hw0D8YRNq..." + } + } + }, + "type": "object" + }, + "UserClients": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/ClientId" + }, + "type": "array" + }, + "description": "Map of user id to list of client ids.", + "example": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + }, + "type": "object" + }, + "UserConnection": { + "properties": { + "conversation": { + "$ref": "#/definitions/UUID" + }, + "from": { + "$ref": "#/definitions/UUID" + }, + "last_update": { + "$ref": "#/definitions/UTCTime" + }, + "qualified_conversation": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "qualified_to": { + "$ref": "#/definitions/Qualified_UserId" + }, + "status": { + "$ref": "#/definitions/Relation" + }, + "to": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "from", + "qualified_to", + "status", + "last_update" + ], + "type": "object" + }, + "UserIdList": { + "properties": { + "user_ids": { + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + } + }, + "required": [ + "user_ids" + ], + "type": "object" + }, + "UserLegalHoldStatus": { + "description": "states whether a user is under legal hold, or whether legal hold is pending approval.", + "enum": [ + "enabled", + "pending", + "disabled", + "no_consent" + ], + "type": "string" + }, + "UserLegalHoldStatusResponse": { + "properties": { + "client": { + "$ref": "#/definitions/Id" + }, + "last_prekey": { + "$ref": "#/definitions/Prekey" + }, + "status": { + "$ref": "#/definitions/UserLegalHoldStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "UserMap_Set_PubClient": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/PubClient" + }, + "type": "array", + "uniqueItems": true + }, + "description": "Map of UserId to (Set PubClient)", + "example": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + { + "class": "legalhold", + "id": "d0" + } + ] + }, + "type": "object" + }, + "UserProfile": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "expires_at": { + "$ref": "#/definitions/UTCTime" + }, + "handle": { + "$ref": "#/definitions/Handle" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "legalhold_status": { + "$ref": "#/definitions/UserLegalHoldStatus" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/definitions/Pict" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "supported_protocols": { + "items": { + "$ref": "#/definitions/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "legalhold_status" + ], + "type": "object" + }, + "UserSSOId": { + "properties": { + "scim_external_id": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "tenant": { + "type": "string" + } + }, + "type": "object" + }, + "UserUpdate": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/definitions/Pict" + } + }, + "type": "object" + }, + "ValidateSAMLEmailsConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "VerificationAction": { + "enum": [ + "create_scim_token", + "login", + "delete_team" + ], + "type": "string" + }, + "VerifyDeleteUser": { + "description": "Data for verifying an account deletion.", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "key": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "VersionInfo": { + "example": { + "development": [ + 5 + ], + "domain": "example.com", + "federation": false, + "supported": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + }, + "properties": { + "development": { + "items": { + "$ref": "#/definitions/VersionNumber" + }, + "type": "array" + }, + "domain": { + "$ref": "#/definitions/Domain" + }, + "federation": { + "type": "boolean" + }, + "supported": { + "items": { + "$ref": "#/definitions/VersionNumber" + }, + "type": "array" + } + }, + "required": [ + "supported", + "development", + "federation", + "domain" + ], + "type": "object" + }, + "VersionNumber": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer" + }, + "ViewLegalHoldService": { + "properties": { + "settings": { + "$ref": "#/definitions/ViewLegalHoldServiceInfo" + }, + "status": { + "$ref": "#/definitions/LHServiceStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ViewLegalHoldServiceInfo": { + "properties": { + "auth_token": { + "$ref": "#/definitions/ASCII" + }, + "base_url": { + "$ref": "#/definitions/HttpsUrl" + }, + "fingerprint": { + "$ref": "#/definitions/Fingerprint" + }, + "public_key": { + "$ref": "#/definitions/ServiceKeyPEM" + }, + "team_id": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "team_id", + "base_url", + "fingerprint", + "auth_token", + "public_key" + ], + "type": "object" + }, + "WireIdP": { + "properties": { + "apiVersion": { + "$ref": "#/definitions/WireIdPAPIVersion" + }, + "handle": { + "type": "string" + }, + "oldIssuers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "replacedBy": { + "$ref": "#/definitions/UUID" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "team", + "oldIssuers", + "handle" + ], + "type": "object" + }, + "WireIdPAPIVersion": { + "enum": [ + "WireIdPAPIV1", + "WireIdPAPIV2" + ], + "type": "string" + }, + "XmlText": { + "properties": { + "fromXmlText": { + "type": "string" + } + }, + "required": [ + "fromXmlText" + ], + "type": "object" + }, + "new-otr-message": { + "properties": { + "data": { + "type": "string" + }, + "native_priority": { + "$ref": "#/definitions/Priority" + }, + "native_push": { + "type": "boolean" + }, + "recipients": { + "$ref": "#/definitions/UserClientMap" + }, + "report_missing": { + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + }, + "sender": { + "$ref": "#/definitions/ClientId" + }, + "transient": { + "type": "boolean" + } + }, + "required": [ + "sender", + "recipients" + ], + "type": "object" + } + }, + "info": { + "description": "## Authentication / Authorization\n\nThe end-points in this API support differing authorization protocols:\nsome are unauthenticated (`/api-version`, `/login`), some require\n[zauth](), and some support both [zauth]() and [oauth]().\n\nThe end-points that require zauth are labelled so in the description\nbelow. The end-points that support oauth as an alternative to zauth\nhave the required oauth scopes listed in the same description.\n\nFuther reading:\n- https://docs.wire.com/developer/reference/oauth.html\n- https://github.com/wireapp/wire-server/blob/develop/libs/wire-api/src/Wire/API/Routes/Public.hs (search for HasSwagger instances)\n- `curl https://staging-nginz-https.zinfra.io/v4/api/swagger.json | jq '.security, .securityDefinitions`\n\n### SSO Endpoints\n\n#### Overview\n\n`/sso/metadata` will be requested by the IdPs to learn how to talk to wire.\n\n`/sso/initiate-login`, `/sso/finalize-login` are for the SAML authentication handshake performed by a user in order to log into wire. They are not exactly standard in their details: they may return HTML or XML; redirect to error URLs instead of throwing errors, etc.\n\n`/identity-providers` end-points are for use in the team settings page when IdPs are registered. They talk json.\n\n\n#### Configuring IdPs\n\nIdPs usually allow you to copy the metadata into your clipboard. That should contain all the details you need to post the idp in your team under `/identity-providers`. (Team id is derived from the authorization credentials of the request.)\n\n##### okta.com\n\nOkta will ask you to provide two URLs when you set it up for talking to wireapp:\n\n1. The `Single sign on URL`. This is the end-point that accepts the user's credentials after successful authentication against the IdP. Choose `/sso/finalize-login` with schema and hostname of the wire server you are configuring.\n\n2. The `Audience URI`. You can find this in the metadata returned by the `/sso/metadata` end-point. It is the contents of the `md:OrganizationURL` element.\n\n##### centrify.com\n\nCentrify allows you to upload the metadata xml document that you get from the `/sso/metadata` end-point. You can also enter the metadata url and have centrify retrieve the xml, but to guarantee integrity of the setup, the metadata should be copied from the team settings page and pasted into the centrify setup page without any URL indirections.\n\n## Federation errors\n\nEndpoints involving federated calls to other domains can return some extra failure responses, common to all endpoints. Instead of listing them as possible responses for each endpoint, we document them here.\n\nFor errors that are more likely to be transient, we suggest clients to retry whatever request resulted in the error. Transient errors are indicated explicitly below.\n\n**Note**: when a failure occurs as a result of making a federated RPC to another backend, the error response contains the following extra fields:\n\n - `type`: \"federation\" (just the literal string in quotes, which can be used as an error type identifier when parsing errors)\n - `domain`: the target backend of the RPC that failed;\n - `path`: the path of the RPC that failed.\n\n### Domain errors\n\nErrors in this category result from trying to communicate with a backend that is considered non-existent or invalid. They can result from invalid user input or client issues, but they can also be a symptom of misconfiguration in one or multiple backends. These errors have a 4xx status code.\n\n - **Remote backend not found** (status: 422, label: `invalid-domain`): This backend attempted to contact a backend which does not exist or is not properly configured. For the most part, clients can consider this error equivalent to a domain not existing, although it should be noted that certain mistakes in the DNS configuration on a remote backend can lead to the backend not being recognized, and hence to this error. It is therefore not advisable to take any destructive action upon encountering this error, such as deleting remote users from conversations.\n - **Federation denied locally** (status: 400, label: `federation-denied`): This backend attempted an RPC to a non-whitelisted backend. Similar considerations as for the previous error apply.\n - **Federation not enabled** (status: 400, label: `federation-not-enabled`): Federation has not been configured for this backend. This will happen if a federation-aware client tries to talk to a backend for which federation is disabled, or if federation was disabled on the backend after reaching a federation-specific state (e.g. conversations with remote users). There is no way to cleanly recover from these errors at this point.\n\n### Local federation errors\n\nAn error in this category likely indicates an issue with the configuration of federation on the local backend. Possibly transient errors are indicated explicitly below. All these errors have a 500 status code.\n\n - **Federation unavailable** (status: 500, label: `federation-not-available`): Federation is configured for this backend, but the local federator cannot be reached. This can be transient, so clients should retry the request.\n - **Federation not implemented** (status: 500, label: `federation-not-implemented`): Federated behaviour for a certain endpoint is not yet implemented.\n - **Federator discovery failed** (status: 400, label: `discovery-failure`): A DNS error occurred during discovery of a remote backend. This can be transient, so clients should retry the request.\n - **Local federation error** (status: 500, label: `federation-local-error`): An error occurred in the communication between this backend and its local federator. These errors are most likely caused by bugs in the backend, and should be reported as such.\n\n### Remote federation errors\n\nErrors in this category are returned in case of communication issues between the local backend and a remote one, or if the remote side encountered an error while processing an RPC. Some errors in this category might be caused by incorrect client behaviour, wrong user input, or incorrect certificate configuration. Possibly transient errors are indicated explicitly. We use non-standard 5xx status codes for these errors.\n\n - **HTTP2 error** (status: 533, label: `federation-http2-error`): The current federator encountered an error when making an HTTP2 request to a remote one. Check the error message for more details.\n - **Connection refused** (status: 521, label: `federation-connection-refused`): The local federator could not connect to a remote one. This could be transient, so clients should retry the request.\n - **TLS failure**: (status: 525, label: `federation-tls-error`): An error occurred during the TLS handshake between the local federator and a remote one. This is most likely due to an issue with the certificate on the remote end.\n - **Remote federation error** (status: 533, label: `federation-remote-error`): The remote backend could not process a request coming from this backend. Check the error message for more details.\n - **Version negotiation error** (status: 533, label: `federation-version-error`): The remote backend returned invalid version information.\n\n### Backend compatibility errors\n\nAn error in this category will be returned when this backend makes an invalid or unsupported RPC to another backend. This can indicate some incompatibility between backends or a backend bug. These errors are unlikely to be transient, so retrying requests is *not* advised.\n\n - **Version mismatch** (status: 531, label: `federation-version-mismatch`): A remote backend is running an unsupported version of the federator.\n - **Invalid content type** (status: 533, label: `federation-invalid-content-type`): An RPC to another backend returned with an invalid content type.\n - **Unsupported content type** (status: 533, label: `federation-unsupported-content-type`): An RPC to another backend returned with an unsupported content type.\n", + "title": "Wire-Server API", + "version": "" + }, + "paths": { + "/access": { + "post": { + "description": " [internal route ID: \"access\"]\n\nYou can provide only a cookie or a cookie and token. Every other combination is invalid. Access tokens can be given as query parameter or authorisation header, with the latter being preferred.", + "parameters": [ + { + "in": "query", + "name": "client_id", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "Set-Cookie": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/AccessToken" + } + }, + "400": { + "description": "Invalid `client_id`" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Obtain an access tokens for a cookie" + } + }, + "/access/logout": { + "post": { + "description": " [internal route ID: \"logout\"]\n\nCalling this endpoint will effectively revoke the given cookie and subsequent calls to /access with the same cookie will result in a 403.", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Logout" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Log out in order to remove a cookie from the server" + } + }, + "/access/self/email": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"change-self-email\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/EmailUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "202": { + "description": "Update accepted and pending activation of the new email", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "204": { + "description": "No update, current and new email address are the same", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "400": { + "description": "Invalid e-mail address. (label: `invalid-email`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "blacklisted-phone", + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your email address" + } + }, + "/activate": { + "get": { + "description": " [internal route ID: \"get-activate\"]\n\nSee also 'POST /activate' which has a larger feature set.", + "parameters": [ + { + "description": "Activation key", + "in": "query", + "name": "key", + "required": true, + "type": "string" + }, + { + "description": "Activation code", + "in": "query", + "name": "code", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful.", + "schema": { + "$ref": "#/definitions/ActivationResponse" + } + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "description": "Invalid `code` or `key`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)", + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Activate (i.e. confirm) an email address or phone number." + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-activate\"]\n\nActivation only succeeds once and the number of failed attempts for a valid key is limited.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Activate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful.", + "schema": { + "$ref": "#/definitions/ActivationResponse" + } + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)", + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Activate (i.e. confirm) an email address or phone number." + } + }, + "/activate/send": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-activate-send\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SendActivationCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Activation code sent." + }, + "400": { + "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "description": "The given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)", + "schema": { + "example": { + "code": 403, + "label": "blacklisted-phone", + "message": "The given phone number has been blacklisted due to suspected abuse or a complaint" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "blacklisted-phone", + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "451": { + "description": "[Customer extension] the email domain example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department. (label: `domain-blocked-for-registration`)", + "schema": { + "example": { + "code": 451, + "label": "domain-blocked-for-registration", + "message": "[Customer extension] the email domain example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department." + }, + "properties": { + "code": { + "enum": [ + 451 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-blocked-for-registration" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Send (or resend) an email or phone activation code." + } + }, + "/api-version": { + "get": { + "description": " [internal route ID: \"get-version\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/VersionInfo" + } + } + } + } + }, + "/assets": { + "post": { + "consumes": [ + "multipart/mixed" + ], + "parameters": [ + { + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server.", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AssetSource" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Asset" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "description": "Asset too large (label: `client-error`)", + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Upload an asset" + } + }, + "/assets/{key_domain}/{key}": { + "delete": { + "description": "**Note**: only local assets can be deleted.", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "400": { + "description": "Invalid `key` or `key_domain`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": "**Note**: local assets result in a redirect, while remote assets are streamed directly.", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Asset returned directly with content type `application/octet-stream`" + }, + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + } + }, + "400": { + "description": "Invalid `asset_token` or `Asset-Token` or `key` or `key_domain`" + }, + "404": { + "description": "Asset not found (label: `not-found`)\n\nAsset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Download an asset", + "x-wire-makes-federated-call-to": [ + [ + "cargohold", + "get-asset" + ], + [ + "cargohold", + "stream-asset" + ] + ] + } + }, + "/assets/{key}/token": { + "delete": { + "description": "**Note**: deleting the token makes the asset public.", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset token deleted" + }, + "400": { + "description": "Invalid `key`" + } + }, + "summary": "Delete an asset token" + }, + "post": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/NewAssetToken" + } + }, + "400": { + "description": "Invalid `key`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Renew an asset token" + } + }, + "/await": { + "get": { + "description": " [internal route ID: \"await-notifications\"]\n\n", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "type": "string" + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "400": { + "description": "Invalid `client`" + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Establish websocket connection" + } + }, + "/bot/assets": { + "post": { + "consumes": [ + "multipart/mixed" + ], + "parameters": [ + { + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server.", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AssetSource" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Asset" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "description": "Asset too large (label: `client-error`)", + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Upload an asset" + } + }, + "/bot/assets/{key}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "400": { + "description": "Invalid `key`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete an asset" + }, + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + } + }, + "400": { + "description": "Invalid `asset_token` or `Asset-Token` or `key`" + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Download an asset" + } + }, + "/bot/client": { + "get": { + "description": " [internal route ID: \"bot-get-client\"]\n\n", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Client found", + "schema": { + "$ref": "#/definitions/Client" + } + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Client not found (label: `client-not-found`)\n\nClient not found (label: `client-not-found`)", + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get client for bot" + } + }, + "/bot/client/prekeys": { + "get": { + "description": " [internal route ID: \"bot-list-prekeys\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "List prekeys for bot" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"bot-update-prekeys\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateBotPrekeys" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Client not found (label: `client-not-found`)", + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update prekeys for bot" + } + }, + "/bot/messages": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-bot-message-unqualified\"]\n\n", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/new-otr-message" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + }, + "400": { + "description": "Invalid `body` or `report_missing` or `ignore_missing`" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + } + }, + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-message-sent" + ], + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/bot/self": { + "delete": { + "description": " [internal route ID: \"bot-delete-self\"]\n\n", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "" + }, + "403": { + "description": "The targeted user is not a bot. (label: `invalid-bot`)\n\nAccess denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-bot", + "message": "The targeted user is not a bot." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-bot", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete self" + }, + "get": { + "description": " [internal route ID: \"bot-get-self\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/UserProfile" + } + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "User not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get self" + } + }, + "/bot/users": { + "get": { + "description": " [internal route ID: \"bot-list-users\"]\n\n", + "parameters": [ + { + "in": "query", + "name": "ids", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "$ref": "#/definitions/BotUserView" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `ids`" + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "List users" + } + }, + "/bot/users/prekeys": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"bot-claim-users-prekeys\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UserClients" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/UserClientPrekeyMap" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nToo many clients (label: `too-many-clients`)\n\nAccess denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "too-many-clients", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Claim users prekeys" + } + }, + "/bot/users/{User ID}/clients": { + "get": { + "description": " [internal route ID: \"bot-get-user-clients\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "User ID", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "$ref": "#/definitions/PubClient" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `User ID`" + }, + "403": { + "description": "Access denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get user clients" + } + }, + "/broadcast/otr/messages": { + "post": { + "consumes": [ + "application/json;charset=utf-8", + "application/x-protobuf" + ], + "description": " [internal route ID: \"post-otr-broadcast-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/new-otr-message" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + }, + "400": { + "description": "Invalid `body` or `report_missing` or `ignore_missing`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + } + }, + "summary": "Broadcast an encrypted message to all team members and all contacts (accepts JSON or Protobuf)" + } + }, + "/broadcast/proteus/messages": { + "post": { + "consumes": [ + "application/x-protobuf" + ], + "description": " [internal route ID: \"post-proteus-broadcast\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `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\nThe 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, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QualifiedNewOtrMessage" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + }, + "400": { + "description": "Invalid `body`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + } + }, + "summary": "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" + } + }, + "/calls/config": { + "get": { + "description": " [internal route ID: \"get-calls-config\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RTCConfiguration" + } + } + }, + "summary": "[deprecated] Retrieve TURN server addresses and credentials for IP addresses, scheme `turn` and transport `udp` only" + } + }, + "/calls/config/v2": { + "get": { + "description": " [internal route ID: \"get-calls-config-v2\"]\n\n", + "parameters": [ + { + "description": "Limit resulting list. Allowed values [1..10]", + "in": "query", + "maximum": 10, + "minimum": 1, + "name": "limit", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RTCConfiguration" + } + }, + "400": { + "description": "Invalid `limit`" + } + }, + "summary": "Retrieve all TURN server addresses and credentials. Clients are expected to do a DNS lookup to resolve the IP addresses of the given hostnames " + } + }, + "/clients": { + "get": { + "description": " [internal route ID: \"list-clients\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "$ref": "#/definitions/Client" + }, + "type": "array" + } + } + }, + "summary": "List the registered clients" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"add-client\"]\n\n", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewClient" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "201": { + "description": "", + "headers": { + "Location": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Client" + } + }, + "400": { + "description": "Invalid `body` or `X-Forwarded-For`\n\nMalformed prekeys uploaded (label: `bad-request`)" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nToo many clients (label: `too-many-clients`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "missing-auth", + "too-many-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Register a new client" + } + }, + "/clients/{cid}/access-token": { + "post": { + "description": " [internal route ID: \"create-access-token\"]\n\nCreate an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. The access token will be returned as JWT DPoP token in the `DPoP` header.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "cid", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "DPoP", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Access token created", + "headers": { + "Cache-Control": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/DPoPAccessTokenResponse" + } + }, + "400": { + "description": "Invalid `DPoP` or `cid`" + } + }, + "summary": "Create a JWT DPoP access token" + } + }, + "/clients/{client}": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"delete-client\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteClient" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Client deleted" + }, + "400": { + "description": "Invalid `body` or `client`" + } + }, + "summary": "Delete an existing client" + }, + "get": { + "description": " [internal route ID: \"get-client\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Client found", + "schema": { + "$ref": "#/definitions/Client" + } + }, + "400": { + "description": "Invalid `client`" + }, + "404": { + "description": "Client not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a registered client by ID" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-client\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateClient" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Client updated" + }, + "400": { + "description": "Invalid `body` or `client`\n\nMalformed prekeys uploaded (label: `bad-request`)" + } + }, + "summary": "Update a registered client" + } + }, + "/clients/{client}/capabilities": { + "get": { + "description": " [internal route ID: \"get-client-capabilities\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ClientCapabilityList" + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "Read back what the client has been posting about itself" + } + }, + "/clients/{client}/nonce": { + "get": { + "description": " [internal route ID: \"get-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "No Content", + "headers": { + "Cache-Control": { + "type": "string" + }, + "Replay-Nonce": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "Get a new nonce for a client CSR" + }, + "head": { + "description": " [internal route ID: \"head-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "No Content", + "headers": { + "Cache-Control": { + "type": "string" + }, + "Replay-Nonce": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "Get a new nonce for a client CSR" + } + }, + "/clients/{client}/prekeys": { + "get": { + "description": " [internal route ID: \"get-client-prekeys\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "List the remaining prekey IDs of a client" + } + }, + "/connections/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-connection\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Connection found", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + }, + "404": { + "description": "Connection not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get an existing connection to another user (local or remote)" + }, + "post": { + "description": " [internal route ID: \"create-connection\"]\n\nYou can have no more than 1000 connections in accepted or sent state", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Connection existed", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "201": { + "description": "Connection was created", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "description": "The user has no verified identity (email or phone number) (label: `no-identity`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified identity (email or phone number)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "connection-limit", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create a connection to another user", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "send-connection-action" + ] + ] + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-connection\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConnectionUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Connection updated", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "204": { + "description": "Connection unchanged" + }, + "400": { + "description": "Invalid `body` or `uid` or `uid_domain`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "description": "The user has no verified identity (email or phone number) (label: `no-identity`)\n\nInvalid status transition (label: `bad-conn-update`)\n\nUsers are not connected (label: `not-connected`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified identity (email or phone number)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "bad-conn-update", + "not-connected", + "connection-limit", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update a connection to another user", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "send-connection-action" + ] + ] + } + }, + "/conversations": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"create-group-conversation\"]\n\nThis returns 201 when a new conversation is created, and 200 when the conversation already existed", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewConv" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "201": { + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/CreateGroupConversation" + } + }, + "400": { + "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nAttempting to add group members outside MLS (label: `non-empty-member-list`)" + }, + "403": { + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nUsers are not connected (label: `not-connected`)\n\nConversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "operation-denied", + "no-team-member", + "not-connected", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph", + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + }, + "533": { + "description": "Some domains are unreachable", + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "summary": "Create a new conversation", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "api-version" + ], + [ + "brig", + "get-not-fully-connected-backends" + ], + [ + "galley", + "on-conversation-created" + ], + [ + "galley", + "on-conversation-updated" + ] + ] + } + }, + "/conversations/code-check": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"code-check\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Valid" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Invalid conversation password (label: `invalid-conversation-password`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation-password", + "message": "Invalid conversation password" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Check validity of a conversation code.If the guest links team feature is disabled, this will fail with 404 CodeNotFound.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/join` which responds with 409 GuestLinksDisabled if guest links are disabled." + } + }, + "/conversations/join": { + "get": { + "description": " [internal route ID: \"get-conversation-by-reusable-code\"]\n\n", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "query", + "name": "code", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationCoverView" + } + }, + "400": { + "description": "Invalid `code` or `key`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get limited conversation information by key/code pair" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"join-conversation-by-code-unqualified\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/JoinConversationByCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation joined", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Maximum number of members per conversation reached (label: `too-many-members`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)", + "schema": { + "example": { + "code": 403, + "label": "too-many-members", + "message": "Maximum number of members per conversation reached" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-members", + "no-team-member", + "invalid-op", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Join a conversation using a reusable code.If the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled.", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ] + ] + } + }, + "/conversations/list": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"list-conversations\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ListConversations" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationsResponse" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Get conversation metadata for a list of conversation ids", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "get-conversations" + ] + ] + } + }, + "/conversations/list-ids": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"list-conversation-ids\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GetPaginated_ConversationIds" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationIds_Page" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Get all conversation IDs." + } + }, + "/conversations/one2one": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"create-one-to-one-conversation\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewConv" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "201": { + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nBoth users must be members of the same binding team (label: `non-binding-team-members`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "operation-denied", + "not-connected", + "no-team-member", + "non-binding-team-members", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)\n\nNot a member of a binding team (label: `non-binding-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "non-binding-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "533": { + "description": "Some domains are unreachable", + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "summary": "Create a 1:1 conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-created" + ] + ] + } + }, + "/conversations/self": { + "post": { + "description": " [internal route ID: \"create-self-conversation\"]\n\n", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "201": { + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + } + }, + "summary": "Create a self-conversation" + } + }, + "/conversations/{Conversation ID}/bots": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"add-bot\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "Conversation ID", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AddBot" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "", + "schema": { + "$ref": "#/definitions/AddBotResponse" + } + }, + "400": { + "description": "Invalid `body` or `Conversation ID`" + }, + "403": { + "description": "The desired service is currently disabled. (label: `service-disabled`)\n\nMaximum number of members per conversation reached. (label: `too-many-members`)\n\nThe operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "service-disabled", + "message": "The desired service is currently disabled." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "service-disabled", + "too-many-members", + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Add bot" + } + }, + "/conversations/{Conversation ID}/bots/{Bot ID}": { + "delete": { + "description": " [internal route ID: \"remove-bot\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "Conversation ID", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "Bot ID", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "User found", + "schema": { + "$ref": "#/definitions/RemoveBotResponse" + } + }, + "204": { + "description": "" + }, + "400": { + "description": "Invalid `Bot ID` or `Conversation ID`" + }, + "403": { + "description": "The operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation", + "message": "The operation is not allowed in this conversation." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove bot" + } + }, + "/conversations/{cnv_domain}/{cnv}": { + "get": { + "description": " [internal route ID: \"get-conversation\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "400": { + "description": "Invalid `cnv` or `cnv_domain`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a conversation by ID", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "get-conversations" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/access": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-access\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationAccessDatav3" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Access updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Access unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid target access (label: `invalid-op`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing modify_conversation_access) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid target access" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update access modes for a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/members": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"add-members-to-conversation\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/InviteQualified" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "not-connected", + "no-team-member", + "access-denied", + "too-many-members", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph", + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + }, + "533": { + "description": "Some domains are unreachable", + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "summary": "Add qualified members to an existing conversation.", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}": { + "delete": { + "description": " [internal route ID: \"remove-member\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "type": "string" + }, + { + "description": "Target User ID", + "format": "uuid", + "in": "path", + "name": "usr", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Member removed", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "No change" + }, + "400": { + "description": "Invalid `usr` or `usr_domain` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove a member from a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "leave-conversation" + ], + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-other-member\"]\n\n**Note**: at least one field has to be provided.", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "type": "string" + }, + { + "description": "Target User ID", + "format": "uuid", + "in": "path", + "name": "usr", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OtherMemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Membership updated" + }, + "400": { + "description": "Invalid `body` or `usr` or `usr_domain` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation-member", + "message": "Conversation member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation-member", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update membership of the specified user", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/message-timer": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-message-timer\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationMessageTimerUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Message timer updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Message timer unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update the message timer for a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/name": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-name\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationRename" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Name unchanged", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Name updated" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update conversation name", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/proteus/messages": { + "post": { + "consumes": [ + "application/x-protobuf" + ], + "description": " [internal route ID: \"post-proteus-message\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `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\nThe 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, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QualifiedNewOtrMessage" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + } + }, + "summary": "Post an encrypted message to a conversation (accepts only Protobuf)", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ], + [ + "galley", + "on-message-sent" + ], + [ + "galley", + "send-message" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/receipt-mode": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-receipt-mode\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationReceiptModeUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Receipt mode updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Receipt mode unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update receipt mode for a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "update-conversation" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/self": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-self\"]\n\n**Note**: at least one field has to be provided.", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Update successful" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update self membership properties" + } + }, + "/conversations/{cnv_domain}/{cnv}/typing": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"member-typing-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TypingData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification sent" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Sending typing notifications", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "update-typing-indicator" + ], + [ + "galley", + "on-typing-indicator-updated" + ] + ] + } + }, + "/conversations/{cnv}": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-name-deprecated\"]\n\nUse `/conversations/:domain/:conv/name` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationRename" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Name updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Name unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update conversation name (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/conversations/{cnv}/code": { + "delete": { + "description": " [internal route ID: \"remove-code-unqualified\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation code deleted.", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete conversation code" + }, + "get": { + "description": " [internal route ID: \"get-code\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation Code", + "schema": { + "$ref": "#/definitions/ConversationCodeInfo" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get existing conversation code" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"create-conversation-code-unqualified\"]\n\n\nOAuth scope: `write:conversations_code`", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateConversationCodeRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation code already exists.", + "schema": { + "$ref": "#/definitions/ConversationCodeInfo" + } + }, + "201": { + "description": "Conversation code created.", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "Conversation code already exists with a different password setting than the requested one. (label: `create-conv-code-conflict`)\n\nThe guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "create-conv-code-conflict", + "message": "Conversation code already exists with a different password setting than the requested one." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "create-conv-code-conflict", + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create or recreate a conversation code" + } + }, + "/conversations/{cnv}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: \"get-conversation-guest-links-status\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get the status of the guest links feature for a conversation that potentially has been created by someone from another team." + } + }, + "/conversations/{cnv}/join": { + "post": { + "description": " [internal route ID: \"join-conversation-by-id-unqualified\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation joined", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Maximum number of members per conversation reached (label: `too-many-members`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "too-many-members", + "message": "Maximum number of members per conversation reached" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-members", + "no-team-member", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Join a conversation by its ID (if link access enabled)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ] + ] + } + }, + "/conversations/{cnv}/members/{usr}": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-other-member-unqualified\"]\n\nUse `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "description": "Target User ID", + "format": "uuid", + "in": "path", + "name": "usr", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OtherMemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Membership updated" + }, + "400": { + "description": "Invalid `body` or `usr` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation-member", + "message": "Conversation member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation-member", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update membership of the specified user (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/conversations/{cnv}/message-timer": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-message-timer-unqualified\"]\n\nUse `/conversations/:domain/:cnv/message-timer` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationMessageTimerUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Message timer updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Message timer unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update the message timer for a conversation (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/conversations/{cnv}/name": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-name-unqualified\"]\n\nUse `/conversations/:domain/:conv/name` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationRename" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Name updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Name unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update conversation name (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/conversations/{cnv}/otr/messages": { + "post": { + "consumes": [ + "application/json;charset=utf-8", + "application/x-protobuf" + ], + "description": " [internal route ID: \"post-otr-message-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "query", + "name": "ignore_missing", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/new-otr-message" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + }, + "400": { + "description": "Invalid `body` or `report_missing` or `ignore_missing` or `cnv`" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + } + }, + "summary": "Post an encrypted message to a conversation (accepts JSON or Protobuf)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-message-sent" + ], + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/conversations/{cnv}/receipt-mode": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-receipt-mode-unqualified\"]\n\nUse `PUT /conversations/:domain/:cnv/receipt-mode` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationReceiptModeUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Receipt mode updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Receipt mode unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update receipt mode for a conversation (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "update-conversation" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/conversations/{cnv}/roles": { + "get": { + "description": " [internal route ID: \"get-conversation-roles\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationRolesList" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get existing roles available for the given conversation" + } + }, + "/conversations/{cnv}/self": { + "get": { + "description": " [internal route ID: \"get-conversation-self-unqualified\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Member" + } + }, + "400": { + "description": "Invalid `cnv`" + } + }, + "summary": "Get self membership properties (deprecated)" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-conversation-self-unqualified\"]\n\nUse `/conversations/:domain/:conv/self` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Update successful" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update self membership properties (deprecated)" + } + }, + "/cookies": { + "get": { + "description": " [internal route ID: \"list-cookies\"]\n\n", + "parameters": [ + { + "description": "Filter by label (comma-separated list)", + "in": "query", + "name": "labels", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of cookies", + "schema": { + "$ref": "#/definitions/CookieList" + } + }, + "400": { + "description": "Invalid `labels`" + } + }, + "summary": "Retrieve the list of cookies currently stored for the user" + } + }, + "/cookies/remove": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"remove-cookies\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RemoveCookies" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Cookies revoked" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Revoke stored cookies" + } + }, + "/custom-backend/by-domain/{domain}": { + "get": { + "description": " [internal route ID: \"get-custom-backend-by-domain\"]\n\n", + "parameters": [ + { + "description": "URL-encoded email domain", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/CustomBackend" + } + }, + "400": { + "description": "Invalid `domain`" + }, + "404": { + "description": "Custom backend not found (label: `custom-backend-not-found`)", + "schema": { + "example": { + "code": 404, + "label": "custom-backend-not-found", + "message": "Custom backend not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "custom-backend-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Shows information about custom backends related to a given email domain" + } + }, + "/delete": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"verify-delete\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/VerifyDeleteUser" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Invalid verification code (label: `invalid-code`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Verify account deletion with a code." + } + }, + "/feature-configs": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-user\"]\n\nGets feature configs for a user. If the user is a member of a team and has the required permissions, this will return the team's feature configs.If the user is not a member of a team, this will return the personal feature configs (the server defaults).\nOAuth scope: `read:feature_configs`", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AllFeatureConfigs" + } + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Gets feature configs for a user" + } + }, + "/identity-providers": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPList" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/IdPMetadataInfo" + } + }, + { + "format": "uuid", + "in": "query", + "name": "replaces", + "required": false, + "type": "string" + }, + { + "default": "v2", + "enum": [ + "v1", + "v2" + ], + "in": "query", + "name": "api_version", + "required": false, + "type": "string" + }, + { + "in": "query", + "maxLength": 1, + "minLength": 32, + "name": "handle", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "201": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPConfig" + } + }, + "400": { + "description": "Invalid `handle` or `api_version` or `replaces` or `body`" + } + } + } + }, + "/identity-providers/{id}": { + "delete": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + }, + { + "in": "query", + "name": "purge", + "required": false, + "type": "boolean" + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "Invalid `purge` or `id`" + } + } + }, + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPConfig" + } + }, + "400": { + "description": "Invalid `id`" + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/IdPMetadataInfo" + } + }, + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + }, + { + "in": "query", + "maxLength": 1, + "minLength": 32, + "name": "handle", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPConfig" + } + }, + "400": { + "description": "Invalid `handle` or `id` or `body`" + } + } + } + }, + "/identity-providers/{id}/raw": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/xml" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid `id`" + } + } + } + }, + "/list-connections": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"list-connections\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GetPaginated_Connections" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Connections_Page" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "List the connections to other users, including remote users" + } + }, + "/list-users": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"list-users-by-ids-or-handles\"]\n\nThe 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ListUsersQuery" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ListUsersById" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "List users", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/login": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"login\"]\n\nLogins are throttled at the server's discretion", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Login" + } + }, + { + "description": "Request a persistent cookie instead of a session cookie", + "in": "query", + "name": "persist", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "Set-Cookie": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/AccessToken" + } + }, + "400": { + "description": "Invalid `persist` or `body`" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nAccount pending activation (label: `pending-activation`)\n\nAccount suspended (label: `suspended`)\n\nAuthentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "pending-activation", + "suspended", + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Authenticate a user to obtain a cookie and first access token" + } + }, + "/login/send": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"send-login-code\"]\n\nThis operation generates and sends a login code via sms for phone login. A login code can be used only once and times out after 10 minutes. Only one login code may be pending at a time. For 2nd factor authentication login with email and password, use the `/verification-code/send` endpoint.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SendLoginCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/LoginCodeTimeout" + } + }, + "400": { + "description": "Invalid mobile phone number (label: `invalid-phone`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "The operation is not permitted because the user has a password set (label: `password-exists`)", + "schema": { + "example": { + "code": 403, + "label": "password-exists", + "message": "The operation is not permitted because the user has a password set" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Send a login code to a verified phone number" + } + }, + "/notifications": { + "get": { + "description": " [internal route ID: \"get-notifications\"]\n\n", + "parameters": [ + { + "description": "Only return notifications more recent than this", + "format": "uuid", + "in": "query", + "name": "since", + "required": false, + "type": "string" + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "type": "string" + }, + { + "description": "Maximum number of notifications to return", + "format": "int32", + "in": "query", + "maximum": 10000, + "minimum": 100, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification list", + "schema": { + "$ref": "#/definitions/QueuedNotificationList" + } + }, + "400": { + "description": "Invalid `size` or `client` or `since`" + }, + "404": { + "description": "Some notifications not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Fetch notifications" + } + }, + "/notifications/last": { + "get": { + "description": " [internal route ID: \"get-last-notification\"]\n\n", + "parameters": [ + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification found", + "schema": { + "$ref": "#/definitions/QueuedNotification" + } + }, + "400": { + "description": "Invalid `client`" + }, + "404": { + "description": "Some notifications not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Fetch the last notification" + } + }, + "/notifications/{id}": { + "get": { + "description": " [internal route ID: \"get-notification-by-id\"]\n\n", + "parameters": [ + { + "description": "Notification ID", + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification found", + "schema": { + "$ref": "#/definitions/QueuedNotification" + } + }, + "400": { + "description": "Invalid `client` or `id`" + }, + "404": { + "description": "Some notifications not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Fetch a notification by ID" + } + }, + "/oauth/applications": { + "get": { + "description": " [internal route ID: \"get-oauth-applications\"]\n\nGet all OAuth applications with active account access for a user.", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OAuth applications found", + "schema": { + "items": { + "$ref": "#/definitions/OAuthApplication" + }, + "type": "array" + } + } + }, + "summary": "Get OAuth applications with account access" + } + }, + "/oauth/applications/{OAuthClientId}": { + "delete": { + "description": " [internal route ID: \"revoke-oauth-account-access\"]\n\n", + "parameters": [ + { + "description": "The ID of the OAuth client", + "format": "uuid", + "in": "path", + "name": "OAuthClientId", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "OAuth application access revoked" + }, + "400": { + "description": "Invalid `OAuthClientId`" + } + }, + "summary": "Revoke account access from an OAuth application" + } + }, + "/oauth/authorization/codes": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"create-oauth-auth-code\"]\n\nCurrently only supports the 'code' response type, which corresponds to the authorization code flow.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateOAuthAuthorizationCodeRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request\n\nThe redirect URL does not match the one registered with the client (label: `redirect-url-miss-match`) or `body`", + "headers": { + "Location": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "headers": { + "Location": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Location": { + "type": "string" + } + } + } + }, + "summary": "Create an OAuth authorization code" + } + }, + "/oauth/clients/{OAuthClientId}": { + "get": { + "description": " [internal route ID: \"get-oauth-client\"]\n\n", + "parameters": [ + { + "description": "The ID of the OAuth client", + "format": "uuid", + "in": "path", + "name": "OAuthClientId", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OAuth client found", + "schema": { + "$ref": "#/definitions/OAuthClient" + } + }, + "400": { + "description": "Invalid `OAuthClientId`" + }, + "403": { + "description": "OAuth is disabled (label: `forbidden`)", + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "OAuth is disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "OAuth client not found (label: `not-found`)\n\nOAuth client not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get OAuth client information" + } + }, + "/oauth/revoke": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"revoke-oauth-refresh-token\"]\n\nRevoke an access token.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OAuthRevokeRefreshTokenRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Invalid refresh token (label: `forbidden`)", + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "Invalid refresh token" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "OAuth client not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "Internal error while handling JWT token (label: `jwt-error`)", + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Revoke an OAuth refresh token" + } + }, + "/oauth/token": { + "post": { + "consumes": [ + "application/x-www-form-urlencoded" + ], + "description": " [internal route ID: \"create-oauth-access-token\"]\n\nObtain a new access token from an authorization code or a refresh token.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Either" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/OAuthAccessTokenResponse" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Invalid grant (label: `invalid_grant`)\n\nInvalid client credentials (label: `forbidden`)\n\nInvalid grant type (label: `forbidden`)\n\nInvalid refresh token (label: `forbidden`)\n\nOAuth is disabled (label: `forbidden`)", + "schema": { + "example": { + "code": 403, + "label": "invalid_grant", + "message": "Invalid grant" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid_grant", + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "OAuth client not found (label: `not-found`)\n\nOAuth authorization code not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "Internal error while handling JWT token (label: `jwt-error`)", + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create an OAuth access token" + } + }, + "/onboarding/v3": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"onboarding\"]\n\nDEPRECATED: the feature has been turned off, the end-point does nothing and always returns '{\"results\":[],\"auto-connects\":[]}'.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Body" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/DeprecatedMatchingResult" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Upload contacts and invoke matching." + } + }, + "/password-reset": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-password-reset\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewPasswordReset" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Password reset code created and sent by email." + }, + "400": { + "description": "Invalid `body`\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + }, + "409": { + "description": "A password reset is already in progress. (label: `code-exists`)", + "schema": { + "example": { + "code": 409, + "label": "code-exists", + "message": "A password reset is already in progress." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Initiate a password reset." + } + }, + "/password-reset/complete": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-password-reset-complete\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CompletePasswordReset" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + } + }, + "summary": "Complete a password reset." + } + }, + "/password-reset/{key}": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"post-password-reset-key-deprecated\"]\n\nDEPRECATED: Use 'POST /password-reset/complete'.", + "parameters": [ + { + "description": "An opaque key for a pending password reset.", + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PasswordReset" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { + "description": "Invalid `body` or `key`\n\nInvalid password reset code. (label: `invalid-code`)\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + }, + "409": { + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)\n\nA password reset is already in progress. (label: `code-exists`)", + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ", + "code-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Complete a password reset." + } + }, + "/properties": { + "delete": { + "description": " [internal route ID: \"clear-properties\"]\n\n", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Properties cleared" + } + }, + "summary": "Clear all properties" + }, + "get": { + "description": " [internal route ID: \"list-property-keys\"]\n\n", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of property keys", + "schema": { + "items": { + "$ref": "#/definitions/ASCII" + }, + "type": "array" + } + } + }, + "summary": "List all property keys" + } + }, + "/properties-values": { + "get": { + "description": " [internal route ID: \"list-properties\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PropertyKeysAndValues" + } + } + }, + "summary": "List all properties with key and value" + } + }, + "/properties/{key}": { + "delete": { + "description": " [internal route ID: \"delete-property\"]\n\n", + "parameters": [ + { + "format": "printable", + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Property deleted" + }, + "400": { + "description": "Invalid `key`" + } + }, + "summary": "Delete a property" + }, + "get": { + "description": " [internal route ID: \"get-property\"]\n\n", + "parameters": [ + { + "format": "printable", + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "The property value", + "schema": { + "$ref": "#/definitions/PropertyValue" + } + }, + "400": { + "description": "Invalid `key`" + }, + "404": { + "description": "Property not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a property value" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"set-property\"]\n\n", + "parameters": [ + { + "format": "printable", + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PropertyValue" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Property set" + }, + "400": { + "description": "Invalid `body` or `key`" + } + }, + "summary": "Set a user property" + } + }, + "/provider/assets": { + "post": { + "consumes": [ + "multipart/mixed" + ], + "parameters": [ + { + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server.", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AssetSource" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Asset" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "description": "Asset too large (label: `client-error`)", + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Upload an asset" + } + }, + "/provider/assets/{key}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "400": { + "description": "Invalid `key`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete an asset" + }, + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + } + }, + "400": { + "description": "Invalid `asset_token` or `Asset-Token` or `key`" + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Download an asset" + } + }, + "/proxy/giphy/v1/gifs": {}, + "/proxy/googlemaps/api/staticmap": {}, + "/proxy/googlemaps/maps/api/geocode": {}, + "/proxy/youtube/v3": {}, + "/push/tokens": { + "get": { + "description": " [internal route ID: \"get-push-tokens\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PushTokenList" + } + } + }, + "summary": "List the user's registered push tokens" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"register-push-token\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PushToken" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Push token registered", + "headers": { + "Location": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/PushToken" + } + }, + "400": { + "description": "Invalid `body`" + }, + "404": { + "description": "App does not exist (label: `app-not-found`)\n\nInvalid push token (label: `invalid-token`)", + "schema": { + "example": { + "code": 404, + "label": "app-not-found", + "message": "App does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "app-not-found", + "invalid-token" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "413": { + "description": "Too many concurrent calls to SNS; is SNS down? (label: `sns-thread-budget-reached`)\n\nPush token length must be < 8192 for GCM or 400 for APNS (label: `token-too-long`)\n\nTried to add token to endpoint resulting in metadata length > 2048 (label: `metadata-too-long`)", + "schema": { + "example": { + "code": 413, + "label": "sns-thread-budget-reached", + "message": "Too many concurrent calls to SNS; is SNS down?" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "sns-thread-budget-reached", + "token-too-long", + "metadata-too-long" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Register a native push token" + } + }, + "/push/tokens/{pid}": { + "delete": { + "description": " [internal route ID: \"delete-push-token\"]\n\n", + "parameters": [ + { + "description": "The push token to delete", + "in": "path", + "name": "pid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "Push token unregistered" + }, + "400": { + "description": "Invalid `pid`" + }, + "404": { + "description": "Push token not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Push token not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Unregister a native push token" + } + }, + "/register": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"register\"]\n\nIf the environment where the registration takes place is private and a registered email address or phone number is not whitelisted, a 403 error is returned.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewUser" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "User created and pending activation", + "headers": { + "Location": { + "description": "UserId", + "format": "uuid", + "type": "string" + }, + "Set-Cookie": { + "description": "Cookie", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/User" + } + }, + "400": { + "description": "Invalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)\n\nInvalid mobile phone number (label: `invalid-phone`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email", + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "Unauthorized e-mail address or phone number. (label: `unauthorized`)\n\nUsing an invitation code requires registering the given email and/or phone. (label: `missing-identity`)\n\nThe given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nToo many members in this team. (label: `too-many-team-members`)\n\nThis instance does not allow creation of personal users or teams. (label: `user-creation-restricted`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address or phone number." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-phone", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "User does not exist (label: `invalid-code`)\n\nInvalid activation code (label: `invalid-code`)", + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "User does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Register a new user." + } + }, + "/scim/auth-tokens": { + "delete": { + "parameters": [ + { + "format": "uuid", + "in": "query", + "name": "id", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "Invalid `id`" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + } + }, + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ScimTokenList" + } + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + } + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateScimToken" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/CreateScimTokenResponse" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + } + } + }, + "/search/contacts": { + "get": { + "description": " [internal route ID: \"search-contacts\"]\n\n", + "parameters": [ + { + "description": "Search query", + "in": "query", + "name": "q", + "required": true, + "type": "string" + }, + { + "description": "Searched domain. Note: This is optional only for backwards compatibility, future versions will mandate this.", + "in": "query", + "name": "domain", + "required": false, + "type": "string" + }, + { + "description": "Number of results to return (min: 1, max: 500, default 15)", + "format": "int32", + "in": "query", + "maximum": 500, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchResult" + } + }, + "400": { + "description": "Invalid `size` or `domain` or `q`" + } + }, + "summary": "Search for users", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-users-by-ids" + ], + [ + "brig", + "search-users" + ] + ] + } + }, + "/self": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"delete-self\"]\n\nif the account has a verified identity, a verification code is sent and needs to be confirmed to authorise the deletion. if the account has no verified identity but a password, it must be provided. if password is correct, or if neither a verified identity nor a password exists, account deletion is scheduled immediately.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteUser" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "202": { + "description": "Deletion is pending verification with a code.", + "schema": { + "$ref": "#/definitions/DeletionCodeTimeout" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "description": "Team owners are not allowed to delete themselves; ask a fellow owner (label: `no-self-delete-for-team-owner`)\n\nA verification code for account deletion is still pending (label: `pending-delete`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)", + "schema": { + "example": { + "code": 403, + "label": "no-self-delete-for-team-owner", + "message": "Team owners are not allowed to delete themselves; ask a fellow owner" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-self-delete-for-team-owner", + "pending-delete", + "missing-auth", + "invalid-credentials", + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Initiate account deletion." + }, + "get": { + "description": " [internal route ID: \"get-self\"]\n\n\nOAuth scope: `read:self`", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/User" + } + } + }, + "summary": "Get your own profile" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"put-self\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UserUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "User updated" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Updating name is not allowed, because it is managed by SCIM (label: `managed-by-scim`)", + "schema": { + "example": { + "code": 403, + "label": "managed-by-scim", + "message": "Updating name is not allowed, because it is managed by SCIM" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "managed-by-scim" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "User not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update your profile." + } + }, + "/self/email": { + "delete": { + "description": " [internal route ID: \"remove-email\"]\n\nYour email address can only be removed if you also have a phone number.", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Identity Removed" + }, + "403": { + "description": "The last user identity (email or phone number) cannot be removed. (label: `last-identity`)\n\nThe user has no password. (label: `no-password`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)", + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity (email or phone number) cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-password", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove your email address." + } + }, + "/self/handle": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"change-handle\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/HandleUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Handle Changed" + }, + "400": { + "description": "The given handle is invalid (label: `invalid-handle`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-handle", + "message": "The given handle is invalid" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-handle" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "The user has no verified identity (email or phone number) (label: `no-identity`)\n\nUpdating handle is not allowed, because it is managed by SCIM (label: `managed-by-scim`)", + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified identity (email or phone number)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "managed-by-scim" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given handle is already taken (label: `handle-exists`)", + "schema": { + "example": { + "code": 409, + "label": "handle-exists", + "message": "The given handle is already taken" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "handle-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your handle." + } + }, + "/self/locale": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"change-locale\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LocaleUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Local Changed" + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Change your locale." + } + }, + "/self/password": { + "head": { + "description": " [internal route ID: \"check-password-exists\"]\n\n", + "responses": { + "200": { + "description": "Password is set" + }, + "404": { + "description": "Password is not set" + } + }, + "summary": "Check that your password is set." + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"change-password\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PasswordChange" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Password Changed" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "For password change, new and old password must be different. (label: `password-must-differ`)", + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password change, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your password." + } + }, + "/self/phone": { + "delete": { + "description": " [internal route ID: \"remove-phone\"]\n\nYour phone number can only be removed if you also have an email address and a password.", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Identity Removed" + }, + "403": { + "description": "The last user identity (email or phone number) cannot be removed. (label: `last-identity`)\n\nThe user has no password. (label: `no-password`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)", + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity (email or phone number) cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-password", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove your phone number." + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"change-phone\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PhoneUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "202": { + "description": "Phone updated" + }, + "400": { + "description": "Invalid mobile phone number (label: `invalid-phone`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "The given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)", + "schema": { + "example": { + "code": 403, + "label": "blacklisted-phone", + "message": "The given phone number has been blacklisted due to suspected abuse or a complaint" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "blacklisted-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your phone number." + } + }, + "/sso/finalize-login": { + "post": { + "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "produces": [ + "text/plain;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + } + } + } + }, + "/sso/finalize-login/{team}": { + "post": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "team", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/plain;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid `team`" + } + } + } + }, + "/sso/initiate-login/{idp}": { + "get": { + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "idp", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/html" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/FormRedirect" + } + }, + "400": { + "description": "Invalid `idp` or `error_redirect` or `success_redirect`" + } + } + }, + "head": { + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "idp", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/plain;charset=utf-8" + ], + "responses": { + "200": { + "description": "" + }, + "400": { + "description": "Invalid `idp` or `error_redirect` or `success_redirect`" + } + } + } + }, + "/sso/metadata": { + "get": { + "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "produces": [ + "application/xml" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + } + } + } + }, + "/sso/metadata/{team}": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "team", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/xml" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid `team`" + } + } + } + }, + "/sso/settings": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SsoSettings" + } + } + } + } + }, + "/system/settings": { + "get": { + "description": " [internal route ID: \"get-system-settings\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SystemSettings" + } + } + }, + "summary": "Returns a curated set of system configuration settings for authorized users." + } + }, + "/system/settings/unauthorized": { + "get": { + "description": " [internal route ID: \"get-system-settings-unauthorized\"]\n\n", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SystemSettingsPublic" + } + } + }, + "summary": "Returns a curated set of system configuration settings." + } + }, + "/teams/invitations/by-email": { + "head": { + "description": " [internal route ID: \"head-team-invitations\"]\n\n", + "parameters": [ + { + "description": "Email address", + "in": "query", + "name": "email", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Pending invitation exists." + }, + "400": { + "description": "Invalid `email`" + }, + "404": { + "description": "No pending invitations exists. (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "No pending invitations exists." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "Multiple conflicting invitations to different teams exists. (label: `conflicting-invitations`)", + "schema": { + "example": { + "code": 409, + "label": "conflicting-invitations", + "message": "Multiple conflicting invitations to different teams exists." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "conflicting-invitations" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Check if there is an invitation pending given an email address." + } + }, + "/teams/invitations/info": { + "get": { + "description": " [internal route ID: \"get-team-invitation-info\"]\n\n", + "parameters": [ + { + "description": "Invitation code", + "in": "query", + "name": "code", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Invitation info", + "schema": { + "$ref": "#/definitions/Invitation" + } + }, + "400": { + "description": "Invalid `code`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get invitation info given a code." + } + }, + "/teams/notifications": { + "get": { + "description": " [internal route ID: \"get-team-notifications\"]\n\nThis is a work-around for scalability issues with gundeck user event fan-out. It does not track all team-wide events, but only `member-join`.\nNote that `/teams/notifications` behaves differently from `/notifications`:\n- If there is a gap between the notification id requested with `since` and the available data, team queues respond with 200 and the data that could be found. They do NOT respond with status 404, but valid data in the body.\n- The notification with the id given via `since` is included in the response if it exists. You should remove this and only use it to decide whether there was a gap between your last request and this one.\n- If the notification id does *not* exist, you get the more recent events from the queue (instead of all of them). This can be done because a notification id is a UUIDv1, which is essentially a time stamp.\n- There is no corresponding `/last` end-point to get only the most recent event. That end-point was only useful to avoid having to pull the entire queue. In team queues, if you have never requested the queue before and have no prior notification id, just pull with timestamp 'now'.", + "parameters": [ + { + "description": "Notification id to start with in the response (UUIDv1)", + "format": "uuid", + "in": "query", + "name": "since", + "required": false, + "type": "string" + }, + { + "description": "Maximum number of events to return (1..10000; default: 1000)", + "format": "int32", + "in": "query", + "maximum": 10000, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/QueuedNotificationList" + } + }, + "400": { + "description": "Invalid `size` or `since`\n\nCould not parse notification id (must be UUIDv1). (label: `invalid-notification-id`)" + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Read recently added team members from team queue" + } + }, + "/teams/{tid}": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"delete-team\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TeamDeleteData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "202": { + "description": "Team is scheduled for removal" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Verification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (missing DeleteTeam) (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Verification code required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "access-denied", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "503": { + "description": "The delete queue is full; no further delete requests can be processed at the moment (label: `queue-full`)", + "schema": { + "example": { + "code": 503, + "label": "queue-full", + "message": "The delete queue is full; no further delete requests can be processed at the moment" + }, + "properties": { + "code": { + "enum": [ + 503 + ], + "type": "integer" + }, + "label": { + "enum": [ + "queue-full" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete a team" + }, + "get": { + "description": " [internal route ID: \"get-team\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Team" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a team by ID" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-team\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TeamUpdateData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Team updated" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Insufficient permissions (missing SetTeamData) (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions (missing SetTeamData)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update team properties" + } + }, + "/teams/{tid}/conversations": { + "get": { + "description": " [internal route ID: \"get-team-conversations\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamConversationList" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get team conversations" + } + }, + "/teams/{tid}/conversations/roles": { + "get": { + "description": " [internal route ID: \"get-team-conversation-roles\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationRolesList" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get existing roles available for the given team" + } + }, + "/teams/{tid}/conversations/{cid}": { + "delete": { + "description": " [internal route ID: \"delete-team-conversation\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation deleted" + }, + "400": { + "description": "Invalid `cid` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing delete_conversation) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove a team conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + }, + "get": { + "description": " [internal route ID: \"get-team-conversation\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamConversation" + } + }, + "400": { + "description": "Invalid `cid` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get one team conversation" + } + }, + "/teams/{tid}/features": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-team\"]\n\nGets feature configs for a team. User must be a member of the team and have permission to view team features.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AllFeatureConfigs" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Gets feature configs for a team" + } + }, + "/teams/{tid}/features/appLock": { + "get": { + "description": " [internal route ID: (\"get\", AppLockConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AppLockConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for appLock" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", AppLockConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AppLockConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AppLockConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for appLock" + } + }, + "/teams/{tid}/features/classifiedDomains": { + "get": { + "description": " [internal route ID: (\"get\", ClassifiedDomainsConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ClassifiedDomainsConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for classifiedDomains" + } + }, + "/teams/{tid}/features/conferenceCalling": { + "get": { + "description": " [internal route ID: (\"get\", ConferenceCallingConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConferenceCallingConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for conferenceCalling" + } + }, + "/teams/{tid}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: (\"get\", GuestLinksConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for conversationGuestLinks" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", GuestLinksConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for conversationGuestLinks" + } + }, + "/teams/{tid}/features/digitalSignatures": { + "get": { + "description": " [internal route ID: (\"get\", DigitalSignaturesConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/DigitalSignaturesConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for digitalSignatures" + } + }, + "/teams/{tid}/features/exposeInvitationURLsToTeamAdmin": { + "get": { + "description": " [internal route ID: (\"get\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for exposeInvitationURLsToTeamAdmin" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for exposeInvitationURLsToTeamAdmin" + } + }, + "/teams/{tid}/features/fileSharing": { + "get": { + "description": " [internal route ID: (\"get\", FileSharingConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/FileSharingConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for fileSharing" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", FileSharingConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/FileSharingConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/FileSharingConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for fileSharing" + } + }, + "/teams/{tid}/features/legalhold": { + "get": { + "description": " [internal route ID: (\"get\", LegalholdConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/LegalholdConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for legalhold" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", LegalholdConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LegalholdConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/LegalholdConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nCannot enable legalhold on large teams (reason: for removing LH from team, we need to iterate over all members, which is only supported for teams with less than 2k members) (label: `too-large-team-for-legalhold`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "too-large-team-for-legalhold", + "code-authentication-required", + "code-authentication-failed", + "access-denied", + "action-denied", + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for legalhold", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ] + ] + } + }, + "/teams/{tid}/features/outlookCalIntegration": { + "get": { + "description": " [internal route ID: (\"get\", OutlookCalIntegrationConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for outlookCalIntegration" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", OutlookCalIntegrationConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for outlookCalIntegration" + } + }, + "/teams/{tid}/features/searchVisibility": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityAvailableConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for searchVisibility" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", SearchVisibilityAvailableConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for searchVisibility" + } + }, + "/teams/{tid}/features/searchVisibilityInbound": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityInboundConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for searchVisibilityInbound" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", SearchVisibilityInboundConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for searchVisibilityInbound" + } + }, + "/teams/{tid}/features/selfDeletingMessages": { + "get": { + "description": " [internal route ID: (\"get\", SelfDeletingMessagesConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for selfDeletingMessages" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", SelfDeletingMessagesConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for selfDeletingMessages" + } + }, + "/teams/{tid}/features/sndFactorPasswordChallenge": { + "get": { + "description": " [internal route ID: (\"get\", SndFactorPasswordChallengeConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for sndFactorPasswordChallenge" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: (\"put\", SndFactorPasswordChallengeConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for sndFactorPasswordChallenge" + } + }, + "/teams/{tid}/features/sso": { + "get": { + "description": " [internal route ID: (\"get\", SSOConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SSOConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for sso" + } + }, + "/teams/{tid}/features/validateSAMLemails": { + "get": { + "description": " [internal route ID: (\"get\", ValidateSAMLEmailsConfig)]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ValidateSAMLEmailsConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for validateSAMLemails" + } + }, + "/teams/{tid}/get-members-by-ids-using-post": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"get-team-members-by-ids\"]\n\nThe `has_more` field in the response body is always `false`.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Maximum results to be returned", + "format": "int32", + "in": "query", + "maximum": 2000, + "minimum": 1, + "name": "maxResults", + "required": false, + "type": "integer" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UserIdList" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamMemberList" + } + }, + "400": { + "description": "Invalid `body` or `maxResults` or `tid`\n\nCan only process 2000 user ids per request. (label: `too-many-uids`)" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get team members by user id list" + } + }, + "/teams/{tid}/invitations": { + "get": { + "description": " [internal route ID: \"get-team-invitations\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Invitation id to start from (ascending).", + "format": "uuid", + "in": "query", + "name": "start", + "required": false, + "type": "string" + }, + { + "description": "Number of results to return (default 100, max 500).", + "format": "int32", + "in": "query", + "maximum": 500, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of sent invitations", + "schema": { + "$ref": "#/definitions/InvitationList" + } + }, + "400": { + "description": "Invalid `size` or `start` or `tid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "List the sent team invitations" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"send-team-invitation\"]\n\nInvitations are sent by email. The maximum allowed number of pending team invitations is equal to the team size.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/InvitationRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Invitation was created and sent.", + "headers": { + "Location": { + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Invitation" + } + }, + "400": { + "description": "Invalid `body` or `tid`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)\n\nToo many team invitations for this team (label: `too-many-team-invitations`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)\n\nThis operation requires the user to have a verified email address. (label: `no-email`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions", + "too-many-team-invitations", + "blacklisted-email", + "no-identity", + "no-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create and send a new team invitation." + } + }, + "/teams/{tid}/invitations/{iid}": { + "delete": { + "description": " [internal route ID: \"delete-team-invitation\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "iid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Invitation deleted" + }, + "400": { + "description": "Invalid `iid` or `tid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete a pending team invitation by ID." + }, + "get": { + "description": " [internal route ID: \"get-team-invitation\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "iid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Invitation", + "schema": { + "$ref": "#/definitions/Invitation" + } + }, + "400": { + "description": "Invalid `iid` or `tid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Notification not found. (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Notification not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a pending team invitation by ID." + } + }, + "/teams/{tid}/legalhold/consent": { + "post": { + "description": " [internal route ID: \"consent-to-legal-hold\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Grant consent successful" + }, + "204": { + "description": "Consent already granted" + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Consent to legal hold", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/teams/{tid}/legalhold/settings": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"delete-legal-hold-settings\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to members with a legalhold client (via brig)\n- UserLegalHoldDisabled event to contacts of members with a legalhold client (via brig)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RemoveLegalHoldSettingsRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "Legal hold service settings deleted" + }, + "400": { + "description": "Invalid `body` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "invalid-op", + "action-denied", + "no-team-member", + "operation-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete legal hold service settings", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + }, + "get": { + "description": " [internal route ID: \"get-legal-hold-settings\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ViewLegalHoldService" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get legal hold service settings" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"create-legal-hold-settings\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewLegalHoldService" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Legal hold service settings created", + "schema": { + "$ref": "#/definitions/ViewLegalHoldService" + } + }, + "400": { + "description": "Invalid `body` or `tid`\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)\n\nlegal hold service pubkey is invalid (label: `legalhold-invalid-key`)" + }, + "403": { + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create legal hold service settings" + } + }, + "/teams/{tid}/legalhold/{uid}": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"disable-legal-hold-for-user\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to the user owning the client (via brig)\n- UserLegalHoldDisabled event to contacts of the user owning the client (via brig)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DisableLegalHoldForUserRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Disable legal hold successful" + }, + "204": { + "description": "Legal hold was not enabled" + }, + "400": { + "description": "Invalid `body` or `uid` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "action-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Disable legal hold for user", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + }, + "get": { + "description": " [internal route ID: \"get-legal-hold\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/UserLegalHoldStatusResponse" + } + }, + "400": { + "description": "Invalid `uid` or `tid`" + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get legal hold status" + }, + "post": { + "description": " [internal route ID: \"request-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- LegalHoldClientRequested event to contacts of the user the device is requested for, if they didn't already have a legalhold client (via brig)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Request device successful" + }, + "204": { + "description": "Request device already pending" + }, + "400": { + "description": "Invalid `uid` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)" + }, + "403": { + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "user has not given consent to using legal hold (label: `legalhold-no-consent`)\n\nlegal hold is already enabled for this user (label: `legalhold-already-enabled`)", + "schema": { + "example": { + "code": 409, + "label": "legalhold-no-consent", + "message": "user has not given consent to using legal hold" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-no-consent", + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "internal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)\n\nlegal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-illegal-op", + "message": "internal server error: inconsistent change of user's legalhold state" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-illegal-op", + "legalhold-internal" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Request legal hold device", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/teams/{tid}/legalhold/{uid}/approve": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"approve-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientAdded event to the user owning the client (via brig)\n- UserLegalHoldEnabled event to contacts of the user owning the client (via brig)\n- ClientRemoved event to the user, if removing old client due to max number (via brig)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ApproveLegalHoldForUserRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Legal hold approved" + }, + "400": { + "description": "Invalid `body` or `uid` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "no-team-member", + "action-denied", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow. (label: `legalhold-no-device-allocated`)", + "schema": { + "example": { + "code": 404, + "label": "legalhold-no-device-allocated", + "message": "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-no-device-allocated" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "legal hold is already enabled for this user (label: `legalhold-already-enabled`)", + "schema": { + "example": { + "code": 409, + "label": "legalhold-already-enabled", + "message": "legal hold is already enabled for this user" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "legal hold cannot be approved without being in a pending state (label: `legalhold-not-pending`)", + "schema": { + "example": { + "code": 412, + "label": "legalhold-not-pending", + "message": "legal hold cannot be approved without being in a pending state" + }, + "properties": { + "code": { + "enum": [ + 412 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-pending" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Approve legal hold device", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/teams/{tid}/members": { + "get": { + "description": " [internal route ID: \"get-team-members\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Maximum results to be returned", + "format": "int32", + "in": "query", + "maximum": 2000, + "minimum": 1, + "name": "maxResults", + "required": false, + "type": "integer" + }, + { + "description": "Optional, when not specified, the first page will be returned.Every returned page contains a `pagingState`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamMembersPage" + } + }, + "400": { + "description": "Invalid `pagingState` or `maxResults` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get team members" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-team-member\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewTeamMember" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nMaximum number of admins per team reached (label: `too-many-team-admins`)\n\nThe specified permissions are invalid (label: `invalid-permissions`)\n\nYou do not have permission to access this resource (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "too-many-team-admins", + "invalid-permissions", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)\n\nTeam not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update an existing team member" + } + }, + "/teams/{tid}/members/csv": { + "get": { + "description": " [internal route ID: \"get-team-members-csv\"]\n\nThe endpoint returns data in chunked transfer encoding. Internal server errors might result in a failed transfer instead of a 500 response.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/csv" + ], + "responses": { + "200": { + "description": "CSV of team members" + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "You do not have permission to access this resource (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "You do not have permission to access this resource" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get all members of the team as a CSV file" + } + }, + "/teams/{tid}/members/{uid}": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"delete-team-member\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TeamMemberDeleteData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "" + }, + "202": { + "description": "Team member scheduled for deletion" + }, + "400": { + "description": "Invalid `body` or `uid` or `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)\n\nTeam member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove an existing team member" + }, + "get": { + "description": " [internal route ID: \"get-team-member\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamMember" + } + }, + "400": { + "description": "Invalid `uid` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get single team member" + } + }, + "/teams/{tid}/search": { + "get": { + "description": " [internal route ID: \"browse-team\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Search expression", + "in": "query", + "name": "q", + "required": false, + "type": "string" + }, + { + "collectionFormat": null, + "description": "Role filter, eg. `member,partner`. Empty list means do not filter.", + "in": "query", + "items": { + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "name": "frole", + "required": false, + "type": "array" + }, + { + "description": "Can be one of name, handle, email, saml_idp, managed_by, role, created_at.", + "enum": [ + "name", + "handle", + "email", + "saml_idp", + "managed_by", + "role", + "created_at" + ], + "in": "query", + "name": "sortby", + "required": false, + "type": "string" + }, + { + "description": "Can be one of asc, desc.", + "enum": [ + "asc", + "desc" + ], + "in": "query", + "name": "sortorder", + "required": false, + "type": "string" + }, + { + "description": "Number of results to return (min: 1, max: 500, default: 15)", + "format": "int32", + "in": "query", + "maximum": 500, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + }, + { + "description": "Optional, when not specified, the first page will be returned. Every returned page contains a `paging_state`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Search results", + "schema": { + "$ref": "#/definitions/SearchResult" + } + }, + "400": { + "description": "Invalid `pagingState` or `size` or `sortorder` or `sortby` or `frole` or `q` or `tid`" + } + }, + "summary": "Browse team for members (requires add-user permission)" + } + }, + "/teams/{tid}/search-visibility": { + "get": { + "description": " [internal route ID: \"get-search-visibility\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamSearchVisibilityView" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Shows the value for search visibility" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"set-search-visibility\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TeamSearchVisibilityView" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "Search visibility set" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Custom search is not available for this team (label: `team-search-visibility-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "team-search-visibility-not-enabled", + "message": "Custom search is not available for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "team-search-visibility-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Sets the search visibility for the whole team" + } + }, + "/teams/{tid}/size": { + "get": { + "description": " [internal route ID: \"get-team-size\"]\n\n", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Number of team members", + "schema": { + "$ref": "#/definitions/TeamSize" + } + }, + "400": { + "description": "Invalid `tid`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Returns the number of team members as an integer. Can be out of sync by roughly the `refresh_interval` of the ES index." + } + }, + "/users/handles": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"check-user-handles\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CheckHandles" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of free handles", + "schema": { + "items": { + "$ref": "#/definitions/Handle" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Check availability of user handles" + } + }, + "/users/handles/{handle}": { + "head": { + "description": " [internal route ID: \"check-user-handle\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "handle", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Handle is taken", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "400": { + "description": "Invalid `handle`\n\nThe given handle is invalid (label: `invalid-handle`)" + }, + "404": { + "description": "Handle not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Handle not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Check whether a user handle can be taken" + } + }, + "/users/list-clients": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"list-clients-bulk@v2\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LimitedQualifiedUserIdList" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "properties": { + "qualified_user_map": { + "$ref": "#/definitions/QualifiedUserMap_Set_PubClient" + } + }, + "type": "object" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "List all clients for a set of user ids", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/users/list-prekeys": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"get-multi-user-prekey-bundle-qualified\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QualifiedUserClients" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/QualifiedUserClientPrekeyMapV4" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Given a map of domain to (map of user IDs to client IDs) return a prekey for each one. You can't request information for more users than maximum conversation size.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "claim-multi-prekey-bundle" + ] + ] + } + }, + "/users/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-user-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "User found", + "schema": { + "$ref": "#/definitions/UserProfile" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + }, + "404": { + "description": "User not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a user by Domain and UserId", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/clients": { + "get": { + "description": " [internal route ID: \"get-user-clients-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "$ref": "#/definitions/PubClient" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + } + }, + "summary": "Get all of a user's clients", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/clients/{client}": { + "get": { + "description": " [internal route ID: \"get-user-client-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PubClient" + } + }, + "400": { + "description": "Invalid `client` or `uid` or `uid_domain`" + } + }, + "summary": "Get a specific client of a user", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/prekeys": { + "get": { + "description": " [internal route ID: \"get-users-prekey-bundle-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PrekeyBundle" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + } + }, + "summary": "Get a prekey for each client of a user.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "claim-prekey-bundle" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/prekeys/{client}": { + "get": { + "description": " [internal route ID: \"get-users-prekeys-client-qualified\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ClientPrekey" + } + }, + "400": { + "description": "Invalid `client` or `uid` or `uid_domain`" + } + }, + "summary": "Get a prekey for a specific client of a user.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "claim-prekey" + ] + ] + } + }, + "/users/{uid}/email": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"update-user-email\"]\n\nIf the user has a pending email validation, the validation email will be resent.", + "parameters": [ + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/EmailUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "400": { + "description": "Invalid `body` or `uid`" + } + }, + "summary": "Resend email address validation email." + } + }, + "/users/{uid}/rich-info": { + "get": { + "description": " [internal route ID: \"get-rich-info\"]\n\n", + "parameters": [ + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Rich info about the user", + "schema": { + "$ref": "#/definitions/RichInfoAssocList" + } + }, + "400": { + "description": "Invalid `uid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a user's rich info" + } + }, + "/verification-code/send": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": " [internal route ID: \"send-verification-code\"]\n\n", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SendVerificationCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Verification code sent." + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Send a verification code to a given email address." + } + } + }, + "security": [ + { + "ZAuth": [] + } + ], + "securityDefinitions": { + "ZAuth": { + "description": "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'.", + "in": "header", + "name": "Authorization", + "type": "apiKey" + } + }, + "swagger": "2.0" +} From 602247a778b205804da8786842897d35a434ee35 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 11 Sep 2023 09:17:10 +0200 Subject: [PATCH 152/225] WPB-4361 upgrade jwt tools in wire server (#3572) --- changelog.d/5-internal/pr-3572 | 1 + libs/jwt-tools/test/Spec.hs | 22 +++++++++++++------ nix/overlay.nix | 7 +++++- nix/pkgs/rusty_jwt_tools_ffi/default.nix | 6 ++--- nix/sources.json | 12 ++++++++++ services/brig/brig.cabal | 1 - services/brig/default.nix | 1 - .../brig/test/integration/API/User/Client.hs | 14 +++++++----- 8 files changed, 45 insertions(+), 19 deletions(-) create mode 100644 changelog.d/5-internal/pr-3572 diff --git a/changelog.d/5-internal/pr-3572 b/changelog.d/5-internal/pr-3572 new file mode 100644 index 00000000000..2b6825bd5e5 --- /dev/null +++ b/changelog.d/5-internal/pr-3572 @@ -0,0 +1 @@ +`rusty-jwt-tools` is upgraded to version 0.5.0 diff --git a/libs/jwt-tools/test/Spec.hs b/libs/jwt-tools/test/Spec.hs index 7843d45dece..a2881bc5328 100644 --- a/libs/jwt-tools/test/Spec.hs +++ b/libs/jwt-tools/test/Spec.hs @@ -24,11 +24,19 @@ main :: IO () main = hspec $ do describe "generateDpopToken FFI when passing valid inputs" $ do it "should return an access token" $ do + -- FUTUREWORK(leif): fix this test, we need new valid test data, + -- this test exists mainly for debugging purposes + -- a functionality test is also coverd in the integration tests in services/brig/test/integration/API/User/Client.hs (`testCreateAccessToken`) + pending actual <- runExceptT $ generateDpopToken proof uid cid domain nonce uri method maxSkewSecs expires now pem print actual isRight actual `shouldBe` True describe "generateDpopToken FFI when passing a wrong nonce value" $ do it "should return BackendNonceMismatchError" $ do + -- FUTUREWORK(leif): fix this test, we need new valid test data, + -- this test exists mainly for debugging purposes + -- a functionality test is also coverd in the integration tests in services/brig/test/integration/API/User/Client.hs (`testCreateAccessToken`) + pending actual <- runExceptT $ generateDpopToken proof uid cid domain (Nonce "foobar") uri method maxSkewSecs expires now pem actual `shouldBe` Left BackendNonceMismatchError describe "toResult" $ do @@ -73,16 +81,16 @@ main = hspec $ do toResult Nothing Nothing `shouldBe` Left UnknownError where token = "" - proof = Proof "eyJhbGciOiJFZERTQSIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoidUhNR0paWllUbU9zOEdiaTdaRUJLT255TnJYYnJzNTI1dE1QQUZoYjBzbyJ9fQ.eyJpYXQiOjE2Nzg4MDUyNTgsImV4cCI6MjA4ODc3MzI1OCwibmJmIjoxNjc4ODA1MjU4LCJzdWIiOiJpbTp3aXJlYXBwPVpHSmlNRGRsT1RRM1pESTVOREU0TUdFM09UQmhOVGN6WkdWbU16VmtaRFUvN2M2MzExYTFjNDNjMmJhNkB3aXJlLmNvbSIsImp0aSI6ImQyOWFkYTQ2LTBjMzYtNGNiMS05OTVlLWFlMWNiYTY5M2IzNCIsIm5vbmNlIjoiYzB0RWNtOUNUME00TXpKU04zRjRkMEZIV0V4TGIxUm5aMDQ1U3psSFduTSIsImh0bSI6IlBPU1QiLCJodHUiOiJodHRwczovL3dpcmUuZXhhbXBsZS5jb20vY2xpZW50cy84OTYzMDI3MDY5ODc3MTAzNTI2L2FjY2Vzcy10b2tlbiIsImNoYWwiOiJaa3hVV25GWU1HbHFUVVpVU1hnNFdHdHBOa3h1WWpWU09XRnlVRU5hVGxnIn0.8p0lvdOPjJ8ogjjLP6QtOo216qD9ujP7y9vSOhdYb-O8ikmW09N00gjCf0iGT-ZkxBT-LfDE3eQx27tWQ3JPBQ" - uid = UserId "dbb07e94-7d29-4180-a790-a573def35dd5" - cid = ClientId 8963027069877103526 + proof = Proof "eyJhbGciOiJFZERTQSIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoidXE2c1hXcDdUM1E3YlNtUFd3eFNlRHJoUHFid1RfcTd4SFBQeGpGT0g5VSJ9fQ.eyJpYXQiOjE2OTQxMTc0MjgsImV4cCI6MTY5NDcyMjIyOCwibmJmIjoxNjk0MTE3NDIzLCJzdWIiOiJpbTp3aXJlYXBwPUlHOVl2enVXUUlLVWFSazEyRjVDSVEvOGUxODk2MjZlYWUwMTExZEBlbG5hLndpcmUubGluayIsImp0aSI6ImM0OGZmOTAyLTc5OGEtNDNjYi04YTk2LTE3NzM0NTgxNjIyMCIsIm5vbmNlIjoiR0FxNG5SajlSWVNzUnhoOVh1MWFtQSIsImh0bSI6IlBPU1QiLCJodHUiOiJodHRwczovL2VsbmEud2lyZS5saW5rL2NsaWVudHMvOGUxODk2MjZlYWUwMTExZC9hY2Nlc3MtdG9rZW4iLCJjaGFsIjoiMkxLbEFWMjR2VGtIMHlaaFdacEZrT01mSEE1d3lGQkgifQ.FW5i40CvndSSo3wQdA1DMUkGRmxk86cORAllwC2PCejVuk7TsdZuIKuJZFVa1VTJKWwNCPqPZ05Gsxxeh1DiDA" + uid = UserId "206f58bf-3b96-4082-9469-1935d85e4221" + cid = ClientId 10239098846720299293 domain = Domain "wire.com" - nonce = Nonce "c0tEcm9CT0M4MzJSN3F4d0FHWExLb1RnZ045SzlHWnM" - uri = Uri "https://wire.example.com/clients/8963027069877103526/access-token" + nonce = Nonce "GAq4nRj9RYSsRxh9Xu1amA" + uri = Uri "https://elna.wire.link/clients/10239098846720299293/access-token" method = POST maxSkewSecs = MaxSkewSecs 5 - now = NowEpoch 5435234232 - expires = ExpiryEpoch $ 2136351646 + now = NowEpoch 360 + expires = ExpiryEpoch 2136351646 pem = PemBundle $ "-----BEGIN PRIVATE KEY-----\n\ diff --git a/nix/overlay.nix b/nix/overlay.nix index 3bcd85b5a18..4d533dea9c8 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -49,15 +49,20 @@ let ''; }; + sources = import ./sources.nix; + pkgsCargo = import sources.nixpkgs-cargo {}; in self: super: { + cryptobox = self.callPackage ./pkgs/cryptobox { }; zauth = self.callPackage ./pkgs/zauth { }; mls-test-cli = self.callPackage ./pkgs/mls-test-cli { }; # Named like this so cabal2nix can find it - rusty_jwt_tools_ffi = self.callPackage ./pkgs/rusty_jwt_tools_ffi { }; + rusty_jwt_tools_ffi = self.callPackage ./pkgs/rusty_jwt_tools_ffi { + inherit (pkgsCargo) rustPlatform; + }; nginxModules = super.nginxModules // { zauth = { diff --git a/nix/pkgs/rusty_jwt_tools_ffi/default.nix b/nix/pkgs/rusty_jwt_tools_ffi/default.nix index 6fb4b58470e..1f0764c3b7a 100644 --- a/nix/pkgs/rusty_jwt_tools_ffi/default.nix +++ b/nix/pkgs/rusty_jwt_tools_ffi/default.nix @@ -7,12 +7,12 @@ }: let - version = "0.3.4"; + version = "0.5.0"; src = fetchFromGitHub { owner = "wireapp"; repo = "rusty-jwt-tools"; - rev = "fc4569c5b84d00a5cc8fc77b450714a5261cd3d9"; - sha256 = "sha256-cZffVKfH0FzA4Eo7YVxivT3JWTwz9uu1HWhPVlvbYqM="; + rev = "6704e08376bb49168133d8f4ce66155adeb6bfb0"; + sha256 = "sha256-ocmeFXjU3psCO+hpDuEAIzYIm4QzP+jHJR/V8yyw6Lw="; }; cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/ffi/Cargo.lock"); diff --git a/nix/sources.json b/nix/sources.json index 80e326af497..e1adedd2306 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -10,5 +10,17 @@ "type": "tarball", "url": "https://github.com/NixOS/nixpkgs/archive/402cc3633cc60dfc50378197305c984518b30773.tar.gz", "url_template": "https://github.com///archive/.tar.gz" + }, + "nixpkgs-cargo": { + "branch": "nixpkgs-unstable", + "description": "Nix Packages collection", + "homepage": "https://github.com/NixOS/nixpkgs", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "efd23a1c9ae8c574e2ca923c2b2dc336797f4cc4", + "sha256": "0pb1dgdgfsnsngw2ci807wln2jnlsha4zkm1y14x497qbw4izir3", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/efd23a1c9ae8c574e2ca923c2b2dc336797f4cc4.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 5e067fcbba2..3540841b633 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -406,7 +406,6 @@ executable brig-integration , attoparsec , base , base16-bytestring - , base64-bytestring , bilge , bloodhound , brig diff --git a/services/brig/default.nix b/services/brig/default.nix index d679f1a80f0..ad99f818f63 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -295,7 +295,6 @@ mkDerivation { attoparsec base base16-bytestring - base64-bytestring bilge bloodhound brig-types diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index d8228cc375c..f474f65cd76 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -40,7 +40,6 @@ import Data.Aeson hiding (json) import Data.Aeson qualified as A import Data.Aeson.KeyMap qualified as M import Data.Aeson.Lens -import Data.ByteString.Base64.URL qualified as B64 import Data.ByteString.Conversion import Data.Coerce (coerce) import Data.Default @@ -52,10 +51,10 @@ import Data.Nonce (isValidBase64UrlEncodedUUID) import Data.Qualified (Qualified (..)) import Data.Range (unsafeRange) import Data.Set qualified as Set -import Data.Text (replace) -import Data.Text.Ascii (AsciiChars (validate)) +import Data.Text.Ascii (AsciiChars (validate), encodeBase64UrlUnpadded, toText) import Data.Time (addUTCTime) import Data.Time.Clock.POSIX +import Data.UUID (toByteString) import Data.Vector qualified as Vec import Imports import Network.Wai.Utilities.Error qualified as Error @@ -1424,22 +1423,25 @@ testCreateAccessToken opts n brig = do let localDomain = opts ^. Opt.optionSettings & Opt.setFederationDomain u <- randomUser brig let uid = userId u + -- convert the user Id into 16 octets of binary and then base64url + let uidBS = Data.UUID.toByteString (toUUID uid) + let uidB64 = encodeBase64UrlUnpadded (cs uidBS) let email = fromMaybe (error "invalid email") $ userEmail u rs <- login n (defEmailLogin email) PersistentCookie (floor <$> getPOSIXTime) - let clientIdentity = cs $ "im:wireapp=" <> uidb64 <> "/" <> toByteString' cid <> "@" <> toByteString' localDomain + let clientIdentity = cs $ "im:wireapp=" <> cs (toText uidB64) <> "/" <> toByteString' cid <> "@" <> toByteString' localDomain let httpsUrl = cs $ "https://" <> toByteString' localDomain <> "/clients/" <> toByteString' cid <> "/access-token" + let expClaim = NumericDate (addUTCTime 10 now) let claimsSet' = emptyClaimsSet & claimIat ?~ NumericDate now - & claimExp ?~ NumericDate (addUTCTime 10 now) + & claimExp ?~ expClaim & claimNbf ?~ NumericDate now & claimSub ?~ fromMaybe (error "invalid sub claim") ((clientIdentity :: Text) ^? stringOrUri) & claimJti ?~ "6fc59e7f-b666-4ffc-b738-4f4760c884ca" From 85aa13bae94ca6112f01cf8f0339bb2cbcdd8a28 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 14 Sep 2023 11:07:33 +0200 Subject: [PATCH 153/225] Remove MLS endpoints from API v4 (#3583) This commit removes some remaining MLS endpoints (only present in the mls branch) from v4 and regenerates the v4 swagger documentation. --- .../API/Routes/Public/Galley/Conversation.hs | 2 + services/brig/docs/swagger-v4.json | 159 ++++++++++++++++-- 2 files changed, 151 insertions(+), 10 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 3f718a63e88..e37ef51f6b6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -612,6 +612,7 @@ type ConversationAPI = :<|> Named "get-one-to-one-mls-conversation" ( Summary "Get an MLS 1:1 conversation" + :> From 'V5 :> ZLocalUser :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected @@ -1256,6 +1257,7 @@ type ConversationAPI = :<|> Named "update-conversation-protocol" ( Summary "Update the protocol of the conversation" + :> From 'V5 :> Description "**Note**: Only proteus->mixed upgrade is supported." :> CanThrow 'ConvNotFound :> CanThrow 'ConvInvalidProtocolTransition diff --git a/services/brig/docs/swagger-v4.json b/services/brig/docs/swagger-v4.json index 8362974c3e9..86a7f633321 100644 --- a/services/brig/docs/swagger-v4.json +++ b/services/brig/docs/swagger-v4.json @@ -219,6 +219,9 @@ "mlsE2EId": { "$ref": "#/definitions/MlsE2EIdConfig.WithStatus" }, + "mlsMigration": { + "$ref": "#/definitions/MlsMigration.WithStatus" + }, "outlookCalIntegration": { "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatus" }, @@ -258,7 +261,8 @@ "mls", "exposeInvitationURLsToTeamAdmin", "outlookCalIntegration", - "mlsE2EId" + "mlsE2EId", + "mlsMigration" ], "type": "object" }, @@ -1062,6 +1066,9 @@ "minimum": 0, "type": "integer" }, + "epoch_timestamp": { + "$ref": "#/definitions/Epoch Timestamp" + }, "group_id": { "$ref": "#/definitions/GroupId" }, @@ -1110,11 +1117,11 @@ "required": [ "qualified_id", "type", - "creator", "access", "members", "group_id", "epoch", + "epoch_timestamp", "cipher_suite" ], "type": "object" @@ -1446,6 +1453,9 @@ "minimum": 0, "type": "integer" }, + "epoch_timestamp": { + "$ref": "#/definitions/Epoch Timestamp" + }, "failed_to_add": { "items": { "$ref": "#/definitions/Qualified_UserId" @@ -1500,12 +1510,12 @@ "required": [ "qualified_id", "type", - "creator", "access", "access_role", "members", "group_id", "epoch", + "epoch_timestamp", "cipher_suite", "failed_to_add" ], @@ -1736,6 +1746,12 @@ ], "type": "object" }, + "Epoch Timestamp": { + "description": "The timestamp of the epoch number", + "example": "2021-05-12T10:52:02.671Z", + "format": "yyyy-mm-ddThh:MM:ss.qqq", + "type": "string" + }, "Event": { "properties": { "conversation": { @@ -1786,6 +1802,9 @@ "minimum": 0, "type": "integer" }, + "epoch_timestamp": { + "$ref": "#/definitions/Epoch Timestamp" + }, "group_id": { "$ref": "#/definitions/GroupId" }, @@ -1918,10 +1937,10 @@ "has_password", "qualified_id", "type", - "creator", "members", "group_id", "epoch", + "epoch_timestamp", "cipher_suite", "qualified_recipient", "receipt_mode", @@ -1977,7 +1996,8 @@ "conversation.typing", "conversation.otr-message-add", "conversation.mls-message-add", - "conversation.mls-welcome" + "conversation.mls-welcome", + "conversation.protocol-update" ], "type": "string" }, @@ -2647,13 +2667,20 @@ "$ref": "#/definitions/UUID" }, "type": "array" + }, + "supportedProtocols": { + "items": { + "$ref": "#/definitions/Protocol" + }, + "type": "array" } }, "required": [ "protocolToggleUsers", "defaultProtocol", "allowedCipherSuites", - "defaultCipherSuite" + "defaultCipherSuite", + "supportedProtocols" ], "type": "object" }, @@ -2883,6 +2910,42 @@ ], "type": "object" }, + "MlsMigration": { + "properties": { + "finaliseRegardlessAfter": { + "$ref": "#/definitions/UTCTime" + }, + "startTime": { + "$ref": "#/definitions/UTCTime" + } + }, + "type": "object" + }, + "MlsMigration.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/MlsMigration" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, "NameIDFormat": { "enum": [ "NameIDFUnspecified", @@ -3011,7 +3074,7 @@ "type": "string" }, "protocol": { - "$ref": "#/definitions/Protocol" + "$ref": "#/definitions/BaseProtocol" }, "qualified_users": { "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", @@ -3584,10 +3647,19 @@ "Protocol": { "enum": [ "proteus", - "mls" + "mls", + "mixed" ], "type": "string" }, + "ProtocolUpdate": { + "properties": { + "protocol": { + "$ref": "#/definitions/Protocol" + } + }, + "type": "object" + }, "PubClient": { "properties": { "class": { @@ -8301,7 +8373,7 @@ "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nAttempting to add group members outside MLS (label: `non-empty-member-list`)" }, "403": { - "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nUsers are not connected (label: `not-connected`)\n\nThe client has to refresh their access token and provide their client ID (label: `mls-missing-sender-client`)\n\nConversation access denied (label: `access-denied`)", + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nUsers are not connected (label: `not-connected`)\n\nConversation access denied (label: `access-denied`)", "schema": { "example": { "code": 403, @@ -8321,7 +8393,6 @@ "operation-denied", "no-team-member", "not-connected", - "mls-missing-sender-client", "access-denied" ], "type": "string" @@ -9416,6 +9487,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -9717,6 +9792,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] }, @@ -9852,6 +9931,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -9981,6 +10064,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -10109,6 +10196,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -10376,6 +10467,10 @@ [ "galley", "update-conversation" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -10664,6 +10759,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -11366,6 +11465,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -11489,6 +11592,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -11611,6 +11718,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -11875,6 +11986,10 @@ [ "galley", "update-conversation" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -16274,6 +16389,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] }, @@ -19567,6 +19686,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -19690,6 +19813,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] }, @@ -19955,6 +20082,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] }, @@ -20204,6 +20335,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] } @@ -20430,6 +20565,10 @@ [ "galley", "on-mls-message-sent" + ], + [ + "brig", + "get-users-by-ids" ] ] } From 3af967558bd05e72352756fa064188532edd90eb Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 27 Sep 2023 16:01:37 +0200 Subject: [PATCH 154/225] Add note about localhost. resolution (#3617) This is important when running federation integration tests locally. --- docs/src/developer/developer/building.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/src/developer/developer/building.md b/docs/src/developer/developer/building.md index e913a41b24e..5576f1545da 100644 --- a/docs/src/developer/developer/building.md +++ b/docs/src/developer/developer/building.md @@ -68,8 +68,8 @@ These services require most of the deployment dependencies as seen in the archit - SNS - S3 - DynamoDB -- Required additional software: - - netcat (in order to allow the services being tested to talk to the dependencies above) + +Furthermore, testing federation requires a local DNS server set up with appropriate SRV records. Setting up these real, but in-memory internal and "fake" external dependencies is done easiest using [`docker-compose`](https://docs.docker.com/compose/install/). Run the following in a separate terminal (it will block that terminal, C-c to shut all these docker images down again): @@ -77,6 +77,8 @@ Setting up these real, but in-memory internal and "fake" external dependencies i deploy/dockerephemeral/run.sh ``` +Also make sure your system is able to resolve the fully qualified domain `localhost.` (note the trailing dot). This is surprisingly not trivial, because of limitations in how libc parses `/etc/hosts`. You can check that with, for example, `ping localhost.`. If you get a name resolution error, you need to add `localhost.` explictly to your `/etc/hosts` file. + After all containers are up you can use these Makefile targets to run the tests locally: 1. Build and run all integration tests From a84c4844129ea32fd2b29d4d283338665c40c3aa Mon Sep 17 00:00:00 2001 From: Amit Sagtani Date: Wed, 27 Sep 2023 16:22:32 -0700 Subject: [PATCH 155/225] Add Rabbitmq external chart (#3594) * add rabbitmq-external chart * update rabbitmq port * fix typo --- charts/rabbitmq-external/Chart.yaml | 4 ++ .../rabbitmq-external/templates/endpoint.yaml | 38 +++++++++++++++++++ .../rabbitmq-external/templates/helpers.tpl | 11 ++++++ charts/rabbitmq-external/values.yaml | 6 +++ 4 files changed, 59 insertions(+) create mode 100644 charts/rabbitmq-external/Chart.yaml create mode 100644 charts/rabbitmq-external/templates/endpoint.yaml create mode 100644 charts/rabbitmq-external/templates/helpers.tpl create mode 100644 charts/rabbitmq-external/values.yaml diff --git a/charts/rabbitmq-external/Chart.yaml b/charts/rabbitmq-external/Chart.yaml new file mode 100644 index 00000000000..bb6f95452eb --- /dev/null +++ b/charts/rabbitmq-external/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: Refer to rabbitmq IPs located outside kubernetes by specifying IPs manually +name: rabbitmq-external +version: 0.0.42 diff --git a/charts/rabbitmq-external/templates/endpoint.yaml b/charts/rabbitmq-external/templates/endpoint.yaml new file mode 100644 index 00000000000..0a4f9b728ef --- /dev/null +++ b/charts/rabbitmq-external/templates/endpoint.yaml @@ -0,0 +1,38 @@ +# create a headless clusterIP service to create dns name "rabbitmq-external" +# and a custom endpoint, thus forwarding traffic when resolving DNS to custom IPs +kind: Service +apiVersion: v1 +metadata: + name: {{ .Chart.Name }} + labels: + app: {{ .Chart.Name }} + chart: {{ template "rabbitmq-external.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: ClusterIP + clusterIP: None # headless service + ports: + - name: http + port: {{ .Values.portHttp }} + targetPort: {{ .Values.portHttp }} +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: {{ .Chart.Name }} + labels: + app: {{ .Chart.Name }} + chart: {{ template "rabbitmq-external.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +subsets: + - addresses: + {{- range .Values.IPs }} + - ip: {{ . }} + {{- end }} + ports: + # port and name in the endpoint must match port and name in the service + # see also https://docs.openshift.com/enterprise/3.0/dev_guide/integrating_external_services.html + - name: http + port: {{ .Values.portHttp }} diff --git a/charts/rabbitmq-external/templates/helpers.tpl b/charts/rabbitmq-external/templates/helpers.tpl new file mode 100644 index 00000000000..4241fe46aa7 --- /dev/null +++ b/charts/rabbitmq-external/templates/helpers.tpl @@ -0,0 +1,11 @@ +{{- define "rabbitmq-external.fullname" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- printf "%s" $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "rabbitmq-external.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/charts/rabbitmq-external/values.yaml b/charts/rabbitmq-external/values.yaml new file mode 100644 index 00000000000..bf5b1464814 --- /dev/null +++ b/charts/rabbitmq-external/values.yaml @@ -0,0 +1,6 @@ +portHttp: 5672 + +## Configure this helm chart with: +# IPs: +# - 1.2.3.4 +# - 5.6.7.8 From fac705c36bfce68ae5b73305c6ef175db49e5316 Mon Sep 17 00:00:00 2001 From: Owen Harvey Date: Fri, 29 Sep 2023 21:10:08 +1000 Subject: [PATCH 156/225] WPB-1103: Adding relative URLs to swagger docs. (#3619) --- changelog.d/4-docs/WPB-1103 | 1 + services/brig/docs/swagger-v0.json | 1 + services/brig/docs/swagger-v1.json | 1 + services/brig/docs/swagger-v2.json | 1 + services/brig/docs/swagger-v3.json | 1 + services/brig/docs/swagger-v4.json | 1 + services/brig/src/Brig/API/Public.hs | 1 + 7 files changed, 7 insertions(+) create mode 100644 changelog.d/4-docs/WPB-1103 diff --git a/changelog.d/4-docs/WPB-1103 b/changelog.d/4-docs/WPB-1103 new file mode 100644 index 00000000000..ff6644084df --- /dev/null +++ b/changelog.d/4-docs/WPB-1103 @@ -0,0 +1 @@ +Fix: support api versions other than v0 in swagger docs. \ No newline at end of file diff --git a/services/brig/docs/swagger-v0.json b/services/brig/docs/swagger-v0.json index c6eaca520c5..39ba49998d3 100644 --- a/services/brig/docs/swagger-v0.json +++ b/services/brig/docs/swagger-v0.json @@ -1,4 +1,5 @@ { + "basePath": "/v0", "definitions": { "ASCII": { "example": "aGVsbG8", diff --git a/services/brig/docs/swagger-v1.json b/services/brig/docs/swagger-v1.json index 82a9e565841..3a81bc59e54 100644 --- a/services/brig/docs/swagger-v1.json +++ b/services/brig/docs/swagger-v1.json @@ -1,4 +1,5 @@ { + "basePath": "/v1", "definitions": { "ASCII": { "example": "aGVsbG8", diff --git a/services/brig/docs/swagger-v2.json b/services/brig/docs/swagger-v2.json index 875aeffc991..437aeb2380a 100644 --- a/services/brig/docs/swagger-v2.json +++ b/services/brig/docs/swagger-v2.json @@ -1,4 +1,5 @@ { + "basePath": "/v2", "definitions": { "ASCII": { "example": "aGVsbG8", diff --git a/services/brig/docs/swagger-v3.json b/services/brig/docs/swagger-v3.json index c366175b56b..e252a739717 100644 --- a/services/brig/docs/swagger-v3.json +++ b/services/brig/docs/swagger-v3.json @@ -1,4 +1,5 @@ { + "basePath": "/v3", "definitions": { "": { "description": "Username to use for authenticating against the given TURN servers", diff --git a/services/brig/docs/swagger-v4.json b/services/brig/docs/swagger-v4.json index 8362974c3e9..c6220046c3a 100644 --- a/services/brig/docs/swagger-v4.json +++ b/services/brig/docs/swagger-v4.json @@ -1,4 +1,5 @@ { + "basePath": "/v4", "definitions": { "": { "description": "Username to use for authenticating against the given TURN servers", diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 24996bde99a..95d22a8a3e5 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -184,6 +184,7 @@ versionedSwaggerDocsAPI (Just (VersionNumber V5)) = ) & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger.md") + & S.servers .~ [S.Server ("/" <> toUrlPiece V5) Nothing mempty] & cleanupSwagger versionedSwaggerDocsAPI (Just (VersionNumber V0)) = swaggerPregenUIServer $(pregenSwagger V0) versionedSwaggerDocsAPI (Just (VersionNumber V1)) = swaggerPregenUIServer $(pregenSwagger V1) From eabd35bf155a8dbf1fbeef0c605cd0ebe462eb54 Mon Sep 17 00:00:00 2001 From: fisx Date: Fri, 29 Sep 2023 13:10:53 +0200 Subject: [PATCH 157/225] Cleanup code in integration package (#3614) --- integration/integration.cabal | 3 +- integration/test/SetupHelpers.hs | 18 +++--- integration/test/Test/AccessUpdate.hs | 6 +- integration/test/Test/Brig.hs | 77 ++++++++++++------------- integration/test/Test/Client.hs | 2 +- integration/test/Test/Conversation.hs | 59 +++++++++++-------- integration/test/Test/Demo.hs | 52 ++++++++--------- integration/test/Test/Federation.hs | 19 +++--- integration/test/Test/MessageTimer.hs | 4 +- integration/test/Test/Roles.hs | 11 ++-- integration/test/Testlib/RunServices.hs | 2 - nix/wire-server.nix | 5 +- 12 files changed, 135 insertions(+), 123 deletions(-) diff --git a/integration/integration.cabal b/integration/integration.cabal index 4edcf08b67c..7f4547f0c24 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -21,8 +21,7 @@ custom-setup common common-all default-language: GHC2021 ghc-options: - -Wall -Wpartial-fields -fwarn-tabs - -optP-Wno-nonportable-include-path + -Wall -Wpartial-fields -fwarn-tabs -Wno-incomplete-uni-patterns default-extensions: NoImplicitPrelude diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 3e2f2313894..28f25826954 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -76,16 +76,14 @@ connectUsers alice bob = do bindResponse (Brig.postConnection alice bob) (\resp -> resp.status `shouldMatchInt` 201) bindResponse (Brig.putConnection bob alice "accepted") (\resp -> resp.status `shouldMatchInt` 200) -createAndConnectUsers :: (HasCallStack, MakesValue domain) => [domain] -> App [Value] -createAndConnectUsers domains = do - users <- for domains (flip randomUser def) - let userPairs = do - t <- tails users - (a, others) <- maybeToList (uncons t) - b <- others - pure (a, b) - for_ userPairs (uncurry connectUsers) - pure users +createAndConnectUsers :: (HasCallStack, MakesValue domain) => domain -> domain -> App (Value, Value) +createAndConnectUsers d1 d2 = do + [u1, u2] <- for [d1, d2] (flip randomUser def) + connectUsers u1 u2 + pure (u1, u2) + +createUsers :: (HasCallStack, MakesValue domain) => [domain] -> App [Value] +createUsers domains = for domains (flip randomUser def) getAllConvs :: (HasCallStack, MakesValue u) => u -> App [Value] getAllConvs u = do diff --git a/integration/test/Test/AccessUpdate.hs b/integration/test/Test/AccessUpdate.hs index 7202717a8a7..52358f435ae 100644 --- a/integration/test/Test/AccessUpdate.hs +++ b/integration/test/Test/AccessUpdate.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2023 Wire Swiss GmbH @@ -105,7 +103,9 @@ testAccessUpdateGuestRemovedUnreachableRemotes = do testAccessUpdateWithRemotes :: HasCallStack => App () testAccessUpdateWithRemotes = do - [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OwnDomain] + [alice, bob, charlie] <- createUsers [OwnDomain, OtherDomain, OwnDomain] + connectUsers alice bob + connectUsers alice charlie conv <- postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) >>= getJSON 201 diff --git a/integration/test/Test/Brig.hs b/integration/test/Test/Brig.hs index 8179641174b..ae23438363c 100644 --- a/integration/test/Test/Brig.hs +++ b/integration/test/Test/Brig.hs @@ -1,11 +1,9 @@ -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} - module Test.Brig where -import API.Brig qualified as Public -import API.BrigInternal qualified as Internal +import API.Brig qualified as BrigP +import API.BrigInternal qualified as BrigI import API.Common qualified as API -import API.GalleyInternal qualified as Internal +import API.GalleyInternal qualified as GalleyI import Control.Concurrent (threadDelay) import Data.Aeson.Types hiding ((.=)) import Data.Set qualified as Set @@ -19,13 +17,13 @@ import Testlib.Prelude testSearchContactForExternalUsers :: HasCallStack => App () testSearchContactForExternalUsers = do - owner <- randomUser OwnDomain def {Internal.team = True} - partner <- randomUser OwnDomain def {Internal.team = True} + owner <- randomUser OwnDomain def {BrigI.team = True} + partner <- randomUser OwnDomain def {BrigI.team = True} - bindResponse (Internal.putTeamMember partner (partner %. "team") (API.teamRole "partner")) $ \resp -> + bindResponse (GalleyI.putTeamMember partner (partner %. "team") (API.teamRole "partner")) $ \resp -> resp.status `shouldMatchInt` 200 - bindResponse (Public.searchContacts partner (owner %. "name") OwnDomain) $ \resp -> + bindResponse (BrigP.searchContacts partner (owner %. "name") OwnDomain) $ \resp -> resp.status `shouldMatchInt` 403 testCrudFederationRemotes :: HasCallStack => App () @@ -42,29 +40,29 @@ testCrudFederationRemotes = do addTest :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => fedConn -> [fedConn2] -> App () addTest fedConn want = do - bindResponse (Internal.createFedConn ownDomain fedConn) $ \res -> do + bindResponse (BrigI.createFedConn ownDomain fedConn) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 200 - res2 <- parseFedConns =<< Internal.readFedConns ownDomain + res2 <- parseFedConns =<< BrigI.readFedConns ownDomain sort res2 `shouldMatch` sort want updateTest :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => String -> fedConn -> [fedConn2] -> App () updateTest domain fedConn want = do - bindResponse (Internal.updateFedConn ownDomain domain fedConn) $ \res -> do + bindResponse (BrigI.updateFedConn ownDomain domain fedConn) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 200 - res2 <- parseFedConns =<< Internal.readFedConns ownDomain + res2 <- parseFedConns =<< BrigI.readFedConns ownDomain sort res2 `shouldMatch` sort want dom1 :: String <- (<> ".example.com") . UUID.toString <$> liftIO UUID.nextRandom - let remote1, remote1' :: Internal.FedConn - remote1 = Internal.FedConn dom1 "no_search" - remote1' = remote1 {Internal.searchStrategy = "full_search"} + let remote1, remote1' :: BrigI.FedConn + remote1 = BrigI.FedConn dom1 "no_search" + remote1' = remote1 {BrigI.searchStrategy = "full_search"} - cfgRemotesExpect :: Internal.FedConn - cfgRemotesExpect = Internal.FedConn (cs otherDomain) "full_search" + cfgRemotesExpect :: BrigI.FedConn + cfgRemotesExpect = BrigI.FedConn (cs otherDomain) "full_search" liftIO $ threadDelay 5_000_000 - cfgRemotes <- parseFedConns =<< Internal.readFedConns ownDomain + cfgRemotes <- parseFedConns =<< BrigI.readFedConns ownDomain cfgRemotes `shouldMatch` ([] @Value) -- entries present in the config file can be idempotently added if identical, but cannot be -- updated. @@ -73,29 +71,29 @@ testCrudFederationRemotes = do addTest remote1 [cfgRemotesExpect, remote1] addTest remote1 [cfgRemotesExpect, remote1] -- idempotency -- update - updateTest (Internal.domain remote1) remote1' [cfgRemotesExpect, remote1'] + updateTest (BrigI.domain remote1) remote1' [cfgRemotesExpect, remote1'] testCrudOAuthClient :: HasCallStack => App () testCrudOAuthClient = do user <- randomUser OwnDomain def let appName = "foobar" let url = "https://example.com/callback.html" - clientId <- bindResponse (Internal.registerOAuthClient user appName url) $ \resp -> do + clientId <- bindResponse (BrigI.registerOAuthClient user appName url) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "client_id" - bindResponse (Internal.getOAuthClient user clientId) $ \resp -> do + bindResponse (BrigI.getOAuthClient user clientId) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "application_name" `shouldMatch` appName resp.json %. "redirect_url" `shouldMatch` url let newName = "barfoo" let newUrl = "https://example.com/callback2.html" - bindResponse (Internal.updateOAuthClient user clientId newName newUrl) $ \resp -> do + bindResponse (BrigI.updateOAuthClient user clientId newName newUrl) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "application_name" `shouldMatch` newName resp.json %. "redirect_url" `shouldMatch` newUrl - bindResponse (Internal.deleteOAuthClient user clientId) $ \resp -> do + bindResponse (BrigI.deleteOAuthClient user clientId) $ \resp -> do resp.status `shouldMatchInt` 200 - bindResponse (Internal.getOAuthClient user clientId) $ \resp -> do + bindResponse (BrigI.getOAuthClient user clientId) $ \resp -> do resp.status `shouldMatchInt` 404 -- | See https://docs.wire.com/understand/api-client-perspective/swagger.html @@ -107,7 +105,7 @@ testSwagger = do internalApis :: [String] internalApis = ["brig", "cannon", "cargohold", "cannon", "spar"] - bindResponse Public.getApiVersions $ \resp -> do + bindResponse BrigP.getApiVersions $ \resp -> do resp.status `shouldMatchInt` 200 actualVersions :: [Int] <- do sup <- resp.json %. "supported" & asListOf asInt @@ -119,40 +117,41 @@ testSwagger = do -- documented.) Set.fromList actualVersions `Set.isSubsetOf` Set.fromList existingVersions - bindResponse Public.getSwaggerPublicTOC $ \resp -> do + bindResponse BrigP.getSwaggerPublicTOC $ \resp -> do resp.status `shouldMatchInt` 200 cs resp.body `shouldContainString` "" forM_ existingVersions $ \v -> do - bindResponse (Public.getSwaggerPublicAllUI v) $ \resp -> do + bindResponse (BrigP.getSwaggerPublicAllUI v) $ \resp -> do resp.status `shouldMatchInt` 200 cs resp.body `shouldContainString` "" - bindResponse (Public.getSwaggerPublicAllJson v) $ \resp -> do + bindResponse (BrigP.getSwaggerPublicAllJson v) $ \resp -> do resp.status `shouldMatchInt` 200 void resp.json - -- FUTUREWORK: Implement Public.getSwaggerInternalTOC (including the end-point); make sure + -- ! + -- FUTUREWORK: Implement BrigP.getSwaggerInternalTOC (including the end-point); make sure -- newly added internal APIs make this test fail if not added to `internalApis`. forM_ internalApis $ \api -> do - bindResponse (Public.getSwaggerInternalUI api) $ \resp -> do + bindResponse (BrigP.getSwaggerInternalUI api) $ \resp -> do resp.status `shouldMatchInt` 200 cs resp.body `shouldContainString` "" - bindResponse (Public.getSwaggerInternalJson api) $ \resp -> do + bindResponse (BrigP.getSwaggerInternalJson api) $ \resp -> do resp.status `shouldMatchInt` 200 void resp.json testRemoteUserSearch :: HasCallStack => App () testRemoteUserSearch = do startDynamicBackends [def, def] $ \[d1, d2] -> do - void $ Internal.createFedConn d2 (Internal.FedConn d1 "full_search") + void $ BrigI.createFedConn d2 (BrigI.FedConn d1 "full_search") u1 <- randomUser d1 def u2 <- randomUser d2 def - Internal.refreshIndex d2 + BrigI.refreshIndex d2 uidD2 <- objId u2 - bindResponse (Public.searchContacts u1 (u2 %. "name") d2) $ \resp -> do + bindResponse (BrigP.searchContacts u1 (u2 %. "name") d2) $ \resp -> do resp.status `shouldMatchInt` 200 docs <- resp.json %. "documents" >>= asList case docs of @@ -162,15 +161,15 @@ testRemoteUserSearch = do testRemoteUserSearchExactHandle :: HasCallStack => App () testRemoteUserSearchExactHandle = do startDynamicBackends [def, def] $ \[d1, d2] -> do - void $ Internal.createFedConn d2 (Internal.FedConn d1 "exact_handle_search") + void $ BrigI.createFedConn d2 (BrigI.FedConn d1 "exact_handle_search") u1 <- randomUser d1 def u2 <- randomUser d2 def u2Handle <- API.randomHandle - bindResponse (Public.putHandle u2 u2Handle) $ assertSuccess - Internal.refreshIndex d2 + bindResponse (BrigP.putHandle u2 u2Handle) $ assertSuccess + BrigI.refreshIndex d2 - bindResponse (Public.searchContacts u1 u2Handle d2) $ \resp -> do + bindResponse (BrigP.searchContacts u1 u2Handle d2) $ \resp -> do resp.status `shouldMatchInt` 200 docs <- resp.json %. "documents" >>= asList case docs of diff --git a/integration/test/Test/Client.hs b/integration/test/Test/Client.hs index 4ee88901419..8ff58bd62b3 100644 --- a/integration/test/Test/Client.hs +++ b/integration/test/Test/Client.hs @@ -46,7 +46,7 @@ testListClientsIfBackendIsOffline = do resourcePool <- asks (.resourcePool) ownDomain <- asString OwnDomain otherDomain <- asString OtherDomain - [ownUser1, ownUser2] <- createAndConnectUsers [OwnDomain, OtherDomain] + (ownUser1, ownUser2) <- createAndConnectUsers OwnDomain OtherDomain ownClient1 <- objId $ bindResponse (API.addClient ownUser1 def) $ getJSON 201 ownClient2 <- objId $ bindResponse (API.addClient ownUser2 def) $ getJSON 201 ownUser1Id <- objId ownUser1 diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 9a42bf0493d..cc0e73e219d 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -1,5 +1,4 @@ {-# OPTIONS_GHC -Wno-ambiguous-fields #-} -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} -- This file is part of the Wire Server implementation. -- @@ -140,7 +139,9 @@ testFederationStatus = do testCreateConversationFullyConnected :: HasCallStack => App () testCreateConversationFullyConnected = do startDynamicBackends [def, def, def] $ \[domainA, domainB, domainC] -> do - [u1, u2, u3] <- createAndConnectUsers [domainA, domainB, domainC] + [u1, u2, u3] <- createUsers [domainA, domainB, domainC] + connectUsers u1 u2 + connectUsers u1 u3 bindResponse (postConversation u1 (defProteus {qualifiedUsers = [u2, u3]})) $ \resp -> do resp.status `shouldMatchInt` 201 @@ -167,7 +168,9 @@ testCreateConversationNonFullyConnected = do testAddMembersFullyConnectedProteus :: HasCallStack => App () testAddMembersFullyConnectedProteus = do startDynamicBackends [def, def, def] $ \[domainA, domainB, domainC] -> do - [u1, u2, u3] <- createAndConnectUsers [domainA, domainB, domainC] + [u1, u2, u3] <- createUsers [domainA, domainB, domainC] + connectUsers u1 u2 + connectUsers u1 u3 -- create conversation with no users cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 -- add members from remote backends @@ -241,7 +244,7 @@ testAddMember = do testAddMemberV1 :: HasCallStack => Domain -> App () testAddMemberV1 domain = do - [alice, bob] <- createAndConnectUsers [OwnDomain, domain] + (alice, bob) <- createAndConnectUsers OwnDomain domain conv <- postConversation alice defProteus >>= getJSON 201 bobId <- bob %. "qualified_id" let opts = @@ -264,7 +267,8 @@ testConvWithUnreachableRemoteUsers = do startDynamicBackends [def, def] $ \domains -> do own <- make OwnDomain & asString other <- make OtherDomain & asString - users <- createAndConnectUsers $ [own, own, other] <> domains + users@(alice : others) <- createUsers $ [own, own, other] <> domains + forM_ others $ connectUsers alice pure (users, domains) let newConv = defProteus {qualifiedUsers = [alex, bob, charlie, dylan]} @@ -282,11 +286,12 @@ testAddReachableWithUnreachableRemoteUsers = do startDynamicBackends [def, def] $ \domains -> do own <- make OwnDomain & asString other <- make OtherDomain & asString - [alice, alex, bob, charlie, dylan] <- - createAndConnectUsers $ [own, own, other] <> domains + [alice, alex, bob, charlie, dylan] <- createUsers $ [own, own, other] <> domains + forM_ [alex, bob, charlie, dylan] $ connectUsers alice let newConv = defProteus {qualifiedUsers = [alex, charlie, dylan]} conv <- postConversation alice newConv >>= getJSON 201 + connectUsers alex bob pure ([alex, bob], conv, domains) bobId <- bob %. "qualified_id" @@ -304,11 +309,12 @@ testAddUnreachable = do ([alex, charlie], [charlieDomain, dylanDomain], conv) <- startDynamicBackends [def, def] $ \domains -> do own <- make OwnDomain & asString - [alice, alex, charlie, dylan] <- - createAndConnectUsers $ [own, own] <> domains + [alice, alex, charlie, dylan] <- createUsers $ [own, own] <> domains + forM_ [alex, charlie, dylan] $ connectUsers alice let newConv = defProteus {qualifiedUsers = [alex, dylan]} conv <- postConversation alice newConv >>= getJSON 201 + connectUsers alex charlie pure ([alex, charlie], domains, conv) charlieId <- charlie %. "qualified_id" @@ -438,8 +444,8 @@ testAddUserWhenOtherBackendOffline = do ([alice, alex], conv) <- startDynamicBackends [def] $ \domains -> do own <- make OwnDomain & asString - [alice, alex, charlie] <- - createAndConnectUsers $ [own, own] <> domains + [alice, alex, charlie] <- createUsers $ [own, own] <> domains + forM_ [alex, charlie] $ connectUsers alice let newConv = defProteus {qualifiedUsers = [charlie]} conv <- postConversation alice newConv >>= getJSON 201 @@ -450,7 +456,7 @@ testAddUserWhenOtherBackendOffline = do testSynchroniseUserRemovalNotification :: HasCallStack => App () testSynchroniseUserRemovalNotification = do resourcePool <- asks resourcePool - [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + (alice, bob) <- createAndConnectUsers OwnDomain OtherDomain runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> do (conv, charlie, client) <- runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do @@ -476,7 +482,7 @@ testSynchroniseUserRemovalNotification = do testConvRenaming :: HasCallStack => App () testConvRenaming = do - [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + (alice, bob) <- createAndConnectUsers OwnDomain OtherDomain conv <- postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 @@ -490,7 +496,7 @@ testConvRenaming = do testReceiptModeWithRemotesOk :: HasCallStack => App () testReceiptModeWithRemotesOk = do - [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + (alice, bob) <- createAndConnectUsers OwnDomain OtherDomain conv <- postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 @@ -520,7 +526,9 @@ testReceiptModeWithRemotesUnreachable = do testDeleteLocalMember :: HasCallStack => App () testDeleteLocalMember = do - [alice, alex, bob] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + [alice, alex, bob] <- createUsers [OwnDomain, OwnDomain, OtherDomain] + connectUsers alice alex + connectUsers alice bob conv <- postConversation alice (defProteus {qualifiedUsers = [alex, bob]}) >>= getJSON 201 @@ -537,7 +545,9 @@ testDeleteLocalMember = do testDeleteRemoteMember :: HasCallStack => App () testDeleteRemoteMember = do - [alice, alex, bob] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + [alice, alex, bob] <- createUsers [OwnDomain, OwnDomain, OtherDomain] + connectUsers alice alex + connectUsers alice bob conv <- postConversation alice (defProteus {qualifiedUsers = [alex, bob]}) >>= getJSON 201 @@ -554,9 +564,11 @@ testDeleteRemoteMember = do testDeleteRemoteMemberRemoteUnreachable :: HasCallStack => App () testDeleteRemoteMemberRemoteUnreachable = do - [alice, bob, bart] <- createAndConnectUsers [OwnDomain, OtherDomain, OtherDomain] + [alice, bob, bart] <- createUsers [OwnDomain, OtherDomain, OtherDomain] conv <- startDynamicBackends [mempty] $ \[dynBackend] -> do charlie <- randomUser dynBackend def + connectUsers alice bob + connectUsers alice bart connectUsers alice charlie postConversation alice @@ -619,14 +631,13 @@ testDeleteTeamConversationWithUnreachableRemoteMembers = do testLeaveConversationSuccess :: HasCallStack => App () testLeaveConversationSuccess = do - [alice, bob, chad, dee] <- - createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain, OtherDomain] + [alice, bob, chad, dee] <- createUsers [OwnDomain, OwnDomain, OtherDomain, OtherDomain] [aClient, bClient] <- forM [alice, bob] $ \user -> objId $ bindResponse (addClient user def) $ getJSON 201 startDynamicBackends [def] $ \[dynDomain] -> do eve <- randomUser dynDomain def eClient <- objId $ bindResponse (addClient eve def) $ getJSON 201 - connectUsers alice eve + forM_ [bob, chad, dee, eve] $ connectUsers alice conv <- postConversation alice @@ -644,8 +655,8 @@ testOnUserDeletedConversations :: HasCallStack => App () testOnUserDeletedConversations = do startDynamicBackends [def] $ \[dynDomain] -> do [ownDomain, otherDomain] <- forM [OwnDomain, OtherDomain] asString - [alice, alex, bob, bart, chad] <- - createAndConnectUsers [ownDomain, ownDomain, otherDomain, otherDomain, dynDomain] + [alice, alex, bob, bart, chad] <- createUsers [ownDomain, ownDomain, otherDomain, otherDomain, dynDomain] + forM_ [alex, bob, bart, chad] $ connectUsers alice bobId <- bob %. "qualified_id" ooConvId <- do l <- getAllConvs alice @@ -681,7 +692,9 @@ testOnUserDeletedConversations = do testUpdateConversationByRemoteAdmin :: HasCallStack => App () testUpdateConversationByRemoteAdmin = do - [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OtherDomain] + [alice, bob, charlie] <- createUsers [OwnDomain, OtherDomain, OtherDomain] + connectUsers alice bob + connectUsers alice charlie conv <- postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) >>= getJSON 201 diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 36d58cbab57..98d81a4d29a 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -3,9 +3,9 @@ -- | This module is meant to show how Testlib can be used module Test.Demo where -import API.Brig qualified as Public -import API.BrigInternal qualified as Internal -import API.GalleyInternal qualified as Internal +import API.Brig qualified as BrigP +import API.BrigInternal qualified as BrigI +import API.GalleyInternal qualified as GalleyI import API.Nginz qualified as Nginz import Control.Monad.Cont import GHC.Stack @@ -17,10 +17,10 @@ testCantDeleteLHClient :: HasCallStack => App () testCantDeleteLHClient = do user <- randomUser OwnDomain def client <- - Public.addClient user def {Public.ctype = "legalhold", Public.internal = True} + BrigP.addClient user def {BrigP.ctype = "legalhold", BrigP.internal = True} >>= getJSON 201 - bindResponse (Public.deleteClient user client) $ \resp -> do + bindResponse (BrigP.deleteClient user client) $ \resp -> do resp.status `shouldMatchInt` 400 -- | Deleting unknown clients should fail with 404. @@ -28,7 +28,7 @@ testDeleteUnknownClient :: HasCallStack => App () testDeleteUnknownClient = do user <- randomUser OwnDomain def let fakeClientId = "deadbeefdeadbeef" - bindResponse (Public.deleteClient user fakeClientId) $ \resp -> do + bindResponse (BrigP.deleteClient user fakeClientId) $ \resp -> do resp.status `shouldMatchInt` 404 resp.json %. "label" `shouldMatch` "client-not-found" @@ -37,7 +37,7 @@ testModifiedBrig = do withModifiedBackend (def {brigCfg = setField "optSettings.setFederationDomain" "overridden.example.com"}) $ \domain -> do - bindResponse (Public.getAPIVersion domain) + bindResponse (BrigP.getAPIVersion domain) $ \resp -> do resp.status `shouldMatchInt` 200 (resp.json %. "domain") `shouldMatch` "overridden.example.com" @@ -48,7 +48,7 @@ testModifiedGalley = do let getFeatureStatus :: (MakesValue domain) => domain -> String -> App Value getFeatureStatus domain team = do - bindResponse (Internal.getTeamFeature domain "searchVisibility" team) $ \res -> do + bindResponse (GalleyI.getTeamFeature domain "searchVisibility" team) $ \res -> do res.status `shouldMatchInt` 200 res.json %. "status" @@ -86,11 +86,11 @@ testModifiedServices = do withModifiedBackend serviceMap $ \domain -> do (_user, tid, _) <- createTeam domain 1 - bindResponse (Internal.getTeamFeature domain "searchVisibility" tid) $ \res -> do + bindResponse (GalleyI.getTeamFeature domain "searchVisibility" tid) $ \res -> do res.status `shouldMatchInt` 200 res.json %. "status" `shouldMatch` "enabled" - bindResponse (Public.getAPIVersion domain) $ + bindResponse (BrigP.getAPIVersion domain) $ \resp -> do resp.status `shouldMatchInt` 200 (resp.json %. "domain") `shouldMatch` "overridden.example.com" @@ -105,7 +105,7 @@ testDynamicBackend = do ownDomain <- objDomain OwnDomain user <- randomUser OwnDomain def uid <- objId user - bindResponse (Public.getSelf ownDomain uid) $ \resp -> do + bindResponse (BrigP.getSelf ownDomain uid) $ \resp -> do resp.status `shouldMatchInt` 200 (resp.json %. "id") `shouldMatch` objId user @@ -117,24 +117,24 @@ testDynamicBackend = do resp.json %. "setRestrictUserCreation" `shouldMatch` False -- user created in own domain should not be found in dynamic backend - bindResponse (Public.getSelf dynDomain uid) $ \resp -> do + bindResponse (BrigP.getSelf dynDomain uid) $ \resp -> do resp.status `shouldMatchInt` 404 -- now create a user in the dynamic backend userD1 <- randomUser dynDomain def uidD1 <- objId userD1 - bindResponse (Public.getSelf dynDomain uidD1) $ \resp -> do + bindResponse (BrigP.getSelf dynDomain uidD1) $ \resp -> do resp.status `shouldMatchInt` 200 (resp.json %. "id") `shouldMatch` objId userD1 -- the d1 user should not be found in the own domain - bindResponse (Public.getSelf ownDomain uidD1) $ \resp -> do + bindResponse (BrigP.getSelf ownDomain uidD1) $ \resp -> do resp.status `shouldMatchInt` 404 testStartMultipleDynamicBackends :: HasCallStack => App () testStartMultipleDynamicBackends = do let assertCorrectDomain domain = - bindResponse (Public.getAPIVersion domain) $ + bindResponse (BrigP.getAPIVersion domain) $ \resp -> do resp.status `shouldMatchInt` 200 (resp.json %. "domain") `shouldMatch` domain @@ -146,8 +146,8 @@ testIndependentESIndices = do u2 <- randomUser OwnDomain def uid2 <- objId u2 connectUsers u1 u2 - Internal.refreshIndex OwnDomain - bindResponse (Public.searchContacts u1 (u2 %. "name") OwnDomain) $ \resp -> do + BrigI.refreshIndex OwnDomain + bindResponse (BrigP.searchContacts u1 (u2 %. "name") OwnDomain) $ \resp -> do resp.status `shouldMatchInt` 200 docs <- resp.json %. "documents" >>= asList case docs of @@ -157,16 +157,16 @@ testIndependentESIndices = do [dynDomain] <- pure dynDomains uD1 <- randomUser dynDomain def -- searching for u1 on the dyn backend should yield no result - bindResponse (Public.searchContacts uD1 (u2 %. "name") dynDomain) $ \resp -> do + bindResponse (BrigP.searchContacts uD1 (u2 %. "name") dynDomain) $ \resp -> do resp.status `shouldMatchInt` 200 docs <- resp.json %. "documents" >>= asList null docs `shouldMatch` True uD2 <- randomUser dynDomain def uidD2 <- objId uD2 connectUsers uD1 uD2 - Internal.refreshIndex dynDomain + BrigI.refreshIndex dynDomain -- searching for uD2 on the dyn backend should yield a result - bindResponse (Public.searchContacts uD1 (uD2 %. "name") dynDomain) $ \resp -> do + bindResponse (BrigP.searchContacts uD1 (uD2 %. "name") dynDomain) $ \resp -> do resp.status `shouldMatchInt` 200 docs <- resp.json %. "documents" >>= asList case docs of @@ -176,22 +176,22 @@ testIndependentESIndices = do testDynamicBackendsFederation :: HasCallStack => App () testDynamicBackendsFederation = do startDynamicBackends [def, def] $ \[aDynDomain, anotherDynDomain] -> do - [u1, u2] <- createAndConnectUsers [aDynDomain, anotherDynDomain] - bindResponse (Public.getConnection u1 u2) assertSuccess - bindResponse (Public.getConnection u2 u1) assertSuccess + (u1, u2) <- createAndConnectUsers aDynDomain anotherDynDomain + bindResponse (BrigP.getConnection u1 u2) assertSuccess + bindResponse (BrigP.getConnection u2 u1) assertSuccess testWebSockets :: HasCallStack => App () testWebSockets = do user <- randomUser OwnDomain def withWebSocket user $ \ws -> do - client <- Public.addClient user def >>= getJSON 201 + client <- BrigP.addClient user def >>= getJSON 201 n <- awaitMatch 3 (\n -> nPayload n %. "type" `isEqual` "user.client-add") ws nPayload n %. "client.id" `shouldMatch` (client %. "id") testMultipleBackends :: App () testMultipleBackends = do - ownDomainRes <- (Public.getAPIVersion OwnDomain >>= getJSON 200) %. "domain" - otherDomainRes <- (Public.getAPIVersion OtherDomain >>= getJSON 200) %. "domain" + ownDomainRes <- (BrigP.getAPIVersion OwnDomain >>= getJSON 200) %. "domain" + otherDomainRes <- (BrigP.getAPIVersion OtherDomain >>= getJSON 200) %. "domain" ownDomainRes `shouldMatch` OwnDomain otherDomainRes `shouldMatch` OtherDomain OwnDomain `shouldNotMatch` OtherDomain diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs index b690430146c..899141ee313 100644 --- a/integration/test/Test/Federation.hs +++ b/integration/test/Test/Federation.hs @@ -1,9 +1,8 @@ {-# LANGUAGE OverloadedLabels #-} -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} module Test.Federation where -import API.Brig qualified as API +import API.Brig qualified as BrigP import API.Galley import Control.Lens import Control.Monad.Codensity @@ -22,10 +21,10 @@ testNotificationsForOfflineBackends :: HasCallStack => App () testNotificationsForOfflineBackends = do resourcePool <- asks (.resourcePool) -- `delUser` will eventually get deleted. - [delUser, otherUser, otherUser2] <- createAndConnectUsers [OwnDomain, OtherDomain, OtherDomain] - delClient <- objId $ bindResponse (API.addClient delUser def) $ getJSON 201 - otherClient <- objId $ bindResponse (API.addClient otherUser def) $ getJSON 201 - otherClient2 <- objId $ bindResponse (API.addClient otherUser2 def) $ getJSON 201 + [delUser, otherUser, otherUser2] <- createUsers [OwnDomain, OtherDomain, OtherDomain] + delClient <- objId $ bindResponse (BrigP.addClient delUser def) $ getJSON 201 + otherClient <- objId $ bindResponse (BrigP.addClient otherUser def) $ getJSON 201 + otherClient2 <- objId $ bindResponse (BrigP.addClient otherUser2 def) $ getJSON 201 -- We call it 'downBackend' because it is down for most of this test -- except for setup and assertions. Perhaps there is a better name. @@ -33,10 +32,14 @@ testNotificationsForOfflineBackends = do (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) <- runCodensity (startDynamicBackend downBackend mempty) $ \_ -> do downUser1 <- randomUser downBackend.berDomain def downUser2 <- randomUser downBackend.berDomain def - downClient1 <- objId $ bindResponse (API.addClient downUser1 def) $ getJSON 201 + downClient1 <- objId $ bindResponse (BrigP.addClient downUser1 def) $ getJSON 201 + + connectUsers delUser otherUser + connectUsers delUser otherUser2 connectUsers delUser downUser1 connectUsers delUser downUser2 - connectUsers otherUser downUser1 + connectUsers downUser1 otherUser + upBackendConv <- bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, otherUser2, downUser1]})) $ getJSON 201 downBackendConv <- bindResponse (postConversation downUser1 (defProteus {qualifiedUsers = [otherUser, delUser]})) $ getJSON 201 pure (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) diff --git a/integration/test/Test/MessageTimer.hs b/integration/test/Test/MessageTimer.hs index 6a401e3d68e..91ddc579860 100644 --- a/integration/test/Test/MessageTimer.hs +++ b/integration/test/Test/MessageTimer.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2023 Wire Swiss GmbH @@ -30,7 +28,7 @@ import Testlib.ResourcePool testMessageTimerChangeWithRemotes :: HasCallStack => App () testMessageTimerChangeWithRemotes = do - [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + (alice, bob) <- createAndConnectUsers OwnDomain OtherDomain conv <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 withWebSockets [alice, bob] $ \wss -> do void $ updateMessageTimer alice conv 1000 >>= getBody 200 diff --git a/integration/test/Test/Roles.hs b/integration/test/Test/Roles.hs index 906d9d9632c..5a8eefc80bf 100644 --- a/integration/test/Test/Roles.hs +++ b/integration/test/Test/Roles.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2023 Wire Swiss GmbH @@ -28,7 +26,9 @@ import Testlib.Prelude testRoleUpdateWithRemotesOk :: HasCallStack => App () testRoleUpdateWithRemotesOk = do - [bob, charlie, alice] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + [bob, charlie, alice] <- createUsers [OwnDomain, OwnDomain, OtherDomain] + connectUsers bob charlie + connectUsers bob alice conv <- postConversation bob (defProteus {qualifiedUsers = [charlie, alice]}) >>= getJSON 201 @@ -47,10 +47,11 @@ testRoleUpdateWithRemotesOk = do testRoleUpdateWithRemotesUnreachable :: HasCallStack => App () testRoleUpdateWithRemotesUnreachable = do - [bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain] + [bob, charlie] <- createUsers [OwnDomain, OwnDomain] startDynamicBackends [mempty] $ \[dynBackend] -> do alice <- randomUser dynBackend def - mapM_ (connectUsers alice) [bob, charlie] + connectUsers bob alice + connectUsers bob charlie conv <- postConversation bob (defProteus {qualifiedUsers = [charlie, alice]}) >>= getJSON 201 diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index a2b774f5c5e..38b68b2e485 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -Wno-unused-matches #-} - module Testlib.RunServices where import Control.Concurrent diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 14b45676516..129d5e8c2b2 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -104,7 +104,10 @@ let hsuper hself; - werror = _: hlib.failOnAllWarnings; + # append `-Werror` to ghc options for all packages. + # failOnAllWarnings implies `-Wall`, which overrides any `-Wno-*` from the package cabal file. + # https://github.com/NixOS/nixpkgs/blob/1e411c55166539b130b330dafcc4034152f8d4fd/pkgs/development/haskell-modules/lib/compose.nix#L327 + werror = _: (drv: hlib.appendConfigureFlag drv "--ghc-option=-Werror"); opt = _: drv: if enableOptimization then drv From 6659cceb3387750f06ddbebed6711dec7fc52d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 29 Sep 2023 13:55:16 +0200 Subject: [PATCH 158/225] [WPB-1908] Unverified user creating conversation (#3622) * Check if the user is activated/verified * integration: support creating guest (non-verified) users Guest users are not activated as they do not verify their email address (or phone number) * Test: a guest cannot create a conversation * Add a changelog --- .../WPB-1908-guest-creating-conversation | 1 + integration/test/API/BrigInternal.hs | 17 ++++++++++------- integration/test/Test/Conversation.hs | 7 +++++++ services/galley/src/Galley/API/Create.hs | 5 ++++- 4 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-1908-guest-creating-conversation diff --git a/changelog.d/3-bug-fixes/WPB-1908-guest-creating-conversation b/changelog.d/3-bug-fixes/WPB-1908-guest-creating-conversation new file mode 100644 index 00000000000..2e06f0b2bb3 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-1908-guest-creating-conversation @@ -0,0 +1 @@ +Disable a guest user from creating a group conversation diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index c675c835585..c72a6df2895 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -11,6 +11,7 @@ data CreateUser = CreateUser password :: Maybe String, name :: Maybe String, team :: Bool, + activate :: Bool, supportedProtocols :: Maybe [String] } @@ -21,23 +22,25 @@ instance Default CreateUser where password = Nothing, name = Nothing, team = False, + activate = True, supportedProtocols = Nothing } createUser :: (HasCallStack, MakesValue domain) => domain -> CreateUser -> App Response createUser domain cu = do - email <- maybe randomEmail pure cu.email + re <- randomEmail + let email :: Maybe String = guard cu.activate $> fromMaybe re cu.email let password = fromMaybe defPassword cu.password - name = fromMaybe email cu.name + name = fromMaybe "default" (cu.name <|> email) req <- baseRequest domain Brig Unversioned "/i/users" submit "POST" $ req & addJSONObject - ( [ "email" .= email, - "name" .= name, - "password" .= password, - "icon" .= "default" - ] + ( ["email" .= e | e <- toList email] + <> [ "name" .= name, + "password" .= password, + "icon" .= "default" + ] <> ["supported_protocols" .= prots | prots <- toList cu.supportedProtocols] <> [ "team" .= object diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index cc0e73e219d..548c5b6e611 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -702,3 +702,10 @@ testUpdateConversationByRemoteAdmin = do void $ withWebSockets [alice, bob, charlie] $ \wss -> do void $ updateReceiptMode bob conv (41 :: Int) >>= getBody 200 for_ wss $ \ws -> awaitMatch 10 isReceiptModeUpdateNotif ws + +testGuestCreatesConversation :: HasCallStack => App () +testGuestCreatesConversation = do + alice <- randomUser OwnDomain def {activate = False} + bindResponse (postConversation alice defProteus) $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "operation-denied" diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index c786ae1ab9a..e976322adfc 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -52,6 +52,7 @@ import Galley.App (Env) import Galley.Data.Conversation qualified as Data import Galley.Data.Conversation.Types import Galley.Effects +import Galley.Effects.BrigAccess import Galley.Effects.ConversationStore qualified as E import Galley.Effects.FederatorAccess qualified as E import Galley.Effects.GundeckAccess qualified as E @@ -262,7 +263,9 @@ checkCreateConvPermissions :: Maybe ConvTeamInfo -> UserList UserId -> Sem r () -checkCreateConvPermissions lusr _newConv Nothing allUsers = +checkCreateConvPermissions lusr _newConv Nothing allUsers = do + activated <- listToMaybe <$> lookupActivatedUsers [tUnqualified lusr] + void $ noteS @OperationDenied activated ensureConnected lusr allUsers checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do let convTeam = cnvTeamId tinfo From 086e5b2d716bed036ab54af48b632666de8f3dc0 Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Fri, 29 Sep 2023 15:47:17 +0200 Subject: [PATCH 159/225] Helm: Push rabbitmq-external to Helm repo (#3626) Otherwise, it's not available in deployments, CI pipelines, etc.. Add (missing) changelog entry for rabbitmq-external Helm chart. --- Makefile | 2 +- changelog.d/2-features/rabbitmq-external_helm_chart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/2-features/rabbitmq-external_helm_chart diff --git a/Makefile b/Makefile index b2706f460a2..6fad98a6218 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ CHARTS_INTEGRATION := wire-server databases-ephemeral redis-cluster rabbitmq # (e.g. move charts/brig to charts/wire-server/brig) # this list could be generated from the folder names under ./charts/ like so: # CHARTS_RELEASE := $(shell find charts/ -maxdepth 1 -type d | xargs -n 1 basename | grep -v charts) -CHARTS_RELEASE := wire-server redis-ephemeral redis-cluster rabbitmq databases-ephemeral \ +CHARTS_RELEASE := wire-server redis-ephemeral redis-cluster rabbitmq rabbitmq-external databases-ephemeral \ fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice \ calling-test demo-smtp elasticsearch-curator elasticsearch-external \ elasticsearch-ephemeral minio-external cassandra-external \ diff --git a/changelog.d/2-features/rabbitmq-external_helm_chart b/changelog.d/2-features/rabbitmq-external_helm_chart new file mode 100644 index 00000000000..57a9f4e7ccc --- /dev/null +++ b/changelog.d/2-features/rabbitmq-external_helm_chart @@ -0,0 +1 @@ +Add Helm chart (`rabbitmq-external`) to interface RabbitMQ instances outside of the Kubernetes cluster. From 2ca6e3a88196fa6b83c945c9e9c2980b655bff49 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 29 Sep 2023 16:19:09 +0200 Subject: [PATCH 160/225] WPB-4910 include build timestamp in s3 upload path for test logs (#3621) --- changelog.d/5-internal/WPB-4910 | 1 + hack/bin/integration-test.sh | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 changelog.d/5-internal/WPB-4910 diff --git a/changelog.d/5-internal/WPB-4910 b/changelog.d/5-internal/WPB-4910 new file mode 100644 index 00000000000..4d2155b181c --- /dev/null +++ b/changelog.d/5-internal/WPB-4910 @@ -0,0 +1 @@ +Include timestamp in s3 upload path for test logs diff --git a/hack/bin/integration-test.sh b/hack/bin/integration-test.sh index 841a3172fd8..0667ebeac28 100755 --- a/hack/bin/integration-test.sh +++ b/hack/bin/integration-test.sh @@ -14,7 +14,7 @@ CHART=wire-server tests=(integration stern galley cargohold gundeck federator spar brig) cleanup() { - if (( CLEANUP_LOCAL_FILES > 0 )); then + if ((CLEANUP_LOCAL_FILES > 0)); then for t in "${tests[@]}"; do rm -f "stat-$t" rm -f "logs-$t" @@ -24,11 +24,12 @@ cleanup() { # Copy to the concourse output (indetified by $OUTPUT_DIR) for propagation to # following steps. -copyToAwsS3(){ - if (( UPLOAD_LOGS > 0 )); then +copyToAwsS3() { + build_ts=$(date +%s) + if ((UPLOAD_LOGS > 0)); then for t in "${tests[@]}"; do - echo "Copy logs-$t to s3://wire-server-test-logs/test-logs-$VERSION/$t-$VERSION.log" - aws s3 cp "logs-$t" "s3://wire-server-test-logs/test-logs-$VERSION/$t-$VERSION.log" + echo "Copy logs-$t to s3://wire-server-test-logs/test-logs-$VERSION/$t-$VERSION-$build_ts.log" + aws s3 cp "logs-$t" "s3://wire-server-test-logs/test-logs-$VERSION/$t-$VERSION-$build_ts.log" done fi } @@ -89,7 +90,7 @@ if ((exit_code > 0)); then x=$(cat "stat-$t") if ((x > 0)); then echo "=== (relevant) logs for failed $t-integration ===" - "$DIR/integration-logs-relevant-bits.sh" < "logs-$t" + "$DIR/integration-logs-relevant-bits.sh" <"logs-$t" fi done summary From 5f246fbbb96a6e5cdfeaae0e89e9227d6c1d4e62 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 2 Oct 2023 08:54:04 +0200 Subject: [PATCH 161/225] [WPB-664] Servantify brig Provider API (#3547) * provider register * swagger * linter * provider activate * account approval is not needed * login * removed redundant apitags * provider begin password reset * complete password reset * wip * delete account * provider update * provider update email * update provider email * get provider and provider profile * require user id, but ignore it * changelog --- changelog.d/5-internal/WPB-664 | 1 + libs/wire-api/src/Wire/API/Error/Brig.hs | 9 + libs/wire-api/src/Wire/API/Provider.hs | 188 ++++++------- .../src/Wire/API/Routes/Public/Brig.hs | 2 + .../src/Wire/API/Routes/Public/Brig/Bot.hs | 6 - .../Wire/API/Routes/Public/Brig/Provider.hs | 183 +++++++++++++ libs/wire-api/src/Wire/API/User/Auth.hs | 61 ++++- libs/wire-api/wire-api.cabal | 1 + services/brig/src/Brig/API/Error.hs | 3 - services/brig/src/Brig/API/Public.hs | 5 +- services/brig/src/Brig/Provider/API.hs | 248 ++++-------------- 11 files changed, 402 insertions(+), 305 deletions(-) create mode 100644 changelog.d/5-internal/WPB-664 create mode 100644 libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs diff --git a/changelog.d/5-internal/WPB-664 b/changelog.d/5-internal/WPB-664 new file mode 100644 index 00000000000..764b4019042 --- /dev/null +++ b/changelog.d/5-internal/WPB-664 @@ -0,0 +1 @@ +Provider API has been migrated to servant diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 85a280b171c..a77416c7921 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -86,10 +86,19 @@ data BrigError | TooManyConversationMembers | ServiceDisabled | InvalidBot + | VerificationCodeThrottled + | InvalidProvider + | ProviderNotFound instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: BrigError) where addToOpenApi = addStaticErrorToSwagger @(MapError e) +type instance MapError 'ProviderNotFound = 'StaticError 404 "not-found" "Provider not found." + +type instance MapError 'InvalidProvider = 'StaticError 403 "invalid-provider" "The provider does not exist." + +type instance MapError 'VerificationCodeThrottled = 'StaticError 429 "too-many-requests" "Too many request to generate a verification code." + type instance MapError 'ServiceDisabled = 'StaticError 403 "service-disabled" "The desired service is currently disabled." type instance MapError 'InvalidBot = 'StaticError 403 "invalid-bot" "The targeted user is not a bot." diff --git a/libs/wire-api/src/Wire/API/Provider.hs b/libs/wire-api/src/Wire/API/Provider.hs index 99b58238b2d..b4efe355c5e 100644 --- a/libs/wire-api/src/Wire/API/Provider.hs +++ b/libs/wire-api/src/Wire/API/Provider.hs @@ -1,6 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. -- @@ -52,12 +51,12 @@ module Wire.API.Provider ) where -import Data.Aeson -import Data.Aeson.TH +import Data.Aeson qualified as A import Data.Id -import Data.Json.Util import Data.Misc (HttpsUrl (..), PlainTextPassword6, PlainTextPassword8) import Data.Range +import Data.Schema +import Data.Swagger qualified as S import Imports import Wire.API.Conversation.Code as Code import Wire.API.Provider.Service (ServiceToken (..)) @@ -79,32 +78,24 @@ data Provider = Provider } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform Provider) - -instance ToJSON Provider where - toJSON p = - object $ - "id" .= providerId p - # "name" .= providerName p - # "email" .= providerEmail p - # "url" .= providerUrl p - # "description" .= providerDescr p - # [] - -instance FromJSON Provider where - parseJSON = withObject "Provider" $ \o -> - Provider - <$> o .: "id" - <*> o .: "name" - <*> o .: "email" - <*> o .: "url" - <*> o .: "description" + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema Provider + +instance ToSchema Provider where + schema = + object "Provider" $ + Provider + <$> providerId .= field "id" schema + <*> providerName .= field "name" schema + <*> providerEmail .= field "email" schema + <*> providerUrl .= field "url" schema + <*> providerDescr .= field "description" schema -- | A provider profile as seen by regular users. -- Note: This is a placeholder that may evolve to contain only a subset of -- the full provider information. newtype ProviderProfile = ProviderProfile Provider deriving stock (Eq, Show) - deriving newtype (FromJSON, ToJSON, Arbitrary) + deriving newtype (A.FromJSON, A.ToJSON, Arbitrary, S.ToSchema) -------------------------------------------------------------------------------- -- NewProvider @@ -120,25 +111,17 @@ data NewProvider = NewProvider } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewProvider) - -instance ToJSON NewProvider where - toJSON p = - object $ - "name" .= newProviderName p - # "email" .= newProviderEmail p - # "url" .= newProviderUrl p - # "description" .= newProviderDescr p - # "password" .= newProviderPassword p - # [] - -instance FromJSON NewProvider where - parseJSON = withObject "NewProvider" $ \o -> - NewProvider - <$> o .: "name" - <*> o .: "email" - <*> o .: "url" - <*> o .: "description" - <*> o .:? "password" + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema NewProvider + +instance ToSchema NewProvider where + schema = + object "NewProvider" $ + NewProvider + <$> newProviderName .= field "name" schema + <*> newProviderEmail .= field "email" schema + <*> newProviderUrl .= field "url" schema + <*> newProviderDescr .= field "description" schema + <*> newProviderPassword .= maybe_ (optField "password" schema) -- | Response data upon registering a new provider. data NewProviderResponse = NewProviderResponse @@ -149,19 +132,14 @@ data NewProviderResponse = NewProviderResponse } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewProviderResponse) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema NewProviderResponse -instance ToJSON NewProviderResponse where - toJSON r = - object $ - "id" .= rsNewProviderId r - # "password" .= rsNewProviderPassword r - # [] - -instance FromJSON NewProviderResponse where - parseJSON = withObject "NewProviderResponse" $ \o -> - NewProviderResponse - <$> o .: "id" - <*> o .:? "password" +instance ToSchema NewProviderResponse where + schema = + object "NewProviderResponse" $ + NewProviderResponse + <$> rsNewProviderId .= field "id" schema + <*> rsNewProviderPassword .= maybe_ (optField "password" schema) -------------------------------------------------------------------------------- -- UpdateProvider @@ -174,21 +152,15 @@ data UpdateProvider = UpdateProvider } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UpdateProvider) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema UpdateProvider -instance ToJSON UpdateProvider where - toJSON p = - object $ - "name" .= updateProviderName p - # "url" .= updateProviderUrl p - # "description" .= updateProviderDescr p - # [] - -instance FromJSON UpdateProvider where - parseJSON = withObject "UpdateProvider" $ \o -> - UpdateProvider - <$> o .:? "name" - <*> o .:? "url" - <*> o .:? "description" +instance ToSchema UpdateProvider where + schema = + object "UpdateProvider" $ + UpdateProvider + <$> updateProviderName .= maybe_ (optField "name" schema) + <*> updateProviderUrl .= maybe_ (optField "url" schema) + <*> updateProviderDescr .= maybe_ (optField "description" schema) -------------------------------------------------------------------------------- -- ProviderActivationResponse @@ -199,14 +171,13 @@ newtype ProviderActivationResponse = ProviderActivationResponse {activatedProviderIdentity :: Email} deriving stock (Eq, Show) deriving newtype (Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema ProviderActivationResponse -instance ToJSON ProviderActivationResponse where - toJSON (ProviderActivationResponse e) = - object ["email" .= e] - -instance FromJSON ProviderActivationResponse where - parseJSON = withObject "ProviderActivationResponse" $ \o -> - ProviderActivationResponse <$> o .: "email" +instance ToSchema ProviderActivationResponse where + schema = + object "ProviderActivationResponse" $ + ProviderActivationResponse + <$> activatedProviderIdentity .= field "email" schema -------------------------------------------------------------------------------- -- ProviderLogin @@ -218,19 +189,14 @@ data ProviderLogin = ProviderLogin } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ProviderLogin) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema ProviderLogin -instance ToJSON ProviderLogin where - toJSON l = - object - [ "email" .= providerLoginEmail l, - "password" .= providerLoginPassword l - ] - -instance FromJSON ProviderLogin where - parseJSON = withObject "ProviderLogin" $ \o -> - ProviderLogin - <$> o .: "email" - <*> o .: "password" +instance ToSchema ProviderLogin where + schema = + object "ProviderLogin" $ + ProviderLogin + <$> providerLoginEmail .= field "email" schema + <*> providerLoginPassword .= field "password" schema -------------------------------------------------------------------------------- -- DeleteProvider @@ -240,16 +206,13 @@ newtype DeleteProvider = DeleteProvider {deleteProviderPassword :: PlainTextPassword6} deriving stock (Eq, Show) deriving newtype (Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema DeleteProvider -instance ToJSON DeleteProvider where - toJSON d = - object - [ "password" .= deleteProviderPassword d - ] - -instance FromJSON DeleteProvider where - parseJSON = withObject "DeleteProvider" $ \o -> - DeleteProvider <$> o .: "password" +instance ToSchema DeleteProvider where + schema = + object "DeleteProvider" $ + DeleteProvider + <$> deleteProviderPassword .= field "password" schema -------------------------------------------------------------------------------- -- Password Change/Reset @@ -258,8 +221,13 @@ instance FromJSON DeleteProvider where newtype PasswordReset = PasswordReset {email :: Email} deriving stock (Eq, Show) deriving newtype (Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema PasswordReset -deriveJSON toJSONFieldName ''PasswordReset +instance ToSchema PasswordReset where + schema = + object "PasswordReset" $ + PasswordReset + <$> (.email) .= field "email" schema -- | The payload for completing a password reset. data CompletePasswordReset = CompletePasswordReset @@ -269,8 +237,15 @@ data CompletePasswordReset = CompletePasswordReset } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform CompletePasswordReset) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema CompletePasswordReset -deriveJSON toJSONFieldName ''CompletePasswordReset +instance ToSchema CompletePasswordReset where + schema = + object "CompletePasswordReset" $ + CompletePasswordReset + <$> key .= field "key" schema + <*> (.code) .= field "code" schema + <*> (.password) .= field "password" schema -- | The payload for changing a password. data PasswordChange = PasswordChange @@ -279,12 +254,23 @@ data PasswordChange = PasswordChange } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform PasswordChange) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema PasswordChange -deriveJSON toJSONFieldName ''PasswordChange +instance ToSchema PasswordChange where + schema = + object "PasswordChange" $ + PasswordChange + <$> oldPassword .= field "old_password" schema + <*> newPassword .= field "new_password" schema -- | The payload for updating an email address newtype EmailUpdate = EmailUpdate {email :: Email} deriving stock (Eq, Show, Generic) deriving newtype (Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema EmailUpdate -deriveJSON toJSONFieldName ''EmailUpdate +instance ToSchema EmailUpdate where + schema = + object "EmailUpdate" $ + EmailUpdate + <$> (.email) .= field "email" schema diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 42080cbd4b9..0c620aea113 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -58,6 +58,7 @@ import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.Routes.Public.Brig.Bot (BotAPI) import Wire.API.Routes.Public.Brig.OAuth (OAuthAPI) +import Wire.API.Routes.Public.Brig.Provider (ProviderAPI) import Wire.API.Routes.Public.Util import Wire.API.Routes.QualifiedCapture import Wire.API.Routes.Version @@ -93,6 +94,7 @@ type BrigAPI = :<|> SystemSettingsAPI :<|> OAuthAPI :<|> BotAPI + :<|> ProviderAPI data BrigAPITag diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs index 7e3259d8fca..b7eba29b037 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs @@ -27,7 +27,6 @@ import Wire.API.Conversation.Bot import Wire.API.Error (CanThrow, ErrorResponse) import Wire.API.Error.Brig (BrigError (..)) import Wire.API.Provider.Bot (BotUserView) -import Wire.API.Routes.API (ServiceAPI (..)) import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named (..)) import Wire.API.Routes.Public @@ -161,8 +160,3 @@ type BotAPI = :> "clients" :> Get '[JSON] [PubClient] ) - -data BotAPITag - -instance ServiceAPI BotAPITag v where - type ServiceAPIRoutes BotAPITag = BotAPI diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs new file mode 100644 index 00000000000..21d12c2ff8d --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs @@ -0,0 +1,183 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.Routes.Public.Brig.Provider where + +import Data.Code qualified as Code +import Data.Id (ProviderId) +import Imports +import Servant (JSON) +import Servant hiding (Handler, JSON, Tagged, addHeader, respond) +import Servant.Swagger.Internal.Orphans () +import Wire.API.Error +import Wire.API.Error.Brig +import Wire.API.Provider +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named (Named (..)) +import Wire.API.Routes.Public +import Wire.API.User.Auth + +type ActivateResponses = + '[ RespondEmpty 204 "", + Respond 200 "" ProviderActivationResponse + ] + +type GetProviderResponses = + '[ ErrorResponse 'ProviderNotFound, + Respond 200 "" Provider + ] + +type GetProviderProfileResponses = + '[ ErrorResponse 'ProviderNotFound, + Respond 200 "" ProviderProfile + ] + +type ProviderAPI = + Named + "provider-register" + ( Summary "Register a new provider" + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidEmail + :> CanThrow 'VerificationCodeThrottled + :> "provider" + :> "register" + :> ReqBody '[JSON] NewProvider + :> MultiVerb1 'POST '[JSON] (Respond 201 "" NewProviderResponse) + ) + :<|> Named + "provider-activate" + ( Summary "Activate a provider" + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidCode + :> "provider" + :> "activate" + :> QueryParam' '[Required, Strict] "key" Code.Key + :> QueryParam' '[Required, Strict] "code" Code.Value + :> MultiVerb 'GET '[JSON] ActivateResponses (Maybe ProviderActivationResponse) + ) + :<|> Named + "provider-login" + ( Summary "Login as a provider" + :> CanThrow 'AccessDenied + :> CanThrow 'BadCredentials + :> "provider" + :> "login" + :> ReqBody '[JSON] ProviderLogin + :> MultiVerb + 'POST + '[JSON] + '[ WithHeaders + '[ Header "Set-Cookie" ProviderTokenCookie + ] + ProviderTokenCookie + (RespondEmpty 200 "OK") + ] + ProviderTokenCookie + ) + :<|> Named + "provider-password-reset" + ( Summary "Begin a password reset" + :> CanThrow 'AccessDenied + :> CanThrow 'BadCredentials + :> CanThrow 'InvalidPasswordResetKey + :> CanThrow 'InvalidPasswordResetCode + :> CanThrow 'PasswordResetInProgress + :> CanThrow 'PasswordResetInProgress + :> CanThrow 'ResetPasswordMustDiffer + :> CanThrow 'VerificationCodeThrottled + :> "provider" + :> "password-reset" + :> ReqBody '[JSON] PasswordReset + :> MultiVerb1 'POST '[JSON] (RespondEmpty 201 "") + ) + :<|> Named + "provider-password-reset-complete" + ( Summary "Complete a password reset" + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidCode + :> CanThrow 'ResetPasswordMustDiffer + :> CanThrow 'BadCredentials + :> CanThrow 'InvalidPasswordResetCode + :> "provider" + :> "password-reset" + :> "complete" + :> ReqBody '[JSON] CompletePasswordReset + :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "") + ) + :<|> Named + "provider-delete" + ( Summary "Delete a provider" + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidProvider + :> CanThrow 'BadCredentials + :> ZProvider + :> "provider" + :> ReqBody '[JSON] DeleteProvider + :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 200 "") + ) + :<|> Named + "provider-update" + ( Summary "Update a provider" + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidProvider + :> ZProvider + :> "provider" + :> ReqBody '[JSON] UpdateProvider + :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "") + ) + :<|> Named + "provider-update-email" + ( Summary "Update a provider email" + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidEmail + :> CanThrow 'InvalidProvider + :> CanThrow 'VerificationCodeThrottled + :> ZProvider + :> "provider" + :> "email" + :> ReqBody '[JSON] EmailUpdate + :> MultiVerb1 'PUT '[JSON] (RespondEmpty 202 "") + ) + :<|> Named + "provider-update-password" + ( Summary "Update a provider password" + :> CanThrow 'AccessDenied + :> CanThrow 'BadCredentials + :> CanThrow 'ResetPasswordMustDiffer + :> ZProvider + :> "provider" + :> "password" + :> ReqBody '[JSON] PasswordChange + :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "") + ) + :<|> Named + "provider-get-account" + ( Summary "Get account" + :> CanThrow 'AccessDenied + :> CanThrow 'ProviderNotFound + :> ZProvider + :> "provider" + :> MultiVerb 'GET '[JSON] GetProviderResponses (Maybe Provider) + ) + :<|> Named + "provider-get-profile" + ( Summary "Get profile" + :> ZUser + :> "providers" + :> Capture "pid" ProviderId + :> MultiVerb 'GET '[JSON] GetProviderProfileResponses (Maybe ProviderProfile) + ) diff --git a/libs/wire-api/src/Wire/API/User/Auth.hs b/libs/wire-api/src/Wire/API/User/Auth.hs index df15827e2e2..2f11fb202be 100644 --- a/libs/wire-api/src/Wire/API/User/Auth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth.hs @@ -46,6 +46,8 @@ module Wire.API.User.Auth SomeUserToken (..), SomeAccessToken (..), UserTokenCookie (..), + ProviderToken (..), + ProviderTokenCookie (..), -- * Access AccessWithCookie (..), @@ -58,7 +60,7 @@ module Wire.API.User.Auth where import Control.Applicative -import Control.Lens ((?~)) +import Control.Lens ((?~), (^.)) import Control.Lens.TH import Data.Aeson (FromJSON, ToJSON) import Data.Aeson.Types qualified as A @@ -78,7 +80,9 @@ import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Text.Lazy.Encoding qualified as LT import Data.Time.Clock (UTCTime) +import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Data.Tuple.Extra hiding (first) +import Data.ZAuth.Token (header, time) import Data.ZAuth.Token qualified as ZAuth import Imports import Servant @@ -568,6 +572,61 @@ instance ToHttpApiData UserTokenCookie where . utcToSetCookie toUrlPiece = T.decodeUtf8 . toHeader +-------------------------------------------------------------------------------- +-- Provider + +data ProviderToken = ProviderToken (ZAuth.Token ZAuth.Provider) + deriving (Show) + +instance FromByteString ProviderToken where + parser = ProviderToken <$> parser + +data ProviderTokenCookie = ProviderTokenCookie + { ptcToken :: ProviderToken, + ptcSecure :: Bool + } + +instance FromHttpApiData ProviderTokenCookie where + parseHeader = ptcFromSetCookie . parseSetCookie + parseUrlPiece = parseHeader . T.encodeUtf8 + +ptcFromSetCookie :: SetCookie -> Either Text ProviderTokenCookie +ptcFromSetCookie c = do + v <- first T.pack $ runParser parser (setCookieValue c) + pure + ProviderTokenCookie + { ptcToken = v, + ptcSecure = setCookieSecure c + } + +instance ToHttpApiData ProviderTokenCookie where + toHeader = + LBS.toStrict + . toLazyByteString + . renderSetCookie + . ptcToSetCookie + toUrlPiece = T.decodeUtf8 . toHeader + +ptcToSetCookie :: ProviderTokenCookie -> SetCookie +ptcToSetCookie c = + def + { setCookieName = "zprovider", + setCookieValue = toByteString' (providerToken (ptcToken c)), + setCookiePath = Just "/provider", + setCookieExpires = Just (tokenExpiresUTC (providerToken (ptcToken c))), + setCookieSecure = ptcSecure c, + setCookieHttpOnly = True + } + where + providerToken :: ProviderToken -> ZAuth.Token ZAuth.Provider + providerToken (ProviderToken t) = t + + tokenExpiresUTC :: ZAuth.Token a -> UTCTime + tokenExpiresUTC t = posixSecondsToUTCTime (fromIntegral (t ^. header . time)) + +instance S.ToParamSchema ProviderTokenCookie where + toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + -------------------------------------------------------------------------------- -- Servant diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index b7d3af68205..fde861ec6d0 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -106,6 +106,7 @@ library Wire.API.Routes.Public.Brig Wire.API.Routes.Public.Brig.Bot Wire.API.Routes.Public.Brig.OAuth + Wire.API.Routes.Public.Brig.Provider Wire.API.Routes.Public.Cannon Wire.API.Routes.Public.Cargohold Wire.API.Routes.Public.Galley diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index cf2489f5f2f..376047ba874 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -297,9 +297,6 @@ loginCodePending = Wai.mkError status403 "pending-login" "A login code is still loginCodeNotFound :: Wai.Error loginCodeNotFound = Wai.mkError status404 "no-pending-login" "No login code was found." -newPasswordMustDiffer :: Wai.Error -newPasswordMustDiffer = Wai.mkError status409 "password-must-differ" "For provider password change or reset, new and old password must be different." - notFound :: LText -> Wai.Error notFound = Wai.mkError status404 "not-found" diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 95d22a8a3e5..0289be453d3 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -55,7 +55,7 @@ import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Options hiding (internalEvents, sesQueue) -import Brig.Provider.API (botAPI) +import Brig.Provider.API import Brig.Provider.API qualified as Provider import Brig.Team.API qualified as Team import Brig.Team.Email qualified as Team @@ -123,7 +123,6 @@ import Wire.API.Routes.Internal.Spar qualified as SparInternalAPI import Wire.API.Routes.MultiTablePaging qualified as Public import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig -import Wire.API.Routes.Public.Brig.Bot import Wire.API.Routes.Public.Brig.OAuth import Wire.API.Routes.Public.Cannon import Wire.API.Routes.Public.Cargohold @@ -180,7 +179,6 @@ versionedSwaggerDocsAPI (Just (VersionNumber V5)) = <> serviceSwagger @GundeckAPITag @'V5 <> serviceSwagger @ProxyAPITag @'V5 <> serviceSwagger @OAuthAPITag @'V5 - <> serviceSwagger @BotAPITag @'V5 ) & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger.md") @@ -273,6 +271,7 @@ servantSitemap = :<|> systemSettingsAPI :<|> oauthAPI :<|> botAPI + :<|> providerAPI where userAPI :: ServerT UserAPI (Handler r) userAPI = diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index a836861f8f5..ce85474b235 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -20,6 +20,7 @@ module Brig.Provider.API routesPublic, routesInternal, botAPI, + providerAPI, -- * Event handlers finishDeleteService, @@ -76,7 +77,6 @@ import Data.Range import Data.Set qualified as Set import Data.Text.Ascii qualified as Ascii import Data.Text.Encoding qualified as Text -import Data.ZAuth.Token qualified as ZAuth import GHC.TypeNats import Imports import Network.HTTP.Types.Status @@ -86,7 +86,7 @@ import Network.Wai.Routing import Network.Wai.Utilities.Error ((!>>)) import Network.Wai.Utilities.Error qualified as Wai import Network.Wai.Utilities.Request (JsonRequest, jsonRequest) -import Network.Wai.Utilities.Response (addHeader, empty, json, setStatus) +import Network.Wai.Utilities.Response (empty, json, setStatus) import Network.Wai.Utilities.ZAuth import OpenSSL.EVP.Digest qualified as SSL import OpenSSL.EVP.PKey qualified as SSL @@ -98,7 +98,6 @@ import Servant (ServerT, (:<|>) (..)) import Ssl.Util qualified as SSL import System.Logger.Class (MonadLogger) import UnliftIO.Async (pooledMapConcurrentlyN_) -import Web.Cookie qualified as Cookie import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Bot import Wire.API.Conversation.Bot qualified as Public @@ -118,11 +117,13 @@ import Wire.API.Provider.Service qualified as Public import Wire.API.Provider.Service.Tag qualified as Public import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig.Bot (BotAPI) +import Wire.API.Routes.Public.Brig.Provider (ProviderAPI) import Wire.API.Team.Feature qualified as Feature import Wire.API.Team.LegalHold (LegalholdProtectee (UnprotectedBot)) import Wire.API.Team.Permission import Wire.API.User hiding (cpNewPassword, cpOldPassword) import Wire.API.User qualified as Public (UserProfile, publicProfile) +import Wire.API.User.Auth import Wire.API.User.Client import Wire.API.User.Client qualified as Public (Client, ClientCapability (ClientSupportsLegalholdImplicitConsent), PubClient (..), UserClientPrekeyMap, UserClients, userClients) import Wire.API.User.Client.Prekey qualified as Public (PrekeyId) @@ -146,66 +147,27 @@ botAPI = :<|> Named @"bot-list-users" botListUserProfiles :<|> Named @"bot-get-user-clients" botGetUserClients +providerAPI :: Member GalleyProvider r => ServerT ProviderAPI (Handler r) +providerAPI = + Named @"provider-register" newAccount + :<|> Named @"provider-activate" activateAccountKey + :<|> Named @"provider-login" login + :<|> Named @"provider-password-reset" beginPasswordReset + :<|> Named @"provider-password-reset-complete" completePasswordReset + :<|> Named @"provider-delete" deleteAccount + :<|> Named @"provider-update" updateAccountProfile + :<|> Named @"provider-update-email" updateAccountEmail + :<|> Named @"provider-update-password" updateAccountPassword + :<|> Named @"provider-get-account" getAccount + :<|> Named @"provider-get-profile" getProviderProfile + routesPublic :: ( Member GalleyProvider r ) => Routes () (Handler r) () routesPublic = do - -- Public API (Unauthenticated) -------------------------------------------- - - post "/provider/register" (continue newAccountH) $ - accept "application" "json" - .&> jsonRequest @Public.NewProvider - - get "/provider/activate" (continue activateAccountKeyH) $ - accept "application" "json" - .&> query "key" - .&. query "code" - - get "/provider/approve" (continue approveAccountKeyH) $ - accept "application" "json" - .&> query "key" - .&. query "code" - - post "/provider/login" (continue loginH) $ - jsonRequest @Public.ProviderLogin - - post "/provider/password-reset" (continue beginPasswordResetH) $ - accept "application" "json" - .&> jsonRequest @Public.PasswordReset - - post "/provider/password-reset/complete" (continue completePasswordResetH) $ - accept "application" "json" - .&> jsonRequest @Public.CompletePasswordReset - -- Provider API ------------------------------------------------------------ - delete "/provider" (continue deleteAccountH) $ - zauth ZAuthProvider - .&> zauthProviderId - .&. jsonRequest @Public.DeleteProvider - - put "/provider" (continue updateAccountProfileH) $ - accept "application" "json" - .&> zauth ZAuthProvider - .&> zauthProviderId - .&. jsonRequest @Public.UpdateProvider - - put "/provider/email" (continue updateAccountEmailH) $ - zauth ZAuthProvider - .&> zauthProviderId - .&. jsonRequest @Public.EmailUpdate - - put "/provider/password" (continue updateAccountPasswordH) $ - zauth ZAuthProvider - .&> zauthProviderId - .&. jsonRequest @Public.PasswordChange - - get "/provider" (continue getAccountH) $ - accept "application" "json" - .&> zauth ZAuthProvider - .&> zauthProviderId - post "/provider/services" (continue addServiceH) $ accept "application" "json" .&> zauth ZAuthProvider @@ -248,11 +210,6 @@ routesPublic = do -- User API ---------------------------------------------------------------- - get "/providers/:pid" (continue getProviderProfileH) $ - accept "application" "json" - .&> zauth ZAuthAccess - .&> capture "pid" - get "/providers/:pid/services" (continue listServiceProfilesH) $ accept "application" "json" .&> zauth ZAuthAccess @@ -300,13 +257,9 @@ routesInternal = do -------------------------------------------------------------------------------- -- Public API (Unauthenticated) -newAccountH :: Member GalleyProvider r => JsonRequest Public.NewProvider -> (Handler r) Response -newAccountH req = do - guardSecondFactorDisabled Nothing - setStatus status201 . json <$> (newAccount =<< parseJsonBody req) - -newAccount :: Public.NewProvider -> (Handler r) Public.NewProviderResponse +newAccount :: Member GalleyProvider r => Public.NewProvider -> (Handler r) Public.NewProviderResponse newAccount new = do + guardSecondFactorDisabled Nothing email <- case validateEmail (Public.newProviderEmail new) of Right em -> pure em Left _ -> throwStd (errorToWai @'E.InvalidEmail) @@ -337,13 +290,9 @@ newAccount new = do lift $ sendActivationMail name email key val False pure $ Public.NewProviderResponse pid newPass -activateAccountKeyH :: Member GalleyProvider r => Code.Key ::: Code.Value -> (Handler r) Response -activateAccountKeyH (key ::: val) = do - guardSecondFactorDisabled Nothing - maybe (setStatus status204 empty) json <$> activateAccountKey key val - -activateAccountKey :: Code.Key -> Code.Value -> (Handler r) (Maybe Public.ProviderActivationResponse) +activateAccountKey :: Member GalleyProvider r => Code.Key -> Code.Value -> (Handler r) (Maybe Public.ProviderActivationResponse) activateAccountKey key val = do + guardSecondFactorDisabled Nothing c <- wrapClientE (Code.verify key Code.IdentityVerification val) >>= maybeInvalidCode (pid, email) <- case (Code.codeAccount c, Code.codeForEmail c) of (Just p, Just e) -> pure (Id p, e) @@ -385,42 +334,20 @@ instance ToJSON FoundActivationCode where toJSON $ Code.KeyValuePair (Code.codeKey vcode) (Code.codeValue vcode) -approveAccountKeyH :: Member GalleyProvider r => Code.Key ::: Code.Value -> (Handler r) Response -approveAccountKeyH (key ::: val) = do - guardSecondFactorDisabled Nothing - empty <$ approveAccountKey key val - -approveAccountKey :: Code.Key -> Code.Value -> (Handler r) () -approveAccountKey key val = do - c <- wrapClientE (Code.verify key Code.AccountApproval val) >>= maybeInvalidCode - case (Code.codeAccount c, Code.codeForEmail c) of - (Just pid, Just email) -> do - (name, _, _, _) <- wrapClientE (DB.lookupAccountData (Id pid)) >>= maybeInvalidCode - activate (Id pid) Nothing email - lift $ sendApprovalConfirmMail name email - _ -> throwStd (errorToWai @'E.InvalidCode) - -loginH :: Member GalleyProvider r => JsonRequest Public.ProviderLogin -> (Handler r) Response -loginH req = do - guardSecondFactorDisabled Nothing - tok <- login =<< parseJsonBody req - setProviderCookie tok empty - -login :: Public.ProviderLogin -> Handler r (ZAuth.Token ZAuth.Provider) +login :: Member GalleyProvider r => ProviderLogin -> Handler r ProviderTokenCookie login l = do + guardSecondFactorDisabled Nothing pid <- wrapClientE (DB.lookupKey (mkEmailKey (providerLoginEmail l))) >>= maybeBadCredentials pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials unless (verifyPassword (providerLoginPassword l) pass) $ throwStd (errorToWai @'E.BadCredentials) - ZAuth.newProviderToken pid - -beginPasswordResetH :: Member GalleyProvider r => JsonRequest Public.PasswordReset -> (Handler r) Response -beginPasswordResetH req = do - guardSecondFactorDisabled Nothing - setStatus status201 empty <$ (beginPasswordReset =<< parseJsonBody req) + token <- ZAuth.newProviderToken pid + s <- view settings + pure $ ProviderTokenCookie (ProviderToken token) (not (setCookieInsecure s)) -beginPasswordReset :: Public.PasswordReset -> (Handler r) () +beginPasswordReset :: Member GalleyProvider r => Public.PasswordReset -> (Handler r) () beginPasswordReset (Public.PasswordReset target) = do + guardSecondFactorDisabled Nothing pid <- wrapClientE (DB.lookupKey (mkEmailKey target)) >>= maybeBadCredentials gen <- Code.mkGen (Code.ForEmail target) pending <- lift . wrapClient $ Code.lookup (Code.genKey gen) Code.PasswordReset @@ -436,20 +363,16 @@ beginPasswordReset (Public.PasswordReset target) = do tryInsertVerificationCode code $ verificationCodeThrottledError . VerificationCodeThrottled lift $ sendPasswordResetMail target (Code.codeKey code) (Code.codeValue code) -completePasswordResetH :: Member GalleyProvider r => JsonRequest Public.CompletePasswordReset -> (Handler r) Response -completePasswordResetH req = do - guardSecondFactorDisabled Nothing - empty <$ (completePasswordReset =<< parseJsonBody req) - -completePasswordReset :: Public.CompletePasswordReset -> (Handler r) () +completePasswordReset :: Member GalleyProvider r => Public.CompletePasswordReset -> (Handler r) () completePasswordReset (Public.CompletePasswordReset key val newpwd) = do + guardSecondFactorDisabled Nothing code <- wrapClientE (Code.verify key Code.PasswordReset val) >>= maybeInvalidCode case Id <$> Code.codeAccount code of - Nothing -> throwE $ pwResetError InvalidPasswordResetCode + Nothing -> throwStd (errorToWai @'E.InvalidPasswordResetCode) Just pid -> do oldpass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials when (verifyPassword newpwd oldpass) $ do - throwStd newPasswordMustDiffer + throwStd (errorToWai @'E.ResetPasswordMustDiffer) wrapClientE $ do DB.updateAccountPassword pid newpwd Code.delete key Code.PasswordReset @@ -457,23 +380,14 @@ completePasswordReset (Public.CompletePasswordReset key val newpwd) = do -------------------------------------------------------------------------------- -- Provider API -getAccountH :: Member GalleyProvider r => ProviderId -> (Handler r) Response -getAccountH pid = do - guardSecondFactorDisabled Nothing - getAccount pid <&> \case - Just p -> json p - Nothing -> setStatus status404 empty - -getAccount :: ProviderId -> (Handler r) (Maybe Public.Provider) -getAccount = wrapClientE . DB.lookupAccount - -updateAccountProfileH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.UpdateProvider -> (Handler r) Response -updateAccountProfileH (pid ::: req) = do +getAccount :: Member GalleyProvider r => ProviderId -> (Handler r) (Maybe Public.Provider) +getAccount pid = do guardSecondFactorDisabled Nothing - empty <$ (updateAccountProfile pid =<< parseJsonBody req) + wrapClientE $ DB.lookupAccount pid -updateAccountProfile :: ProviderId -> Public.UpdateProvider -> (Handler r) () +updateAccountProfile :: Member GalleyProvider r => ProviderId -> Public.UpdateProvider -> (Handler r) () updateAccountProfile pid upd = do + guardSecondFactorDisabled Nothing _ <- wrapClientE (DB.lookupAccount pid) >>= maybeInvalidProvider wrapClientE $ DB.updateAccountProfile @@ -482,13 +396,9 @@ updateAccountProfile pid upd = do (updateProviderUrl upd) (updateProviderDescr upd) -updateAccountEmailH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.EmailUpdate -> (Handler r) Response -updateAccountEmailH (pid ::: req) = do - guardSecondFactorDisabled Nothing - setStatus status202 empty <$ (updateAccountEmail pid =<< parseJsonBody req) - -updateAccountEmail :: ProviderId -> Public.EmailUpdate -> (Handler r) () +updateAccountEmail :: Member GalleyProvider r => ProviderId -> Public.EmailUpdate -> (Handler r) () updateAccountEmail pid (Public.EmailUpdate new) = do + guardSecondFactorDisabled Nothing email <- case validateEmail new of Right em -> pure em Left _ -> throwStd (errorToWai @'E.InvalidEmail) @@ -505,18 +415,14 @@ updateAccountEmail pid (Public.EmailUpdate new) = do tryInsertVerificationCode code $ verificationCodeThrottledError . VerificationCodeThrottled lift $ sendActivationMail (Name "name") email (Code.codeKey code) (Code.codeValue code) True -updateAccountPasswordH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.PasswordChange -> (Handler r) Response -updateAccountPasswordH (pid ::: req) = do - guardSecondFactorDisabled Nothing - empty <$ (updateAccountPassword pid =<< parseJsonBody req) - -updateAccountPassword :: ProviderId -> Public.PasswordChange -> (Handler r) () +updateAccountPassword :: Member GalleyProvider r => ProviderId -> Public.PasswordChange -> (Handler r) () updateAccountPassword pid upd = do + guardSecondFactorDisabled Nothing pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials unless (verifyPassword (oldPassword upd) pass) $ throwStd (errorToWai @'E.BadCredentials) when (verifyPassword (newPassword upd) pass) $ - throwStd newPasswordMustDiffer + throwStd (errorToWai @'E.ResetPasswordMustDiffer) wrapClientE $ DB.updateAccountPassword pid (newPassword upd) addServiceH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.NewService -> (Handler r) Response @@ -683,56 +589,35 @@ finishDeleteService pid sid = do where kick (bid, cid, _) = deleteBot (botUserId bid) Nothing bid cid -deleteAccountH :: - Member GalleyProvider r => - ProviderId ::: JsonRequest Public.DeleteProvider -> - ExceptT Error (AppT r) Response -deleteAccountH (pid ::: req) = do - guardSecondFactorDisabled Nothing - empty - <$ mapExceptT - wrapHttpClient - ( deleteAccount pid - =<< parseJsonBody req - ) - deleteAccount :: - ( MonadReader Env m, - MonadMask m, - MonadHttp m, - MonadClient m, - HasRequestId m, - MonadLogger m + ( Member GalleyProvider r ) => ProviderId -> Public.DeleteProvider -> - ExceptT Error m () + (Handler r) () deleteAccount pid del = do - prov <- DB.lookupAccount pid >>= maybeInvalidProvider - pass <- DB.lookupPassword pid >>= maybeBadCredentials + guardSecondFactorDisabled Nothing + prov <- wrapClientE (DB.lookupAccount pid) >>= maybeInvalidProvider + pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials unless (verifyPassword (deleteProviderPassword del) pass) $ throwStd (errorToWai @'E.BadCredentials) - svcs <- DB.listServices pid + svcs <- wrapClientE $ DB.listServices pid forM_ svcs $ \svc -> do let sid = serviceId svc let tags = unsafeRange (serviceTags svc) name = serviceName svc - lift $ RPC.removeServiceConn pid sid - DB.deleteService pid sid name tags - DB.deleteKey (mkEmailKey (providerEmail prov)) - DB.deleteAccount pid + lift $ wrapHttpClient $ RPC.removeServiceConn pid sid + wrapClientE $ DB.deleteService pid sid name tags + wrapClientE $ DB.deleteKey (mkEmailKey (providerEmail prov)) + wrapClientE $ DB.deleteAccount pid -------------------------------------------------------------------------------- -- User API -getProviderProfileH :: Member GalleyProvider r => ProviderId -> (Handler r) Response -getProviderProfileH pid = do +getProviderProfile :: Member GalleyProvider r => UserId -> ProviderId -> (Handler r) (Maybe Public.ProviderProfile) +getProviderProfile _ pid = do guardSecondFactorDisabled Nothing - json <$> getProviderProfile pid - -getProviderProfile :: ProviderId -> (Handler r) Public.ProviderProfile -getProviderProfile pid = - wrapClientE (DB.lookupAccountProfile pid) >>= maybeProviderNotFound + wrapClientE (DB.lookupAccountProfile pid) listServiceProfilesH :: Member GalleyProvider r => ProviderId -> (Handler r) Response listServiceProfilesH pid = do @@ -1112,22 +997,6 @@ mkBotUserView u = Ext.botUserViewTeam = userTeam u } -setProviderCookie :: ZAuth.Token ZAuth.Provider -> Response -> (Handler r) Response -setProviderCookie t r = do - s <- view settings - let hdr = toByteString' (Cookie.renderSetCookie (cookie s)) - pure (addHeader "Set-Cookie" hdr r) - where - cookie s = - Cookie.def - { Cookie.setCookieName = "zprovider", - Cookie.setCookieValue = toByteString' t, - Cookie.setCookiePath = Just "/provider", - Cookie.setCookieExpires = Just (ZAuth.tokenExpiresUTC t), - Cookie.setCookieSecure = not (setCookieInsecure s), - Cookie.setCookieHttpOnly = True - } - maybeInvalidProvider :: Monad m => Maybe a -> (ExceptT Error m) a maybeInvalidProvider = maybe (throwStd invalidProvider) pure @@ -1137,9 +1006,6 @@ maybeInvalidCode = maybe (throwStd (errorToWai @'E.InvalidCode)) pure maybeServiceNotFound :: Monad m => Maybe a -> (ExceptT Error m) a maybeServiceNotFound = maybe (throwStd (notFound "Service not found")) pure -maybeProviderNotFound :: Monad m => Maybe a -> (ExceptT Error m) a -maybeProviderNotFound = maybe (throwStd (notFound "Provider not found")) pure - maybeConvNotFound :: Monad m => Maybe a -> (ExceptT Error m) a maybeConvNotFound = maybe (throwStd (notFound "Conversation not found")) pure @@ -1159,7 +1025,7 @@ invalidServiceKey :: Wai.Error invalidServiceKey = Wai.mkError status400 "invalid-service-key" "Invalid service key." invalidProvider :: Wai.Error -invalidProvider = Wai.mkError status403 "invalid-provider" "The provider does not exist." +invalidProvider = errorToWai @'E.InvalidProvider badGateway :: Wai.Error badGateway = Wai.mkError status502 "bad-gateway" "The upstream service returned an invalid response." From 42e182407e364a269c2fa504868734667ff46939 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 2 Oct 2023 13:02:15 +0200 Subject: [PATCH 162/225] wire-api: Fix issues due to swagger2 -> openapi3 migration and concurrent merges (#3627) --- libs/wire-api/src/Wire/API/Provider.hs | 2 +- libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs | 2 +- libs/wire-api/src/Wire/API/User/Auth.hs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Provider.hs b/libs/wire-api/src/Wire/API/Provider.hs index b4efe355c5e..1fc5c34c114 100644 --- a/libs/wire-api/src/Wire/API/Provider.hs +++ b/libs/wire-api/src/Wire/API/Provider.hs @@ -54,9 +54,9 @@ where import Data.Aeson qualified as A import Data.Id import Data.Misc (HttpsUrl (..), PlainTextPassword6, PlainTextPassword8) +import Data.OpenApi qualified as S import Data.Range import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.Conversation.Code as Code import Wire.API.Provider.Service (ServiceToken (..)) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs index 21d12c2ff8d..b1b6310dfe4 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs @@ -22,7 +22,7 @@ import Data.Id (ProviderId) import Imports import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Provider diff --git a/libs/wire-api/src/Wire/API/User/Auth.hs b/libs/wire-api/src/Wire/API/User/Auth.hs index 2f11fb202be..135c1cb89ba 100644 --- a/libs/wire-api/src/Wire/API/User/Auth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth.hs @@ -625,7 +625,7 @@ ptcToSetCookie c = tokenExpiresUTC t = posixSecondsToUTCTime (fromIntegral (t ^. header . time)) instance S.ToParamSchema ProviderTokenCookie where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString -------------------------------------------------------------------------------- -- Servant From 0f4623a6df7e9125177b11668e10d39fbd81a464 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 2 Oct 2023 15:34:20 +0200 Subject: [PATCH 163/225] WPB-663 servantify brig provider service API (#3566) --- changelog.d/5-internal/WPB-663 | 14 + libs/wire-api/src/Wire/API/Error/Brig.hs | 6 + .../wire-api/src/Wire/API/Provider/Service.hs | 274 +++++++---------- .../src/Wire/API/Provider/Service/Tag.hs | 52 +++- .../src/Wire/API/Routes/Public/Brig.hs | 4 +- .../Wire/API/Routes/Public/Brig/Services.hs | 178 +++++++++++ libs/wire-api/wire-api.cabal | 1 + services/brig/src/Brig/API.hs | 2 - services/brig/src/Brig/API/Public.hs | 13 +- services/brig/src/Brig/Provider/API.hs | 278 ++++++------------ .../brig/test/integration/API/Provider.hs | 68 ++++- services/brig/test/integration/Main.hs | 2 +- .../integration-test/conf/nginz/nginx.conf | 5 + 13 files changed, 508 insertions(+), 389 deletions(-) create mode 100644 changelog.d/5-internal/WPB-663 create mode 100644 libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs diff --git a/changelog.d/5-internal/WPB-663 b/changelog.d/5-internal/WPB-663 new file mode 100644 index 00000000000..303cf529f7b --- /dev/null +++ b/changelog.d/5-internal/WPB-663 @@ -0,0 +1,14 @@ +Migrating the following routes to the Servant API form. + +POST /provider/services +GET /provider/services +GET /provider/services/:sid +PUT /provider/services/:sid +PUT /provider/services/:sid/connection +DELETE /provider/services/:sid +GET /providers/:pid/services +GET /providers/:pid/services/:sid +GET /services +GET /services/tags +GET /teams/:tid/services/whitelisted +POST /teams/:tid/services/whitelist \ No newline at end of file diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index a77416c7921..29ea5f7716c 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -86,6 +86,8 @@ data BrigError | TooManyConversationMembers | ServiceDisabled | InvalidBot + | InvalidServiceKey + | ServiceNotFound | VerificationCodeThrottled | InvalidProvider | ProviderNotFound @@ -93,6 +95,10 @@ data BrigError instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: BrigError) where addToOpenApi = addStaticErrorToSwagger @(MapError e) +type instance MapError 'ServiceNotFound = 'StaticError 404 "not-found" "Service not found." + +type instance MapError 'InvalidServiceKey = 'StaticError 400 "invalid-service-key" "Invalid service key." + type instance MapError 'ProviderNotFound = 'StaticError 404 "not-found" "Provider not found." type instance MapError 'InvalidProvider = 'StaticError 403 "invalid-provider" "The provider does not exist." diff --git a/libs/wire-api/src/Wire/API/Provider/Service.hs b/libs/wire-api/src/Wire/API/Provider/Service.hs index c1110a58f25..7b181183e1e 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service.hs @@ -47,6 +47,7 @@ module Wire.API.Provider.Service -- * UpdateServiceWhitelist UpdateServiceWhitelist (..), + UpdateServiceWhitelistResp (..), ) where @@ -58,19 +59,20 @@ import Data.ByteString.Builder qualified as BB import Data.ByteString.Char8 qualified as BS import Data.ByteString.Conversion import Data.Id -import Data.Json.Util ((#)) import Data.List1 (List1) import Data.Misc (HttpsUrl (..), PlainTextPassword6) import Data.OpenApi qualified as S import Data.PEM (PEM, pemParseBS, pemWriteLBS) import Data.Proxy -import Data.Range (Range) +import Data.Range (Range, fromRange, rangedSchema) +import Data.SOP import Data.Schema import Data.Text qualified as Text import Data.Text.Ascii import Data.Text.Encoding qualified as Text import Imports import Wire.API.Provider.Service.Tag (ServiceTag (..)) +import Wire.API.Routes.MultiVerb import Wire.API.User.Profile (Asset, Name) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) @@ -205,35 +207,22 @@ data Service = Service } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform Service) + deriving (S.ToSchema, ToJSON, FromJSON) via (Schema Service) -instance ToJSON Service where - toJSON s = - A.object $ - "id" A..= serviceId s - # "name" A..= serviceName s - # "summary" A..= serviceSummary s - # "description" A..= serviceDescr s - # "base_url" A..= serviceUrl s - # "auth_tokens" A..= serviceTokens s - # "public_keys" A..= serviceKeys s - # "assets" A..= serviceAssets s - # "tags" A..= serviceTags s - # "enabled" A..= serviceEnabled s - # [] - -instance FromJSON Service where - parseJSON = A.withObject "Service" $ \o -> - Service - <$> o A..: "id" - <*> o A..: "name" - <*> o A..: "summary" - <*> o A..: "description" - <*> o A..: "base_url" - <*> o A..: "auth_tokens" - <*> o A..: "public_keys" - <*> o A..: "assets" - <*> o A..: "tags" - <*> o A..: "enabled" +instance ToSchema Service where + schema = + object "Service" $ + Service + <$> serviceId .= field "id" schema + <*> serviceName .= field "name" schema + <*> serviceSummary .= field "summary" schema + <*> serviceDescr .= field "description" schema + <*> serviceUrl .= field "base_url" schema + <*> serviceTokens .= field "auth_tokens" schema + <*> serviceKeys .= field "public_keys" schema + <*> serviceAssets .= field "assets" (array schema) + <*> serviceTags .= field "tags" (set schema) + <*> serviceEnabled .= field "enabled" schema -- | A /secret/ bearer token used to authenticate and authorise requests @towards@ -- a 'Service' via inclusion in the HTTP 'Authorization' header. @@ -265,31 +254,20 @@ data ServiceProfile = ServiceProfile } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ServiceProfile) + deriving (S.ToSchema, ToJSON, FromJSON) via (Schema ServiceProfile) -instance ToJSON ServiceProfile where - toJSON s = - A.object $ - "id" A..= serviceProfileId s - # "provider" A..= serviceProfileProvider s - # "name" A..= serviceProfileName s - # "summary" A..= serviceProfileSummary s - # "description" A..= serviceProfileDescr s - # "assets" A..= serviceProfileAssets s - # "tags" A..= serviceProfileTags s - # "enabled" A..= serviceProfileEnabled s - # [] - -instance FromJSON ServiceProfile where - parseJSON = A.withObject "ServiceProfile" $ \o -> - ServiceProfile - <$> o A..: "id" - <*> o A..: "provider" - <*> o A..: "name" - <*> o A..: "summary" - <*> o A..: "description" - <*> o A..: "assets" - <*> o A..: "tags" - <*> o A..: "enabled" +instance ToSchema ServiceProfile where + schema = + object "ServiceProfile" $ + ServiceProfile + <$> serviceProfileId .= field "id" schema + <*> serviceProfileProvider .= field "provider" schema + <*> serviceProfileName .= field "name" schema + <*> serviceProfileSummary .= field "summary" schema + <*> serviceProfileDescr .= field "description" schema + <*> serviceProfileAssets .= field "assets" (array schema) + <*> serviceProfileTags .= field "tags" (set schema) + <*> serviceProfileEnabled .= field "enabled" schema -------------------------------------------------------------------------------- -- ServiceProfilePage @@ -300,19 +278,14 @@ data ServiceProfilePage = ServiceProfilePage } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ServiceProfilePage) + deriving (S.ToSchema, FromJSON, ToJSON) via (Schema ServiceProfilePage) -instance ToJSON ServiceProfilePage where - toJSON p = - A.object - [ "has_more" A..= serviceProfilePageHasMore p, - "services" A..= serviceProfilePageResults p - ] - -instance FromJSON ServiceProfilePage where - parseJSON = A.withObject "ServiceProfilePage" $ \o -> - ServiceProfilePage - <$> o A..: "has_more" - <*> o A..: "services" +instance ToSchema ServiceProfilePage where + schema = + object "ServiceProfile" $ + ServiceProfilePage + <$> serviceProfilePageHasMore .= field "has_more" schema + <*> serviceProfilePageResults .= field "services" (array schema) -------------------------------------------------------------------------------- -- NewService @@ -330,31 +303,20 @@ data NewService = NewService } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewService) + deriving (S.ToSchema, ToJSON, FromJSON) via (Schema NewService) -instance ToJSON NewService where - toJSON s = - A.object $ - "name" A..= newServiceName s - # "summary" A..= newServiceSummary s - # "description" A..= newServiceDescr s - # "base_url" A..= newServiceUrl s - # "public_key" A..= newServiceKey s - # "auth_token" A..= newServiceToken s - # "assets" A..= newServiceAssets s - # "tags" A..= newServiceTags s - # [] - -instance FromJSON NewService where - parseJSON = A.withObject "NewService" $ \o -> - NewService - <$> o A..: "name" - <*> o A..: "summary" - <*> o A..: "description" - <*> o A..: "base_url" - <*> o A..: "public_key" - <*> o A..:? "auth_token" - <*> o A..:? "assets" A..!= [] - <*> o A..: "tags" +instance ToSchema NewService where + schema = + object "NewService" $ + NewService + <$> newServiceName .= field "name" schema + <*> newServiceSummary .= field "summary" schema + <*> newServiceDescr .= field "description" schema + <*> newServiceUrl .= field "base_url" schema + <*> newServiceKey .= field "public_key" schema + <*> newServiceToken .= maybe_ (optField "auth_token" schema) + <*> newServiceAssets .= field "assets" (array schema) + <*> newServiceTags .= field "tags" (fromRange .= rangedSchema (set schema)) -- | Response data upon adding a new service. data NewServiceResponse = NewServiceResponse @@ -366,19 +328,14 @@ data NewServiceResponse = NewServiceResponse } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewServiceResponse) + deriving (S.ToSchema, ToJSON, FromJSON) via (Schema NewServiceResponse) -instance ToJSON NewServiceResponse where - toJSON r = - A.object $ - "id" A..= rsNewServiceId r - # "auth_token" A..= rsNewServiceToken r - # [] - -instance FromJSON NewServiceResponse where - parseJSON = A.withObject "NewServiceResponse" $ \o -> - NewServiceResponse - <$> o A..: "id" - <*> o A..:? "auth_token" +instance ToSchema NewServiceResponse where + schema = + object "NewServiceResponse" $ + NewServiceResponse + <$> rsNewServiceId .= field "id" schema + <*> rsNewServiceToken .= maybe_ (optField "auth_token" schema) -------------------------------------------------------------------------------- -- UpdateService @@ -393,25 +350,17 @@ data UpdateService = UpdateService } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UpdateService) + deriving (S.ToSchema, FromJSON, ToJSON) via (Schema UpdateService) -instance ToJSON UpdateService where - toJSON u = - A.object $ - "name" A..= updateServiceName u - # "summary" A..= updateServiceSummary u - # "description" A..= updateServiceDescr u - # "assets" A..= updateServiceAssets u - # "tags" A..= updateServiceTags u - # [] - -instance FromJSON UpdateService where - parseJSON = A.withObject "UpdateService" $ \o -> - UpdateService - <$> o A..:? "name" - <*> o A..:? "summary" - <*> o A..:? "description" - <*> o A..:? "assets" - <*> o A..:? "tags" +instance ToSchema UpdateService where + schema = + object "UpdateService" $ + UpdateService + <$> updateServiceName .= maybe_ (optField "name" schema) + <*> updateServiceSummary .= maybe_ (optField "summary" schema) + <*> updateServiceDescr .= maybe_ (optField "description" schema) + <*> updateServiceAssets .= maybe_ (optField "assets" $ array schema) + <*> updateServiceTags .= maybe_ (optField "tags" (fromRange .= rangedSchema (set schema))) -------------------------------------------------------------------------------- -- UpdateServiceConn @@ -427,29 +376,21 @@ data UpdateServiceConn = UpdateServiceConn } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UpdateServiceConn) + deriving (S.ToSchema, ToJSON, FromJSON) via (Schema UpdateServiceConn) + +instance ToSchema UpdateServiceConn where + schema = + object "UpdateServiceConn" $ + UpdateServiceConn + <$> updateServiceConnPassword .= field "password" schema + <*> updateServiceConnUrl .= maybe_ (optField "base_url" schema) + <*> updateServiceConnKeys .= maybe_ (optField "public_keys" (fromRange .= rangedSchema (array schema))) + <*> updateServiceConnTokens .= maybe_ (optField "auth_tokens" (fromRange .= rangedSchema (array schema))) + <*> updateServiceConnEnabled .= maybe_ (optField "enabled" schema) mkUpdateServiceConn :: PlainTextPassword6 -> UpdateServiceConn mkUpdateServiceConn pw = UpdateServiceConn pw Nothing Nothing Nothing Nothing -instance ToJSON UpdateServiceConn where - toJSON u = - A.object $ - "password" A..= updateServiceConnPassword u - # "base_url" A..= updateServiceConnUrl u - # "public_keys" A..= updateServiceConnKeys u - # "auth_tokens" A..= updateServiceConnTokens u - # "enabled" A..= updateServiceConnEnabled u - # [] - -instance FromJSON UpdateServiceConn where - parseJSON = A.withObject "UpdateServiceConn" $ \o -> - UpdateServiceConn - <$> o A..: "password" - <*> o A..:? "base_url" - <*> o A..:? "public_keys" - <*> o A..:? "auth_tokens" - <*> o A..:? "enabled" - -------------------------------------------------------------------------------- -- DeleteService @@ -458,16 +399,13 @@ newtype DeleteService = DeleteService {deleteServicePassword :: PlainTextPassword6} deriving stock (Eq, Show) deriving newtype (Arbitrary) + deriving (S.ToSchema, ToJSON, FromJSON) via (Schema DeleteService) -instance ToJSON DeleteService where - toJSON d = - A.object - [ "password" A..= deleteServicePassword d - ] - -instance FromJSON DeleteService where - parseJSON = A.withObject "DeleteService" $ \o -> - DeleteService <$> o A..: "password" +instance ToSchema DeleteService where + schema = + object "DeleteService" $ + DeleteService + <$> deleteServicePassword .= field "password" schema -------------------------------------------------------------------------------- -- UpdateServiceWhitelist @@ -479,18 +417,30 @@ data UpdateServiceWhitelist = UpdateServiceWhitelist } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UpdateServiceWhitelist) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema UpdateServiceWhitelist) -instance ToJSON UpdateServiceWhitelist where - toJSON u = - A.object - [ "provider" A..= updateServiceWhitelistProvider u, - "id" A..= updateServiceWhitelistService u, - "whitelisted" A..= updateServiceWhitelistStatus u - ] - -instance FromJSON UpdateServiceWhitelist where - parseJSON = A.withObject "UpdateServiceWhitelist" $ \o -> - UpdateServiceWhitelist - <$> o A..: "provider" - <*> o A..: "id" - <*> o A..: "whitelisted" +instance ToSchema UpdateServiceWhitelist where + schema = + object "UpdateServiceWhitelist" $ + UpdateServiceWhitelist + <$> updateServiceWhitelistProvider .= field "provider" schema + <*> updateServiceWhitelistService .= field "id" schema + <*> updateServiceWhitelistStatus .= field "whitelisted" schema + +data UpdateServiceWhitelistResp + = UpdateServiceWhitelistRespChanged + | UpdateServiceWhitelistRespUnchanged + +-- basically the same as the instance for CheckBlacklistResponse +instance + AsUnion + '[ RespondEmpty 200 "UpdateServiceWhitelistRespChanged", + RespondEmpty 204 "UpdateServiceWhitelistRespUnchanged" + ] + UpdateServiceWhitelistResp + where + toUnion UpdateServiceWhitelistRespChanged = Z (I ()) + toUnion UpdateServiceWhitelistRespUnchanged = S (Z (I ())) + fromUnion (Z (I ())) = UpdateServiceWhitelistRespChanged + fromUnion (S (Z (I ()))) = UpdateServiceWhitelistRespUnchanged + fromUnion (S (S x)) = case x of {} diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index 07f0910ce5e..1df9b6a14bc 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -39,8 +39,9 @@ module Wire.API.Provider.Service.Tag ) where -import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON)) -import Data.Aeson qualified as JSON +import Control.Lens (Prism', prism) +import Data.Aeson (FromJSON, ToJSON (toJSON)) +import Data.Attoparsec.ByteString (IResult (..), parse) import Data.ByteString (toStrict) import Data.ByteString.Builder qualified as BB import Data.ByteString.Char8 qualified as C8 @@ -50,12 +51,14 @@ import Data.Range (Range, fromRange, rangedSchema) import Data.Range qualified as Range import Data.Schema import Data.Set qualified as Set +import Data.Text qualified as Text import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding qualified as Text import Data.Text.Encoding.Error (lenientDecode) import Data.Type.Ord import GHC.TypeLits (KnownNat, Nat) import Imports +import Web.HttpApiData (FromHttpApiData (parseUrlPiece)) import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) -------------------------------------------------------------------------------- @@ -64,6 +67,13 @@ import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) newtype ServiceTagList = ServiceTagList [ServiceTag] deriving stock (Eq, Ord, Show) deriving newtype (FromJSON, ToJSON, Arbitrary) + deriving (S.ToSchema) via (Schema ServiceTagList) + +_ServiceTagList :: Prism' ServiceTagList [ServiceTag] +_ServiceTagList = prism ServiceTagList (\(ServiceTagList l) -> pure l) + +instance ToSchema ServiceTagList where + schema = named "ServiceTagList" $ tag _ServiceTagList $ array schema -- | A fixed enumeration of tags for services. data ServiceTag @@ -100,6 +110,7 @@ data ServiceTag | WeatherTag deriving stock (Eq, Show, Ord, Enum, Bounded, Generic) deriving (Arbitrary) via (GenericUniform ServiceTag) + deriving (S.ToSchema, ToJSON, FromJSON) via (Schema ServiceTag) instance FromByteString ServiceTag where parser = @@ -170,14 +181,6 @@ instance ToByteString ServiceTag where builder VideoTag = "video" builder WeatherTag = "weather" -instance ToJSON ServiceTag where - toJSON = JSON.String . Text.decodeUtf8 . toByteString' - -instance FromJSON ServiceTag where - parseJSON = - JSON.withText "ServiceTag" $ - either fail pure . runParser parser . Text.encodeUtf8 - instance ToSchema ServiceTag where schema = enum @Text "" . mconcat $ (\a -> element (decodeUtf8With lenientDecode $ toStrict $ toByteString a) a) <$> [minBound ..] @@ -235,11 +238,31 @@ instance (KnownNat n, KnownNat m, m <= n) => FromByteString (QueryAnyTags m n) w rs <- either fail pure (Range.checkedEither (Set.fromList ts)) pure $! QueryAnyTags rs +runPartial :: IsString i => Bool -> IResult i b -> Either Text b +runPartial alreadyRun result = case result of + Fail _ _ e -> Left $ Text.pack e + Partial f -> + if alreadyRun + then Left "A partial parse returned another partial parse." + else runPartial True $ f "" + Done _ r -> pure r + +instance (KnownNat n, KnownNat m, m <= n) => FromHttpApiData (QueryAnyTags m n) where + parseUrlPiece t = do + txt <- parseUrlPiece t + runPartial False $ parse parser $ Text.encodeUtf8 txt + -- | Bounded logical conjunction of 'm' to 'n' 'ServiceTag's to match. newtype QueryAllTags (m :: Nat) (n :: Nat) = QueryAllTags {queryAllTagsRange :: Range m n (Set ServiceTag)} deriving stock (Eq, Show, Ord) +instance (KnownNat n, KnownNat m, m <= n) => ToSchema (QueryAllTags m n) where + schema = + let sch :: ValueSchema NamedSwaggerDoc (Range m n (Set ServiceTag)) + sch = fromRange .= rangedSchema (named "QueryAllTags" $ set schema) + in queryAllTagsRange .= (QueryAllTags <$> sch) + instance (KnownNat m, KnownNat n, m <= n) => Arbitrary (QueryAllTags m n) where arbitrary = QueryAllTags <$> arbitrary @@ -264,11 +287,10 @@ instance (KnownNat m, KnownNat n, m <= n) => FromByteString (QueryAllTags m n) w rs <- either fail pure (Range.checkedEither (Set.fromList ts)) pure $! QueryAllTags rs -instance (KnownNat m, KnownNat n, m <= n) => ToSchema (QueryAllTags m n) where - schema = - let sch :: ValueSchema NamedSwaggerDoc (Range m n (Set ServiceTag)) - sch = fromRange .= rangedSchema (named "QueryAllTags" $ set schema) - in queryAllTagsRange .= fmap QueryAllTags sch +instance (KnownNat n, KnownNat m, m <= n) => FromHttpApiData (QueryAllTags m n) where + parseUrlPiece t = do + txt <- parseUrlPiece t + runPartial False $ parse parser $ Text.encodeUtf8 txt -------------------------------------------------------------------------------- -- ServiceTag Matchers diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 0c620aea113..2acf005623e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -49,7 +49,7 @@ import Wire.API.Error.Brig import Wire.API.Error.Empty import Wire.API.MakesFederatedCall import Wire.API.OAuth -import Wire.API.Properties +import Wire.API.Properties (PropertyKey, PropertyKeysAndValues, RawPropertyValue) import Wire.API.Routes.API import Wire.API.Routes.Bearer import Wire.API.Routes.Cookies @@ -59,6 +59,7 @@ import Wire.API.Routes.Public import Wire.API.Routes.Public.Brig.Bot (BotAPI) import Wire.API.Routes.Public.Brig.OAuth (OAuthAPI) import Wire.API.Routes.Public.Brig.Provider (ProviderAPI) +import Wire.API.Routes.Public.Brig.Services (ServicesAPI) import Wire.API.Routes.Public.Util import Wire.API.Routes.QualifiedCapture import Wire.API.Routes.Version @@ -94,6 +95,7 @@ type BrigAPI = :<|> SystemSettingsAPI :<|> OAuthAPI :<|> BotAPI + :<|> ServicesAPI :<|> ProviderAPI data BrigAPITag diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs new file mode 100644 index 00000000000..8fab900fca6 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs @@ -0,0 +1,178 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Wire.API.Routes.Public.Brig.Services where + +import Data.Id as Id +import Data.Range +import Imports hiding (head) +import Servant (JSON) +import Servant hiding (Handler, JSON, addHeader, respond) +import Wire.API.Error (CanThrow) +import Wire.API.Error.Brig +import Wire.API.Provider.Service qualified as Public +import Wire.API.Provider.Service.Tag +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public + +type ServicesAPI = + Named + "post-provider-services" + ( Summary "Create a new service" + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidServiceKey + :> ZProvider + :> "provider" + :> "services" + :> ReqBody '[JSON] Public.NewService + :> MultiVerb1 'POST '[JSON] (Respond 201 "" Public.NewServiceResponse) + ) + :<|> Named + "get-provider-services" + ( Summary "List provider services" + :> CanThrow 'AccessDenied + :> ZProvider + :> "provider" + :> "services" + :> Get '[JSON] [Public.Service] + ) + :<|> Named + "get-provider-services-by-service-id" + ( Summary "Get provider service by service id" + :> CanThrow 'AccessDenied + :> CanThrow 'ServiceNotFound + :> ZProvider + :> "provider" + :> "services" + :> Capture "service-id" ServiceId + :> Get '[JSON] Public.Service + ) + :<|> Named + "put-provider-services-by-service-id" + ( Summary "Update provider service" + :> CanThrow 'AccessDenied + :> CanThrow 'ServiceNotFound + :> CanThrow 'ProviderNotFound + :> ZProvider + :> "provider" + :> "services" + :> Capture "service-id" ServiceId + :> ReqBody '[JSON] Public.UpdateService + :> Put '[PlainText] NoContent + ) + :<|> Named + "put-provider-services-connection-by-service-id" + ( Summary "Update provider service connection" + :> CanThrow 'AccessDenied + :> CanThrow 'ServiceNotFound + :> CanThrow 'BadCredentials + :> CanThrow 'InvalidServiceKey + :> ZProvider + :> "provider" + :> "services" + :> Capture "service-id" ServiceId + :> "connection" + :> ReqBody '[JSON] Public.UpdateServiceConn + :> Put '[PlainText] NoContent + ) + :<|> Named + "delete-provider-services-by-service-id" + ( Summary "Delete service" + :> CanThrow 'AccessDenied + :> CanThrow 'BadCredentials + :> CanThrow 'ServiceNotFound + :> ZProvider + :> "provider" + :> "services" + :> Capture "service-id" ServiceId + :> ReqBody '[JSON] Public.DeleteService + :> MultiVerb1 'DELETE '[PlainText] (RespondEmpty 202 "") + ) + :<|> Named + "get-provider-services-by-provider-id" + ( Summary "Get provider services by provider id" + :> CanThrow 'AccessDenied + :> ZUser + :> "providers" + :> Capture "provider-id" ProviderId + :> "services" + :> Get '[JSON] [Public.ServiceProfile] + ) + :<|> Named + "get-services" + ( Summary "List services" + :> CanThrow 'AccessDenied + :> ZUser + :> "services" + :> QueryParam "tags" (QueryAnyTags 1 3) + :> QueryParam "start" Text + :> QueryParam "size" (Range 10 100 Int32) -- Default to 20 + :> Get '[JSON] Public.ServiceProfilePage + ) + :<|> Named + "get-services-tags" + ( Summary "Get services tags" + :> CanThrow 'AccessDenied + :> ZUser + :> Get '[JSON] ServiceTagList + ) + :<|> Named + "get-provider-services-by-provider-id-and-service-id" + ( Summary "Get provider service by provider id and service id" + :> CanThrow 'AccessDenied + :> CanThrow 'ServiceNotFound + :> ZUser + :> "providers" + :> Capture "provider-id" ProviderId + :> "services" + :> Capture "service-id" ServiceId + :> Get '[JSON] Public.ServiceProfile + ) + :<|> Named + "get-whitelisted-services-by-team-id" + ( Summary "Get whitelisted services by team id" + :> ZUser + :> "teams" + :> Capture "team-id" TeamId + :> "services" + :> "whitelisted" + :> QueryParam "prefix" (Range 1 128 Text) + -- Default to True + :> QueryParam "filter_disabled" Bool + -- Default to 20 + :> QueryParam "size" (Range 10 100 Int32) + :> Get '[JSON] Public.ServiceProfilePage + ) + :<|> Named + "post-team-whitelist-by-team-id" + ( Summary "Update service whitelist" + :> ZUser + :> ZConn + :> "teams" + :> Capture "team-id" TeamId + :> "services" + :> "whitelist" + :> ReqBody '[JSON] Public.UpdateServiceWhitelist + :> MultiVerb + 'POST + '[PlainText] + '[ RespondEmpty 200 "UpdateServiceWhitelistRespChanged", + RespondEmpty 204 "UpdateServiceWhitelistRespUnchanged" + ] + Public.UpdateServiceWhitelistResp + ) diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index fde861ec6d0..cb5ea5c163a 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -107,6 +107,7 @@ library Wire.API.Routes.Public.Brig.Bot Wire.API.Routes.Public.Brig.OAuth Wire.API.Routes.Public.Brig.Provider + Wire.API.Routes.Public.Brig.Services Wire.API.Routes.Public.Cannon Wire.API.Routes.Public.Cargohold Wire.API.Routes.Public.Galley diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index 1c03e0376ca..3580f888511 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -22,7 +22,6 @@ where import Brig.API.Handler (Handler) import Brig.API.Internal qualified as Internal -import Brig.API.Public qualified as Public import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) @@ -37,5 +36,4 @@ sitemap :: ) => Routes () (Handler r) () sitemap = do - Public.sitemap Internal.sitemap diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 0289be453d3..5e22dfcb500 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -19,8 +19,7 @@ -- with this program. If not, see . module Brig.API.Public - ( sitemap, - servantSitemap, + ( servantSitemap, docsAPI, DocsAPI, ) @@ -56,7 +55,6 @@ import Brig.Effects.PublicKeyBundle (PublicKeyBundle) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Options hiding (internalEvents, sesQueue) import Brig.Provider.API -import Brig.Provider.API qualified as Provider import Brig.Team.API qualified as Team import Brig.Team.Email qualified as Team import Brig.Types.Activation (ActivationPair) @@ -99,7 +97,6 @@ import FileEmbedLzma import Galley.Types.Teams (HiddenPerm (..), hasPermission) import Imports hiding (head) import Network.Socket (PortNumber) -import Network.Wai.Routing import Network.Wai.Utilities as Utilities import Polysemy import Servant hiding (Handler, JSON, addHeader, respond) @@ -271,6 +268,7 @@ servantSitemap = :<|> systemSettingsAPI :<|> oauthAPI :<|> botAPI + :<|> servicesAPI :<|> providerAPI where userAPI :: ServerT UserAPI (Handler r) @@ -406,13 +404,6 @@ servantSitemap = -- - UserDeleted event to contacts of the user -- - MemberLeave event to members for all conversations the user was in (via galley) -sitemap :: - ( Member GalleyProvider r - ) => - Routes () (Handler r) () -sitemap = do - Provider.routesPublic - --------------------------------------------------------------------------- -- Handlers diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index ce85474b235..8c4af7f34ff 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -17,9 +17,9 @@ module Brig.Provider.API ( -- * Main stuff - routesPublic, routesInternal, botAPI, + servicesAPI, providerAPI, -- * Event handlers @@ -71,7 +71,6 @@ import Data.List qualified as List import Data.List1 (maybeList1) import Data.Map.Strict qualified as Map import Data.Misc (Fingerprint (..), FutureWork (FutureWork), Rsa) -import Data.Predicate import Data.Qualified import Data.Range import Data.Set qualified as Set @@ -81,12 +80,11 @@ import GHC.TypeNats import Imports import Network.HTTP.Types.Status import Network.Wai (Response) -import Network.Wai.Predicate (accept, def, opt, query) +import Network.Wai.Predicate (accept) import Network.Wai.Routing import Network.Wai.Utilities.Error ((!>>)) import Network.Wai.Utilities.Error qualified as Wai -import Network.Wai.Utilities.Request (JsonRequest, jsonRequest) -import Network.Wai.Utilities.Response (empty, json, setStatus) +import Network.Wai.Utilities.Response (json) import Network.Wai.Utilities.ZAuth import OpenSSL.EVP.Digest qualified as SSL import OpenSSL.EVP.PKey qualified as SSL @@ -94,7 +92,7 @@ import OpenSSL.PEM qualified as SSL import OpenSSL.RSA qualified as SSL import OpenSSL.Random (randBytes) import Polysemy -import Servant (ServerT, (:<|>) (..)) +import Servant (NoContent (..), ServerT, (:<|>) (..)) import Ssl.Util qualified as SSL import System.Logger.Class (MonadLogger) import UnliftIO.Async (pooledMapConcurrentlyN_) @@ -118,6 +116,7 @@ import Wire.API.Provider.Service.Tag qualified as Public import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig.Bot (BotAPI) import Wire.API.Routes.Public.Brig.Provider (ProviderAPI) +import Wire.API.Routes.Public.Brig.Services (ServicesAPI) import Wire.API.Team.Feature qualified as Feature import Wire.API.Team.LegalHold (LegalholdProtectee (UnprotectedBot)) import Wire.API.Team.Permission @@ -147,6 +146,21 @@ botAPI = :<|> Named @"bot-list-users" botListUserProfiles :<|> Named @"bot-get-user-clients" botGetUserClients +servicesAPI :: (Member GalleyProvider r) => ServerT ServicesAPI (Handler r) +servicesAPI = + Named @"post-provider-services" addService + :<|> Named @"get-provider-services" listServices + :<|> Named @"get-provider-services-by-service-id" getService + :<|> Named @"put-provider-services-by-service-id" updateService + :<|> Named @"put-provider-services-connection-by-service-id" updateServiceConn + :<|> Named @"delete-provider-services-by-service-id" deleteService + :<|> Named @"get-provider-services-by-provider-id" listServiceProfiles + :<|> Named @"get-services" searchServiceProfiles + :<|> Named @"get-services-tags" getServiceTagList + :<|> Named @"get-provider-services-by-provider-id-and-service-id" getServiceProfile + :<|> Named @"get-whitelisted-services-by-team-id" searchTeamServiceProfiles + :<|> Named @"post-team-whitelist-by-team-id" updateServiceWhitelist + providerAPI :: Member GalleyProvider r => ServerT ProviderAPI (Handler r) providerAPI = Named @"provider-register" newAccount @@ -161,93 +175,6 @@ providerAPI = :<|> Named @"provider-get-account" getAccount :<|> Named @"provider-get-profile" getProviderProfile -routesPublic :: - ( Member GalleyProvider r - ) => - Routes () (Handler r) () -routesPublic = do - -- Provider API ------------------------------------------------------------ - - post "/provider/services" (continue addServiceH) $ - accept "application" "json" - .&> zauth ZAuthProvider - .&> zauthProviderId - .&. jsonRequest @Public.NewService - - get "/provider/services" (continue listServicesH) $ - accept "application" "json" - .&> zauth ZAuthProvider - .&> zauthProviderId - - get "/provider/services/:sid" (continue getServiceH) $ - accept "application" "json" - .&> zauth ZAuthProvider - .&> zauthProviderId - .&. capture "sid" - - put "/provider/services/:sid" (continue updateServiceH) $ - zauth ZAuthProvider - .&> zauthProviderId - .&. capture "sid" - .&. jsonRequest @Public.UpdateService - - put "/provider/services/:sid/connection" (continue updateServiceConnH) $ - zauth ZAuthProvider - .&> zauthProviderId - .&. capture "sid" - .&. jsonRequest @Public.UpdateServiceConn - - -- TODO - -- post "/provider/services/:sid/token" (continue genServiceTokenH) $ - -- accept "application" "json" - -- .&. zauthProvider - - delete "/provider/services/:sid" (continue deleteServiceH) $ - zauth ZAuthProvider - .&> zauthProviderId - .&. capture "sid" - .&. jsonRequest @Public.DeleteService - - -- User API ---------------------------------------------------------------- - - get "/providers/:pid/services" (continue listServiceProfilesH) $ - accept "application" "json" - .&> zauth ZAuthAccess - .&> capture "pid" - - get "/providers/:pid/services/:sid" (continue getServiceProfileH) $ - accept "application" "json" - .&> zauth ZAuthAccess - .&> capture "pid" - .&. capture "sid" - - get "/services" (continue searchServiceProfilesH) $ - accept "application" "json" - .&> zauth ZAuthAccess - .&> opt (query "tags") - .&. opt (query "start") - .&. def (unsafeRange 20) (query "size") - - get "/services/tags" (continue getServiceTagListH) $ - accept "application" "json" - .&> zauth ZAuthAccess - - get "/teams/:tid/services/whitelisted" (continue searchTeamServiceProfilesH) $ - accept "application" "json" - .&> zauthUserId - .&. capture "tid" - .&. opt (query "prefix") - .&. def True (query "filter_disabled") - .&. def (unsafeRange 20) (query "size") - - post "/teams/:tid/services/whitelist" (continue updateServiceWhitelistH) $ - accept "application" "json" - .&> zauth ZAuthAccess - .&> zauthUserId - .&. zauthConnId - .&. capture "tid" - .&. jsonRequest @Public.UpdateServiceWhitelist - routesInternal :: Member GalleyProvider r => Routes a (Handler r) () routesInternal = do get "/i/provider/activation-code" (continue getActivationCodeH) $ @@ -425,13 +352,13 @@ updateAccountPassword pid upd = do throwStd (errorToWai @'E.ResetPasswordMustDiffer) wrapClientE $ DB.updateAccountPassword pid (newPassword upd) -addServiceH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.NewService -> (Handler r) Response -addServiceH (pid ::: req) = do - guardSecondFactorDisabled Nothing - setStatus status201 . json <$> (addService pid =<< parseJsonBody req) - -addService :: ProviderId -> Public.NewService -> (Handler r) Public.NewServiceResponse +addService :: + Member GalleyProvider r => + ProviderId -> + Public.NewService -> + (Handler r) Public.NewServiceResponse addService pid new = do + guardSecondFactorDisabled Nothing _ <- wrapClientE (DB.lookupAccount pid) >>= maybeInvalidProvider let name = newServiceName new let summary = fromRange (newServiceSummary new) @@ -446,30 +373,28 @@ addService pid new = do let rstoken = maybe (Just token) (const Nothing) (newServiceToken new) pure $ Public.NewServiceResponse sid rstoken -listServicesH :: Member GalleyProvider r => ProviderId -> (Handler r) Response -listServicesH pid = do +listServices :: Member GalleyProvider r => ProviderId -> (Handler r) [Public.Service] +listServices pid = do guardSecondFactorDisabled Nothing - json <$> listServices pid - -listServices :: ProviderId -> (Handler r) [Public.Service] -listServices = wrapClientE . DB.listServices + wrapClientE $ DB.listServices pid -getServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId -> (Handler r) Response -getServiceH (pid ::: sid) = do +getService :: + Member GalleyProvider r => + ProviderId -> + ServiceId -> + (Handler r) Public.Service +getService pid sid = do guardSecondFactorDisabled Nothing - json <$> getService pid sid - -getService :: ProviderId -> ServiceId -> (Handler r) Public.Service -getService pid sid = wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound -updateServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateService -> (Handler r) Response -updateServiceH (pid ::: sid ::: req) = do - guardSecondFactorDisabled Nothing - empty <$ (updateService pid sid =<< parseJsonBody req) - -updateService :: ProviderId -> ServiceId -> Public.UpdateService -> (Handler r) () +updateService :: + Member GalleyProvider r => + ProviderId -> + ServiceId -> + Public.UpdateService -> + (Handler r) NoContent updateService pid sid upd = do + guardSecondFactorDisabled Nothing _ <- wrapClientE (DB.lookupAccount pid) >>= maybeInvalidProvider -- Update service profile svc <- wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound @@ -495,14 +420,16 @@ updateService pid sid upd = do newAssets tagsChange (serviceEnabled svc) + $> NoContent -updateServiceConnH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateServiceConn -> (Handler r) Response -updateServiceConnH (pid ::: sid ::: req) = do - guardSecondFactorDisabled Nothing - empty <$ (updateServiceConn pid sid =<< parseJsonBody req) - -updateServiceConn :: ProviderId -> ServiceId -> Public.UpdateServiceConn -> (Handler r) () +updateServiceConn :: + Member GalleyProvider r => + ProviderId -> + ServiceId -> + Public.UpdateServiceConn -> + (Handler r) NoContent updateServiceConn pid sid upd = do + guardSecondFactorDisabled Nothing pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials unless (verifyPassword (updateServiceConnPassword upd) pass) $ throwStd (errorToWai @'E.BadCredentials) @@ -534,26 +461,23 @@ updateServiceConn pid sid upd = do if sconEnabled scon then DB.deleteServiceIndexes pid sid name tags else DB.insertServiceIndexes pid sid name tags + pure NoContent -- TODO: Send informational email to provider. --- | Member GalleyProvider r => The endpoint that is called to delete a service. --- --- Since deleting a service can be costly, it just marks the service as --- disabled and then creates an event that will, when processed, actually --- delete the service. See 'finishDeleteService'. -deleteServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.DeleteService -> (Handler r) Response -deleteServiceH (pid ::: sid ::: req) = do - guardSecondFactorDisabled Nothing - setStatus status202 empty <$ (deleteService pid sid =<< parseJsonBody req) - -- | The endpoint that is called to delete a service. -- -- Since deleting a service can be costly, it just marks the service as -- disabled and then creates an event that will, when processed, actually -- delete the service. See 'finishDeleteService'. -deleteService :: ProviderId -> ServiceId -> Public.DeleteService -> (Handler r) () +deleteService :: + Member GalleyProvider r => + ProviderId -> + ServiceId -> + Public.DeleteService -> + (Handler r) () deleteService pid sid del = do + guardSecondFactorDisabled Nothing pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials unless (verifyPassword (deleteServicePassword del) pass) $ throwStd (errorToWai @'E.BadCredentials) @@ -619,92 +543,64 @@ getProviderProfile _ pid = do guardSecondFactorDisabled Nothing wrapClientE (DB.lookupAccountProfile pid) -listServiceProfilesH :: Member GalleyProvider r => ProviderId -> (Handler r) Response -listServiceProfilesH pid = do +listServiceProfiles :: Member GalleyProvider r => UserId -> ProviderId -> (Handler r) [Public.ServiceProfile] +listServiceProfiles _ pid = do guardSecondFactorDisabled Nothing - json <$> listServiceProfiles pid + wrapClientE $ DB.listServiceProfiles pid -listServiceProfiles :: ProviderId -> (Handler r) [Public.ServiceProfile] -listServiceProfiles = wrapClientE . DB.listServiceProfiles - -getServiceProfileH :: Member GalleyProvider r => ProviderId ::: ServiceId -> (Handler r) Response -getServiceProfileH (pid ::: sid) = do +getServiceProfile :: Member GalleyProvider r => UserId -> ProviderId -> ServiceId -> (Handler r) Public.ServiceProfile +getServiceProfile _ pid sid = do guardSecondFactorDisabled Nothing - json <$> getServiceProfile pid sid - -getServiceProfile :: ProviderId -> ServiceId -> (Handler r) Public.ServiceProfile -getServiceProfile pid sid = wrapClientE (DB.lookupServiceProfile pid sid) >>= maybeServiceNotFound -searchServiceProfilesH :: Member GalleyProvider r => Maybe (Public.QueryAnyTags 1 3) ::: Maybe Text ::: Range 10 100 Int32 -> (Handler r) Response -searchServiceProfilesH (qt ::: start ::: size) = do - guardSecondFactorDisabled Nothing - json <$> searchServiceProfiles qt start size - -- TODO: in order to actually make it possible for clients to implement -- pagination here, we need both 'start' and 'prefix'. -- -- Also see Note [buggy pagination]. -searchServiceProfiles :: Maybe (Public.QueryAnyTags 1 3) -> Maybe Text -> Range 10 100 Int32 -> (Handler r) Public.ServiceProfilePage -searchServiceProfiles Nothing (Just start) size = do +searchServiceProfiles :: Member GalleyProvider r => UserId -> Maybe (Public.QueryAnyTags 1 3) -> Maybe Text -> Maybe (Range 10 100 Int32) -> (Handler r) Public.ServiceProfilePage +searchServiceProfiles _ Nothing (Just start) mSize = do + guardSecondFactorDisabled Nothing prefix :: Range 1 128 Text <- rangeChecked start + let size = fromMaybe (unsafeRange 20) mSize wrapClientE . DB.paginateServiceNames (Just prefix) (fromRange size) . setProviderSearchFilter =<< view settings -searchServiceProfiles (Just tags) start size = do +searchServiceProfiles _ (Just tags) start mSize = do + guardSecondFactorDisabled Nothing + let size = fromMaybe (unsafeRange 20) mSize (wrapClientE . DB.paginateServiceTags tags start (fromRange size)) . setProviderSearchFilter =<< view settings -searchServiceProfiles Nothing Nothing _ = do +searchServiceProfiles _ Nothing Nothing _ = do + guardSecondFactorDisabled Nothing throwStd $ badRequest "At least `tags` or `start` must be provided." -searchTeamServiceProfilesH :: - Member GalleyProvider r => - UserId ::: TeamId ::: Maybe (Range 1 128 Text) ::: Bool ::: Range 10 100 Int32 -> - (Handler r) Response -searchTeamServiceProfilesH (uid ::: tid ::: prefix ::: filterDisabled ::: size) = do - guardSecondFactorDisabled (Just uid) - json <$> searchTeamServiceProfiles uid tid prefix filterDisabled size - -- NB: unlike 'searchServiceProfiles', we don't filter by service provider here searchTeamServiceProfiles :: UserId -> TeamId -> Maybe (Range 1 128 Text) -> - Bool -> - Range 10 100 Int32 -> + Maybe Bool -> + Maybe (Range 10 100 Int32) -> (Handler r) Public.ServiceProfilePage -searchTeamServiceProfiles uid tid prefix filterDisabled size = do +searchTeamServiceProfiles uid tid prefix mFilterDisabled mSize = do -- Check that the user actually belong to the team they claim they -- belong to. (Note: the 'tid' team might not even exist but we'll throw -- 'insufficientTeamPermissions' anyway) + let filterDisabled = fromMaybe True mFilterDisabled + let size = fromMaybe (unsafeRange 20) mSize teamId <- lift $ wrapClient $ User.lookupUserTeam uid unless (Just tid == teamId) $ throwStd insufficientTeamPermissions -- Get search results wrapClientE $ DB.paginateServiceWhitelist tid prefix filterDisabled (fromRange size) -getServiceTagListH :: Member GalleyProvider r => () -> (Handler r) Response -getServiceTagListH () = do +getServiceTagList :: Member GalleyProvider r => UserId -> (Handler r) Public.ServiceTagList +getServiceTagList _ = do guardSecondFactorDisabled Nothing - json <$> getServiceTagList () - -getServiceTagList :: () -> Monad m => m Public.ServiceTagList -getServiceTagList () = pure (Public.ServiceTagList allTags) + pure (Public.ServiceTagList allTags) where allTags = [(minBound :: Public.ServiceTag) ..] -updateServiceWhitelistH :: Member GalleyProvider r => UserId ::: ConnId ::: TeamId ::: JsonRequest Public.UpdateServiceWhitelist -> (Handler r) Response -updateServiceWhitelistH (uid ::: con ::: tid ::: req) = do - guardSecondFactorDisabled (Just uid) - resp <- updateServiceWhitelist uid con tid =<< parseJsonBody req - let status = case resp of - UpdateServiceWhitelistRespChanged -> status200 - UpdateServiceWhitelistRespUnchanged -> status204 - pure $ setStatus status empty - -data UpdateServiceWhitelistResp - = UpdateServiceWhitelistRespChanged - | UpdateServiceWhitelistRespUnchanged - updateServiceWhitelist :: Member GalleyProvider r => UserId -> ConnId -> TeamId -> Public.UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp updateServiceWhitelist uid con tid upd = do + guardSecondFactorDisabled (Just uid) let pid = updateServiceWhitelistProvider upd sid = updateServiceWhitelistService upd newWhitelisted = updateServiceWhitelistStatus upd @@ -998,13 +894,13 @@ mkBotUserView u = } maybeInvalidProvider :: Monad m => Maybe a -> (ExceptT Error m) a -maybeInvalidProvider = maybe (throwStd invalidProvider) pure +maybeInvalidProvider = maybe (throwStd (errorToWai @'E.ProviderNotFound)) pure maybeInvalidCode :: Monad m => Maybe a -> (ExceptT Error m) a maybeInvalidCode = maybe (throwStd (errorToWai @'E.InvalidCode)) pure maybeServiceNotFound :: Monad m => Maybe a -> (ExceptT Error m) a -maybeServiceNotFound = maybe (throwStd (notFound "Service not found")) pure +maybeServiceNotFound = maybe (throwStd (errorToWai @'E.ServiceNotFound)) pure maybeConvNotFound :: Monad m => Maybe a -> (ExceptT Error m) a maybeConvNotFound = maybe (throwStd (notFound "Conversation not found")) pure @@ -1013,7 +909,7 @@ maybeBadCredentials :: Monad m => Maybe a -> (ExceptT Error m) a maybeBadCredentials = maybe (throwStd (errorToWai @'E.BadCredentials)) pure maybeInvalidServiceKey :: Monad m => Maybe a -> (ExceptT Error m) a -maybeInvalidServiceKey = maybe (throwStd invalidServiceKey) pure +maybeInvalidServiceKey = maybe (throwStd (errorToWai @'E.InvalidServiceKey)) pure maybeInvalidUser :: Monad m => Maybe a -> (ExceptT Error m) a maybeInvalidUser = maybe (throwStd (errorToWai @'E.InvalidUser)) pure @@ -1021,12 +917,6 @@ maybeInvalidUser = maybe (throwStd (errorToWai @'E.InvalidUser)) pure rangeChecked :: (KnownNat n, KnownNat m, Within a n m, Monad monad) => a -> (ExceptT Error monad) (Range n m a) rangeChecked = either (throwStd . invalidRange . fromString) pure . checkedEither -invalidServiceKey :: Wai.Error -invalidServiceKey = Wai.mkError status400 "invalid-service-key" "Invalid service key." - -invalidProvider :: Wai.Error -invalidProvider = errorToWai @'E.InvalidProvider - badGateway :: Wai.Error badGateway = Wai.mkError status502 "bad-gateway" "The upstream service returned an invalid response." diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 02d33c55333..cd90227017e 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -97,11 +97,12 @@ import Wire.API.Team.Feature (featureNameBS) import Wire.API.Team.Feature qualified as Public import Wire.API.Team.Permission import Wire.API.User as User hiding (EmailUpdate, PasswordChange, mkName) +import Wire.API.User.Auth (CookieType (..)) import Wire.API.User.Client import Wire.API.User.Client.Prekey -tests :: Domain -> Config -> Manager -> DB.ClientState -> Brig -> Cannon -> Galley -> IO TestTree -tests dom conf p db b c g = do +tests :: Domain -> Config -> Manager -> DB.ClientState -> Brig -> Cannon -> Galley -> Nginz -> IO TestTree +tests dom conf p db b c g n = do pure $ testGroup "provider" @@ -139,7 +140,8 @@ tests dom conf p db b c g = do test p "de-whitelisted bots are removed" $ testWhitelistKickout dom conf db b g c, test p "de-whitelisting works with deleted conversations" $ - testDeWhitelistDeletedConv conf db b g c + testDeWhitelistDeletedConv conf db b g c, + test p "whitelist via nginz" $ testWhitelistNginz conf db b n ], testGroup "bot" @@ -358,6 +360,12 @@ testAddGetService config db brig = do assertEqual "assets" (serviceAssets svc) (serviceProfileAssets svp) assertEqual "tags" (serviceTags svc) (serviceProfileTags svp) assertBool "enabled" (not (serviceProfileEnabled svp)) + services :: [Service] <- responseJsonError =<< getServices brig pid ProviderId -> Http ResponseLBS +getServices brig pid = + get $ + brig + . path "/provider/services" + . header "Z-Type" "provider" + . header "Z-Provider" (toByteString' pid) + +getProviderServices :: Brig -> UserId -> ProviderId -> Http ResponseLBS +getProviderServices brig uid pid = + get $ + brig + . paths ["providers", toByteString' pid, "services"] + . header "Z-Type" "access" + . header "Z-User" (toByteString' uid) + getServiceProfile :: Brig -> UserId -> @@ -1734,6 +1758,36 @@ disableService brig pid sid = do updateServiceConn brig pid sid upd !!! const 200 === statusCode +whitelistServiceNginz :: + HasCallStack => + Nginz -> + -- | Team owner + User -> + -- | Team + TeamId -> + ProviderId -> + ServiceId -> + Http () +whitelistServiceNginz nginz user tid pid sid = + updateServiceWhitelistNginz nginz user tid (UpdateServiceWhitelist pid sid True) !!! const 200 === statusCode + +updateServiceWhitelistNginz :: + Nginz -> + User -> + TeamId -> + UpdateServiceWhitelist -> + Http ResponseLBS +updateServiceWhitelistNginz nginz user tid upd = do + let Just email = userEmail user + rs <- login nginz (defEmailLogin email) PersistentCookie toByteString' t) + . contentJson + . body (RequestBodyLBS (encode upd)) + whitelistService :: HasCallStack => Brig -> @@ -2291,6 +2345,14 @@ prepareBotUsersTeam brig galley sref = do cid <- Team.createTeamConv galley tid uid1 [uid2] Nothing pure (u1, u2, h, tid, cid, pid, sid) +testWhitelistNginz :: Config -> DB.ClientState -> Brig -> Nginz -> Http () +testWhitelistNginz config db brig nginz = withTestService config db brig defServiceApp $ \sref _ -> do + let pid = sref ^. serviceRefProvider + let sid = sref ^. serviceRefId + (admin, tid) <- Team.createUserWithTeam brig + adminUser <- selfUser <$> getSelfProfile brig admin + whitelistServiceNginz nginz adminUser tid pid sid + addBotConv :: HasCallStack => Domain -> diff --git a/services/brig/test/integration/Main.hs b/services/brig/test/integration/Main.hs index 13cd48c7489..8a3a0d5b9c0 100644 --- a/services/brig/test/integration/Main.hs +++ b/services/brig/test/integration/Main.hs @@ -148,7 +148,7 @@ runTests iConf brigOpts otherArgs = do awsEnv <- AWS.mkEnv lg awsOpts emailAWSOpts mg mUserJournalWatcher <- for (view AWS.userJournalQueue awsEnv) $ SQS.watchSQSQueue (view AWS.amazonkaEnv awsEnv) userApi <- User.tests brigOpts fedBrigClient fedGalleyClient mg b c ch g n awsEnv db mUserJournalWatcher - providerApi <- Provider.tests localDomain (provider iConf) mg db b c g + providerApi <- Provider.tests localDomain (provider iConf) mg db b c g n searchApis <- Search.tests brigOpts mg g b teamApis <- Team.tests brigOpts mg n b c g mUserJournalWatcher turnApi <- Calling.tests mg b brigOpts turnFile turnFileV2 diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index d737361a7e4..529e4cdfc88 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -244,6 +244,11 @@ http { proxy_pass http://brig; } + location ~* /teams/([^/]+)/services { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + location /connections { include common_response_with_zauth.conf; proxy_pass http://brig; From 900c43ddd85b55c32a1d582fa7cd4b3a21aeaa08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 4 Oct 2023 11:38:52 +0200 Subject: [PATCH 164/225] [WPB-3867] Use queueing federation client for a federation API endpoint (#3629) * Limit which fed endpoints can be queued * Enqueue remote MLS messages * Add changelogs * Formatting --------- Co-authored-by: Paolo Capriotti --- .../6-federation/wpb-3867-queue-endpoints | 1 + .../6-federation/wpb-3867-unreachable-users | 1 + .../src/Wire/API/Federation/API.hs | 22 ++- .../src/Wire/API/Federation/API/Galley.hs | 5 +- services/galley/src/Galley/API/Action.hs | 31 ++-- services/galley/src/Galley/API/Federation.hs | 159 ++++++++++-------- services/galley/src/Galley/API/MLS/Message.hs | 14 +- .../galley/src/Galley/API/MLS/Propagate.hs | 54 ++---- services/galley/src/Galley/API/Util.hs | 12 ++ .../Effects/BackendNotificationQueueAccess.hs | 2 +- .../Galley/Intra/BackendNotificationQueue.hs | 47 +++--- .../galley/test/integration/API/MLS/Mocks.hs | 1 - 12 files changed, 174 insertions(+), 175 deletions(-) create mode 100644 changelog.d/6-federation/wpb-3867-queue-endpoints create mode 100644 changelog.d/6-federation/wpb-3867-unreachable-users diff --git a/changelog.d/6-federation/wpb-3867-queue-endpoints b/changelog.d/6-federation/wpb-3867-queue-endpoints new file mode 100644 index 00000000000..b3f9efb6b7d --- /dev/null +++ b/changelog.d/6-federation/wpb-3867-queue-endpoints @@ -0,0 +1 @@ +Constrain which federation endpoints can be used via the queueing federation client diff --git a/changelog.d/6-federation/wpb-3867-unreachable-users b/changelog.d/6-federation/wpb-3867-unreachable-users new file mode 100644 index 00000000000..19dde73310e --- /dev/null +++ b/changelog.d/6-federation/wpb-3867-unreachable-users @@ -0,0 +1 @@ +There is a breaking change in the "on-mls-message-sent" federation endpoint due to queueing. Now that there is retrying because of queueing, the endpoint can no longer respond with a list of unreachable users. diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API.hs b/libs/wire-api-federation/src/Wire/API/Federation/API.hs index f344a80ced2..476df183032 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -37,10 +37,12 @@ import Data.Kind import Data.Proxy import GHC.TypeLits import Imports +import Servant import Servant.Client import Servant.Client.Core import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Cargohold +import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Client @@ -64,6 +66,20 @@ type HasFedEndpoint comp api name = (HasUnsafeFedEndpoint comp api name) -- you to forget about some federated calls. type HasUnsafeFedEndpoint comp api name = 'Just api ~ LookupEndpoint (FedApi comp) name +-- | Constrains which endpoints can be used with FedQueueClient. +-- +-- Since the servant client implementation underlying FedQueueClient is +-- returning a "fake" response consisting of an empty object, we need to make +-- sure that an API type is compatible with an empty response if we want to +-- invoke it using `fedQueueClient` +class HasEmptyResponse api + +instance HasEmptyResponse (Post '[JSON] EmptyResponse) + +instance HasEmptyResponse api => HasEmptyResponse (x :> api) + +instance HasEmptyResponse api => HasEmptyResponse (Named name api) + -- | Return a client for a named endpoint. -- -- This function introduces an 'AddAnnotation' constraint, which is @@ -79,7 +95,11 @@ fedClient = clientIn (Proxy @api) (Proxy @m) fedQueueClient :: forall (comp :: Component) (name :: Symbol) m api. - (HasFedEndpoint comp api name, HasClient m api, m ~ FedQueueClient comp) => + ( HasEmptyResponse api, + HasFedEndpoint comp api name, + HasClient m api, + m ~ FedQueueClient comp + ) => Client m api fedQueueClient = clientIn (Proxy @api) (Proxy @m) 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 3c4d1db2fa6..783df1a2281 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 @@ -40,7 +40,6 @@ import Wire.API.MLS.SubConversation import Wire.API.MakesFederatedCall import Wire.API.Message import Wire.API.Routes.Public.Galley.Messaging -import Wire.API.Unreachable import Wire.API.Util.Aeson (CustomEncoded (..), CustomEncodedLensable (..)) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -97,7 +96,7 @@ type GalleyApi = ConversationUpdateRequest ConversationUpdateResponse :<|> FedEndpoint "mls-welcome" MLSWelcomeRequest MLSWelcomeResponse - :<|> FedEndpoint "on-mls-message-sent" RemoteMLSMessage RemoteMLSMessageResponse + :<|> FedEndpoint "on-mls-message-sent" RemoteMLSMessage EmptyResponse :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", @@ -419,7 +418,7 @@ data MLSMessageResponse MLSMessageResponseUnreachableBackends (Set Domain) | -- | If the list of unreachable users is non-empty, it corresponds to users -- that an application message could not be sent to. - MLSMessageResponseUpdates [ConversationUpdate] (Maybe UnreachableUsers) + MLSMessageResponseUpdates [ConversationUpdate] | MLSMessageResponseNonFederatingBackends NonFederatingBackends deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded MLSMessageResponse) diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 2efa6ca4d75..b3ac477ebed 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -786,19 +786,22 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do now <- input let lcnv = fmap (.convId) lconv e = conversationActionToEvent tag now quid (tUntagged lcnv) Nothing action - - let mkUpdate uids = + mkUpdate uids = ConversationUpdate now quid (tUnqualified lcnv) uids (SomeConversationAction tag action) - - update <- do - let remoteTargets = toList (bmRemotes targets) - updates <- - enqueueNotificationsConcurrently Q.Persistent remoteTargets $ \ruids -> do + handleError :: FederationError -> Sem r (Maybe ConversationUpdate) + handleError fedErr = + logRemoteNotificationError @"on-conversation-updated" fedErr $> Nothing + + update <- + fmap (fromMaybe (mkUpdate [])) + . (either handleError (pure . asum . map tUnqualified)) + <=< enqueueNotificationsConcurrently Q.Persistent (toList (bmRemotes targets)) + $ \ruids -> do let update = mkUpdate (tUnqualified ruids) -- if notifyOrigDomain is false, filter out user from quid's domain, -- because quid's backend will update local state and notify its users @@ -806,13 +809,6 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do if notifyOrigDomain || tDomain ruids /= qDomain quid then fedQueueClient @'Galley @"on-conversation-updated" update $> Nothing else pure (Just update) - case partitionEithers updates of - (ls :: [Remote ([UserId], FederationError)], rs) -> do - for_ ls $ - logError - "on-conversation-updated" - "An error occurred while communicating with federated server: " - pure $ fromMaybe (mkUpdate []) . asum . map tUnqualified $ rs -- notify local participants and bots pushConversationEvent con e (qualifyAs lcnv (bmLocals targets)) (bmBots targets) @@ -820,13 +816,6 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do -- return both the event and the 'ConversationUpdate' structure corresponding -- to the originating domain (if it is remote) pure $ LocalConversationUpdate e update - where - logError :: String -> String -> Remote (a, FederationError) -> Sem r () - logError field msg e = - P.warn $ - Log.field "federation call" field - . Log.field "domain" (_domainText (tDomain e)) - . Log.msg (msg <> displayException (snd (tUnqualified e))) -- | Update the local database with information on conversation members joining -- or leaving. Finally, push out notifications to local users. diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 18bb120620b..8913b86d9e7 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -78,7 +78,7 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Common (EmptyResponse (..)) -import Wire.API.Federation.API.Galley qualified as F +import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.MLS.CommitBundle import Wire.API.MLS.Credential @@ -127,7 +127,7 @@ onClientRemoved :: Member TinyLog r ) => Domain -> - F.ClientRemovedRequest -> + ClientRemovedRequest -> Sem r EmptyResponse onClientRemoved domain req = do let qusr = Qualified req.user domain @@ -136,7 +136,7 @@ onClientRemoved domain req = do mConv <- E.getConversation convId for mConv $ \conv -> do lconv <- qualifyLocal conv - removeClient lconv qusr (F.client req) + removeClient lconv qusr (req.client) pure EmptyResponse onConversationCreated :: @@ -148,17 +148,17 @@ onConversationCreated :: Member P.TinyLog r ) => Domain -> - F.ConversationCreated ConvId -> + ConversationCreated ConvId -> Sem r EmptyResponse onConversationCreated domain rc = do let qrc = fmap (toRemoteUnsafe domain) rc loc <- qualifyLocal () - let (localUserIds, _) = partitionQualified loc (map omQualifiedId (toList (F.nonCreatorMembers rc))) + let (localUserIds, _) = partitionQualified loc (map omQualifiedId (toList (nonCreatorMembers rc))) addedUserIds <- addLocalUsersToRemoteConv - (F.cnvId qrc) - (tUntagged (F.ccRemoteOrigUserId qrc)) + (cnvId qrc) + (tUntagged (ccRemoteOrigUserId qrc)) localUserIds let connectedMembers = @@ -169,16 +169,16 @@ onConversationCreated domain rc = do (const True) . omQualifiedId ) - (F.nonCreatorMembers rc) + (nonCreatorMembers rc) -- Make sure to notify only about local users connected to the adder - let qrcConnected = qrc {F.nonCreatorMembers = connectedMembers} + let qrcConnected = qrc {nonCreatorMembers = connectedMembers} for_ (fromConversationCreated loc qrcConnected) $ \(mem, c) -> do let event = Event - (tUntagged (F.cnvId qrcConnected)) + (tUntagged (cnvId qrcConnected)) Nothing - (tUntagged (F.ccRemoteOrigUserId qrcConnected)) + (tUntagged (ccRemoteOrigUserId qrcConnected)) qrcConnected.time (EdConversation c) pushConversationEvent Nothing event (qualifyAs loc [qUnqualified . Public.memId $ mem]) [] @@ -189,12 +189,12 @@ getConversations :: Member (Input (Local ())) r ) => Domain -> - F.GetConversationsRequest -> - Sem r F.GetConversationsResponse -getConversations domain (F.GetConversationsRequest uid cids) = do + GetConversationsRequest -> + Sem r GetConversationsResponse +getConversations domain (GetConversationsRequest uid cids) = do let ruid = toRemoteUnsafe domain uid loc <- qualifyLocal () - F.GetConversationsResponse + GetConversationsResponse . mapMaybe (Mapping.conversationToRemote (tDomain loc) ruid) <$> E.getConversations cids @@ -209,7 +209,7 @@ onConversationUpdated :: Member P.TinyLog r ) => Domain -> - F.ConversationUpdate -> + ConversationUpdate -> Sem r EmptyResponse onConversationUpdated requestingDomain cu = do let rcu = toRemoteUnsafe requestingDomain cu @@ -232,18 +232,18 @@ leaveConversation :: Member TinyLog r ) => Domain -> - F.LeaveConversationRequest -> - Sem r F.LeaveConversationResponse + LeaveConversationRequest -> + Sem r LeaveConversationResponse leaveConversation requestingDomain lc = do let leaver = Qualified lc.leaver requestingDomain lcnv <- qualifyLocal lc.convId res <- runError - . mapToRuntimeError @'ConvNotFound F.RemoveFromConversationErrorNotFound - . mapToRuntimeError @('ActionDenied 'LeaveConversation) F.RemoveFromConversationErrorRemovalNotAllowed - . mapToRuntimeError @'InvalidOperation F.RemoveFromConversationErrorRemovalNotAllowed - . mapError @NoChanges (const F.RemoveFromConversationErrorUnchanged) + . mapToRuntimeError @'ConvNotFound RemoveFromConversationErrorNotFound + . mapToRuntimeError @('ActionDenied 'LeaveConversation) RemoveFromConversationErrorRemovalNotAllowed + . mapToRuntimeError @'InvalidOperation RemoveFromConversationErrorRemovalNotAllowed + . mapError @NoChanges (const RemoveFromConversationErrorUnchanged) $ do (conv, _self) <- getConversationAndMemberWithError @'ConvNotFound leaver lcnv outcome <- @@ -262,7 +262,7 @@ leaveConversation requestingDomain lc = do Right _ -> pure conv case res of - Left e -> pure $ F.LeaveConversationResponse (Left e) + Left e -> pure $ LeaveConversationResponse (Left e) Right conv -> do let remotes = filter ((== qDomain leaver) . tDomain) (rmId <$> Data.convRemoteMembers conv) let botsAndMembers = BotsAndMembers mempty (Set.fromList remotes) mempty @@ -283,7 +283,7 @@ leaveConversation requestingDomain lc = do throw . internalErr $ e Right _ -> pure () - pure $ F.LeaveConversationResponse (Right ()) + pure $ LeaveConversationResponse (Right ()) where internalErr = InternalErrorWithDescription . LT.pack . displayException @@ -298,17 +298,17 @@ onMessageSent :: Member P.TinyLog r ) => Domain -> - F.RemoteMessage ConvId -> + RemoteMessage ConvId -> Sem r EmptyResponse onMessageSent domain rmUnqualified = do let rm = fmap (toRemoteUnsafe domain) rmUnqualified convId = tUntagged rm.conversation msgMetadata = MessageMetadata - { mmNativePush = F.push rm, - mmTransient = F.transient rm, - mmNativePriority = F.priority rm, - mmData = F._data rm + { mmNativePush = push rm, + mmTransient = transient rm, + mmNativePriority = priority rm, + mmData = _data rm } recipientMap = userClientMap rm.recipients msgs = toMapOf (itraversed <.> itraversed) recipientMap @@ -354,13 +354,13 @@ sendMessage :: Member P.TinyLog r ) => Domain -> - F.ProteusMessageSendRequest -> - Sem r F.MessageSendResponse + ProteusMessageSendRequest -> + Sem r MessageSendResponse sendMessage originDomain msr = do let sender = Qualified msr.sender originDomain msg <- either throwErr pure (fromProto (fromBase64ByteString msr.rawMessage)) lcnv <- qualifyLocal msr.convId - F.MessageSendResponse <$> postQualifiedOtrMessage User sender Nothing lcnv msg + MessageSendResponse <$> postQualifiedOtrMessage User sender Nothing lcnv msg where throwErr = throw . InvalidPayload . LT.pack @@ -379,12 +379,12 @@ onUserDeleted :: Member TinyLog r ) => Domain -> - F.UserDeletedConversationsNotification -> + UserDeletedConversationsNotification -> Sem r EmptyResponse onUserDeleted origDomain udcn = do let deletedUser = toRemoteUnsafe origDomain udcn.user untaggedDeletedUser = tUntagged deletedUser - convIds = F.conversations udcn + convIds = conversations udcn E.spawnMany $ fromRange convIds <&> \c -> do @@ -445,14 +445,14 @@ updateConversation :: Member (Input (Local ())) r ) => Domain -> - F.ConversationUpdateRequest -> - Sem r F.ConversationUpdateResponse + ConversationUpdateRequest -> + Sem r ConversationUpdateResponse updateConversation origDomain updateRequest = do loc <- qualifyLocal () let rusr = toRemoteUnsafe origDomain updateRequest.user lcnv = qualifyAs loc updateRequest.convId - mkResponse $ case F.action updateRequest of + mkResponse $ case action updateRequest of SomeConversationAction tag action -> case tag of SConversationJoinTag -> mapToGalleyError @(HasConversationActionGalleyErrors 'ConversationJoinTag) @@ -499,15 +499,15 @@ updateConversation origDomain updateRequest = do $ updateLocalConversation @'ConversationAccessDataTag lcnv (tUntagged rusr) Nothing action where mkResponse = - fmap (either F.ConversationUpdateResponseError Imports.id) + fmap (either ConversationUpdateResponseError Imports.id) . runError @GalleyError - . fmap (fromRight F.ConversationUpdateResponseNoChanges) + . fmap (fromRight ConversationUpdateResponseNoChanges) . runError @NoChanges - . fmap (either F.ConversationUpdateResponseNonFederatingBackends Imports.id) + . fmap (either ConversationUpdateResponseNonFederatingBackends Imports.id) . runError @NonFederatingBackends - . fmap (either F.ConversationUpdateResponseUnreachableBackends id) + . fmap (either ConversationUpdateResponseUnreachableBackends Imports.id) . runError @UnreachableBackends - . fmap F.ConversationUpdateResponseUpdate + . fmap ConversationUpdateResponseUpdate handleMLSMessageErrors :: ( r1 @@ -521,18 +521,18 @@ handleMLSMessageErrors :: ': r ) ) => - Sem r1 F.MLSMessageResponse -> - Sem r F.MLSMessageResponse + Sem r1 MLSMessageResponse -> + Sem r MLSMessageResponse handleMLSMessageErrors = - fmap (either (F.MLSMessageResponseProtocolError . unTagged) Imports.id) + fmap (either (MLSMessageResponseProtocolError . unTagged) Imports.id) . runError @MLSProtocolError - . fmap (either F.MLSMessageResponseError Imports.id) + . fmap (either MLSMessageResponseError Imports.id) . runError - . fmap (either (F.MLSMessageResponseProposalFailure . pfInner) Imports.id) + . fmap (either (MLSMessageResponseProposalFailure . pfInner) Imports.id) . runError - . fmap (either F.MLSMessageResponseNonFederatingBackends Imports.id) + . fmap (either MLSMessageResponseNonFederatingBackends Imports.id) . runError - . fmap (either (F.MLSMessageResponseUnreachableBackends . Set.fromList . (.backends)) id) + . fmap (either (MLSMessageResponseUnreachableBackends . Set.fromList . (.backends)) Imports.id) . runError @UnreachableBackends . mapToGalleyError @MLSBundleStaticErrors @@ -557,8 +557,8 @@ sendMLSCommitBundle :: Member ProposalStore r ) => Domain -> - F.MLSMessageSendRequest -> - Sem r F.MLSMessageResponse + MLSMessageSendRequest -> + Sem r MLSMessageResponse sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do assertMLSEnabled loc <- qualifyLocal () @@ -566,8 +566,8 @@ sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do bundle <- either (throw . mlsProtocolError) pure $ deserializeCommitBundle (fromBase64ByteString msr.rawMessage) let msg = rmValue (cbCommitMsg bundle) qcnv <- E.getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound - when (Conv (qUnqualified qcnv) /= F.convOrSubId msr) $ throwS @'MLSGroupConversationMismatch - uncurry F.MLSMessageResponseUpdates . (,mempty) . map lcuUpdate + when (Conv (qUnqualified qcnv) /= convOrSubId msr) $ throwS @'MLSGroupConversationMismatch + MLSMessageResponseUpdates . map lcuUpdate <$> postMLSCommitBundle loc (tUntagged sender) Nothing qcnv Nothing bundle sendMLSMessage :: @@ -591,8 +591,8 @@ sendMLSMessage :: Member ProposalStore r ) => Domain -> - F.MLSMessageSendRequest -> - Sem r F.MLSMessageResponse + MLSMessageSendRequest -> + Sem r MLSMessageResponse sendMLSMessage remoteDomain msr = handleMLSMessageErrors $ do assertMLSEnabled loc <- qualifyLocal () @@ -601,9 +601,10 @@ sendMLSMessage remoteDomain msr = handleMLSMessageErrors $ do case rmValue raw of SomeMessage _ msg -> do qcnv <- E.getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound - when (Conv (qUnqualified qcnv) /= F.convOrSubId msr) $ throwS @'MLSGroupConversationMismatch - uncurry F.MLSMessageResponseUpdates - . first (map lcuUpdate) + when (Conv (qUnqualified qcnv) /= convOrSubId msr) $ throwS @'MLSGroupConversationMismatch + MLSMessageResponseUpdates + . map lcuUpdate + . fst <$> postMLSMessage loc (tUntagged sender) Nothing qcnv Nothing raw class ToGalleyRuntimeError (effs :: EffectRow) r where @@ -637,10 +638,10 @@ mlsSendWelcome :: Member (Input UTCTime) r ) => Domain -> - F.MLSWelcomeRequest -> - Sem r F.MLSWelcomeResponse -mlsSendWelcome _origDomain (fromBase64ByteString . F.mlsWelcomeRequest -> rawWelcome) = - fmap (either (const F.MLSWelcomeMLSNotEnabled) (const F.MLSWelcomeSent)) + MLSWelcomeRequest -> + Sem r MLSWelcomeResponse +mlsSendWelcome _origDomain (fromBase64ByteString . mlsWelcomeRequest -> rawWelcome) = + fmap (either (const MLSWelcomeMLSNotEnabled) (const MLSWelcomeSent)) . runError @(Tagged 'MLSNotEnabled ()) $ do assertMLSEnabled @@ -669,10 +670,11 @@ onMLSMessageSent :: Member P.TinyLog r ) => Domain -> - F.RemoteMLSMessage -> - Sem r F.RemoteMLSMessageResponse + RemoteMLSMessage -> + Sem r EmptyResponse onMLSMessageSent domain rmm = - fmap (either (const F.RemoteMLSMessageMLSNotEnabled) (const F.RemoteMLSMessageOk)) + (EmptyResponse <$) + . (logError =<<) . runError @(Tagged 'MLSNotEnabled ()) $ do assertMLSEnabled @@ -699,6 +701,15 @@ onMLSMessageSent domain rmm = runMessagePush loc (Just (tUntagged rcnv)) $ newMessagePush mempty Nothing rmm.metadata recipients e + where + logError :: Member P.TinyLog r => Either (Tagged 'MLSNotEnabled ()) () -> Sem r () + logError (Left _) = + P.warn $ + Log.field "conversation" (toByteString' rmm.conversation) + Log.~~ Log.field "domain" (toByteString' domain) + Log.~~ Log.msg + ("Cannot process remote MLS message because MLS is disabled on this backend" :: ByteString) + logError _ = pure () queryGroupInfo :: ( Member ConversationStore r, @@ -707,10 +718,10 @@ queryGroupInfo :: Member MemberStore r ) => Domain -> - F.GetGroupInfoRequest -> - Sem r F.GetGroupInfoResponse + GetGroupInfoRequest -> + Sem r GetGroupInfoResponse queryGroupInfo origDomain req = - fmap (either F.GetGroupInfoResponseError F.GetGroupInfoResponseState) + fmap (either GetGroupInfoResponseError GetGroupInfoResponseState) . runError @GalleyError . mapToGalleyError @MLSGroupInfoStaticErrors $ do @@ -731,9 +742,9 @@ updateTypingIndicator :: Member (Input (Local ())) r ) => Domain -> - F.TypingDataUpdateRequest -> - Sem r F.TypingDataUpdateResponse -updateTypingIndicator origDomain F.TypingDataUpdateRequest {..} = do + TypingDataUpdateRequest -> + Sem r TypingDataUpdateResponse +updateTypingIndicator origDomain TypingDataUpdateRequest {..} = do let qusr = Qualified userId origDomain lcnv <- qualifyLocal convId @@ -743,15 +754,15 @@ updateTypingIndicator origDomain F.TypingDataUpdateRequest {..} = do (conv, _) <- getConversationAndMemberWithError @'ConvNotFound qusr lcnv notifyTypingIndicator conv qusr Nothing typingStatus - pure (either F.TypingDataUpdateError F.TypingDataUpdateSuccess ret) + pure (either TypingDataUpdateError TypingDataUpdateSuccess ret) onTypingIndicatorUpdated :: ( Member GundeckAccess r ) => Domain -> - F.TypingDataUpdated -> + TypingDataUpdated -> Sem r EmptyResponse -onTypingIndicatorUpdated origDomain F.TypingDataUpdated {..} = do +onTypingIndicatorUpdated origDomain TypingDataUpdated {..} = do let qcnv = Qualified convId origDomain pushTypingIndicatorEvents origUserId time usersInConv Nothing qcnv typingStatus pure EmptyResponse diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 781e8258f29..7781b7379b6 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -33,7 +33,6 @@ import Data.Domain import Data.Id import Data.Json.Util import Data.List.NonEmpty (NonEmpty, nonEmpty) -import Data.List.NonEmpty qualified as NE import Data.Map qualified as Map import Data.Qualified import Data.Set qualified as Set @@ -311,7 +310,6 @@ postMLSCommitBundleToRemoteConv :: ( Member BrigAccess r, Members MLSBundleStaticErrors r, Member (Error FederationError) r, - Member (Error InternalError) r, Member (Error MLSProtocolError) r, Member (Error MLSProposalFailure) r, Member (Error NonFederatingBackends) r, @@ -347,13 +345,7 @@ postMLSCommitBundleToRemoteConv loc qusr con bundle rcnv = do MLSMessageResponseProtocolError e -> throw (mlsProtocolError e) MLSMessageResponseProposalFailure e -> throw (MLSProposalFailure e) MLSMessageResponseUnreachableBackends ds -> throw (UnreachableBackends (toList ds)) - MLSMessageResponseUpdates updates unreachables -> do - for_ unreachables $ \us -> - throw . InternalErrorWithDescription $ - "A commit to a remote conversation should not ever return a \ - \non-empty list of users an application message could not be \ - \sent to. The remote end returned: " - <> LT.pack (intercalate ", " (show <$> NE.toList (unreachableUsers us))) + MLSMessageResponseUpdates updates -> do fmap fst . runOutputList . runInputConst (void loc) $ for_ updates $ \update -> do me <- updateLocalStateOfRemoteConv (qualifyAs rcnv update) con @@ -538,12 +530,12 @@ postMLSMessageToRemoteConv loc qusr _senderClient con smsg rcnv = do \not ever return a non-empty list of domains a commit could not be \ \sent to. The remote end returned: " <> LT.pack (intercalate ", " (show <$> Set.toList (Set.map domainText ds))) - MLSMessageResponseUpdates updates unreachables -> do + MLSMessageResponseUpdates updates -> do lcus <- fmap fst . runOutputList $ for_ updates $ \update -> do me <- updateLocalStateOfRemoteConv (qualifyAs rcnv update) con for_ me $ \e -> output (LocalConversationUpdate e update) - pure (lcus, unreachables) + pure (lcus, Nothing) MLSMessageResponseNonFederatingBackends e -> throw e type HasProposalEffects r = diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index d40758fa735..433d9f791d8 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -18,8 +18,6 @@ module Galley.API.MLS.Propagate where import Control.Comonad -import Data.Aeson qualified as A -import Data.Domain import Data.Id import Data.Json.Util import Data.Map qualified as Map @@ -33,17 +31,12 @@ import Galley.Effects import Galley.Effects.FederatorAccess import Galley.Types.Conversations.Members import Imports -import Network.Wai.Utilities.JSONResponse import Polysemy import Polysemy.Input import Polysemy.TinyLog hiding (trace) -import System.Logger.Class qualified as Logger -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.Federation.Error import Wire.API.Message import Wire.API.Unreachable @@ -81,19 +74,19 @@ propagateMessage qusr lconv cm con raw = do runMessagePush lconv (Just qcnv) (mkPush u c) -- send to remotes - unreachableFromList . concat - <$$> traverse handleError - <=< runFederatedConcurrentlyEither (map remoteMemberQualify rmems) - $ \(tUnqualified -> rs) -> - fedClient @'Galley @"on-mls-message-sent" $ - RemoteMLSMessage - { time = now, - sender = qusr, - metadata = mm, - conversation = tUnqualified lcnv, - recipients = rs >>= remoteMemberMLSClients, - message = Base64ByteString raw - } + void $ + runFederatedConcurrentlyEither (map remoteMemberQualify rmems) $ + \(tUnqualified -> rs) -> + fedClient @'Galley @"on-mls-message-sent" $ + RemoteMLSMessage + { time = now, + sender = qusr, + metadata = mm, + conversation = tUnqualified lcnv, + recipients = rs >>= remoteMemberMLSClients, + message = Base64ByteString raw + } + pure Nothing where localMemberMLSClients :: Local x -> LocalMember -> [(UserId, ClientId)] localMemberMLSClients loc lm = @@ -110,24 +103,3 @@ propagateMessage qusr lconv cm con raw = do in map (\(c, _) -> (remoteUserId, c)) (toList (Map.findWithDefault mempty remoteUserQId cm)) - - remotesToQIds = fmap (tUntagged . rmId) - - handleError :: - Member TinyLog r => - Either (Remote [RemoteMember], FederationError) (Remote RemoteMLSMessageResponse) -> - Sem r [Qualified UserId] - handleError (Right x) = case tUnqualified x of - RemoteMLSMessageOk -> pure [] - RemoteMLSMessageMLSNotEnabled -> do - logFedError x (errorToResponse @'MLSNotEnabled) - pure [] - handleError (Left (r, e)) = do - logFedError r (toResponse e) - pure $ remotesToQIds (tUnqualified r) - logFedError :: Member TinyLog r => Remote x -> JSONResponse -> Sem r () - logFedError r e = - warn $ - Logger.msg ("A message could not be delivered to a remote backend" :: ByteString) - . Logger.field "remote_domain" (domainText (tDomain r)) - . Logger.field "error" (A.encode e.value) diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 9fd9d441d86..d441ee02fd8 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -37,6 +37,7 @@ import Data.Set qualified as Set import Data.Singletons import Data.Text qualified as T import Data.Time +import GHC.TypeLits import Galley.API.Error import Galley.API.Mapping import Galley.Data.Conversation qualified as Data @@ -67,6 +68,7 @@ import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P +import System.Logger qualified as Log import Wire.API.Connection import Wire.API.Conversation hiding (Member, cnvAccess, cnvAccessRoles, cnvName, cnvType) import Wire.API.Conversation qualified as Public @@ -1008,3 +1010,13 @@ instance if err' == demote @e then throwS @e else rethrowErrors @effs @r err' + +logRemoteNotificationError :: + forall rpc r. + (Member P.TinyLog r, KnownSymbol rpc) => + FederationError -> + Sem r () +logRemoteNotificationError e = + P.warn $ + Log.field "federation call" (symbolVal (Proxy @rpc)) + . Log.msg (displayException e) diff --git a/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs b/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs index ac006ded4c5..bdefa146314 100644 --- a/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs +++ b/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs @@ -22,6 +22,6 @@ data BackendNotificationQueueAccess m a where Q.DeliveryMode -> f (Remote x) -> (Remote [x] -> FedQueueClient c a) -> - BackendNotificationQueueAccess m [Either (Remote ([x], FederationError)) (Remote a)] + BackendNotificationQueueAccess m (Either FederationError [Remote a]) makeSem ''BackendNotificationQueueAccess diff --git a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs index b9affe48585..411897a0834 100644 --- a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs +++ b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs @@ -4,8 +4,8 @@ module Galley.Intra.BackendNotificationQueue (interpretBackendNotificationQueueA import Control.Lens (view) import Control.Monad.Catch +import Control.Monad.Trans.Except import Control.Retry -import Data.Bifunctor import Data.Domain import Data.Qualified import Galley.Effects.BackendNotificationQueueAccess (BackendNotificationQueueAccess (..)) @@ -29,22 +29,21 @@ interpretBackendNotificationQueueAccess :: Sem r a interpretBackendNotificationQueueAccess = interpret $ \case EnqueueNotification remote deliveryMode action -> do - embedApp $ enqueueNotification (tDomain remote) deliveryMode action + embedApp . runExceptT $ enqueueNotification (tDomain remote) deliveryMode action EnqueueNotificationsConcurrently m xs rpc -> do - embedApp $ enqueueNotificationsConcurrently m xs rpc + embedApp . runExceptT $ enqueueNotificationsConcurrently m xs rpc -enqueueNotification :: Domain -> Q.DeliveryMode -> FedQueueClient c a -> App (Either FederationError a) -enqueueNotification remoteDomain deliveryMode action = do - mChanVar <- view rabbitmqChannel +getChannel :: ExceptT FederationError App (MVar Q.Channel) +getChannel = view rabbitmqChannel >>= maybe (throwE FederationNotConfigured) pure + +enqueueSingleNotification :: Domain -> Q.DeliveryMode -> MVar Q.Channel -> FedQueueClient c a -> App a +enqueueSingleNotification remoteDomain deliveryMode chanVar action = do ownDomain <- view (options . settings . federationDomain) - case mChanVar of - Nothing -> pure (Left FederationNotConfigured) - Just chanVar -> do - let policy = limitRetries 3 <> constantDelay 1_000_000 - handlers = - skipAsyncExceptions - <> [logRetries (const $ pure True) logError] - Right <$> recovering policy handlers (const $ go ownDomain chanVar) + let policy = limitRetries 3 <> constantDelay 1_000_000 + handlers = + skipAsyncExceptions + <> [logRetries (const $ pure True) logError] + recovering policy handlers (const $ go ownDomain) where logError willRetry (SomeException e) status = do Log.err $ @@ -52,25 +51,29 @@ enqueueNotification remoteDomain deliveryMode action = do . Log.field "error" (displayException e) . Log.field "willRetry" willRetry . Log.field "retryCount" status.rsIterNumber - go ownDomain chanVar = do + go ownDomain = do mChan <- timeout 1_000_000 (readMVar chanVar) case mChan of Nothing -> throwM NoRabbitMqChannel Just chan -> do liftIO $ enqueue chan ownDomain remoteDomain deliveryMode action +enqueueNotification :: Domain -> Q.DeliveryMode -> FedQueueClient c a -> ExceptT FederationError App a +enqueueNotification remoteDomain deliveryMode action = do + chanVar <- getChannel + lift $ enqueueSingleNotification remoteDomain deliveryMode chanVar action + enqueueNotificationsConcurrently :: (Foldable f, Functor f) => Q.DeliveryMode -> f (Remote x) -> (Remote [x] -> FedQueueClient c a) -> - App [(Either (Remote ([x], FederationError)) (Remote a))] -enqueueNotificationsConcurrently m xs f = - pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> - bimap - (qualifyAs r . (tUnqualified r,)) - (qualifyAs r) - <$> enqueueNotification (tDomain r) m (f r) + ExceptT FederationError App [Remote a] +enqueueNotificationsConcurrently m xs f = do + chanVar <- getChannel + lift $ pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> + qualifyAs r + <$> enqueueSingleNotification (tDomain r) m chanVar (f r) data NoRabbitMqChannel = NoRabbitMqChannel deriving (Show) diff --git a/services/galley/test/integration/API/MLS/Mocks.hs b/services/galley/test/integration/API/MLS/Mocks.hs index 1b58cd9df51..10974405b8e 100644 --- a/services/galley/test/integration/API/MLS/Mocks.hs +++ b/services/galley/test/integration/API/MLS/Mocks.hs @@ -80,7 +80,6 @@ sendMessageMock = "send-mls-message" ~> MLSMessageResponseUpdates [] - mempty claimKeyPackagesMock :: KeyPackageBundle -> Mock LByteString claimKeyPackagesMock kpb = "claim-key-packages" ~> kpb From 8e4b24c9bfa9c86bd8679ce9e2b67be78cfa3c1f Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 4 Oct 2023 14:11:14 +0200 Subject: [PATCH 165/225] Align connectUsers to mls branch (#3630) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Align connectUsers to mls branch There have been some diverging changes on the mls branch related to the way users are created and connected in tests. This aligns the version on develop with that on mls. * Rename connectUsers2 → connectTwoUsers --- integration/test/SetupHelpers.hs | 21 +++++--- integration/test/Test/AccessUpdate.hs | 10 ++-- integration/test/Test/Client.hs | 2 +- integration/test/Test/Conversation.hs | 70 +++++++++++++-------------- integration/test/Test/Demo.hs | 6 +-- integration/test/Test/Federation.hs | 12 ++--- integration/test/Test/MessageTimer.hs | 4 +- integration/test/Test/Roles.hs | 8 +-- 8 files changed, 70 insertions(+), 63 deletions(-) diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 28f25826954..2ff353f89ec 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -64,7 +64,7 @@ createTeamMember inviter tid = do <&> addJSONObject registerJSON getJSON 201 =<< submit "POST" registerReq -connectUsers :: +connectTwoUsers :: ( HasCallStack, MakesValue alice, MakesValue bob @@ -72,15 +72,22 @@ connectUsers :: alice -> bob -> App () -connectUsers alice bob = do +connectTwoUsers alice bob = do bindResponse (Brig.postConnection alice bob) (\resp -> resp.status `shouldMatchInt` 201) bindResponse (Brig.putConnection bob alice "accepted") (\resp -> resp.status `shouldMatchInt` 200) -createAndConnectUsers :: (HasCallStack, MakesValue domain) => domain -> domain -> App (Value, Value) -createAndConnectUsers d1 d2 = do - [u1, u2] <- for [d1, d2] (flip randomUser def) - connectUsers u1 u2 - pure (u1, u2) +connectUsers :: HasCallStack => [Value] -> App () +connectUsers users = traverse_ (uncurry connectTwoUsers) $ do + t <- tails users + (a, others) <- maybeToList (uncons t) + b <- others + pure (a, b) + +createAndConnectUsers :: (HasCallStack, MakesValue domain) => [domain] -> App [Value] +createAndConnectUsers domains = do + users <- for domains (flip randomUser def) + connectUsers users + pure users createUsers :: (HasCallStack, MakesValue domain) => [domain] -> App [Value] createUsers domains = for domains (flip randomUser def) diff --git a/integration/test/Test/AccessUpdate.hs b/integration/test/Test/AccessUpdate.hs index 52358f435ae..c2ed1964e16 100644 --- a/integration/test/Test/AccessUpdate.hs +++ b/integration/test/Test/AccessUpdate.hs @@ -37,7 +37,7 @@ testAccessUpdateGuestRemoved = do (alice, tid, [bob]) <- createTeam OwnDomain 2 charlie <- randomUser OwnDomain def dee <- randomUser OtherDomain def - mapM_ (connectUsers alice) [charlie, dee] + mapM_ (connectTwoUsers alice) [charlie, dee] [aliceClient, bobClient, charlieClient, deeClient] <- mapM (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) @@ -70,7 +70,7 @@ testAccessUpdateGuestRemovedUnreachableRemotes = do resourcePool <- asks resourcePool (alice, tid, [bob]) <- createTeam OwnDomain 2 charlie <- randomUser OwnDomain def - connectUsers alice charlie + connectTwoUsers alice charlie [aliceClient, bobClient, charlieClient] <- mapM (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) @@ -78,7 +78,7 @@ testAccessUpdateGuestRemovedUnreachableRemotes = do (conv, dee) <- runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do dee <- randomUser dynBackend.berDomain def - connectUsers alice dee + connectTwoUsers alice dee conv <- postConversation alice @@ -104,8 +104,8 @@ testAccessUpdateGuestRemovedUnreachableRemotes = do testAccessUpdateWithRemotes :: HasCallStack => App () testAccessUpdateWithRemotes = do [alice, bob, charlie] <- createUsers [OwnDomain, OtherDomain, OwnDomain] - connectUsers alice bob - connectUsers alice charlie + connectTwoUsers alice bob + connectTwoUsers alice charlie conv <- postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) >>= getJSON 201 diff --git a/integration/test/Test/Client.hs b/integration/test/Test/Client.hs index 8ff58bd62b3..4ee88901419 100644 --- a/integration/test/Test/Client.hs +++ b/integration/test/Test/Client.hs @@ -46,7 +46,7 @@ testListClientsIfBackendIsOffline = do resourcePool <- asks (.resourcePool) ownDomain <- asString OwnDomain otherDomain <- asString OtherDomain - (ownUser1, ownUser2) <- createAndConnectUsers OwnDomain OtherDomain + [ownUser1, ownUser2] <- createAndConnectUsers [OwnDomain, OtherDomain] ownClient1 <- objId $ bindResponse (API.addClient ownUser1 def) $ getJSON 201 ownClient2 <- objId $ bindResponse (API.addClient ownUser2 def) $ getJSON 201 ownUser1Id <- objId ownUser1 diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 548c5b6e611..79f548471e7 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -140,8 +140,8 @@ testCreateConversationFullyConnected :: HasCallStack => App () testCreateConversationFullyConnected = do startDynamicBackends [def, def, def] $ \[domainA, domainB, domainC] -> do [u1, u2, u3] <- createUsers [domainA, domainB, domainC] - connectUsers u1 u2 - connectUsers u1 u3 + connectTwoUsers u1 u2 + connectTwoUsers u1 u3 bindResponse (postConversation u1 (defProteus {qualifiedUsers = [u2, u3]})) $ \resp -> do resp.status `shouldMatchInt` 201 @@ -158,8 +158,8 @@ testCreateConversationNonFullyConnected = do u1 <- randomUser domainA def u2 <- randomUser domainB def u3 <- randomUser domainC def - connectUsers u1 u2 - connectUsers u1 u3 + connectTwoUsers u1 u2 + connectTwoUsers u1 u3 bindResponse (postConversation u1 (defProteus {qualifiedUsers = [u2, u3]})) $ \resp -> do resp.status `shouldMatchInt` 409 @@ -169,8 +169,8 @@ testAddMembersFullyConnectedProteus :: HasCallStack => App () testAddMembersFullyConnectedProteus = do startDynamicBackends [def, def, def] $ \[domainA, domainB, domainC] -> do [u1, u2, u3] <- createUsers [domainA, domainB, domainC] - connectUsers u1 u2 - connectUsers u1 u3 + connectTwoUsers u1 u2 + connectTwoUsers u1 u3 -- create conversation with no users cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 -- add members from remote backends @@ -194,8 +194,8 @@ testAddMembersNonFullyConnectedProteus = do u1 <- randomUser domainA def u2 <- randomUser domainB def u3 <- randomUser domainC def - connectUsers u1 u2 - connectUsers u1 u3 + connectTwoUsers u1 u2 + connectTwoUsers u1 u3 -- create conversation with no users cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 @@ -217,7 +217,7 @@ testAddMember = do bindResponse addMember $ \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "not-connected" - connectUsers alice bob + connectTwoUsers alice bob bindResponse addMember $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "type" `shouldMatch` "conversation.member-join" @@ -244,7 +244,7 @@ testAddMember = do testAddMemberV1 :: HasCallStack => Domain -> App () testAddMemberV1 domain = do - (alice, bob) <- createAndConnectUsers OwnDomain domain + [alice, bob] <- createAndConnectUsers [OwnDomain, domain] conv <- postConversation alice defProteus >>= getJSON 201 bobId <- bob %. "qualified_id" let opts = @@ -268,7 +268,7 @@ testConvWithUnreachableRemoteUsers = do own <- make OwnDomain & asString other <- make OtherDomain & asString users@(alice : others) <- createUsers $ [own, own, other] <> domains - forM_ others $ connectUsers alice + forM_ others $ connectTwoUsers alice pure (users, domains) let newConv = defProteus {qualifiedUsers = [alex, bob, charlie, dylan]} @@ -287,11 +287,11 @@ testAddReachableWithUnreachableRemoteUsers = do own <- make OwnDomain & asString other <- make OtherDomain & asString [alice, alex, bob, charlie, dylan] <- createUsers $ [own, own, other] <> domains - forM_ [alex, bob, charlie, dylan] $ connectUsers alice + forM_ [alex, bob, charlie, dylan] $ connectTwoUsers alice let newConv = defProteus {qualifiedUsers = [alex, charlie, dylan]} conv <- postConversation alice newConv >>= getJSON 201 - connectUsers alex bob + connectTwoUsers alex bob pure ([alex, bob], conv, domains) bobId <- bob %. "qualified_id" @@ -310,11 +310,11 @@ testAddUnreachable = do startDynamicBackends [def, def] $ \domains -> do own <- make OwnDomain & asString [alice, alex, charlie, dylan] <- createUsers $ [own, own] <> domains - forM_ [alex, charlie, dylan] $ connectUsers alice + forM_ [alex, charlie, dylan] $ connectTwoUsers alice let newConv = defProteus {qualifiedUsers = [alex, dylan]} conv <- postConversation alice newConv >>= getJSON 201 - connectUsers alex charlie + connectTwoUsers alex charlie pure ([alex, charlie], domains, conv) charlieId <- charlie %. "qualified_id" @@ -344,7 +344,7 @@ testAddingUserNonFullyConnectedFederation = do charlie <- randomUser dynBackend def -- We use retryT here so the dynamic federated connection changes can take -- some time to be propagated. Remove after fixing https://wearezeta.atlassian.net/browse/WPB-3797 - mapM_ (retryT . connectUsers alice) [bob, charlie] + mapM_ (retryT . connectTwoUsers alice) [bob, charlie] let newConv = defProteus {qualifiedUsers = []} conv <- postConversation alice newConv >>= getJSON 201 @@ -445,7 +445,7 @@ testAddUserWhenOtherBackendOffline = do startDynamicBackends [def] $ \domains -> do own <- make OwnDomain & asString [alice, alex, charlie] <- createUsers $ [own, own] <> domains - forM_ [alex, charlie] $ connectUsers alice + forM_ [alex, charlie] $ connectTwoUsers alice let newConv = defProteus {qualifiedUsers = [charlie]} conv <- postConversation alice newConv >>= getJSON 201 @@ -456,13 +456,13 @@ testAddUserWhenOtherBackendOffline = do testSynchroniseUserRemovalNotification :: HasCallStack => App () testSynchroniseUserRemovalNotification = do resourcePool <- asks resourcePool - (alice, bob) <- createAndConnectUsers OwnDomain OtherDomain + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> do (conv, charlie, client) <- runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do charlie <- randomUser dynBackend.berDomain def client <- objId $ bindResponse (addClient charlie def) $ getJSON 201 - mapM_ (connectUsers charlie) [alice, bob] + mapM_ (connectTwoUsers charlie) [alice, bob] conv <- postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) >>= getJSON 201 @@ -482,7 +482,7 @@ testSynchroniseUserRemovalNotification = do testConvRenaming :: HasCallStack => App () testConvRenaming = do - (alice, bob) <- createAndConnectUsers OwnDomain OtherDomain + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] conv <- postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 @@ -496,7 +496,7 @@ testConvRenaming = do testReceiptModeWithRemotesOk :: HasCallStack => App () testReceiptModeWithRemotesOk = do - (alice, bob) <- createAndConnectUsers OwnDomain OtherDomain + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] conv <- postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 @@ -514,7 +514,7 @@ testReceiptModeWithRemotesUnreachable = do alice <- randomUser ownDomain def conv <- startDynamicBackends [mempty] $ \[dynBackend] -> do bob <- randomUser dynBackend def - connectUsers alice bob + connectTwoUsers alice bob postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 withWebSocket alice $ \ws -> do @@ -527,8 +527,8 @@ testReceiptModeWithRemotesUnreachable = do testDeleteLocalMember :: HasCallStack => App () testDeleteLocalMember = do [alice, alex, bob] <- createUsers [OwnDomain, OwnDomain, OtherDomain] - connectUsers alice alex - connectUsers alice bob + connectTwoUsers alice alex + connectTwoUsers alice bob conv <- postConversation alice (defProteus {qualifiedUsers = [alex, bob]}) >>= getJSON 201 @@ -546,8 +546,8 @@ testDeleteLocalMember = do testDeleteRemoteMember :: HasCallStack => App () testDeleteRemoteMember = do [alice, alex, bob] <- createUsers [OwnDomain, OwnDomain, OtherDomain] - connectUsers alice alex - connectUsers alice bob + connectTwoUsers alice alex + connectTwoUsers alice bob conv <- postConversation alice (defProteus {qualifiedUsers = [alex, bob]}) >>= getJSON 201 @@ -567,9 +567,9 @@ testDeleteRemoteMemberRemoteUnreachable = do [alice, bob, bart] <- createUsers [OwnDomain, OtherDomain, OtherDomain] conv <- startDynamicBackends [mempty] $ \[dynBackend] -> do charlie <- randomUser dynBackend def - connectUsers alice bob - connectUsers alice bart - connectUsers alice charlie + connectTwoUsers alice bob + connectTwoUsers alice bart + connectTwoUsers alice charlie postConversation alice (defProteus {qualifiedUsers = [bob, bart, charlie]}) @@ -591,7 +591,7 @@ testDeleteTeamConversationWithRemoteMembers = do (alice, team, _) <- createTeam OwnDomain 1 conv <- postConversation alice (defProteus {team = Just team}) >>= getJSON 201 bob <- randomUser OtherDomain def - connectUsers alice bob + connectTwoUsers alice bob mem <- bob %. "qualified_id" void $ addMembers alice conv def {users = [mem]} >>= getBody 200 @@ -617,7 +617,7 @@ testDeleteTeamConversationWithUnreachableRemoteMembers = do (bob, bobClient) <- runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do bob <- randomUser dynBackend.berDomain def bobClient <- objId $ bindResponse (addClient bob def) $ getJSON 201 - connectUsers alice bob + connectTwoUsers alice bob mem <- bob %. "qualified_id" void $ addMembers alice conv def {users = [mem]} >>= getBody 200 pure (bob, bobClient) @@ -637,7 +637,7 @@ testLeaveConversationSuccess = do startDynamicBackends [def] $ \[dynDomain] -> do eve <- randomUser dynDomain def eClient <- objId $ bindResponse (addClient eve def) $ getJSON 201 - forM_ [bob, chad, dee, eve] $ connectUsers alice + forM_ [bob, chad, dee, eve] $ connectTwoUsers alice conv <- postConversation alice @@ -656,7 +656,7 @@ testOnUserDeletedConversations = do startDynamicBackends [def] $ \[dynDomain] -> do [ownDomain, otherDomain] <- forM [OwnDomain, OtherDomain] asString [alice, alex, bob, bart, chad] <- createUsers [ownDomain, ownDomain, otherDomain, otherDomain, dynDomain] - forM_ [alex, bob, bart, chad] $ connectUsers alice + forM_ [alex, bob, bart, chad] $ connectTwoUsers alice bobId <- bob %. "qualified_id" ooConvId <- do l <- getAllConvs alice @@ -693,8 +693,8 @@ testOnUserDeletedConversations = do testUpdateConversationByRemoteAdmin :: HasCallStack => App () testUpdateConversationByRemoteAdmin = do [alice, bob, charlie] <- createUsers [OwnDomain, OtherDomain, OtherDomain] - connectUsers alice bob - connectUsers alice charlie + connectTwoUsers alice bob + connectTwoUsers alice charlie conv <- postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) >>= getJSON 201 diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 98d81a4d29a..3e73c8be4e9 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -145,7 +145,7 @@ testIndependentESIndices = do u1 <- randomUser OwnDomain def u2 <- randomUser OwnDomain def uid2 <- objId u2 - connectUsers u1 u2 + connectTwoUsers u1 u2 BrigI.refreshIndex OwnDomain bindResponse (BrigP.searchContacts u1 (u2 %. "name") OwnDomain) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -163,7 +163,7 @@ testIndependentESIndices = do null docs `shouldMatch` True uD2 <- randomUser dynDomain def uidD2 <- objId uD2 - connectUsers uD1 uD2 + connectTwoUsers uD1 uD2 BrigI.refreshIndex dynDomain -- searching for uD2 on the dyn backend should yield a result bindResponse (BrigP.searchContacts uD1 (uD2 %. "name") dynDomain) $ \resp -> do @@ -176,7 +176,7 @@ testIndependentESIndices = do testDynamicBackendsFederation :: HasCallStack => App () testDynamicBackendsFederation = do startDynamicBackends [def, def] $ \[aDynDomain, anotherDynDomain] -> do - (u1, u2) <- createAndConnectUsers aDynDomain anotherDynDomain + [u1, u2] <- createAndConnectUsers [aDynDomain, anotherDynDomain] bindResponse (BrigP.getConnection u1 u2) assertSuccess bindResponse (BrigP.getConnection u2 u1) assertSuccess diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs index 899141ee313..e24d6c6657e 100644 --- a/integration/test/Test/Federation.hs +++ b/integration/test/Test/Federation.hs @@ -34,11 +34,11 @@ testNotificationsForOfflineBackends = do downUser2 <- randomUser downBackend.berDomain def downClient1 <- objId $ bindResponse (BrigP.addClient downUser1 def) $ getJSON 201 - connectUsers delUser otherUser - connectUsers delUser otherUser2 - connectUsers delUser downUser1 - connectUsers delUser downUser2 - connectUsers downUser1 otherUser + connectTwoUsers delUser otherUser + connectTwoUsers delUser otherUser2 + connectTwoUsers delUser downUser1 + connectTwoUsers delUser downUser2 + connectTwoUsers downUser1 otherUser upBackendConv <- bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, otherUser2, downUser1]})) $ getJSON 201 downBackendConv <- bindResponse (postConversation downUser1 (defProteus {qualifiedUsers = [otherUser, delUser]})) $ getJSON 201 @@ -79,7 +79,7 @@ testNotificationsForOfflineBackends = do -- however, if the backend of the user to be added is already part of the conversation, we do not need to do the check -- and the user can be added as long as the backend is reachable otherUser3 <- randomUser OtherDomain def - connectUsers delUser otherUser3 + connectTwoUsers delUser otherUser3 bindResponse (addMembers delUser upBackendConv def {users = [otherUser3]}) $ \resp -> resp.status `shouldMatchInt` 200 diff --git a/integration/test/Test/MessageTimer.hs b/integration/test/Test/MessageTimer.hs index 91ddc579860..7a8aff06c87 100644 --- a/integration/test/Test/MessageTimer.hs +++ b/integration/test/Test/MessageTimer.hs @@ -28,7 +28,7 @@ import Testlib.ResourcePool testMessageTimerChangeWithRemotes :: HasCallStack => App () testMessageTimerChangeWithRemotes = do - (alice, bob) <- createAndConnectUsers OwnDomain OtherDomain + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] conv <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 withWebSockets [alice, bob] $ \wss -> do void $ updateMessageTimer alice conv 1000 >>= getBody 200 @@ -44,7 +44,7 @@ testMessageTimerChangeWithUnreachableRemotes = do conv <- runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do bob <- randomUser dynBackend.berDomain def - connectUsers alice bob + connectTwoUsers alice bob postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 withWebSocket alice $ \ws -> do void $ updateMessageTimer alice conv 1000 >>= getBody 200 diff --git a/integration/test/Test/Roles.hs b/integration/test/Test/Roles.hs index 5a8eefc80bf..1fde4b95c4b 100644 --- a/integration/test/Test/Roles.hs +++ b/integration/test/Test/Roles.hs @@ -27,8 +27,8 @@ import Testlib.Prelude testRoleUpdateWithRemotesOk :: HasCallStack => App () testRoleUpdateWithRemotesOk = do [bob, charlie, alice] <- createUsers [OwnDomain, OwnDomain, OtherDomain] - connectUsers bob charlie - connectUsers bob alice + connectTwoUsers bob charlie + connectTwoUsers bob alice conv <- postConversation bob (defProteus {qualifiedUsers = [charlie, alice]}) >>= getJSON 201 @@ -50,8 +50,8 @@ testRoleUpdateWithRemotesUnreachable = do [bob, charlie] <- createUsers [OwnDomain, OwnDomain] startDynamicBackends [mempty] $ \[dynBackend] -> do alice <- randomUser dynBackend def - connectUsers bob alice - connectUsers bob charlie + connectTwoUsers bob alice + connectTwoUsers bob charlie conv <- postConversation bob (defProteus {qualifiedUsers = [charlie, alice]}) >>= getJSON 201 From 96ee8a51ebc59763f15d04f043fb01f898dddae5 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 5 Oct 2023 09:17:13 +0200 Subject: [PATCH 166/225] Migrate testAppMessageSomeReachable --- integration/integration.cabal | 1 + integration/test/Notifications.hs | 3 ++ integration/test/Test/MLS/Message.hs | 29 +++++++++++++++++ services/galley/test/integration/API/MLS.hs | 35 --------------------- 4 files changed, 33 insertions(+), 35 deletions(-) create mode 100644 integration/test/Test/MLS/Message.hs diff --git a/integration/integration.cabal b/integration/integration.cabal index 604a286d9eb..be9506e123d 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -111,6 +111,7 @@ library Test.MessageTimer Test.MLS Test.MLS.KeyPackage + Test.MLS.Message Test.MLS.One2One Test.MLS.SubConversation Test.Notifications diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index a6ffb6505b1..43c267b2821 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -59,6 +59,9 @@ isDeleteUserNotif n = isNewMessageNotif :: MakesValue a => a -> App Bool isNewMessageNotif n = fieldEquals n "payload.0.type" "conversation.otr-message-add" +isNewMLSMessageNotif :: MakesValue a => a -> App Bool +isNewMLSMessageNotif n = fieldEquals n "payload.0.type" "conversation.mls-message-add" + isMemberJoinNotif :: MakesValue a => a -> App Bool isMemberJoinNotif n = fieldEquals n "payload.0.type" "conversation.member-join" diff --git a/integration/test/Test/MLS/Message.hs b/integration/test/Test/MLS/Message.hs new file mode 100644 index 00000000000..28f68bbfaba --- /dev/null +++ b/integration/test/Test/MLS/Message.hs @@ -0,0 +1,29 @@ +module Test.MLS.Message where + +import API.Galley +import MLS.Util +import Notifications +import SetupHelpers +import Testlib.Prelude + +testAppMessageSomeReachable :: HasCallStack => App () +testAppMessageSomeReachable = do + (alice1, charlie) <- startDynamicBackends [mempty] $ \[thirdDomain] -> do + ownDomain <- make OwnDomain & asString + otherDomain <- make OtherDomain & asString + [alice, bob, charlie] <- createAndConnectUsers [ownDomain, otherDomain, thirdDomain] + + [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] + traverse_ uploadNewKeyPackage [bob1, charlie1] + void $ createNewGroup alice1 + void $ withWebSocket charlie $ \ws -> do + void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle + awaitMatch 10 isMemberJoinNotif ws + pure (alice1, charlie) + + mp <- createApplicationMessage alice1 "hi, bob!" + bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + resp.status `shouldMatchInt` 201 + + charlieId <- charlie %. "qualified_id" + resp.json %. "failed_to_send" `shouldMatchSet` [charlieId] diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 4eb2adb369d..dfba81b0664 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -111,7 +111,6 @@ tests s = [ test s "send application message" testAppMessage, test s "send remote application message" testRemoteAppMessage, test s "another participant sends an application message" testAppMessage2, - test s "send message, some remotes are reachable" testAppMessageSomeReachable, test s "send message, remote users are unreachable" testAppMessageUnreachable ], testGroup @@ -877,40 +876,6 @@ testAppMessage2 = do wsAssertMLSMessage (fmap Conv conversation) bob (mpMessage message) WS.assertNoEvent (2 # WS.Second) [wsBob1] -testAppMessageSomeReachable :: TestM () -testAppMessageSomeReachable = do - let bobDomain = Domain "bob.example.com" - charlieDomain = Domain "charlie.example.com" - users@[_alice, bob, charlie] <- - createAndConnectUsers $ - domainText <$$> [Nothing, Just bobDomain, Just charlieDomain] - - void $ runMLSTest $ do - [alice1, bob1, charlie1] <- - traverse createMLSClient users - - void $ setupMLSGroup alice1 - commit <- createAddCommit alice1 [bob, charlie] - - let commitMocks = - receiveCommitMockByDomain [bob1, charlie1] - <|> welcomeMock - ([event], _) <- - withTempMockFederator' commitMocks $ do - sendAndConsumeCommitBundle commit - - let unreachables = Set.singleton (Domain "charlie.example.com") - let sendMocks = - messageSentMockByDomain [bobDomain] - <|> mockUnreachableFor unreachables - - withTempMockFederator' sendMocks $ do - message <- createApplicationMessage alice1 "hi, bob!" - (_, failed) <- sendAndConsumeMessage message - liftIO $ do - assertBool "Event should be member join" $ is _EdMembersJoin (evtData event) - failed @?= unreachableFromList [charlie] - testAppMessageUnreachable :: TestM () testAppMessageUnreachable = do -- alice is local, bob is remote From 1afd9243008fb25be8c8c641bdd4ecc0424b0b98 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 5 Oct 2023 09:49:39 +0200 Subject: [PATCH 167/225] Save MLS migration statistics in S3 bucket (#3579) --- cabal.project | 3 + .../5-internal/mls-save-migration-statistics | 3 + charts/mlsstats/.helmignore | 21 + charts/mlsstats/Chart.yaml | 4 + charts/mlsstats/README.md | 7 + charts/mlsstats/templates/_helpers.tpl | 13 + charts/mlsstats/templates/cronjob.yaml | 60 ++ charts/mlsstats/templates/secret.yaml | 20 + charts/mlsstats/values.yaml | 30 + charts/wire-server/requirements.yaml | 5 + charts/wire-server/values.yaml | 1 + nix/local-haskell-packages.nix | 1 + tools/mlsstats/.ormolu | 1 + tools/mlsstats/LICENSE | 661 ++++++++++++++++++ tools/mlsstats/README.md | 43 ++ tools/mlsstats/analysis/mlsstats.py | 106 +++ tools/mlsstats/default.nix | 58 ++ tools/mlsstats/exec/Main.hs | 36 + tools/mlsstats/mlsstats.cabal | 155 ++++ tools/mlsstats/src/MlsStats/Options.hs | 171 +++++ tools/mlsstats/src/MlsStats/Run.hs | 257 +++++++ 21 files changed, 1656 insertions(+) create mode 100644 changelog.d/5-internal/mls-save-migration-statistics create mode 100644 charts/mlsstats/.helmignore create mode 100644 charts/mlsstats/Chart.yaml create mode 100644 charts/mlsstats/README.md create mode 100644 charts/mlsstats/templates/_helpers.tpl create mode 100644 charts/mlsstats/templates/cronjob.yaml create mode 100644 charts/mlsstats/templates/secret.yaml create mode 100644 charts/mlsstats/values.yaml create mode 120000 tools/mlsstats/.ormolu create mode 100644 tools/mlsstats/LICENSE create mode 100644 tools/mlsstats/README.md create mode 100755 tools/mlsstats/analysis/mlsstats.py create mode 100644 tools/mlsstats/default.nix create mode 100644 tools/mlsstats/exec/Main.hs create mode 100644 tools/mlsstats/mlsstats.cabal create mode 100644 tools/mlsstats/src/MlsStats/Options.hs create mode 100644 tools/mlsstats/src/MlsStats/Run.hs diff --git a/cabal.project b/cabal.project index 96d20a6f060..03014562602 100644 --- a/cabal.project +++ b/cabal.project @@ -50,6 +50,7 @@ packages: , tools/fedcalls/ , tools/rex/ , tools/stern/ + , tools/mlsstats/ tests: True benchmarks: True @@ -114,6 +115,8 @@ package polysemy-wire-zoo ghc-options: -Werror package proxy ghc-options: -Werror +package mlsstats + ghc-options: -Werror package repair-handles ghc-options: -Werror package rex diff --git a/changelog.d/5-internal/mls-save-migration-statistics b/changelog.d/5-internal/mls-save-migration-statistics new file mode 100644 index 00000000000..c418bae2040 --- /dev/null +++ b/changelog.d/5-internal/mls-save-migration-statistics @@ -0,0 +1,3 @@ +New cron job to save data usable to watch the progress of the Proteus to MLS migration in S3 bucket. + +**IMPORTANT:** This cron job is _not_ meant for general use! It can leak data about one team to other teams. diff --git a/charts/mlsstats/.helmignore b/charts/mlsstats/.helmignore new file mode 100644 index 00000000000..f0c13194444 --- /dev/null +++ b/charts/mlsstats/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/charts/mlsstats/Chart.yaml b/charts/mlsstats/Chart.yaml new file mode 100644 index 00000000000..24d97f8f2e4 --- /dev/null +++ b/charts/mlsstats/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: mlsstats - Push Proteus to MLS statistics to S3 +name: mlsstats +version: 0.0.1 diff --git a/charts/mlsstats/README.md b/charts/mlsstats/README.md new file mode 100644 index 00000000000..b9c8c118340 --- /dev/null +++ b/charts/mlsstats/README.md @@ -0,0 +1,7 @@ +# mlsstats + +The kubernetes cronjob resource will spawn a new `mlsstats-XXXXXX` pod every day. Logs for the pod can be gathered with `kubectl log`. + +## Important note + +This cron job is _not_ meant for general use! It can leak data about one team to other teams. diff --git a/charts/mlsstats/templates/_helpers.tpl b/charts/mlsstats/templates/_helpers.tpl new file mode 100644 index 00000000000..c288d2067da --- /dev/null +++ b/charts/mlsstats/templates/_helpers.tpl @@ -0,0 +1,13 @@ +{{/* Allow KubeVersion to be overridden. */}} +{{- define "kubeVersion" -}} + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} +{{- end -}} + +{{/* Get Batch API Version */}} +{{- define "batch.apiVersion" -}} + {{- if and (.Capabilities.APIVersions.Has "batch/v1") (semverCompare ">= 1.21-0" (include "kubeVersion" .)) -}} + {{- print "batch/v1" -}} + {{- else -}} + {{- print "batch/v1beta1" -}} + {{- end -}} +{{- end -}} diff --git a/charts/mlsstats/templates/cronjob.yaml b/charts/mlsstats/templates/cronjob.yaml new file mode 100644 index 00000000000..5247b15cd38 --- /dev/null +++ b/charts/mlsstats/templates/cronjob.yaml @@ -0,0 +1,60 @@ +apiVersion: {{ include "batch.apiVersion" . }} +kind: CronJob +metadata: + name: {{ .Release.Name }} + labels: + app: mlsstats + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + concurrencyPolicy: Forbid + schedule: {{ .Values.schedule | quote }} + jobTemplate: + metadata: + labels: + app: mlsstats + release: {{ .Release.Name }} + annotations: + # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` + checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + spec: + backoffLimit: 0 + template: + spec: + restartPolicy: Never + containers: + - name: mlsstats + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} + args: [ "mlsstats" + , "--brig-cassandra-host", {{ .Values.config.cassandra.brig.host | quote }} + , "--brig-cassandra-port", {{ .Values.config.cassandra.brig.port | quote }} + , "--brig-cassandra-keyspace", {{ .Values.config.cassandra.brig.keyspace | quote }} + , "--galley-cassandra-host", {{ .Values.config.cassandra.galley.host | quote }} + , "--galley-cassandra-port", {{ .Values.config.cassandra.galley.port | quote }} + , "--galley-cassandra-keyspace", {{ .Values.config.cassandra.galley.keyspace | quote }} + , "--cassandra-pagesize", {{ .Values.config.cassandra.pagesize | quote }} + , "--s3-endpoint", {{ .Values.config.s3.endpoint | quote -}} + , "--s3-region", {{ .Values.config.s3.region | quote -}} + , "--s3-addressing-style", {{ .Values.config.s3.addressingStyle | quote }} + , "--s3-bucket-name", {{ .Values.config.s3.bucket.name | quote }} + , "--s3-bucket-dir", {{ .Values.config.s3.bucket.directory | quote }} + ] + resources: + env: + {{- if hasKey .Values.secrets "awsKeyId" }} + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: mlsstats + key: awsKeyId + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: mlsstats + key: awsSecretKey + {{- end }} + - name: AWS_REGION + value: "{{ .Values.config.s3.region }}" +{{ toYaml .Values.resources | indent 16 }} diff --git a/charts/mlsstats/templates/secret.yaml b/charts/mlsstats/templates/secret.yaml new file mode 100644 index 00000000000..3f93be5120c --- /dev/null +++ b/charts/mlsstats/templates/secret.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }} + labels: + app: mlsstats + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + {{/* for_helm_linting is necessary only since the 'with' block below does not throw an error upon an empty .Values.secrets */}} + for_helm_linting: {{ required "No .secrets found in configuration. Did you forget to helm -f path/to/secrets.yaml ?" .Values.secrets | quote | b64enc | quote }} + + {{- with .Values.secrets }} + {{- if .awsKeyId }} + awsKeyId: {{ .awsKeyId | b64enc | quote }} + awsSecretKey: {{ .awsSecretKey | b64enc | quote }} + {{- end }} + {{- end }} diff --git a/charts/mlsstats/values.yaml b/charts/mlsstats/values.yaml new file mode 100644 index 00000000000..399bc1ec996 --- /dev/null +++ b/charts/mlsstats/values.yaml @@ -0,0 +1,30 @@ +image: + repository: quay.io/wire/mlsstats + tag: 0.1 +resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "100m" +schedule: "23 3 * * *" +config: + cassandra: + brig: + host: cassandra + port: 9042 + keyspace: brig + galley: + host: cassandra + port: 9042 + keyspace: galley + pagesize: 1024 + s3: + endpoint: https://s3.eu-west-1.amazonaws.com/ + region: eu-west-1 + addressingStyle: auto + bucket: + name: mlsstats + directory: "/" +secrets: {} diff --git a/charts/wire-server/requirements.yaml b/charts/wire-server/requirements.yaml index 9a2ff8649b0..b5350be6c80 100644 --- a/charts/wire-server/requirements.yaml +++ b/charts/wire-server/requirements.yaml @@ -129,3 +129,8 @@ dependencies: repository: "file://../integration" tags: - integration +- name: mlsstats + version: "0.0.42" + repository: "file://../mlsstats" + tags: + - mlsstats diff --git a/charts/wire-server/values.yaml b/charts/wire-server/values.yaml index a2ba0c3a518..3a0a3f1f525 100644 --- a/charts/wire-server/values.yaml +++ b/charts/wire-server/values.yaml @@ -12,3 +12,4 @@ tags: federation: false # see also galley.config.enableFederation and brig.config.enableFederation sftd: false backoffice: false + mlsstats: false diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index f06c352c734..59d940f7cf2 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -52,6 +52,7 @@ repair-handles = hself.callPackage ../tools/db/repair-handles/default.nix { inherit gitignoreSource; }; service-backfill = hself.callPackage ../tools/db/service-backfill/default.nix { inherit gitignoreSource; }; fedcalls = hself.callPackage ../tools/fedcalls/default.nix { inherit gitignoreSource; }; + mlsstats = hself.callPackage ../tools/mlsstats/default.nix { inherit gitignoreSource; }; rex = hself.callPackage ../tools/rex/default.nix { inherit gitignoreSource; }; stern = hself.callPackage ../tools/stern/default.nix { inherit gitignoreSource; }; } diff --git a/tools/mlsstats/.ormolu b/tools/mlsstats/.ormolu new file mode 120000 index 00000000000..157b212d7cd --- /dev/null +++ b/tools/mlsstats/.ormolu @@ -0,0 +1 @@ +../../.ormolu \ No newline at end of file diff --git a/tools/mlsstats/LICENSE b/tools/mlsstats/LICENSE new file mode 100644 index 00000000000..dba13ed2ddf --- /dev/null +++ b/tools/mlsstats/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/tools/mlsstats/README.md b/tools/mlsstats/README.md new file mode 100644 index 00000000000..f1ed814b000 --- /dev/null +++ b/tools/mlsstats/README.md @@ -0,0 +1,43 @@ +MLSStats - Data for monitoring Proteus to MLS migration +======================================================= + +MLSStats extracts the data relevant to an ongoing migration from Proteus to MLS and stores the data as four files to an S3 bucket, +- `user-client.csv`, +- `conv-group-team-protocol.csv`, +- `domain-user-client-group.csv`, and +- `user-conv.csv`. + +The tool is supposed to run (not more often than) once a day, preferably from a (Kubernetes) cron job. + +## Important note + +This cron job is _not_ meant for general use! It can leak data about one team to other teams. + +## How to interpret the data + +There are two tables with generic data from both protocols, `user-client.cvs` and `user-conv.cvs`, and two tables with MLS-specific data, `conv-group-team-protocol.csv` and `domain-user-client-group.csv`. In order to draw conclusions about the progress of the migration, the generic data has to be related to MLS-specific data. + + +### Use-case: conversation state ratio + +The protocol used in a conversation can be Proteus, Mixed, and MLS. Mixed conversations support Proteus clients as well as MLS clients. All team conversations and their currently supported protocols can be found in `conv-group-team-protocol.csv`. + +An example counting protocols per team and in total is implemented in the function `team_conversations()` in `analysis/mlsstats.py`. + +### Use-case: Proteus vs MLS client ratio + +In MLS, each conversation is additionally represented by a _group_. +- The mapping from group to conversation can be derived from `conv-group-team-protocol.csv`. +- The MLS clients for each user in each conversation (via the group-to-conversation mapping) can be counted in `domain-user-client-group.csv`. The domain in this table can be ignored. +- The total number of clients for each user can be counted in `user-client.csv`. The number of Proteus clients per user is the difference between total number of clients and MLS clients for this user. +- With the Proteus and MLS clients for each user sorted out, `user-conv.csv` can be used to derive the Proteus and MLS clients for each conversation by summing up the clients for each user in the conversation. + +This is implemented in the function `conversation_clients()` in `analysis/mlsstats.py`. + +## How to locally run MLSStats + +MLSStats accepts a number of arguments for configuring the connection to Cassandra and S3. With the local environment set up, the only argument required is `--s3-bucket-name`. + +## How to run MLSStats in the cluster + +MLSStats displays all its command line arguments when called with the `--help` flag. Close to all arguments have to be provided in order to make the tool correctly conntect to the database and S3. diff --git a/tools/mlsstats/analysis/mlsstats.py b/tools/mlsstats/analysis/mlsstats.py new file mode 100755 index 00000000000..900b85d8ffd --- /dev/null +++ b/tools/mlsstats/analysis/mlsstats.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +import csv + +def team_conversations(): + total = {'proteus': 0, 'mixed': 0, 'mls': 0} + teams = {} + with open('conv-group-team-protocol.csv') as csv_file: + csv_reader = csv.DictReader(csv_file, delimiter=',') + + for row in csv_reader: + team = row['team'] + protocol = row['protocol'] + + if protocol not in ['proteus', 'mixed', 'mls']: + protocol = 'proteus' + if protocol not in total: + total[protocol] = 0 + if team not in teams: + teams[team] = {'proteus': 0, 'mixed': 0, 'mls': 0} + + total[protocol] += 1 + teams[team][protocol] += 1 + + for name, team in teams.items(): + proteus = team['proteus'] + mixed = team['mixed'] + mls = team['mls'] + print(f'In team {name}, conversations ' + + f'in Proteus are {proteus}, ' + + f'in Mixed are {mixed}, and ' + + f'in MLS are {mls}.') + + proteus = total['proteus'] + mixed = total['mixed'] + mls = total['mls'] + print(f'In total, conversations in Proteus are {proteus}, '+ + f'in Mixed are {mixed}, and in MLS are {mls}.') + +def conversation_clients(): + with open('conv-group-team-protocol.csv') as csv_file: + conv = {} + proto = {} + csv_reader = csv.DictReader(csv_file, delimiter=',') + for row in csv_reader: + conversation = row['conversation'] + if row['group'] != '': + conv[row['group']] = conversation + proto[conversation] = row['protocol'] + + with open('domain-user-client-group.csv') as csv_file: + mls_clients = {} + csv_reader = csv.DictReader(csv_file, delimiter=',') + for row in csv_reader: + group = row['group'] + if group in conv: + conversation = conv[group] + user = row['user'] + if conversation not in mls_clients: + mls_clients[conversation] = {user: 0} + if user not in mls_clients[conversation]: + mls_clients[conversation][user] = 0 + mls_clients[conversation][user] += 1 + + with open('user-client.csv') as csv_file: + user_clients = {} + csv_reader = csv.DictReader(csv_file, delimiter=',') + for row in csv_reader: + user = row['user'] + if user not in user_clients: + user_clients[user] = 0 + user_clients[user] += 1 + + with open('user-conv.csv') as csv_file: + conv_clients = {} + csv_reader = csv.DictReader(csv_file, delimiter=',') + for row in csv_reader: + user = row['user'] + conversation = row['conversation'] + if conversation in proto: # only team conversations + if conversation not in conv_clients: + conv_clients[conversation] = {'proteus': 0, 'mls': 0} + if conversation in mls_clients and user in mls_clients[conversation]: + mls = mls_clients[conversation][user] + else: + mls = 0 + if user in user_clients: + proteus = user_clients[user] - mls + else: + proteus = 0 + conv_clients[conversation]['proteus'] += proteus + conv_clients[conversation]['mls'] += mls + + total_proteus = 0 + total_mls = 0 + for name, conversation in conv_clients.items(): + proteus = conversation['proteus'] + mls = conversation['mls'] + protocol = proto[name] + total_proteus += proteus + total_mls += mls + print(f'In conversation {name} ({protocol}), there are ' + + f'{proteus} Proteus clients and {mls} MLS clients.') + + print(f'In total, there are {total_proteus} Proteus clients ' + + f'and {total_mls} MLS clients.') diff --git a/tools/mlsstats/default.nix b/tools/mlsstats/default.nix new file mode 100644 index 00000000000..7c8c9068107 --- /dev/null +++ b/tools/mlsstats/default.nix @@ -0,0 +1,58 @@ +# WARNING: GENERATED FILE, DO NOT EDIT. +# This file is generated by running hack/bin/generate-local-nix-packages.sh and +# must be regenerated whenever local packages are added or removed, or +# dependencies are added or removed. +{ mkDerivation +, aeson +, amazonka +, amazonka-s3 +, base +, base64-bytestring +, bytestring +, cassandra-util +, conduit +, filepath +, gitignoreSource +, http-types +, imports +, lens +, lib +, optparse-applicative +, schema-profunctor +, text +, time +, tinylog +, types-common +, wire-api +}: +mkDerivation { + pname = "mlsstats"; + version = "0.1.0"; + src = gitignoreSource ./.; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + aeson + amazonka + amazonka-s3 + base + base64-bytestring + bytestring + cassandra-util + conduit + filepath + http-types + imports + lens + optparse-applicative + schema-profunctor + text + time + tinylog + types-common + wire-api + ]; + executableHaskellDepends = [ base imports optparse-applicative ]; + license = lib.licenses.agpl3Only; + mainProgram = "mlsstats"; +} diff --git a/tools/mlsstats/exec/Main.hs b/tools/mlsstats/exec/Main.hs new file mode 100644 index 00000000000..4d495c32062 --- /dev/null +++ b/tools/mlsstats/exec/Main.hs @@ -0,0 +1,36 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Main + ( main, + ) +where + +import Imports +import MlsStats.Options +import MlsStats.Run (run) +import Options.Applicative + +main :: IO () +main = do + opts <- execParser (info (helper <*> optsParser) desc) + run opts + where + desc = + header "mlsstats" + <> progDesc "MLS Stats - Proteus to MLS migration statistics for admins" + <> fullDesc diff --git a/tools/mlsstats/mlsstats.cabal b/tools/mlsstats/mlsstats.cabal new file mode 100644 index 00000000000..eca13c03d34 --- /dev/null +++ b/tools/mlsstats/mlsstats.cabal @@ -0,0 +1,155 @@ +cabal-version: 1.12 +name: mlsstats +version: 0.1.0 +description: collect and provide MLS migration statistics +category: Network +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2023 Wire Swiss GmbH +license: AGPL-3 +license-file: LICENSE +build-type: Simple + +flag static + description: Enable static linking + manual: True + default: False + +library + exposed-modules: + MlsStats.Options + MlsStats.Run + + other-modules: Paths_mlsstats + hs-source-dirs: src + default-extensions: + NoImplicitPrelude + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + DuplicateRecordFields + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + OverloadedRecordDot + OverloadedStrings + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns + + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -Wredundant-constraints -Wunused-packages + + build-depends: + aeson + , amazonka >=1.3.7 + , amazonka-s3 >=1.3.7 + , base >=4.6 && <5 + , base64-bytestring + , bytestring + , cassandra-util + , conduit + , filepath + , http-types + , imports + , lens >=4.11 + , optparse-applicative + , schema-profunctor + , text + , time + , tinylog + , types-common >=0.8 + , wire-api + + default-language: GHC2021 + +executable mlsstats + main-is: exec/Main.hs + other-modules: Paths_mlsstats + default-extensions: + NoImplicitPrelude + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + DuplicateRecordFields + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + OverloadedRecordDot + OverloadedStrings + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns + + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -threaded -rtsopts -with-rtsopts=-T -Wredundant-constraints + -Wunused-packages + + build-depends: + base + , imports + , mlsstats + , optparse-applicative + + if flag(static) + ld-options: -static + + default-language: GHC2021 diff --git a/tools/mlsstats/src/MlsStats/Options.hs b/tools/mlsstats/src/MlsStats/Options.hs new file mode 100644 index 00000000000..251d9117eb7 --- /dev/null +++ b/tools/mlsstats/src/MlsStats/Options.hs @@ -0,0 +1,171 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 MlsStats.Options + ( Opts (..), + CassandraSettings (..), + S3Settings (..), + optsParser, + ) +where + +import Amazonka +import Cassandra qualified as C +import Data.Text qualified as Text +import Imports +import Options.Applicative +import Options.Applicative.Types (readerAsk) +import Util.Options + +data Opts = Opts + { cassandraSettings :: CassandraSettings, + s3Settings :: S3Settings + } + deriving (Show, Generic) + +data CassandraSettings = CassandraSettings + { brigHost :: String, + brigPort :: Word16, + brigKeyspace :: C.Keyspace, + galleyHost :: String, + galleyPort :: Word16, + galleyKeyspace :: C.Keyspace, + pageSize :: Int32 + } + deriving (Show) + +data S3Settings = S3Settings + { endpoint :: AWSEndpoint, + region :: Maybe Text, + addressingStyle :: S3AddressingStyle, + bucketName :: Text, + bucketDir :: Maybe String + } + deriving (Show) + +optsParser :: Parser Opts +optsParser = + Opts + <$> cassandraSettingsParser + <*> s3SettingsParser + +cassandraSettingsParser :: Parser CassandraSettings +cassandraSettingsParser = + CassandraSettings + <$> strOption + ( long "brig-cassandra-host" + <> metavar "HOST" + <> help "Cassandra host for Brig" + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long "brig-cassandra-port" + <> metavar "PORT" + <> help "Cassandra port for Brig" + <> value 9042 + <> showDefault + ) + <*> ( C.Keyspace . Text.pack + <$> strOption + ( long "brig-cassandra-keyspace" + <> metavar "STRING" + <> help "Cassandra Keyspaces for Brig" + <> value ("brig_test") + <> showDefault + ) + ) + <*> strOption + ( long "galley-cassandra-host" + <> metavar "HOST" + <> help "Cassandra host for Galley" + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long "galley-cassandra-port" + <> metavar "PORT" + <> help "Cassandra port for Brig" + <> value 9042 + <> showDefault + ) + <*> ( C.Keyspace . Text.pack + <$> strOption + ( long "galley-cassandra-keyspace" + <> metavar "STRING" + <> help "Cassandra Keyspaces for Galley" + <> value ("galley_test") + <> showDefault + ) + ) + <*> option + auto + ( long "cassandra-pagesize" + <> metavar "PAGESIZE" + <> help "Cassandra pagesize for queries" + <> value 1024 + <> showDefault + ) + +s3SettingsParser :: Parser S3Settings +s3SettingsParser = + S3Settings + <$> option + parseAWSEndpoint + ( long "s3-endpoint" + <> metavar "URL" + <> help "S3 endpoint" + <> value (AWSEndpoint "localhost" False 4570) + <> showDefault + ) + <*> optional + ( strOption + ( long "s3-region" + <> metavar "S3REGION" + <> help "S3 region" + ) + ) + <*> option + addressingStyleParser + ( long "s3-addressing-style" + <> metavar "ADDRESSINGSTYLE" + <> help "S3 addressing style (path for minio)" + <> value S3AddressingStylePath + <> showDefault + ) + <*> strOption + ( long "s3-bucket-name" + <> metavar "BUCKET" + <> help "S3 bucket" + ) + <*> optional + ( strOption + ( long "s3-bucket-dir" + <> metavar "DIRECTORY" + <> help "S3 bucket directory" + ) + ) + +addressingStyleParser :: ReadM S3AddressingStyle +addressingStyleParser = do + readerAsk >>= \case + "path" -> pure S3AddressingStylePath + "auto" -> pure S3AddressingStyleAuto + "virtual" -> pure S3AddressingStyleVirtual + _ -> readerError "unknown S3 addressing style" diff --git a/tools/mlsstats/src/MlsStats/Run.hs b/tools/mlsstats/src/MlsStats/Run.hs new file mode 100644 index 00000000000..7132d0b8a9c --- /dev/null +++ b/tools/mlsstats/src/MlsStats/Run.hs @@ -0,0 +1,257 @@ +{-# LANGUAGE TupleSections #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} +{-# OPTIONS_GHC -Wno-orphans #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 MlsStats.Run + ( run, + ) +where + +import Amazonka hiding (await) +import Amazonka.S3 +import Amazonka.S3.CreateMultipartUpload +import Amazonka.S3.Lens +import Cassandra as C +import Cassandra.Settings as C +import Conduit +import Control.Exception +import Control.Lens ((.~), (?~), (^.)) +import Data.Aeson qualified as A +import Data.ByteString.Base64 qualified as BS64 +import Data.ByteString.Lazy qualified as LBS +import Data.Conduit.Combinators hiding (foldMap, stderr, stdout) +import Data.Domain +import Data.Id +import Data.List.NonEmpty (nonEmpty) +import Data.Schema +import Data.Text qualified as T +import Data.Text.Encoding qualified as T +import Data.Time +import Data.Time.Format.ISO8601 +import Imports hiding (concat, filter, print) +import MlsStats.Options +import Network.HTTP.Types +import System.FilePath.Posix +import System.Logger qualified as Log +import Util.Options +import Wire.API.Conversation.Protocol +import Wire.API.MLS.Group + +run :: Opts -> IO () +run o = do + logger' <- initLogger + let settings = o.cassandraSettings + galleyTables <- initCas settings.galleyHost settings.galleyPort settings.galleyKeyspace logger' + brigTables <- initCas settings.brigHost settings.brigPort settings.brigKeyspace logger' + runCommand o.s3Settings galleyTables brigTables o.cassandraSettings.pageSize + where + initLogger = + Log.new + . Log.setOutput Log.StdOut + . Log.setFormat Nothing + . Log.setBufSize 0 + $ Log.defSettings + initCas casHost casPort casKeyspace l = + C.init + . C.setLogger (C.mkLogger l) + . C.setContacts casHost [] + . C.setPortNumber (fromIntegral casPort) + . C.setProtocolVersion C.V4 + . C.setKeyspace casKeyspace + $ C.defSettings + +runCommand :: S3Settings -> ClientState -> ClientState -> Int32 -> IO () +runCommand s3 galleyTables brigTables queryPageSize = do + logger <- newLogger Debug stderr + let service = + setEndpoint (s3.endpoint ^. awsSecure) (s3.endpoint ^. awsHost) (s3.endpoint ^. awsPort) defaultService + & service_s3AddressingStyle .~ s3.addressingStyle + env <- + maybe id (\reg e -> (e :: Env) {region = Region' reg}) s3.region + . (\e -> e {logger = logger}) + . configureService service + <$> newEnv discover + now <- formatShowM iso8601Format <$> getCurrentTime + let upload = + uploadStream env (BucketName s3.bucketName) + . ObjectKey + . T.pack + . maybe id () s3.bucketDir + . maybe id () now + runResourceT $ do + upload "user-client.csv" (userClient brigTables queryPageSize) + upload "conv-group-team-protocol.csv" (convGroupTeamProtocol galleyTables queryPageSize) + upload "domain-user-client-group.csv" (domainUserClientGroup galleyTables queryPageSize) + upload "user-conv.csv" (userConv galleyTables queryPageSize) + +userClient :: MonadIO m => ClientState -> Int32 -> ConduitT () ByteString m () +userClient cassandra queryPageSize = do + yield "user,client\r\n" + ( transPipe + (runClient cassandra) + (paginateC userClientCql (paramsP LocalQuorum () queryPageSize) x1) + .| concat + .| mapC (\(u, c) -> T.encodeUtf8 $ T.pack (show u) <> "," <> c.client <> "\r\n") + ) + where + userClientCql :: PrepQuery R () (UserId, ClientId) + userClientCql = "SELECT user, client FROM clients" + +convGroupTeamProtocol :: MonadIO m => ClientState -> Int32 -> ConduitT () ByteString m () +convGroupTeamProtocol cassandra queryPageSize = do + yield "conversation,group,team,protocol\r\n" + ( transPipe + (runClient cassandra) + (paginateC convGroupTeamProtocolCql (paramsP LocalQuorum () queryPageSize) x1) + .| concat + .| mapC (\(c, g, mt, p) -> fmap (c,g,,p) mt) -- filter out non-team conversations + .| concat + .| mapC + ( \(c, g, t, p) -> + T.encodeUtf8 (T.pack (show c)) + <> "," + <> foldMap (BS64.encode . unGroupId) g + <> "," + <> T.encodeUtf8 (T.pack (show t)) + <> "," + <> T.encodeUtf8 (convertProtocol p) + <> "\r\n" + ) + ) + where + convGroupTeamProtocolCql :: PrepQuery R () (ConvId, Maybe GroupId, Maybe TeamId, Maybe ProtocolTag) + convGroupTeamProtocolCql = "SELECT conv, group_id, team, protocol FROM conversation" + convertProtocol :: Maybe ProtocolTag -> Text + convertProtocol p = case schemaToJSON (fromMaybe ProtocolProteusTag p) of + A.String s -> s + _ -> "?" + +domainUserClientGroup :: MonadIO m => ClientState -> Int32 -> ConduitT () ByteString m () +domainUserClientGroup cassandra queryPageSize = do + yield "user_domain,user,client,group\r\n" + ( transPipe + (runClient cassandra) + (paginateC domainUserClientGroupCql (paramsP LocalQuorum () queryPageSize) x1) + .| concat + .| mapC + ( \(d, u, c, g) -> + (T.encodeUtf8 (domainText d)) + <> "," + <> T.encodeUtf8 (T.pack (show u)) + <> "," + <> T.encodeUtf8 (client c) + <> "," + <> BS64.encode (unGroupId g) + <> "\r\n" + ) + ) + where + domainUserClientGroupCql :: PrepQuery R () (Domain, UserId, ClientId, GroupId) + domainUserClientGroupCql = "SELECT user_domain, user, client, group_id FROM mls_group_member_client" + +userConv :: MonadIO m => ClientState -> Int32 -> ConduitT () ByteString m () +userConv cassandra queryPageSize = do + yield "user,conversation\r\n" + ( transPipe + (runClient cassandra) + (paginateC userConvCql (paramsP LocalQuorum () queryPageSize) x1) + .| concat + .| mapC (\(u, c) -> T.encodeUtf8 $ T.pack (show u) <> "," <> T.pack (show c) <> "\r\n") + ) + where + userConvCql :: PrepQuery R () (UserId, ConvId) + userConvCql = "SELECT user, conv FROM user" + +uploadStream :: + Env -> + BucketName -> + ObjectKey -> + ConduitT () ByteString (ResourceT IO) () -> + (ResourceT IO) () +uploadStream env bucket key stream = do + createMultipartResp <- + sendEither env (newCreateMultipartUpload bucket key) >>= \case + Left (ServiceError e) | e.status.statusCode == 404 && e.code == ErrorCode "NoSuchBucket" -> do + void $ send env (newCreateBucket bucket) + send env (newCreateMultipartUpload bucket key) + Left e -> liftIO $ throwIO e + Right resp -> pure resp + let uploadId' = createMultipartResp ^. createMultipartUploadResponse_uploadId + parts <- + runConduit $ + stream + .| chunksOfE chunkSize + .| uploadParts env bucket key uploadId' 0 + .| mapC (uncurry newCompletedPart) + .| sinkList + void $ + send env $ + newCompleteMultipartUpload bucket key uploadId' + & completeMultipartUpload_multipartUpload + ?~ ( newCompletedMultipartUpload + & completedMultipartUpload_parts .~ nonEmpty parts + ) + where + chunkSize = 5 * 1024 * 1024 + +uploadParts :: + Env -> + BucketName -> + ObjectKey -> + Text -> + Int -> + ConduitT ByteString (Int, ETag) (ResourceT IO) () +uploadParts env bucket key uploadId partNum = do + chunkM <- await + case chunkM of + Just chunk -> do + let req = newUploadPart bucket key partNum uploadId $ toBody chunk + resp <- send env req + for_ (resp ^. uploadPartResponse_eTag) $ \etag -> + yield (partNum, etag) + uploadParts env bucket key uploadId (partNum + 1) + Nothing -> pure () + +instance Cql ProtocolTag where + ctype = Tagged IntColumn + + toCql = CqlInt . fromIntegral . fromEnum + + fromCql (CqlInt i) = do + let i' = fromIntegral i + if i' < fromEnum @ProtocolTag minBound + || i' > fromEnum @ProtocolTag maxBound + then Left $ "unexpected protocol: " ++ show i + else Right $ toEnum i' + fromCql _ = Left "protocol: int expected" + +instance Cql GroupId where + ctype = Tagged BlobColumn + + toCql = CqlBlob . LBS.fromStrict . unGroupId + + fromCql (CqlBlob b) = Right . GroupId . LBS.toStrict $ b + fromCql _ = Left "group_id: blob expected" + +instance Cql Domain where + ctype = Tagged TextColumn + toCql = CqlText . domainText + fromCql (CqlText txt) = mkDomain txt + fromCql _ = Left "Domain: Text expected" From 568a5bb45bf84bd0ae7675869edf6a4e9fc5e364 Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Thu, 5 Oct 2023 11:35:59 +0200 Subject: [PATCH 168/225] [fix] some fixes related to nix (#3628) * [fix] some fixes related to nix - repair fixed output derivation cargo packages - add benchmarkdepends to the devShell - make the missing build-tool-depends more reproducible - explain creation of the .git dir in ring --- cabal.project | 3 +++ changelog.d/3-bug-fixes/WBP-4959 | 1 + changelog.d/3-bug-fixes/WBP-4961 | 1 + nix/pkgs/cryptobox/default.nix | 13 ++++++++++--- nix/pkgs/mls-test-cli/default.nix | 26 ++++++++++++++++++++------ nix/pkgs/zauth/default.nix | 7 ++++++- nix/wire-server.nix | 5 +++++ 7 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WBP-4959 create mode 100644 changelog.d/3-bug-fixes/WBP-4961 diff --git a/cabal.project b/cabal.project index 96d20a6f060..cbe9d0c9e17 100644 --- a/cabal.project +++ b/cabal.project @@ -1,3 +1,6 @@ +repository hackage.haskell.org + url: https://hackage.haskell.org/ +index-state: 2023-10-03T15:17:00Z packages: integration , libs/bilge/ diff --git a/changelog.d/3-bug-fixes/WBP-4959 b/changelog.d/3-bug-fixes/WBP-4959 new file mode 100644 index 00000000000..c053654fa0b --- /dev/null +++ b/changelog.d/3-bug-fixes/WBP-4959 @@ -0,0 +1 @@ +Fix nix derivations for rust packages diff --git a/changelog.d/3-bug-fixes/WBP-4961 b/changelog.d/3-bug-fixes/WBP-4961 new file mode 100644 index 00000000000..ef17d4d7bab --- /dev/null +++ b/changelog.d/3-bug-fixes/WBP-4961 @@ -0,0 +1 @@ +Ensure benchmarking dependencies are provided by nix development environment diff --git a/nix/pkgs/cryptobox/default.nix b/nix/pkgs/cryptobox/default.nix index 5945c616f22..98d5adf8aa3 100644 --- a/nix/pkgs/cryptobox/default.nix +++ b/nix/pkgs/cryptobox/default.nix @@ -7,7 +7,7 @@ }: rustPlatform.buildRustPackage rec { - name = "cryptobox-c-${version}"; + pname = "cryptobox-c"; version = "2019-06-17"; nativeBuildInputs = [ pkg-config ]; buildInputs = [ libsodium ]; @@ -17,12 +17,19 @@ rustPlatform.buildRustPackage rec { rev = "4067ad96b125942545dbdec8c1a89f1e1b65d013"; sha256 = "1i9dlhw0xk1viglyhail9fb36v1awrypps8jmhrkz8k1bhx98ci3"; }; - cargoSha256 = "sha256-Afr3ShCXDCwTQNdeCZbA5/aosRt+KFpGfT1mrob6cog="; - patchLibs = lib.optionalString stdenv.isDarwin '' install_name_tool -id $out/lib/libcryptobox.dylib $out/lib/libcryptobox.dylib ''; + cargoLock = { + lockFile = "${src}/Cargo.lock"; + outputHashes = { + "cryptobox-1.0.0" = "sha256-Ewo+FtEGTZ4/U7Ow6mGTQkxS4IQYcEthr5/xG9BRTWk="; + "hkdf-0.2.0" = "sha256-cdgR94c40JFIjBf8NfZPXPGLU60BlAZX/SQnRHAXGOg="; + "proteus-1.0.0" = "sha256-ppMt56RY5K3rOwO7MEdY6d3t96sbHZzDB/nPNNp35DY="; + }; + }; + postInstall = '' ${patchLibs} mkdir -p $out/include diff --git a/nix/pkgs/mls-test-cli/default.nix b/nix/pkgs/mls-test-cli/default.nix index 8562916aca7..9c283bb6837 100644 --- a/nix/pkgs/mls-test-cli/default.nix +++ b/nix/pkgs/mls-test-cli/default.nix @@ -1,15 +1,13 @@ { fetchFromGitHub -, lib , libsodium , perl , pkg-config , rustPlatform -, stdenv , gitMinimal }: rustPlatform.buildRustPackage rec { - name = "mls-test-cli-${version}"; + pname = "mls-test-cli"; version = "0.6.0"; nativeBuildInputs = [ pkg-config perl gitMinimal ]; buildInputs = [ libsodium ]; @@ -19,9 +17,25 @@ rustPlatform.buildRustPackage rec { sha256 = "sha256-/XQ/9oQTPkRqgMzDGRm+Oh9jgkdeDM1vRJ6/wEf2+bY="; rev = "c6f80be2839ac1ed2894e96044541d1c3cf6ecdf"; }; + cargoLock = { + lockFile = "${src}/Cargo.lock"; + outputHashes = { + "openmls-0.4.1" = "sha256-oEPziXyGmPV6C80lQpi0z7Ehl3/mGFz0HaePT8h3y0Q="; + "ring-0.17.0-not-released-yet" = "sha256-n8KuJRcOdMduPTjDBU1n1eec3w9Eat/8czvGRTGbqsI="; + "x509-parser-0.13.1" = "sha256-ipHZm3MmiOssGkFC5O4h/Y3p1U0aj7wu+LGaBuQImuU="; + }; + }; doCheck = false; - cargoSha256 = "sha256-AlZrxa7f5JwxxrzFBgeFSaYU6QttsUpfLYfq1HzsdbE="; - cargoDepsHook = '' - mkdir -p mls-test-cli-${version}-vendor.tar.gz/ring/.git + /* + if ring does not detect it is a git repository (checks for .git) dir + it will expect pregenerated files that we do not have at our exposure + after pulling with the git fetcher + + we put this patch in the preBuild phase because we need to have the + cargo-vendor-dir available and the dedicated cargo phase for this + in nixpkgs do not trigger + */ + preBuild = '' + mkdir $CARGO_HOME/cargo-vendor-dir/ring-0.17.0-not-released-yet/.git ''; } diff --git a/nix/pkgs/zauth/default.nix b/nix/pkgs/zauth/default.nix index 969a76fc333..19ade192f8f 100644 --- a/nix/pkgs/zauth/default.nix +++ b/nix/pkgs/zauth/default.nix @@ -16,7 +16,12 @@ rustPlatform.buildRustPackage rec { src = nix-gitignore.gitignoreSourcePure [ ../../../.gitignore ] ../../../libs/libzauth; sourceRoot = "libzauth/libzauth-c"; - cargoSha256 = "sha256-f/MNUrEQaPzSUHtnZ0jARMwBswS+Sh0Swe+2D+hpHF4="; + cargoLock = { + lockFile = "${src}/libzauth-c/Cargo.lock"; + outputHashes = { + "jwt-simple-0.11.3" = "sha256-H9gCwqxUlffi8feQ4xjiTbeyT1RMrfZAsPsNWapfR9c="; + }; + }; patchLibs = lib.optionalString stdenv.isDarwin '' install_name_tool -id $out/lib/libzauth.dylib $out/lib/libzauth.dylib diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 129d5e8c2b2..8df97928a7c 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -124,6 +124,9 @@ let then drv else hlib.dontHaddock drv; + bench = _: drv: + hlib.doBenchmark drv; + overrideAll = fn: overrides: attrsets.mapAttrs fn (overrides); in @@ -132,6 +135,7 @@ let opt docs tests + bench ]; manualOverrides = import ./manual-overrides.nix (with pkgs; { inherit hlib libsodium protobuf mls-test-cli fetchpatch; @@ -419,6 +423,7 @@ let }; shell = (hPkgs localModsOnlyTests).shellFor { + doBenchmark = true; packages = p: builtins.map (e: p.${e}) wireServerPackages; }; ghcWithPackages = shell.nativeBuildInputs ++ shell.buildInputs; From c15986afbdbf38db81ed67fad465cc98d882ffd3 Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:05:50 +0200 Subject: [PATCH 169/225] [chore] update the developer documentation (#3632) * [chore] update the developer documentation - add documentation for increasing resources - update documentation to use the proper command --- docs/src/developer/developer/building.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/src/developer/developer/building.md b/docs/src/developer/developer/building.md index 5576f1545da..ee219d3b98b 100644 --- a/docs/src/developer/developer/building.md +++ b/docs/src/developer/developer/building.md @@ -81,9 +81,14 @@ Also make sure your system is able to resolve the fully qualified domain `localh After all containers are up you can use these Makefile targets to run the tests locally: +0. Set your resource limits to a high enough number: + ```bash + ulimit 10240 + ``` + 1. Build and run all integration tests ```bash - make ci + make ci-safe ``` 2. Build and run integration tests for a service (say galley) @@ -93,19 +98,19 @@ After all containers are up you can use these Makefile targets to run the tests 3. Run integration tests written using `tasty` for a service (say galley) that match a pattern ```bash - TASTY_PATTERN="/MLS/" make ci package=galley + TASTY_PATTERN="/MLS/" make ci-safe package=galley ``` For more details on pattern formats, see tasty docs: https://github.com/UnkindPartition/tasty#patterns 4. Run integration tests written using `hspec` for a service (say spar) that match a pattern ```bash - HSPEC_MATCH='Scim' make ci package=spar + HSPEC_MATCH='Scim' make ci-safe package=spar ``` For more details on match formats, see hspec docs: https://hspec.github.io/match.html 5. Run integration tests without any parallelism ```bash - TASTY_NUM_THREADS=1 make ci package=brig + TASTY_NUM_THREADS=1 make ci-safe package=brig ``` `TASTY_NUM_THREADS` can also be set to other values, it defaults to number of cores available. From c5bacec731dbaf8f8302f5f159777e53eff49aa5 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 5 Oct 2023 16:56:19 +0200 Subject: [PATCH 170/225] Store removal key to temporary directory --- integration/integration.cabal | 1 + integration/test/Testlib/Env.hs | 21 ++++--- integration/test/Testlib/Run.hs | 79 +++++++++++++++---------- integration/test/Testlib/RunServices.hs | 4 +- integration/test/Testlib/Types.hs | 3 +- 5 files changed, 64 insertions(+), 44 deletions(-) diff --git a/integration/integration.cabal b/integration/integration.cabal index be9506e123d..8bf42de6aba 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -174,6 +174,7 @@ library , network , network-uri , optparse-applicative + , pem , process , proto-lens , random diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 71977405a5b..40fd56adc8a 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -35,10 +35,10 @@ serviceHostPort m BackgroundWorker = m.backgroundWorker serviceHostPort m Stern = m.stern serviceHostPort m FederatorInternal = m.federatorInternal -mkGlobalEnv :: FilePath -> IO GlobalEnv +mkGlobalEnv :: FilePath -> Codensity IO GlobalEnv mkGlobalEnv cfgFile = do - eith <- Yaml.decodeFileEither cfgFile - intConfig <- case eith of + eith <- liftIO $ Yaml.decodeFileEither cfgFile + intConfig <- liftIO $ case eith of Left err -> do hPutStrLn stderr $ "Could not parse " <> cfgFile <> ": " <> Yaml.prettyPrintParseException err exitFailure @@ -51,17 +51,19 @@ mkGlobalEnv cfgFile = do then Just (joinPath (init ps)) else Nothing - manager <- HTTP.newManager HTTP.defaultManagerSettings + manager <- liftIO $ HTTP.newManager HTTP.defaultManagerSettings let cassSettings = Cassandra.defSettings & Cassandra.setContacts intConfig.cassandra.host [] & Cassandra.setPortNumber (fromIntegral intConfig.cassandra.port) cassClient <- Cassandra.init cassSettings resourcePool <- - createBackendResourcePool - (Map.elems intConfig.dynamicBackends) - intConfig.rabbitmq - cassClient + liftIO $ + createBackendResourcePool + (Map.elems intConfig.dynamicBackends) + intConfig.rabbitmq + cassClient + tempDir <- Codensity $ withSystemTempDirectory "test" pure GlobalEnv { gServiceMap = @@ -77,7 +79,8 @@ mkGlobalEnv cfgFile = do gServicesCwdBase = devEnvProjectRoot <&> ( "services"), gRemovalKeyPath = error "Uninitialised removal key path", gBackendResourcePool = resourcePool, - gRabbitMQConfig = intConfig.rabbitmq + gRabbitMQConfig = intConfig.rabbitmq, + gTempDir = tempDir } mkEnv :: GlobalEnv -> Codensity IO Env diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 82c1b1eaaa0..349c44390cb 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -6,10 +6,15 @@ import Control.Monad import Control.Monad.Codensity import Control.Monad.IO.Class import Control.Monad.Reader +import Crypto.Error +import Crypto.PubKey.Ed25519 qualified as Ed25519 +import Data.ByteArray (convert) +import Data.ByteString qualified as B import Data.Foldable import Data.Function import Data.Functor import Data.List +import Data.PEM import Data.Time.Clock import Data.Traversable (for) import RunAllTests @@ -98,12 +103,11 @@ main = do if opts.listTests then doListTests tests else runTests tests opts.xmlReport cfg -createGlobalEnv :: FilePath -> IO GlobalEnv +createGlobalEnv :: FilePath -> Codensity IO GlobalEnv createGlobalEnv cfg = do genv0 <- mkGlobalEnv cfg - -- save removal key to a file - lowerCodensity $ do + pubkey <- liftIO . lowerCodensity $ do env <- mkEnv genv0 liftIO . runAppWithEnv env $ do config <- readServiceConfig Galley @@ -112,7 +116,21 @@ createGlobalEnv cfg = do asks (.servicesCwdBase) <&> \case Nothing -> relPath Just dir -> dir "galley" relPath - pure genv0 {gRemovalKeyPath = path} + bs <- liftIO $ B.readFile path + pems <- case pemParseBS bs of + Left err -> assertFailure $ "Could not parse removal key PEM: " <> err + Right x -> pure x + asn1 <- pemContent <$> assertOne pems + -- quick and dirty ASN.1 decoding: assume the key is of the correct + -- format, and simply skip the 16 byte header + let bytes = B.drop 16 asn1 + priv <- liftIO . throwCryptoErrorIO $ Ed25519.secretKey bytes + pure (convert (Ed25519.toPublic priv)) + + -- save removal key to a temporary file + let removalPath = gTempDir genv0 "removal.key" + liftIO $ B.writeFile removalPath pubkey + pure genv0 {gRemovalKeyPath = removalPath} runTests :: [(String, x, y, App ())] -> Maybe FilePath -> FilePath -> IO () runTests tests mXMLOutput cfg = do @@ -123,33 +141,32 @@ runTests tests mXMLOutput cfg = do Nothing -> pure () let writeOutput = writeChan output . Just - genv <- createGlobalEnv cfg - - withAsync displayOutput $ \displayThread -> do - report <- fmap mconcat $ for tests $ \(qname, _, _, action) -> do - do - (mErr, tm) <- withTime (runTest genv action) - case mErr of - Left err -> do - writeOutput $ - "----- " - <> qname - <> colored red " FAIL" - <> " (" - <> printTime tm - <> ") -----\n" - <> err - <> "\n" - pure (TestSuiteReport [TestCaseReport qname (TestFailure err) tm]) - Right _ -> do - writeOutput $ qname <> colored green " OK" <> " (" <> printTime tm <> ")" <> "\n" - pure (TestSuiteReport [TestCaseReport qname TestSuccess tm]) - writeChan output Nothing - wait displayThread - printReport report - mapM_ (saveXMLReport report) mXMLOutput - when (any (\testCase -> testCase.result /= TestSuccess) report.cases) $ - exitFailure + runCodensity (createGlobalEnv cfg) $ \genv -> + withAsync displayOutput $ \displayThread -> do + report <- fmap mconcat $ for tests $ \(qname, _, _, action) -> do + do + (mErr, tm) <- withTime (runTest genv action) + case mErr of + Left err -> do + writeOutput $ + "----- " + <> qname + <> colored red " FAIL" + <> " (" + <> printTime tm + <> ") -----\n" + <> err + <> "\n" + pure (TestSuiteReport [TestCaseReport qname (TestFailure err) tm]) + Right _ -> do + writeOutput $ qname <> colored green " OK" <> " (" <> printTime tm <> ")" <> "\n" + pure (TestSuiteReport [TestCaseReport qname TestSuccess tm]) + writeChan output Nothing + wait displayThread + printReport report + mapM_ (saveXMLReport report) mXMLOutput + when (any (\testCase -> testCase.result /= TestSuccess) report.cases) $ + exitFailure doListTests :: [(String, String, String, x)] -> IO () doListTests tests = for_ tests $ \(qname, _desc, _full, _) -> do diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index 474b1e4b71a..23fc024da10 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -41,8 +41,6 @@ main = do Just projectRoot -> pure $ joinPath [projectRoot, "services/integration.yaml"] - genv <- createGlobalEnv cfg - args <- getArgs let run = case args of @@ -54,7 +52,7 @@ main = do (_, _, _, ph) <- createProcess cp exitWith =<< waitForProcess ph - runCodensity (mkEnv genv) $ \env -> + runCodensity (createGlobalEnv cfg >>= mkEnv) $ \env -> runAppWithEnv env $ lowerCodensity $ do _modifyEnv <- diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index c1f8de19076..ca5ac7043f5 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -106,7 +106,8 @@ data GlobalEnv = GlobalEnv gServicesCwdBase :: Maybe FilePath, gRemovalKeyPath :: FilePath, gBackendResourcePool :: ResourcePool BackendResource, - gRabbitMQConfig :: RabbitMQConfig + gRabbitMQConfig :: RabbitMQConfig, + gTempDir :: FilePath } data IntegrationConfig = IntegrationConfig From 2d27ff088f9f2a6de39169aab28feb650813ec38 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 5 Oct 2023 14:20:08 +0200 Subject: [PATCH 171/225] Migrate testLeaveSubConv --- integration/test/API/Galley.hs | 13 ++ integration/test/MLS/Util.hs | 26 ++++ integration/test/Test/MLS/SubConversation.hs | 79 ++++++++++++ services/galley/test/integration/API/MLS.hs | 121 ------------------- 4 files changed, 118 insertions(+), 121 deletions(-) diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 14a8845ce4a..87e5042bc6d 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -165,6 +165,19 @@ deleteSubConversation user sub = do joinHttpPath ["conversations", domain, convId, "subconversations", subId] submit "DELETE" $ req & addJSONObject ["group_id" .= groupId, "epoch" .= epoch] +leaveSubConversation :: + (HasCallStack, MakesValue user, MakesValue sub) => + user -> + sub -> + App Response +leaveSubConversation user sub = do + (conv, Just subId) <- objSubConv sub + (domain, convId) <- objQid conv + req <- + baseRequest user Galley Versioned $ + joinHttpPath ["conversations", domain, convId, "subconversations", subId, "self"] + submit "DELETE" req + getSelfConversation :: (HasCallStack, MakesValue user) => user -> App Response getSelfConversation user = do req <- baseRequest user Galley Versioned "/conversations/mls-self" diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 402fe872564..b525dc99739 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -654,3 +654,29 @@ createApplicationMessage cid messageContent = do setMLSCiphersuite :: Ciphersuite -> App () setMLSCiphersuite suite = modifyMLSState $ \mls -> mls {ciphersuite = suite} + +leaveCurrentConv :: + HasCallStack => + ClientIdentity -> + App () +leaveCurrentConv cid = do + mls <- getMLSState + (_, mSubId) <- objSubConv mls.convId + case mSubId of + -- FUTUREWORK: implement leaving main conversation as well + Nothing -> assertFailure "Leaving conversations is not supported" + Just _ -> do + void $ leaveSubConversation cid mls.convId >>= getBody 200 + modifyMLSState $ \s -> + s + { members = Set.difference mls.members (Set.singleton cid) + } + +getCurrentConv :: HasCallStack => ClientIdentity -> App Value +getCurrentConv cid = do + mls <- getMLSState + (conv, mSubId) <- objSubConv mls.convId + resp <- case mSubId of + Nothing -> getConversation cid conv + Just sub -> getSubConversation cid conv sub + getJSON 200 resp diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 2de59d50f2f..3a909412473 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -2,6 +2,7 @@ module Test.MLS.SubConversation where import API.Galley import MLS.Util +import Notifications import SetupHelpers import Testlib.Prelude @@ -96,3 +97,81 @@ testDeleteSubConversation otherDomain = do sub2' <- getSubConversation alice1 qcnv "conference2" >>= getJSON 200 sub2 `shouldNotMatch` sub2' + +data LeaveSubConvVariant = AliceLeaves | BobLeaves + +instance HasTests x => HasTests (LeaveSubConvVariant -> x) where + mkTests m n s f x = + mkTests m (n <> "[leaver=alice]") s f (x AliceLeaves) + <> mkTests m (n <> "[leaver=bob]") s f (x BobLeaves) + +testLeaveSubConv :: HasCallStack => LeaveSubConvVariant -> App () +testLeaveSubConv variant = do + [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + clients@[alice1, bob1, bob2, charlie1] <- traverse (createMLSClient def) [alice, bob, bob, charlie] + traverse_ uploadNewKeyPackage [bob1, bob2, charlie1] + void $ createNewGroup alice1 + + withWebSockets [bob, charlie] $ \wss -> do + void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle + traverse_ (awaitMatch 10 isMemberJoinNotif) wss + + createSubConv bob1 "conference" + void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit bob2 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit charlie1 Nothing >>= sendAndConsumeCommitBundle + + -- a member leaves the subconversation + let (firstLeaver, idxFirstLeaver) = case variant of + BobLeaves -> (bob1, 0) + AliceLeaves -> (alice1, 1) + let idxCharlie1 = 3 + + let others = filter (/= firstLeaver) clients + withWebSockets others $ \wss -> do + leaveCurrentConv firstLeaver + + for_ (zip others wss) $ \(cid, ws) -> do + notif <- awaitMatch 10 isNewMLSMessageNotif ws + msgData <- notif %. "payload.0.data" & asByteString + msg <- showMessage alice1 msgData + msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` idxFirstLeaver + msg %. "message.content.sender.External" `shouldMatchInt` 0 + consumeMessage1 cid msgData + + withWebSockets (tail others) $ \wss -> do + -- a member commits the pending proposal + void $ createPendingProposalCommit (head others) >>= sendAndConsumeCommitBundle + traverse_ (awaitMatch 10 isNewMLSMessageNotif) wss + + -- send an application message + void $ createApplicationMessage (head others) "good riddance" >>= sendAndConsumeMessage + traverse_ (awaitMatch 10 isNewMLSMessageNotif) wss + + -- check that only 3 clients are left in the subconv + do + conv <- getCurrentConv (head others) + mems <- conv %. "members" & asList + length mems `shouldMatchInt` 3 + + -- charlie1 leaves + let others' = filter (/= charlie1) others + withWebSockets others' $ \wss -> do + leaveCurrentConv charlie1 + + for_ (zip others' wss) $ \(cid, ws) -> do + notif <- awaitMatch 10 isNewMLSMessageNotif ws + msgData <- notif %. "payload.0.data" & asByteString + msg <- showMessage alice1 msgData + msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` idxCharlie1 + msg %. "message.content.sender.External" `shouldMatchInt` 0 + consumeMessage1 cid msgData + + -- a member commits the pending proposal + void $ createPendingProposalCommit (head others') >>= sendAndConsumeCommitBundle + + -- check that only 2 clients are left in the subconv + do + conv <- getCurrentConv (head others) + mems <- conv %. "members" & asList + length mems `shouldMatchInt` 2 diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index dfba81b0664..aeecc82f493 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -197,8 +197,6 @@ tests s = test s "send an application message in a subconversation" testSendMessageSubConv, test s "reset a subconversation and assert no leftover proposals" testJoinDeletedSubConvWithRemoval, test s "fail to reset a subconversation with wrong epoch" testDeleteSubConvStale, - test s "leave a subconversation as a creator" (testLeaveSubConv True), - test s "leave a subconversation as a non-creator" (testLeaveSubConv False), test s "last to leave a subconversation" testLastLeaverSubConv, test s "leave a subconversation as a non-member" testLeaveSubConvNonMember, test s "remove user from parent conversation" testRemoveUserParent, @@ -2393,125 +2391,6 @@ testLastLeaverSubConv = do assertBool "group ID unchanged" $ pscGroupId prePsc /= pscGroupId psc length (pscMembers psc) @?= 0 -testLeaveSubConv :: Bool -> TestM () -testLeaveSubConv isSubConvCreator = do - [alice, bob, charlie] <- createAndConnectUsers [Nothing, Nothing, Just "charlie.example.com"] - - runMLSTest $ do - charlie1 : allLocals@[alice1, bob1, bob2] <- - traverse createMLSClient [charlie, alice, bob, bob] - traverse_ uploadNewKeyPackage [bob1, bob2] - (_, qcnv) <- setupMLSGroup alice1 - - let subId = SubConvId "conference" - (qsub, _) <- withTempMockFederator' - ( receiveCommitMock [charlie1] - <|> welcomeMock - <|> ("on-mls-message-sent" ~> RemoteMLSMessageOk) - ) - $ do - void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle - - qsub <- createSubConv qcnv bob1 subId - void $ createExternalCommit alice1 Nothing qsub >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob2 Nothing qsub >>= sendAndConsumeCommitBundle - void $ createExternalCommit charlie1 Nothing qsub >>= sendAndConsumeCommitBundle - pure qsub - - let firstLeaver = if isSubConvCreator then bob1 else alice1 - -- a member leaves the subconversation - [idxFirstLeaver] <- - map snd . filter (\(cid, _) -> cid == firstLeaver) - <$> getClientsFromGroupState - alice1 - (cidQualifiedUser firstLeaver) - let others = filter (/= firstLeaver) allLocals - mlsBracket (firstLeaver : others) $ \(wsLeaver : wss) -> do - (_, reqs) <- - withTempMockFederator' messageSentMock $ - leaveCurrentConv firstLeaver qsub - req :: RemoteMLSMessage <- - assertOne - ( toList . Aeson.decode . frBody - =<< filter ((== "on-mls-message-sent") . frRPC) reqs - ) - let msg = fromBase64ByteString $ req.message - liftIO $ - req.recipients @?= [(ciUser charlie1, ciClient charlie1)] - consumeMessage1 charlie1 msg - - msgs <- - WS.assertMatchN (5 # WS.Second) wss $ - wsAssertBackendRemoveProposal - (cidQualifiedUser firstLeaver) - (Conv <$> qcnv) - idxFirstLeaver - traverse_ (uncurry consumeMessage1) (zip others msgs) - -- assert the leaver gets no proposal or event - void . liftIO $ WS.assertNoEvent (5 # WS.Second) [wsLeaver] - - -- a member commits the pending proposal - do - leaveCommit <- createPendingProposalCommit (head others) - mlsBracket (firstLeaver : tail others) $ \(wsLeaver : wss) -> do - events <- - fst - <$$> withTempMockFederator' ("on-mls-message-sent" ~> RemoteMLSMessageOk) - $ sendAndConsumeCommitBundle leaveCommit - liftIO $ events @?= [] - WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do - wsAssertMLSMessage qsub (cidQualifiedUser . head $ others) (mpMessage leaveCommit) n - void $ WS.assertNoEvent (5 # WS.Second) [wsLeaver] - - -- send an application message - do - message <- createApplicationMessage (head others) "some text" - mlsBracket (firstLeaver : tail others) $ \(wsLeaver : wss) -> do - (events, _) <- sendAndConsumeMessage message - liftIO $ events @?= [] - WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do - wsAssertMLSMessage qsub (cidQualifiedUser . head $ others) (mpMessage message) n - void $ WS.assertNoEvent (5 # WS.Second) [wsLeaver] - - -- check that only 3 clients are left in the subconv - do - psc <- - liftTest $ - responseJsonError - =<< getSubConv (ciUser (head others)) qcnv subId - cid == charlie1) - <$> getClientsFromGroupState (head others) charlie - mlsBracket others $ \wss -> do - leaveCurrentConv charlie1 qsub - - msgs <- - WS.assertMatchN (5 # WS.Second) wss $ - wsAssertBackendRemoveProposal charlie (Conv <$> qcnv) idxCharlie1 - traverse_ (uncurry consumeMessage1) (zip others msgs) - - -- a member commits the pending proposal - void $ - withTempMockFederator' ("on-mls-message-sent" ~> RemoteMLSMessageOk) $ - createPendingProposalCommit (head others) >>= sendAndConsumeCommitBundle - - -- check that only 2 clients are left in the subconv - do - psc <- - liftTest $ - responseJsonError - =<< getSubConv (ciUser (head others)) qcnv subId - Date: Fri, 6 Oct 2023 13:49:45 +0200 Subject: [PATCH 172/225] Remove redundant testDeleteParentOfSubConv --- services/galley/test/integration/API/MLS.hs | 66 +-------------------- 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index aeecc82f493..5a8322018ab 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -201,8 +201,7 @@ tests s = test s "leave a subconversation as a non-member" testLeaveSubConvNonMember, test s "remove user from parent conversation" testRemoveUserParent, test s "remove creator from parent conversation" testRemoveCreatorParent, - test s "creator removes user from parent conversation" testCreatorRemovesUserFromParent, - test s "delete parent conversation of a subconversation" testDeleteParentOfSubConv + test s "creator removes user from parent conversation" testCreatorRemovesUserFromParent ], testGroup "Local Sender/Remote Subconversation" @@ -2260,69 +2259,6 @@ testDeleteSubConvStale = do deleteSubConv (qUnqualified alice) qcnv sconv dsc !!! do const 409 === statusCode -testDeleteParentOfSubConv :: TestM () -testDeleteParentOfSubConv = do - (tid, aliceUnqualified, [arthurUnqualified]) <- API.Util.createBindingTeamWithMembers 2 - bob <- randomQualifiedId (Domain "bobl.example.com") - - localDomain <- viewFederationDomain - let alice = Qualified aliceUnqualified localDomain - arthur = Qualified arthurUnqualified localDomain - - connectWithRemoteUser aliceUnqualified bob - - let sconv = SubConvId "conference" - (qcnv, _parentGroupId, _subGroupId) <- runMLSTest $ do - [alice1, arthur1, bob1] <- traverse createMLSClient [alice, arthur, bob] - traverse_ uploadNewKeyPackage [arthur1] - (parentGroupId, qcnv) <- setupMLSGroup alice1 - - (qcs, _) <- withTempMockFederator' - ( receiveCommitMock [bob1] - <|> ("on-mls-message-sent" ~> RemoteMLSMessageOk) - ) - $ do - void $ createAddCommit alice1 [arthur, bob] >>= sendAndConsumeCommitBundle - createSubConv qcnv alice1 sconv - - subGid <- getCurrentGroupId - - resetGroup arthur1 qcs subGid - void - $ withTempMockFederator' - ( welcomeMock - <|> ("on-mls-message-sent" ~> RemoteMLSMessageOk) - ) - $ createExternalCommit arthur1 Nothing qcs >>= sendAndConsumeCommitBundle - - resetGroup bob1 qcs subGid - void - $ withTempMockFederator' - ( welcomeMock - <|> ("on-mls-message-sent" ~> RemoteMLSMessageOk) - ) - $ createExternalCommit bob1 Nothing qcs >>= sendAndConsumeCommitBundle - - sub' <- - responseJsonError - =<< liftTest - ( getSubConv (qUnqualified alice) qcnv sconv - TestM () testDeleteRemoteSubConv isAMember = do alice <- randomQualifiedUser From 914ebae4ab580886b27bcbd86a4ae6e559f42a9d Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 6 Oct 2023 13:51:13 +0200 Subject: [PATCH 173/225] Remove redundant testRemoteUserJoinSubConv --- services/galley/test/integration/API/MLS.hs | 38 +-------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 5a8322018ab..7fd56d2c497 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -216,8 +216,7 @@ tests s = testGroup "Remote Sender/Local SubConversation" [ test s "get subconversation as a remote member" (testRemoteMemberGetSubConv True), - test s "get subconversation as a remote non-member" (testRemoteMemberGetSubConv False), - test s "client of a remote user joins subconversation" testRemoteUserJoinSubConv + test s "get subconversation as a remote non-member" (testRemoteMemberGetSubConv False) ], testGroup "Remote Sender/Remote SubConversation" @@ -2052,41 +2051,6 @@ testRemoteSubConvNotificationWhenUserJoins = do withTempMockFederator' (receiveCommitMock [bob1] <|> welcomeMock) $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle -testRemoteUserJoinSubConv :: TestM () -testRemoteUserJoinSubConv = do - [alice, bob] <- createAndConnectUsers [Nothing, Just "bob.example.com"] - - runMLSTest $ do - alice1 <- createMLSClient alice - (_, qcnv) <- setupMLSGroup alice1 - - bob1 <- createFakeMLSClient bob - void $ do - commit <- createAddCommit alice1 [bob] - withTempMockFederator' (receiveCommitMock [bob1] <|> welcomeMock) $ - sendAndConsumeCommitBundle commit - - let mock = messageSentMock - let subId = SubConvId "conference" - (qcs, _reqs) <- withTempMockFederator' mock $ createSubConv qcnv alice1 subId - - -- bob joins the subconversation - void $ - withTempMockFederator' ("on-mls-message-sent" ~> RemoteMLSMessageOk) $ - createExternalCommit bob1 Nothing qcs >>= sendAndConsumeCommitBundle - - -- check that bob is now part of the subconversation - liftTest $ do - psc' <- - responseJsonError - =<< getSubConv (qUnqualified alice) qcnv subId - >= sendAndConsumeCommitBundle - testSendMessageSubConv :: TestM () testSendMessageSubConv = do [alice, bob] <- createAndConnectUsers [Nothing, Nothing] From ab00527950c3eee9ebb24ddeb5ef482d2ceb7ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 6 Oct 2023 15:08:47 +0200 Subject: [PATCH 174/225] Tests: fix testMLSOne2One --- integration/test/Test/MLS/One2One.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index b876725cddc..d23362beb9f 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -4,6 +4,7 @@ import API.Galley import Data.ByteString.Base64 qualified as Base64 import Data.ByteString.Char8 qualified as B8 import MLS.Util +import Notifications import SetupHelpers import Testlib.Prelude @@ -85,6 +86,8 @@ testMLSOne2One scenario = do n <- awaitMatch 3 isWelcome ws nPayload n %. "data" `shouldMatch` B8.unpack (Base64.encode (fold commit.welcome)) + void $ awaitMatch 3 isMemberJoinNotif ws + withWebSocket bob1 $ \ws -> do mp <- createApplicationMessage alice1 "hello, world" void $ sendAndConsumeMessage mp From ac3c187f32d1d000226db65153ccd5fd7e1f4846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 6 Oct 2023 15:28:44 +0200 Subject: [PATCH 175/225] Tests: fix SubConversation.testDeleteParentOfSubConv --- integration/test/Test/MLS/SubConversation.hs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 3a909412473..24ec1c354c0 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -36,9 +36,11 @@ testDeleteParentOfSubConv secondDomain = do [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [alice1, bob1] (_, qcnv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - createSubConv bob1 "conference" + withWebSocket bob $ \ws -> do + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ awaitMatch 3 isMemberJoinNotif ws + createSubConv bob1 "conference" -- bob adds his client to the subconversation void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle @@ -58,8 +60,10 @@ testDeleteParentOfSubConv secondDomain = do resp.status `shouldMatchInt` 201 -- alice deletes main conversation - void . bindResponse (deleteTeamConv tid qcnv alice) $ \resp -> do - resp.status `shouldMatchInt` 200 + withWebSocket bob $ \ws -> do + void . bindResponse (deleteTeamConv tid qcnv alice) $ \resp -> do + resp.status `shouldMatchInt` 200 + void $ awaitMatch 3 isConvDeleteNotif ws -- bob fails to send a message to the subconversation do From 007caa136b0d2c3b20f382fc85c6fccedc49666b Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 6 Oct 2023 15:32:20 +0200 Subject: [PATCH 176/225] Delete duplicated test --- integration/test/Test/MLS.hs | 50 ------------------------------------ 1 file changed, 50 deletions(-) diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index e1bbd3dc5d2..b47355e4504 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -439,56 +439,6 @@ testJoinSubConv = do createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle -testDeleteParentOfSubConv :: HasCallStack => Domain -> App () -testDeleteParentOfSubConv secondDomain = do - (alice, tid, _) <- createTeam OwnDomain 1 - bob <- randomUser secondDomain def - connectUsers [alice, bob] - - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [alice1, bob1] - (_, qcnv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - void $ createSubConv bob1 "conference" - - -- bob adds his client to the subconversation - void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle - - -- alice joins with her own client - void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle - - -- bob sends a message to the subconversation - do - mp <- createApplicationMessage bob1 "hello, alice" - void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do - resp.status `shouldMatchInt` 201 - - -- alice sends a message to the subconversation - do - mp <- createApplicationMessage bob1 "hello, bob" - void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do - resp.status `shouldMatchInt` 201 - - -- alice deletes main conversation - void . bindResponse (deleteTeamConv tid qcnv alice) $ \resp -> do - resp.status `shouldMatchInt` 200 - - -- bob fails to send a message to the subconversation - do - mp <- createApplicationMessage bob1 "hello, alice" - void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do - resp.status `shouldMatchInt` 404 - case secondDomain of - OwnDomain -> resp.json %. "label" `shouldMatch` "no-conversation" - OtherDomain -> resp.json %. "label" `shouldMatch` "no-conversation-member" - - -- alice fails to send a message to the subconversation - do - mp <- createApplicationMessage alice1 "hello, bob" - void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do - resp.status `shouldMatchInt` 404 - resp.json %. "label" `shouldMatch` "no-conversation" - -- | FUTUREWORK: Don't allow partial adds, not even in the first commit testFirstCommitAllowsPartialAdds :: HasCallStack => App () testFirstCommitAllowsPartialAdds = do From aacc327f12810b167d88154a81a6be82879f0a34 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 6 Oct 2023 16:00:22 +0200 Subject: [PATCH 177/225] Update nix files --- integration/default.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration/default.nix b/integration/default.nix index 1955f0037ba..979f965d392 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -41,6 +41,7 @@ , network , network-uri , optparse-applicative +, pem , process , proto-lens , random @@ -110,6 +111,7 @@ mkDerivation { network network-uri optparse-applicative + pem process proto-lens random From 5cbe1822b8cda238b784d79060531bb16807c422 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 25 Sep 2023 10:32:40 +0200 Subject: [PATCH 178/225] Refactor getNotification API in integration --- integration/integration.cabal | 1 + integration/test/API/Gundeck.hs | 38 ++++++++++++++------------ integration/test/Notifications.hs | 8 +++++- integration/test/Test/Client.hs | 4 +-- integration/test/Test/Conversation.hs | 1 + integration/test/Test/Notifications.hs | 24 ++++++++++------ integration/test/Test/Presence.hs | 2 +- 7 files changed, 49 insertions(+), 29 deletions(-) diff --git a/integration/integration.cabal b/integration/integration.cabal index 7f4547f0c24..2def8def0a7 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -22,6 +22,7 @@ common common-all default-language: GHC2021 ghc-options: -Wall -Wpartial-fields -fwarn-tabs -Wno-incomplete-uni-patterns + -Wno-ambiguous-fields default-extensions: NoImplicitPrelude diff --git a/integration/test/API/Gundeck.hs b/integration/test/API/Gundeck.hs index 42fccb29c15..1f2c1a76429 100644 --- a/integration/test/API/Gundeck.hs +++ b/integration/test/API/Gundeck.hs @@ -5,54 +5,58 @@ import Testlib.Prelude data GetNotifications = GetNotifications { since :: Maybe String, - size :: Maybe Int + size :: Maybe Int, + client :: Maybe String } instance Default GetNotifications where - def = GetNotifications {since = Nothing, size = Nothing} + def = GetNotifications {since = Nothing, size = Nothing, client = Nothing} getNotifications :: - (HasCallStack, MakesValue user, MakesValue client) => + (HasCallStack, MakesValue user) => user -> - client -> GetNotifications -> App Response -getNotifications user client r = do - c <- client & asString +getNotifications user r = do req <- baseRequest user Gundeck Versioned "/notifications" let req' = req & addQueryParams ( [("since", since) | since <- toList r.since] - <> [("client", c)] + <> [("client", c) | c <- toList r.client] <> [("size", show size) | size <- toList r.size] ) submit "GET" req' +data GetNotification = GetNotification + { client :: Maybe String + } + +instance Default GetNotification where + def = GetNotification Nothing + getNotification :: - (HasCallStack, MakesValue user, MakesValue client, MakesValue nid) => + (HasCallStack, MakesValue user, MakesValue nid) => user -> - client -> + GetNotification -> nid -> App Response -getNotification user client nid = do - c <- client & asString +getNotification user opts nid = do n <- nid & asString req <- baseRequest user Gundeck Versioned $ joinHttpPath ["notifications", n] - submit "GET" $ req & addQueryParams [("client", c)] + submit "GET" $ req & addQueryParams [("client", c) | c <- toList opts.client] getLastNotification :: - (HasCallStack, MakesValue user, MakesValue client) => + (HasCallStack, MakesValue user) => user -> - client -> + GetNotification -> App Response -getLastNotification user client = do - c <- client & asString +getLastNotification user opts = do req <- baseRequest user Gundeck Versioned "/notifications/last" - submit "GET" $ req & addQueryParams [("client", c)] + submit "GET" $ req & addQueryParams [("client", c) | c <- toList opts.client] data PostPushToken = PostPushToken { transport :: String, diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index a6ffb6505b1..e2606474074 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -22,7 +22,13 @@ awaitNotifications user client since0 tSecs n selector = where go 0 _ res = pure res go timeRemaining since res0 = do - notifs <- bindResponse (getNotifications user client (GetNotifications since Nothing)) $ \resp -> asList (resp.json %. "notifications") + c <- make client & asString + notifs <- bindResponse + ( getNotifications + user + def {since = since, client = Just c} + ) + $ \resp -> asList (resp.json %. "notifications") lastNotifId <- case notifs of [] -> pure since _ -> Just <$> objId (last notifs) diff --git a/integration/test/Test/Client.hs b/integration/test/Test/Client.hs index 4ee88901419..a561afa461b 100644 --- a/integration/test/Test/Client.hs +++ b/integration/test/Test/Client.hs @@ -21,7 +21,7 @@ testClientLastActive :: HasCallStack => App () testClientLastActive = do alice <- randomUser OwnDomain def c0 <- addClient alice def >>= getJSON 201 - cid <- c0 %. "id" + cid <- c0 %. "id" & asString -- newly created clients should not have a last_active value tm0 <- fromMaybe Null <$> lookupField c0 "last_active" @@ -30,7 +30,7 @@ testClientLastActive = do now <- systemSeconds <$> liftIO getSystemTime -- fetching notifications updates last_active - void $ getNotifications alice cid def + void $ getNotifications alice def {client = Just cid} c1 <- getClient alice cid >>= getJSON 200 tm1 <- c1 %. "last_active" & asString diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 79f548471e7..6f6734e5c77 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -23,6 +23,7 @@ import API.Brig import API.BrigInternal import API.Galley import API.GalleyInternal +import API.Gundeck import Control.Applicative import Control.Concurrent (threadDelay) import Control.Monad.Codensity diff --git a/integration/test/Test/Notifications.hs b/integration/test/Test/Notifications.hs index ee17b0da485..33239fdaa40 100644 --- a/integration/test/Test/Notifications.hs +++ b/integration/test/Test/Notifications.hs @@ -25,18 +25,27 @@ testFetchAllNotifications = do bindResponse (postPush user [push]) $ \res -> res.status `shouldMatchInt` 200 - let client = "deadbeeef" - ns <- getNotifications user client def >>= getJSON 200 + let c :: Maybe String = Just "deadbeef" + ns <- getNotifications user (def {client = c} :: GetNotifications) >>= getJSON 200 expected <- replicateM n (push %. "payload") allNotifs <- ns %. "notifications" & asList actual <- traverse (%. "payload") allNotifs actual `shouldMatch` expected - firstNotif <- getNotification user client (head allNotifs %. "id") >>= getJSON 200 + firstNotif <- + getNotification + user + (def {client = c} :: GetNotification) + (head allNotifs %. "id") + >>= getJSON 200 firstNotif `shouldMatch` head allNotifs - lastNotif <- getLastNotification user client >>= getJSON 200 + lastNotif <- + getLastNotification + user + (def {client = c} :: GetNotification) + >>= getJSON 200 lastNotif `shouldMatch` last allNotifs testLastNotification :: App () @@ -59,24 +68,23 @@ testLastNotification = do bindResponse (postPush user [push c]) $ \res -> res.status `shouldMatchInt` 200 - lastNotif <- getLastNotification user "c" >>= getJSON 200 + lastNotif <- getLastNotification user def {client = Just "c"} >>= getJSON 200 lastNotif %. "payload" `shouldMatch` [object ["client" .= "c"]] testInvalidNotification :: HasCallStack => App () testInvalidNotification = do user <- randomUserId OwnDomain - let client = "deadbeef" -- test uuid v4 as "since" do notifId <- randomId void $ - getNotifications user client def {since = Just notifId} + getNotifications user def {since = Just notifId} >>= getJSON 400 -- test arbitrary uuid v1 as "since" do notifId <- randomUUIDv1 void $ - getNotifications user client def {since = Just notifId} + getNotifications user def {since = Just notifId} >>= getJSON 404 diff --git a/integration/test/Test/Presence.hs b/integration/test/Test/Presence.hs index e2bc211b598..5e6d1a04897 100644 --- a/integration/test/Test/Presence.hs +++ b/integration/test/Test/Presence.hs @@ -52,6 +52,6 @@ testRemoveUser = do -- check that notifications are deleted do - ns <- getNotifications alice c def >>= getJSON 200 + ns <- getNotifications alice def {client = Just c} >>= getJSON 200 ns %. "notifications" `shouldMatch` ([] :: [Value]) ns %. "has_more" `shouldMatch` False From e5c8baa1252a77d36c6ea03234b580ced7064d40 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 26 Sep 2023 09:49:02 +0200 Subject: [PATCH 179/225] Fix warning --- integration/test/Test/Conversation.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 6f6734e5c77..79f548471e7 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -23,7 +23,6 @@ import API.Brig import API.BrigInternal import API.Galley import API.GalleyInternal -import API.Gundeck import Control.Applicative import Control.Concurrent (threadDelay) import Control.Monad.Codensity From d92d037c4c5c83edba0314ca5564cab35c9a9b79 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 27 Sep 2023 15:25:56 +0200 Subject: [PATCH 180/225] Disable ambiguous-fields warning by module --- integration/integration.cabal | 1 - integration/test/Notifications.hs | 1 + integration/test/Test/Client.hs | 2 +- integration/test/Test/Notifications.hs | 1 + integration/test/Test/Presence.hs | 1 + 5 files changed, 4 insertions(+), 2 deletions(-) diff --git a/integration/integration.cabal b/integration/integration.cabal index 2def8def0a7..7f4547f0c24 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -22,7 +22,6 @@ common common-all default-language: GHC2021 ghc-options: -Wall -Wpartial-fields -fwarn-tabs -Wno-incomplete-uni-patterns - -Wno-ambiguous-fields default-extensions: NoImplicitPrelude diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index e2606474074..971f16be84e 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -1,3 +1,4 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} module Notifications where import API.Gundeck diff --git a/integration/test/Test/Client.hs b/integration/test/Test/Client.hs index a561afa461b..caacd2051af 100644 --- a/integration/test/Test/Client.hs +++ b/integration/test/Test/Client.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields -Wno-incomplete-uni-patterns #-} module Test.Client where diff --git a/integration/test/Test/Notifications.hs b/integration/test/Test/Notifications.hs index 33239fdaa40..741d9b10b0a 100644 --- a/integration/test/Test/Notifications.hs +++ b/integration/test/Test/Notifications.hs @@ -1,3 +1,4 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} module Test.Notifications where import API.Common diff --git a/integration/test/Test/Presence.hs b/integration/test/Test/Presence.hs index 5e6d1a04897..a733d2bb539 100644 --- a/integration/test/Test/Presence.hs +++ b/integration/test/Test/Presence.hs @@ -1,3 +1,4 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} module Test.Presence where import API.Common From 303d1f6d0ff9c4f9c622e88353fd4fbb77e3601c Mon Sep 17 00:00:00 2001 From: Owen Harvey Date: Mon, 9 Oct 2023 18:41:34 +1000 Subject: [PATCH 181/225] WPB-3797 do not cache federation remote domain config (#3612) --------- Co-authored-by: Matthias Fischmann Co-authored-by: Igor Ranieri --- ...-not-cache-federation-remote-domain-config | 1 + libs/wire-api/default.nix | 7 -- .../wire-api/src/Wire/API/FederationUpdate.hs | 94 +------------------ libs/wire-api/wire-api.cabal | 4 - .../background-worker/background-worker.cabal | 2 - .../background-worker.integration.yaml | 2 +- services/background-worker/default.nix | 2 - .../src/Wire/BackendNotificationPusher.hs | 1 - .../src/Wire/BackgroundWorker/Env.hs | 4 - .../Wire/BackendNotificationPusherSpec.hs | 3 - .../background-worker/test/Test/Wire/Util.hs | 5 +- services/federator/default.nix | 1 + services/federator/federator.cabal | 1 + services/federator/src/Federator/Env.hs | 2 - services/federator/src/Federator/Response.hs | 20 +++- services/federator/src/Federator/Run.hs | 11 +-- 16 files changed, 31 insertions(+), 129 deletions(-) create mode 100644 changelog.d/5-internal/WPB-3797-do-not-cache-federation-remote-domain-config diff --git a/changelog.d/5-internal/WPB-3797-do-not-cache-federation-remote-domain-config b/changelog.d/5-internal/WPB-3797-do-not-cache-federation-remote-domain-config new file mode 100644 index 00000000000..dfd7ed0f27f --- /dev/null +++ b/changelog.d/5-internal/WPB-3797-do-not-cache-federation-remote-domain-config @@ -0,0 +1 @@ +Do not cache federation remote configs on non-brig services diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index b76f3159a94..e63734ae297 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -46,7 +46,6 @@ , hspec , hspec-wai , http-api-data -, http-client , http-media , http-types , imports @@ -72,7 +71,6 @@ , quickcheck-instances , random , resourcet -, retry , saml2-web-sso , schema-profunctor , scientific @@ -95,7 +93,6 @@ , tasty-quickcheck , text , time -, tinylog , transitive-anns , types-common , unliftio @@ -119,7 +116,6 @@ mkDerivation { src = gitignoreSource ./.; libraryHaskellDepends = [ aeson - async attoparsec base base64-bytestring @@ -153,7 +149,6 @@ mkDerivation { hscim HsOpenSSL http-api-data - http-client http-media http-types imports @@ -176,7 +171,6 @@ mkDerivation { quickcheck-instances random resourcet - retry saml2-web-sso schema-profunctor scientific @@ -195,7 +189,6 @@ mkDerivation { tagged text time - tinylog transitive-anns types-common unordered-containers diff --git a/libs/wire-api/src/Wire/API/FederationUpdate.hs b/libs/wire-api/src/Wire/API/FederationUpdate.hs index aeb700fa52b..d1930d7740f 100644 --- a/libs/wire-api/src/Wire/API/FederationUpdate.hs +++ b/libs/wire-api/src/Wire/API/FederationUpdate.hs @@ -1,99 +1,13 @@ module Wire.API.FederationUpdate - ( syncFedDomainConfigs, - SyncFedDomainConfigsCallback (..), - emptySyncFedDomainConfigsCallback, + ( getFederationDomainConfigs, ) where -import Control.Concurrent.Async -import Control.Exception -import Control.Retry qualified as R -import Data.Set qualified as Set -import Data.Text -import Data.Typeable (cast) import Imports -import Network.HTTP.Client (defaultManagerSettings, newManager) -import Servant.Client (BaseUrl (BaseUrl), ClientEnv (ClientEnv), ClientError, Scheme (Http), runClientM) -import Servant.Client.Internal.HttpClient (defaultMakeClientRequest) -import System.Logger qualified as L -import Util.Options +import Servant.Client (ClientEnv, ClientError, runClientM) import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Internal.Brig qualified as IAPI import Wire.API.Routes.Named (namedClient) --- | 'FedUpdateCallback' is not called if a new settings cannot be fetched, or if they are --- equal to the old settings. -syncFedDomainConfigs :: Endpoint -> L.Logger -> SyncFedDomainConfigsCallback -> IO (IORef FederationDomainConfigs, Async ()) -syncFedDomainConfigs (Endpoint h p) log' cb = do - let baseUrl = BaseUrl Http (unpack h) (fromIntegral p) "" - clientEnv <- newManager defaultManagerSettings <&> \mgr -> ClientEnv mgr baseUrl Nothing defaultMakeClientRequest - ioref <- newIORef =<< initialize log' clientEnv - updateDomainsThread <- async $ loop log' clientEnv cb ioref - pure (ioref, updateDomainsThread) - --- | Initial function for getting the set of domains from brig, and an update interval -initialize :: L.Logger -> ClientEnv -> IO FederationDomainConfigs -initialize logger clientEnv = - let policy :: R.RetryPolicy - policy = R.capDelay 30_000_000 $ R.exponentialBackoff 3_000 - - go :: IO (Maybe FederationDomainConfigs) - go = do - fetch clientEnv >>= \case - Right s -> pure $ Just s - Left e -> do - L.log logger L.Info $ - L.msg (L.val "Failed to reach brig for federation setup, retrying...") - L.~~ "error" L..= show e - pure Nothing - in R.retrying policy (const (pure . isNothing)) (const go) >>= \case - Just c -> pure c - Nothing -> throwIO $ ErrorCall "*** Failed to reach brig for federation setup, giving up!" - -loop :: L.Logger -> ClientEnv -> SyncFedDomainConfigsCallback -> IORef FederationDomainConfigs -> IO () -loop logger clientEnv (SyncFedDomainConfigsCallback callback) env = forever $ - catch go $ \(e :: SomeException) -> do - -- log synchronous exceptions - case fromException e of - -- Rethrow async exceptions so that we can kill this thread with the `async` tools - -- The use of cast here comes from https://hackage.haskell.org/package/base-4.18.0.0/docs/src/GHC.IO.Exception.html#asyncExceptionFromException - -- But I only want to check for AsyncCancelled while leaving non-async exception - -- logging in place. - Just (SomeAsyncException e') -> case cast e' of - Just AsyncCancelled -> throwIO e - Nothing -> pure () - Nothing -> - L.log logger L.Error $ - L.msg (L.val "Federation domain sync thread died, restarting domain synchronization.") - L.~~ "error" L..= displayException e - where - go = do - fetch clientEnv >>= \case - Left e -> - L.log logger L.Info $ - L.msg (L.val "Could not retrieve an updated list of federation domains from Brig; I'll keep trying!") - L.~~ "error" L..= displayException e - Right new -> do - old <- readIORef env - unless (domainListsEqual old new) $ callback old new - atomicWriteIORef env new - delay <- updateInterval <$> readIORef env - threadDelay (delay * 1_000_000) - - domainListsEqual o n = - Set.fromList (domain <$> remotes o) - == Set.fromList (domain <$> remotes n) - -fetch :: ClientEnv -> IO (Either ClientError FederationDomainConfigs) -fetch = runClientM (namedClient @IAPI.API @"get-federation-remotes") - --- | The callback takes the previous and the new settings and runs a given action. -newtype SyncFedDomainConfigsCallback = SyncFedDomainConfigsCallback - { fromFedUpdateCallback :: - FederationDomainConfigs -> -- old value - FederationDomainConfigs -> -- new value - IO () - } - -emptySyncFedDomainConfigsCallback :: SyncFedDomainConfigsCallback -emptySyncFedDomainConfigsCallback = SyncFedDomainConfigsCallback $ \_ _ -> pure () +getFederationDomainConfigs :: ClientEnv -> IO (Either ClientError FederationDomainConfigs) +getFederationDomainConfigs = runClientM $ namedClient @IAPI.API @"get-federation-remotes" diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index cb5ea5c163a..ef2917257e1 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -225,7 +225,6 @@ library build-depends: aeson >=2.0.1.0 - , async , attoparsec >=0.10 , base >=4 && <5 , base64-bytestring >=1.0 @@ -259,7 +258,6 @@ library , hscim , HsOpenSSL , http-api-data - , http-client , http-media , http-types , imports @@ -282,7 +280,6 @@ library , quickcheck-instances >=0.3.16 , random >=1.2.0 , resourcet - , retry , saml2-web-sso , schema-profunctor , scientific @@ -301,7 +298,6 @@ library , tagged , text >=0.11 , time >=1.4 - , tinylog , transitive-anns , types-common >=0.16 , unordered-containers >=0.2 diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index 44eaec6d2d7..377e7487ae5 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -29,7 +29,6 @@ library build-depends: aeson , amqp - , base , containers , exceptions , extended @@ -50,7 +49,6 @@ library , types-common , unliftio , wai-utilities - , wire-api , wire-api-federation default-extensions: diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml index 02a2a69851a..32ff94e37ef 100644 --- a/services/background-worker/background-worker.integration.yaml +++ b/services/background-worker/background-worker.integration.yaml @@ -17,4 +17,4 @@ rabbitmq: backendNotificationPusher: pushBackoffMinWait: 1000 # 1ms pushBackoffMaxWait: 1000000 # 1s - remotesRefreshInterval: 10000 # 10ms + remotesRefreshInterval: 10000 # 10ms \ No newline at end of file diff --git a/services/background-worker/default.nix b/services/background-worker/default.nix index 4a6288d5097..910b9a396dd 100644 --- a/services/background-worker/default.nix +++ b/services/background-worker/default.nix @@ -50,7 +50,6 @@ mkDerivation { libraryHaskellDepends = [ aeson amqp - base containers exceptions extended @@ -71,7 +70,6 @@ mkDerivation { types-common unliftio wai-utilities - wire-api wire-api-federation ]; executableHaskellDepends = [ HsOpenSSL imports types-common ]; diff --git a/services/background-worker/src/Wire/BackendNotificationPusher.hs b/services/background-worker/src/Wire/BackendNotificationPusher.hs index 793bafd418d..3bcceafac4c 100644 --- a/services/background-worker/src/Wire/BackendNotificationPusher.hs +++ b/services/background-worker/src/Wire/BackendNotificationPusher.hs @@ -118,7 +118,6 @@ startPusher consumersRef chan = do consumers <- liftIO $ readIORef consumersRef traverse_ (liftIO . Q.cancelConsumer chan . fst) $ Map.elems consumers throwM e - timeBeforeNextRefresh <- asks (.backendNotificationsConfig.remotesRefreshInterval) -- If this thread is cancelled, catch the exception, kill the consumers, and carry on. -- FUTUREWORK?: diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index ef99676c49a..0d3080595f6 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -3,7 +3,6 @@ module Wire.BackgroundWorker.Env where -import Control.Concurrent.Chan import Control.Monad.Base import Control.Monad.Catch import Control.Monad.Trans.Control @@ -22,7 +21,6 @@ import System.Logger qualified as Log import System.Logger.Class (Logger, MonadLogger (..)) import System.Logger.Extended qualified as Log import Util.Options -import Wire.API.Routes.FederationDomainConfig import Wire.BackgroundWorker.Options type IsWorking = Bool @@ -41,7 +39,6 @@ data Env = Env federatorInternal :: Endpoint, httpManager :: Manager, defederationTimeout :: ResponseTimeout, - remoteDomainsChan :: Chan FederationDomainConfigs, backendNotificationMetrics :: BackendNotificationMetrics, backendNotificationsConfig :: BackendNotificationsConfig, statuses :: IORef (Map Worker IsWorking) @@ -65,7 +62,6 @@ mkEnv opts = do http2Manager <- initHttp2Manager logger <- Log.mkLogger opts.logLevel Nothing opts.logFormat httpManager <- newManager defaultManagerSettings - remoteDomainsChan <- newChan let federatorInternal = opts.federatorInternal defederationTimeout = maybe diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index 0aa74d531f4..243eb3d864b 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -4,7 +4,6 @@ module Test.Wire.BackendNotificationPusherSpec where -import Control.Concurrent.Chan import Control.Exception import Control.Monad.Trans.Except import Data.Aeson qualified as Aeson @@ -181,7 +180,6 @@ spec = do ] logger <- Logger.new Logger.defSettings httpManager <- newManager defaultManagerSettings - remoteDomainsChan <- newChan let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined @@ -200,7 +198,6 @@ spec = do mockAdmin <- newMockRabbitMqAdmin True ["backend-notifications.foo.example"] logger <- Logger.new Logger.defSettings httpManager <- newManager defaultManagerSettings - remoteDomainsChan <- newChan let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index 22a7c38dcef..ba698cccc2b 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -2,12 +2,12 @@ module Test.Wire.Util where -import Control.Concurrent.Chan import Imports import Network.HTTP.Client import System.Logger.Class qualified as Logger import Util.Options (Endpoint (..)) -import Wire.BackgroundWorker.Env +import Wire.BackgroundWorker.Env hiding (federatorInternal) +import Wire.BackgroundWorker.Env qualified as E import Wire.BackgroundWorker.Options import Wire.BackgroundWorker.Util @@ -18,7 +18,6 @@ testEnv = do statuses <- newIORef mempty backendNotificationMetrics <- mkBackendNotificationMetrics httpManager <- newManager defaultManagerSettings - remoteDomainsChan <- newChan let federatorInternal = Endpoint "localhost" 0 rabbitmqAdminClient = undefined rabbitmqVHost = undefined diff --git a/services/federator/default.nix b/services/federator/default.nix index 7aafe2dc588..44acd863cc6 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -110,6 +110,7 @@ mkDerivation { polysemy-wire-zoo prometheus-client servant + servant-client servant-client-core servant-server text diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index 4a5228e30c9..0d52c231756 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -137,6 +137,7 @@ library , polysemy-wire-zoo , prometheus-client , servant + , servant-client , servant-client-core , servant-server , text diff --git a/services/federator/src/Federator/Env.hs b/services/federator/src/Federator/Env.hs index 07d75c19add..12f3670ef18 100644 --- a/services/federator/src/Federator/Env.hs +++ b/services/federator/src/Federator/Env.hs @@ -34,7 +34,6 @@ import Prometheus import System.Logger.Class qualified as LC import Util.Options import Wire.API.Federation.Component -import Wire.API.Routes.FederationDomainConfig (FederationDomainConfigs) data FederatorMetrics = FederatorMetrics { outgoingRequests :: Vector Text Counter, @@ -47,7 +46,6 @@ data Env = Env _requestId :: RequestId, _dnsResolver :: Resolver, _runSettings :: RunSettings, - _domainConfigs :: IORef FederationDomainConfigs, _service :: Component -> Endpoint, _externalPort :: Word16, _internalPort :: Word16, diff --git a/services/federator/src/Federator/Response.hs b/services/federator/src/Federator/Response.hs index e7089d9a6d5..ae248f01e18 100644 --- a/services/federator/src/Federator/Response.hs +++ b/services/federator/src/Federator/Response.hs @@ -54,10 +54,14 @@ import Polysemy.Input import Polysemy.Internal import Polysemy.TinyLog import Servant hiding (ServerError, respond, serve) +import Servant.Client (mkClientEnv) import Servant.Client.Core import Servant.Server.Generic import Servant.Types.SourceT -import Wire.API.Routes.FederationDomainConfig +import Util.Options (Endpoint (..)) +import Wire.API.FederationUpdate qualified as FedUp (getFederationDomainConfigs) +import Wire.API.MakesFederatedCall (Component (Brig)) +import Wire.API.Routes.FederationDomainConfig qualified as FedUp (FederationDomainConfigs) import Wire.Network.DNS.Effect import Wire.Sem.Logger.TinyLog @@ -145,7 +149,7 @@ type AllEffects = ServiceStreaming, Input RunSettings, Input Http2Manager, -- needed by Remote - Input FederationDomainConfigs, -- needed for the domain list. + Input FedUp.FederationDomainConfigs, -- needed for the domain list and federation policy. Input Env, -- needed by Service Error ValidationError, Error RemoteError, @@ -170,7 +174,7 @@ runFederator env = DiscoveryFailure ] . runInputConst env - . runInputSem (embed @IO (readIORef (view domainConfigs env))) + . runInputSem (embed @IO (getFederationDomainConfigs env)) . runInputSem (embed @IO (readIORef (view http2Manager env))) . runInputConst (view runSettings env) . interpretServiceHTTP @@ -179,6 +183,16 @@ runFederator env = . interpretRemote . interpretMetrics +getFederationDomainConfigs :: Env -> IO FedUp.FederationDomainConfigs +getFederationDomainConfigs env = do + let mgr = env ^. httpManager + Endpoint h p = env ^. service $ Brig + baseurl = BaseUrl Http (cs h) (fromIntegral p) "" + clientEnv = mkClientEnv mgr baseurl + FedUp.getFederationDomainConfigs clientEnv >>= \case + Right v -> pure v + Left e -> error $ show e + streamingResponseToWai :: StreamingResponse -> Wai.Response streamingResponseToWai resp = let headers = toList (responseHeaders resp) diff --git a/services/federator/src/Federator/Run.hs b/services/federator/src/Federator/Run.hs index 3b73ab7051e..ebdf4cb295e 100644 --- a/services/federator/src/Federator/Run.hs +++ b/services/federator/src/Federator/Run.hs @@ -52,8 +52,6 @@ import System.Logger qualified as Log import System.Logger.Extended qualified as LogExt import Util.Options import Wire.API.Federation.Component -import Wire.API.FederationUpdate -import Wire.API.Routes.FederationDomainConfig import Wire.Network.DNS.Helper qualified as DNS ------------------------------------------------------------------------------ @@ -65,14 +63,13 @@ run opts = do let resolvConf = mkResolvConf (optSettings opts) DNS.defaultResolvConf DNS.withCachingResolver resolvConf $ \res -> do logger <- LogExt.mkLogger (Opt.logLevel opts) (Opt.logNetStrings opts) (Opt.logFormat opts) - (ioref, updateFedDomainsThread) <- syncFedDomainConfigs (brig opts) logger emptySyncFedDomainConfigsCallback - bracket (newEnv opts res logger ioref) closeEnv $ \env -> do + bracket (newEnv opts res logger) closeEnv $ \env -> do let externalServer = serveInward env portExternal internalServer = serveOutward env portInternal withMonitor logger (onNewSSLContext env) (optSettings opts) $ do internalServerThread <- async internalServer externalServerThread <- async externalServer - void $ waitAnyCancel [updateFedDomainsThread, internalServerThread, externalServerThread] + void $ waitAnyCancel [internalServerThread, externalServerThread] where endpointInternal = federatorInternal opts portInternal = fromIntegral $ endpointInternal ^. port @@ -92,8 +89,8 @@ run opts = do ------------------------------------------------------------------------------- -- Environment -newEnv :: Opts -> DNS.Resolver -> Log.Logger -> IORef FederationDomainConfigs -> IO Env -newEnv o _dnsResolver _applog _domainConfigs = do +newEnv :: Opts -> DNS.Resolver -> Log.Logger -> IO Env +newEnv o _dnsResolver _applog = do _metrics <- Metrics.metrics let _requestId = def _runSettings = Opt.optSettings o From 77c8c9b283f9c96500f325972fa7402b3c5e8c8d Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 9 Oct 2023 16:48:50 +0200 Subject: [PATCH 182/225] Hi CI From b1197e84f9efefd227a2646385cf1ceea34946ef Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:29:51 +0200 Subject: [PATCH 183/225] [chore] Fix flaky offline backend notification integration test. (#3636) * Bumped timeout from 1s to 5s. --- integration/test/Test/Federation.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs index e24d6c6657e..3fc5b1d76c6 100644 --- a/integration/test/Test/Federation.hs +++ b/integration/test/Test/Federation.hs @@ -124,7 +124,7 @@ testNotificationsForOfflineBackends = do isNotifConv downBackendConv, isNotifForUser delUser ] - void $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isDelUserLeaveDownConvNotif + void $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 5 isDelUserLeaveDownConvNotif -- FUTUREWORK: Uncomment after fixing this bug: https://wearezeta.atlassian.net/browse/WPB-3664 -- void $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isOtherUser2LeaveUpConvNotif From 903b87e62032085848a13e1455771c6ddb7fcf74 Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Mon, 9 Oct 2023 18:32:44 +0200 Subject: [PATCH 184/225] [feat] improve type safety for Named, servantify brig internal route (#3634) * [feat] improve type safety for Named, servantify brig internal route - improve type safety of Named by making it possible to rule out weakly typed arguments to the type (e.g. Type) - servantify the internal route for querying the teams API for servant --- changelog.d/5-internal/WBP-1224 | 1 + .../src/Wire/API/Federation/API.hs | 2 +- libs/wire-api/src/Wire/API/Error.hs | 4 +- .../src/Wire/API/Routes/Internal/Brig.hs | 84 +++++++++++++++- .../Wire/API/Routes/Internal/Brig/OAuth.hs | 2 +- .../API/Routes/Internal/Brig/SearchIndex.hs | 2 +- libs/wire-api/src/Wire/API/Routes/Named.hs | 37 +++++--- .../src/Wire/API/Routes/Public/Brig/Bot.hs | 2 +- .../src/Wire/API/Routes/Public/Brig/OAuth.hs | 2 +- .../Wire/API/Routes/Public/Brig/Provider.hs | 2 +- libs/wire-api/src/Wire/API/Routes/Version.hs | 2 +- libs/wire-api/src/Wire/API/User.hs | 1 + services/brig/src/Brig/API.hs | 10 +- services/brig/src/Brig/API/Internal.hs | 21 ++-- services/brig/src/Brig/API/OAuth.hs | 2 +- services/brig/src/Brig/API/Public.hs | 2 +- services/brig/src/Brig/Provider/API.hs | 2 +- services/brig/src/Brig/Team/API.hs | 95 +++---------------- services/brig/test/integration/Main.hs | 3 +- .../test/integration/API/Federation/Util.hs | 14 +-- services/gundeck/src/Gundeck/API/Public.hs | 2 +- tools/stern/src/Stern/API.hs | 2 +- 22 files changed, 161 insertions(+), 133 deletions(-) create mode 100644 changelog.d/5-internal/WBP-1224 diff --git a/changelog.d/5-internal/WBP-1224 b/changelog.d/5-internal/WBP-1224 new file mode 100644 index 00000000000..12dd7e6cbab --- /dev/null +++ b/changelog.d/5-internal/WBP-1224 @@ -0,0 +1 @@ +Servantify internal end-points: brig/teams diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API.hs b/libs/wire-api-federation/src/Wire/API/Federation/API.hs index 476df183032..5e6b294e122 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -78,7 +78,7 @@ instance HasEmptyResponse (Post '[JSON] EmptyResponse) instance HasEmptyResponse api => HasEmptyResponse (x :> api) -instance HasEmptyResponse api => HasEmptyResponse (Named name api) +instance HasEmptyResponse api => HasEmptyResponse (UntypedNamed name api) -- | Return a client for a named endpoint. -- diff --git a/libs/wire-api/src/Wire/API/Error.hs b/libs/wire-api/src/Wire/API/Error.hs index 8946a785721..fbc743cfe65 100644 --- a/libs/wire-api/src/Wire/API/Error.hs +++ b/libs/wire-api/src/Wire/API/Error.hs @@ -68,7 +68,7 @@ import Polysemy.Error import Servant import Servant.OpenApi import Wire.API.Routes.MultiVerb -import Wire.API.Routes.Named (Named) +import Wire.API.Routes.Named (UntypedNamed) import Wire.API.Routes.Version -- | Runtime representation of a statically-known error. @@ -209,7 +209,7 @@ type family DeclaredErrorEffects api :: EffectRow where DeclaredErrorEffects (CanThrowMany '(e, es) :> api) = DeclaredErrorEffects (CanThrow e :> CanThrowMany es :> api) DeclaredErrorEffects (x :> api) = DeclaredErrorEffects api - DeclaredErrorEffects (Named n api) = DeclaredErrorEffects api + DeclaredErrorEffects (UntypedNamed n api) = DeclaredErrorEffects api DeclaredErrorEffects api = '[] errorResponseSwagger :: forall e. (Typeable e, KnownError e) => S.Response 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 acf06f3b8a6..f6b97c0769e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -36,6 +36,7 @@ module Wire.API.Routes.Internal.Brig NewKeyPackageRef (..), NewKeyPackage (..), NewKeyPackageResult (..), + FoundInvitationCode (..), ) where @@ -68,10 +69,13 @@ import Wire.API.Routes.Internal.Brig.SearchIndex (ISearchIndexAPI) import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named -import Wire.API.Routes.Public (ZUser {- yes, this is a bit weird -}) +import Wire.API.Routes.Public (ZUser) import Wire.API.Team.Feature +import Wire.API.Team.Invitation (Invitation) import Wire.API.Team.LegalHold.Internal -import Wire.API.User +import Wire.API.Team.Size qualified as Teamsize +import Wire.API.User hiding (InvitationCode) +import Wire.API.User qualified as User import Wire.API.User.Auth import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.ReAuth @@ -664,6 +668,82 @@ type TeamsAPI = :> ReqBody '[Servant.JSON] (Multi.TeamStatus SearchVisibilityInboundConfig) :> Post '[Servant.JSON] () ) + :<|> InvitationByEmail + :<|> InvitationCode + :<|> SuspendTeam + :<|> UnsuspendTeam + :<|> TeamSize + :<|> TeamInvitations + +type InvitationByEmail = + Named + "get-invitation-by-email" + ( "teams" + :> "invitations" + :> "by-email" + :> QueryParam' [Required, Strict] "email" Email + :> Get '[Servant.JSON] Invitation + ) + +type InvitationCode = + Named + "get-invitation-code" + ( "teams" + :> "invitation-code" + :> QueryParam' [Required, Strict] "team" TeamId + :> QueryParam' [Required, Strict] "invitation_id" InvitationId + :> Get '[Servant.JSON] FoundInvitationCode + ) + +newtype FoundInvitationCode = FoundInvitationCode {getFoundInvitationCode :: User.InvitationCode} + deriving stock (Eq, Show, Generic) + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema FoundInvitationCode) + +instance ToSchema FoundInvitationCode where + schema = + FoundInvitationCode + <$> getFoundInvitationCode .= object "FoundInvitationCode" (field "code" (schema @User.InvitationCode)) + +type SuspendTeam = + Named + "suspend-team" + ( "teams" + :> Capture "tid" TeamId + :> "suspend" + :> Post + '[Servant.JSON] + NoContent + ) + +type UnsuspendTeam = + Named + "unsuspend-team" + ( "teams" + :> Capture "tid" TeamId + :> "unsuspend" + :> Post + '[Servant.JSON] + NoContent + ) + +type TeamSize = + Named + "team-size" + ( "teams" + :> Capture "tid" TeamId + :> "size" + :> Get '[JSON] Teamsize.TeamSize + ) + +type TeamInvitations = + Named + "create-invitations-via-scim" + ( "teams" + :> Capture "tid" TeamId + :> "invitations" + :> Servant.ReqBody '[JSON] NewUserScimInvitation + :> Post '[JSON] UserAccount + ) type UserAPI = UpdateUserLocale diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs index 8974da4c27c..70d478643a0 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs @@ -23,7 +23,7 @@ import Servant hiding (Handler, JSON, Tagged, addHeader, respond) import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.OAuth -import Wire.API.Routes.Named (Named (..)) +import Wire.API.Routes.Named (Named) -------------------------------------------------------------------------------- -- API Internal diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs index 0cca4948901..0b90fd43524 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs @@ -20,7 +20,7 @@ module Wire.API.Routes.Internal.Brig.SearchIndex where import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) import Servant.OpenApi.Internal.Orphans () -import Wire.API.Routes.Named (Named (..)) +import Wire.API.Routes.Named (Named) type ISearchIndexAPI = Named diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index f76ada19664..e7bf7224a74 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -33,7 +33,15 @@ import Servant.Client.Core (clientIn) import Servant.OpenApi -- | See http://docs.wire.com/developer/developer/servant.html#named-and-internal-route-ids-in-swagger -newtype Named name x = Named {unnamed :: x} +-- +-- as 'UntypedNamed' is of kind $k -> Type -> Type$, we can pass any +-- argument to it, however, most commonly we want to pass a 'Symbol' to +-- it. To avoid mistakes, we make it possible to rule out untyped arguments +-- like 'Type', this is done by the 'IsStronglyTyped' TyFam that will throw +-- a type error when passed a 'Type' +type Named name = UntypedNamed (IsStronglyTyped name) + +newtype UntypedNamed name x = Named {unnamed :: x} deriving (Functor) -- | For 'HasSwagger' instance of 'Named'. 'KnownSymbol' isn't enough because we're using @@ -47,7 +55,12 @@ instance {-# OVERLAPPABLE #-} KnownSymbol a => RenderableSymbol a where instance {-# OVERLAPPING #-} (RenderableSymbol a, RenderableSymbol b) => RenderableSymbol '(a, b) where renderSymbol = "(" <> (renderSymbol @a) <> ", " <> (renderSymbol @b) <> ")" -instance (HasOpenApi api, RenderableSymbol name) => HasOpenApi (Named name api) where +type IsStronglyTyped :: forall k. k -> k +type family IsStronglyTyped typ where + IsStronglyTyped (typ :: Type) = TypeError ('Text "Please don't use \"Type\" as first parameter to \"Named\"") + IsStronglyTyped typ = typ + +instance (HasOpenApi api, RenderableSymbol name) => HasOpenApi (UntypedNamed name api) where toOpenApi _ = toOpenApi (Proxy @api) & allOperations . description %~ (Just (dscr <> "\n\n") <>) @@ -58,27 +71,27 @@ instance (HasOpenApi api, RenderableSymbol name) => HasOpenApi (Named name api) <> cs (renderSymbol @name) <> "]" -instance HasServer api ctx => HasServer (Named name api) ctx where - type ServerT (Named name api) m = Named name (ServerT api m) +instance HasServer api ctx => HasServer (UntypedNamed name api) ctx where + type ServerT (UntypedNamed name api) m = UntypedNamed name (ServerT api m) route _ ctx action = route (Proxy @api) ctx (fmap unnamed action) hoistServerWithContext _ ctx f = fmap (hoistServerWithContext (Proxy @api) ctx f) -instance HasLink endpoint => HasLink (Named name endpoint) where - type MkLink (Named name endpoint) a = MkLink endpoint a +instance HasLink endpoint => HasLink (UntypedNamed name endpoint) where + type MkLink (UntypedNamed name endpoint) a = MkLink endpoint a toLink toA _ = toLink toA (Proxy @endpoint) -instance RoutesToPaths api => RoutesToPaths (Named name api) where +instance RoutesToPaths api => RoutesToPaths (UntypedNamed name api) where getRoutes = getRoutes @api -instance HasClient m api => HasClient m (Named n api) where - type Client m (Named n api) = Client m api +instance HasClient m api => HasClient m (UntypedNamed n api) where + type Client m (UntypedNamed n api) = Client m api clientWithRoute pm _ req = clientWithRoute pm (Proxy @api) req hoistClientMonad pm _ f = hoistClientMonad pm (Proxy @api) f type family FindName n (api :: Type) :: (n, Type) where - FindName n (Named name api) = '(name, api) + FindName n (UntypedNamed name api) = '(name, api) FindName n (x :> api) = AddPrefix x (FindName n api) FindName n api = '(TypeError ('Text "Named combinator not found"), api) @@ -116,7 +129,7 @@ type family FMap (f :: a -> b) (m :: Maybe a) :: Maybe b where FMap f ('Just a) = 'Just (f a) type family LookupEndpoint api name :: Maybe Type where - LookupEndpoint (Named name endpoint) name = 'Just endpoint + LookupEndpoint (UntypedNamed name endpoint) name = 'Just endpoint LookupEndpoint (api1 :<|> api2) name = MappendMaybe (LookupEndpoint api1 name) @@ -142,5 +155,5 @@ namedClient = clientIn (Proxy @endpoint) (Proxy @m) type family x ::> api type instance - x ::> (Named name api) = + x ::> (UntypedNamed name api) = Named name (x :> api) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs index b7eba29b037..70b75bf40dc 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs @@ -28,7 +28,7 @@ import Wire.API.Error (CanThrow, ErrorResponse) import Wire.API.Error.Brig (BrigError (..)) import Wire.API.Provider.Bot (BotUserView) import Wire.API.Routes.MultiVerb -import Wire.API.Routes.Named (Named (..)) +import Wire.API.Routes.Named (Named) import Wire.API.Routes.Public import Wire.API.User import Wire.API.User.Client diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs index a096c78d975..a3173c0700b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs @@ -27,7 +27,7 @@ import Wire.API.Error import Wire.API.OAuth import Wire.API.Routes.API import Wire.API.Routes.MultiVerb -import Wire.API.Routes.Named (Named (..)) +import Wire.API.Routes.Named (Named) import Wire.API.Routes.Public type OAuthAPI = diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs index b1b6310dfe4..4145161611c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Provider.hs @@ -27,7 +27,7 @@ import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Provider import Wire.API.Routes.MultiVerb -import Wire.API.Routes.Named (Named (..)) +import Wire.API.Routes.Named (Named) import Wire.API.Routes.Public import Wire.API.User.Auth diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index a2a337b61df..98a184592ed 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -220,7 +220,7 @@ type instance s :> SpecialiseToVersion v api type instance - SpecialiseToVersion v (Named n api) = + SpecialiseToVersion v (UntypedNamed n api) = Named n (SpecialiseToVersion v api) type instance diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 16be7999255..ada22cbd43c 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -1896,6 +1896,7 @@ instance Schema.ToSchema UserAccount where -- NewUserScimInvitation data NewUserScimInvitation = NewUserScimInvitation + -- FIXME: the TID should be captured in the route as usual { newUserScimInvTeamId :: TeamId, newUserScimInvLocale :: Maybe Locale, newUserScimInvName :: Name, diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index 3580f888511..ba318c3f2b5 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -22,18 +22,10 @@ where import Brig.API.Handler (Handler) import Brig.API.Internal qualified as Internal -import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.GalleyProvider (GalleyProvider) -import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Network.Wai.Routing (Routes) import Polysemy -sitemap :: - forall r p. - ( Member BlacklistStore r, - Member GalleyProvider r, - Member (UserPendingActivationStore p) r - ) => - Routes () (Handler r) () +sitemap :: forall r. (Member GalleyProvider r) => Routes () (Handler r) () sitemap = do Internal.sitemap diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 75f90bd606c..167ea19fc44 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -197,8 +197,20 @@ accountAPI = :<|> Named @"iLegalholdAddClient" legalHoldClientRequestedH :<|> Named @"iLegalholdDeleteClient" removeLegalHoldClientH -teamsAPI :: ServerT BrigIRoutes.TeamsAPI (Handler r) -teamsAPI = Named @"updateSearchVisibilityInbound" Index.updateSearchVisibilityInbound +teamsAPI :: + ( Member GalleyProvider r, + Member (UserPendingActivationStore p) r, + Member BlacklistStore r + ) => + ServerT BrigIRoutes.TeamsAPI (Handler r) +teamsAPI = + Named @"updateSearchVisibilityInbound" Index.updateSearchVisibilityInbound + :<|> Named @"get-invitation-by-email" Team.getInvitationByEmail + :<|> Named @"get-invitation-code" Team.getInvitationCode + :<|> Named @"suspend-team" Team.suspendTeam + :<|> Named @"unsuspend-team" Team.unsuspendTeam + :<|> Named @"team-size" Team.teamSize + :<|> Named @"create-invitations-via-scim" Team.createInvitationViaScim userAPI :: ServerT BrigIRoutes.UserAPI (Handler r) userAPI = @@ -436,14 +448,11 @@ internalSearchIndexAPI = -- Sitemap (wai-route) sitemap :: - ( Member BlacklistStore r, - Member GalleyProvider r, - Member (UserPendingActivationStore p) r + ( Member GalleyProvider r ) => Routes a (Handler r) () sitemap = unsafeCallsFed @'Brig @"on-user-deleted-connections" $ do Provider.routesInternal - Team.routesInternal --------------------------------------------------------------------------- -- Handlers diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index b2196be7be7..b204fadd065 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -48,7 +48,7 @@ import Wire.API.Error import Wire.API.OAuth as OAuth import Wire.API.Password (Password, mkSafePassword) import Wire.API.Routes.Internal.Brig.OAuth qualified as I -import Wire.API.Routes.Named (Named (..)) +import Wire.API.Routes.Named (UntypedNamed (Named)) import Wire.API.Routes.Public.Brig.OAuth import Wire.Sem.Jwk import Wire.Sem.Jwk qualified as Jwk diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 5e22dfcb500..ae77817ef62 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -118,7 +118,7 @@ import Wire.API.Routes.Internal.Cargohold qualified as CargoholdInternalAPI import Wire.API.Routes.Internal.Galley qualified as GalleyInternalAPI import Wire.API.Routes.Internal.Spar qualified as SparInternalAPI import Wire.API.Routes.MultiTablePaging qualified as Public -import Wire.API.Routes.Named (Named (Named)) +import Wire.API.Routes.Named (UntypedNamed (Named)) import Wire.API.Routes.Public.Brig import Wire.API.Routes.Public.Brig.OAuth import Wire.API.Routes.Public.Cannon diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 8c4af7f34ff..6291aa84f61 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -113,7 +113,7 @@ import Wire.API.Provider.External qualified as Ext import Wire.API.Provider.Service import Wire.API.Provider.Service qualified as Public import Wire.API.Provider.Service.Tag qualified as Public -import Wire.API.Routes.Named (Named (Named)) +import Wire.API.Routes.Named (UntypedNamed (Named)) import Wire.API.Routes.Public.Brig.Bot (BotAPI) import Wire.API.Routes.Public.Brig.Provider (ProviderAPI) import Wire.API.Routes.Public.Brig.Services (ServicesAPI) diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 18a60a9e797..14987469fff 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -17,7 +17,12 @@ module Brig.Team.API ( servantAPI, - routesInternal, + getInvitationByEmail, + getInvitationCode, + suspendTeam, + unsuspendTeam, + teamSize, + createInvitationViaScim, ) where @@ -45,17 +50,12 @@ import Brig.Types.Team (TeamSize) import Brig.User.Search.TeamSize qualified as TeamSize import Control.Lens (view, (^.)) import Control.Monad.Trans.Except (mapExceptT) -import Data.Aeson hiding (json) import Data.ByteString.Conversion (toByteString') import Data.Id import Data.List1 qualified as List1 import Data.Range import Galley.Types.Teams qualified as Team import Imports hiding (head) -import Network.HTTP.Types.Status -import Network.Wai (Response) -import Network.Wai.Predicate hiding (and, result, setStatus) -import Network.Wai.Routing import Network.Wai.Utilities hiding (code, message) import Polysemy (Member) import Servant hiding (Handler, JSON, addHeader) @@ -64,9 +64,10 @@ import System.Logger.Class qualified as Log import Util.Logging (logFunction, logTeam) import Wire.API.Error import Wire.API.Error.Brig qualified as E +import Wire.API.Routes.Internal.Brig (FoundInvitationCode (FoundInvitationCode)) import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Named -import Wire.API.Routes.Public.Brig +import Wire.API.Routes.Public.Brig (TeamsAPI) import Wire.API.Team import Wire.API.Team.Invitation import Wire.API.Team.Invitation qualified as Public @@ -92,64 +93,19 @@ servantAPI = :<|> Named @"head-team-invitations" headInvitationByEmail :<|> Named @"get-team-size" teamSizePublic -routesInternal :: - ( Member BlacklistStore r, - Member GalleyProvider r, - Member (UserPendingActivationStore p) r - ) => - Routes a (Handler r) () -routesInternal = do - get "/i/teams/invitations/by-email" (continue getInvitationByEmailH) $ - accept "application" "json" - .&. query "email" - - get "/i/teams/invitation-code" (continue getInvitationCodeH) $ - accept "application" "json" - .&. param "team" - .&. param "invitation_id" - - post "/i/teams/:tid/suspend" (continue suspendTeamH) $ - accept "application" "json" - .&. capture "tid" - - post "/i/teams/:tid/unsuspend" (continue unsuspendTeamH) $ - accept "application" "json" - .&. capture "tid" - - get "/i/teams/:tid/size" (continue teamSizeH) $ - accept "application" "json" - .&. capture "tid" - - post "/i/teams/:tid/invitations" (continue createInvitationViaScimH) $ - accept "application" "json" - .&. jsonRequest @NewUserScimInvitation - teamSizePublic :: Member GalleyProvider r => UserId -> TeamId -> (Handler r) TeamSize teamSizePublic uid tid = do ensurePermissions uid tid [AddTeamMember] -- limit this to team admins to reduce risk of involuntary DOS attacks teamSize tid -teamSizeH :: JSON ::: TeamId -> (Handler r) Response -teamSizeH (_ ::: t) = json <$> teamSize t - teamSize :: TeamId -> (Handler r) TeamSize teamSize t = lift $ TeamSize.teamSize t -getInvitationCodeH :: JSON ::: TeamId ::: InvitationId -> (Handler r) Response -getInvitationCodeH (_ ::: t ::: r) = do - json <$> getInvitationCode t r - getInvitationCode :: TeamId -> InvitationId -> (Handler r) FoundInvitationCode getInvitationCode t r = do code <- lift . wrapClient $ DB.lookupInvitationCode t r maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) (pure . FoundInvitationCode) code -newtype FoundInvitationCode = FoundInvitationCode InvitationCode - deriving (Eq, Show, Generic) - -instance ToJSON FoundInvitationCode where - toJSON (FoundInvitationCode c) = object ["code" .= c] - createInvitationPublicH :: ( Member BlacklistStore r, Member GalleyProvider r @@ -199,25 +155,15 @@ createInvitationPublic uid tid body = do context (createInvitation' tid inviteeRole (Just (inviterUid inviter)) (inviterEmail inviter) body) -createInvitationViaScimH :: - ( Member BlacklistStore r, - Member GalleyProvider r, - Member (UserPendingActivationStore p) r - ) => - JSON ::: JsonRequest NewUserScimInvitation -> - (Handler r) Response -createInvitationViaScimH (_ ::: req) = do - body <- parseJsonBody req - setStatus status201 . json <$> createInvitationViaScim body - createInvitationViaScim :: ( Member BlacklistStore r, Member GalleyProvider r, Member (UserPendingActivationStore p) r ) => + TeamId -> NewUserScimInvitation -> (Handler r) UserAccount -createInvitationViaScim newUser@(NewUserScimInvitation tid loc name email role) = do +createInvitationViaScim tid newUser@(NewUserScimInvitation _tid loc name email role) = do env <- ask let inviteeRole = role fromEmail = env ^. emailSender @@ -352,39 +298,26 @@ headInvitationByEmail e = do -- | FUTUREWORK: This should also respond with status 409 in case of -- @DB.InvitationByEmailMoreThanOne@. Refactor so that 'headInvitationByEmailH' and -- 'getInvitationByEmailH' are almost the same thing. -getInvitationByEmailH :: JSON ::: Email -> (Handler r) Response -getInvitationByEmailH (_ ::: email) = - json <$> getInvitationByEmail email - getInvitationByEmail :: Email -> (Handler r) Public.Invitation getInvitationByEmail email = do inv <- lift $ wrapClient $ DB.lookupInvitationByEmail HideInvitationUrl email maybe (throwStd (notFound "Invitation not found")) pure inv -suspendTeamH :: (Member GalleyProvider r) => JSON ::: TeamId -> (Handler r) Response -suspendTeamH (_ ::: tid) = do - empty <$ suspendTeam tid - -suspendTeam :: (Member GalleyProvider r) => TeamId -> (Handler r) () +suspendTeam :: (Member GalleyProvider r) => TeamId -> (Handler r) NoContent suspendTeam tid = do changeTeamAccountStatuses tid Suspended lift $ wrapClient $ DB.deleteInvitations tid lift $ liftSem $ GalleyProvider.changeTeamStatus tid Team.Suspended Nothing - -unsuspendTeamH :: - (Member GalleyProvider r) => - JSON ::: TeamId -> - (Handler r) Response -unsuspendTeamH (_ ::: tid) = do - empty <$ unsuspendTeam tid + pure NoContent unsuspendTeam :: (Member GalleyProvider r) => TeamId -> - (Handler r) () + (Handler r) NoContent unsuspendTeam tid = do changeTeamAccountStatuses tid Active lift $ liftSem $ GalleyProvider.changeTeamStatus tid Team.Active Nothing + pure NoContent ------------------------------------------------------------------------------- -- Internal diff --git a/services/brig/test/integration/Main.hs b/services/brig/test/integration/Main.hs index 8a3a0d5b9c0..8b8970faf55 100644 --- a/services/brig/test/integration/Main.hs +++ b/services/brig/test/integration/Main.hs @@ -74,7 +74,6 @@ import Util.Test.SQS qualified as SQS import Web.HttpApiData import Wire.API.Federation.API import Wire.API.Routes.Version -import Wire.Sem.Paging.Cassandra (InternalPaging) data BackendConf = BackendConf { remoteBrig :: Endpoint, @@ -175,7 +174,7 @@ runTests iConf brigOpts otherArgs = do assertEqual "inconcistent sitemap" mempty - (pathsConsistencyCheck . treeToPaths . compile $ Brig.API.sitemap @BrigCanonicalEffects @InternalPaging), + (pathsConsistencyCheck . treeToPaths . compile $ Brig.API.sitemap @BrigCanonicalEffects), userApi, providerApi, searchApis, diff --git a/services/galley/test/integration/API/Federation/Util.hs b/services/galley/test/integration/API/Federation/Util.hs index 9d15edc5ee9..bb12cebb6ba 100644 --- a/services/galley/test/integration/API/Federation/Util.hs +++ b/services/galley/test/integration/API/Federation/Util.hs @@ -64,7 +64,7 @@ instance HasTrivialHandler api => HasTrivialHandler (From v :> api) where trivialNamedHandler :: forall (name :: Symbol) api. (KnownSymbol name, HasTrivialHandler api) => - Server (Named name api) + Server (UntypedNamed name api) trivialNamedHandler = Named (trivialHandler @api (symbolVal (Proxy @name))) -- | Generate a servant handler from an incomplete list of handlers of named @@ -74,40 +74,40 @@ class PartialAPI (api :: Type) (hs :: Type) where instance (KnownSymbol name, HasTrivialHandler endpoint) => - PartialAPI (Named (name :: Symbol) endpoint) EmptyAPI + PartialAPI (UntypedNamed (name :: Symbol) endpoint) EmptyAPI where mkHandler _ = trivialNamedHandler @name @endpoint instance {-# OVERLAPPING #-} (KnownSymbol name, HasTrivialHandler endpoint, PartialAPI api EmptyAPI) => - PartialAPI (Named (name :: Symbol) endpoint :<|> api) EmptyAPI + PartialAPI (UntypedNamed (name :: Symbol) endpoint :<|> api) EmptyAPI where mkHandler h = trivialNamedHandler @name @endpoint :<|> mkHandler @api h instance {-# OVERLAPPING #-} (h ~ Server endpoint, PartialAPI api hs) => - PartialAPI (Named (name :: Symbol) endpoint :<|> api) (Named name h :<|> hs) + PartialAPI (UntypedNamed (name :: Symbol) endpoint :<|> api) (UntypedNamed name h :<|> hs) where mkHandler (h :<|> hs) = h :<|> mkHandler @api hs instance (KnownSymbol name, HasTrivialHandler endpoint, PartialAPI api hs) => - PartialAPI (Named (name :: Symbol) endpoint :<|> api) hs + PartialAPI (UntypedNamed (name :: Symbol) endpoint :<|> api) hs where mkHandler hs = trivialNamedHandler @name @endpoint :<|> mkHandler @api hs instance (h ~ Server endpoint) => - PartialAPI (Named (name :: Symbol) endpoint) (Named name h) + PartialAPI (UntypedNamed (name :: Symbol) endpoint) (UntypedNamed name h) where mkHandler = id instance {-# OVERLAPPING #-} (h ~ Server endpoint, PartialAPI api EmptyAPI) => - PartialAPI (Named (name :: Symbol) endpoint :<|> api) (Named name h) + PartialAPI (UntypedNamed (name :: Symbol) endpoint :<|> api) (UntypedNamed name h) where mkHandler h = h :<|> mkHandler @api EmptyAPI diff --git a/services/gundeck/src/Gundeck/API/Public.hs b/services/gundeck/src/Gundeck/API/Public.hs index e2034b3d62e..b74b8e00f44 100644 --- a/services/gundeck/src/Gundeck/API/Public.hs +++ b/services/gundeck/src/Gundeck/API/Public.hs @@ -31,7 +31,7 @@ import Gundeck.Push qualified as Push import Imports import Servant (HasServer (..), (:<|>) (..)) import Wire.API.Notification qualified as Public -import Wire.API.Routes.Named (Named (Named)) +import Wire.API.Routes.Named (UntypedNamed (Named)) import Wire.API.Routes.Public.Gundeck ------------------------------------------------------------------------------- diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index c559d0e6f20..03832056ac0 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -63,7 +63,7 @@ import Wire.API.Internal.Notification (QueuedNotification) import Wire.API.Routes.Internal.Brig.Connection (ConnectionStatus) import Wire.API.Routes.Internal.Brig.EJPD qualified as EJPD import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team -import Wire.API.Routes.Named (Named (Named)) +import Wire.API.Routes.Named (UntypedNamed (Named)) import Wire.API.Team.Feature hiding (setStatus) import Wire.API.Team.SearchVisibility import Wire.API.User From c1668452496102e0f7f804e66381a554f2205c67 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 10 Oct 2023 09:55:39 +0200 Subject: [PATCH 185/225] Linter --- libs/wire-api/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 16512a399cf..92d2c3c8f3a 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -116,6 +116,7 @@ mkDerivation { src = gitignoreSource ./.; libraryHaskellDepends = [ aeson + async attoparsec base base64-bytestring From 3ce872fc33ca1218515ea2063eeb17985880c8c0 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 2 Oct 2023 14:48:50 +0200 Subject: [PATCH 186/225] Enqueue remote MLS messages --- .../Golden/MLSMessageSendingStatus.hs | 10 ++--- libs/wire-api/src/Wire/API/MLS/Message.hs | 14 +----- services/galley/src/Galley/API/Federation.hs | 14 +++--- services/galley/src/Galley/API/MLS/Message.hs | 22 +++++---- .../galley/src/Galley/API/MLS/Propagate.hs | 45 +++++-------------- services/galley/src/Galley/API/MLS/Removal.hs | 35 +++++++-------- .../src/Galley/API/MLS/SubConversation.hs | 30 +++++-------- services/galley/test/integration/API/MLS.hs | 12 +++-- .../galley/test/integration/API/MLS/Util.hs | 6 +-- 9 files changed, 65 insertions(+), 123 deletions(-) diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs index b230278e198..27fba120068 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs @@ -24,30 +24,26 @@ import Data.Qualified import Data.UUID qualified as UUID import Imports import Wire.API.MLS.Message -import Wire.API.Unreachable testObject_MLSMessageSendingStatus1 :: MLSMessageSendingStatus testObject_MLSMessageSendingStatus1 = MLSMessageSendingStatus { mmssEvents = [], - mmssTime = toUTCTimeMillis (read "1864-04-12 12:22:43.673 UTC"), - mmssFailedToSendTo = mempty + mmssTime = toUTCTimeMillis (read "1864-04-12 12:22:43.673 UTC") } testObject_MLSMessageSendingStatus2 :: MLSMessageSendingStatus testObject_MLSMessageSendingStatus2 = MLSMessageSendingStatus { mmssEvents = [], - mmssTime = toUTCTimeMillis (read "2001-04-12 12:22:43.673 UTC"), - mmssFailedToSendTo = unreachableFromList failed1 + mmssTime = toUTCTimeMillis (read "2001-04-12 12:22:43.673 UTC") } testObject_MLSMessageSendingStatus3 :: MLSMessageSendingStatus testObject_MLSMessageSendingStatus3 = MLSMessageSendingStatus { mmssEvents = [], - mmssTime = toUTCTimeMillis (read "1999-04-12 12:22:43.673 UTC"), - mmssFailedToSendTo = unreachableFromList failed2 + mmssTime = toUTCTimeMillis (read "1999-04-12 12:22:43.673 UTC") } failed1 :: [Qualified UserId] diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs index d9e6aa1f624..c13dcc0d96f 100644 --- a/libs/wire-api/src/Wire/API/MLS/Message.hs +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -59,7 +59,6 @@ import Wire.API.MLS.Proposal import Wire.API.MLS.ProtocolVersion import Wire.API.MLS.Serialisation import Wire.API.MLS.Welcome -import Wire.API.Unreachable import Wire.Arbitrary data WireFormatTag @@ -377,11 +376,7 @@ verifyMessageSignature ctx msgContent authData pubkey = isJust $ do data MLSMessageSendingStatus = MLSMessageSendingStatus { mmssEvents :: [Event], - mmssTime :: UTCTimeMillis, - -- | An optional list of unreachable users an application message could not - -- be sent to. In case of commits and unreachable users use the - -- MLSMessageResponseUnreachableBackends data constructor. - mmssFailedToSendTo :: Maybe UnreachableUsers + mmssTime :: UTCTimeMillis } deriving (Eq, Show) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema MLSMessageSendingStatus @@ -400,10 +395,3 @@ instance ToSchema MLSMessageSendingStatus where "time" (description ?~ "The time of sending the message.") schema - <*> mmssFailedToSendTo - .= maybe_ - ( optFieldWithDocModifier - "failed_to_send" - (description ?~ "List of federated users who could not be reached and did not receive the message") - schema - ) diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index f3fc8fa62d1..cced908c9f6 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -120,9 +120,9 @@ federationSitemap = :<|> Named @"get-one2one-conversation" getOne2OneConversation onClientRemoved :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input Env) r, Member (Input (Local ())) r, @@ -374,7 +374,6 @@ sendMessage originDomain msr = do onUserDeleted :: ( Member BackendNotificationQueueAccess r, Member ConversationStore r, - Member FederatorAccess r, Member FireAndForget r, Member ExternalAccess r, Member GundeckAccess r, @@ -627,7 +626,7 @@ sendMLSMessage remoteDomain msr = handleMLSMessageErrors $ do msg <- noteS @'MLSUnsupportedMessage $ mkIncomingMessage raw (ctype, qConvOrSub) <- getConvFromGroupId msg.groupId when (qUnqualified qConvOrSub /= msr.convOrSubId) $ throwS @'MLSGroupConversationMismatch - MLSMessageResponseUpdates . map lcuUpdate . fst + MLSMessageResponseUpdates . map lcuUpdate <$> postMLSMessage loc (tUntagged sender) @@ -660,11 +659,8 @@ getSubConversationForRemoteUser domain GetSubConversationsRequest {..} = leaveSubConversation :: ( HasLeaveSubConversationEffects r, - Members - '[ Input (Local ()), - Resource - ] - r + Member (Input (Local ())) r, + Member Resource r ) => Domain -> LeaveSubConversationRequest -> diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index ad7ace09d5a..cd2920d87f9 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -81,7 +81,6 @@ import Wire.API.MLS.GroupInfo import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation -import Wire.API.Unreachable -- FUTUREWORK -- - Check that the capabilities of a leaf node in an add proposal contains all @@ -140,11 +139,11 @@ postMLSMessageFromLocalUser lusr c conn smsg = do assertMLSEnabled imsg <- noteS @'MLSUnsupportedMessage $ mkIncomingMessage smsg (ctype, cnvOrSub) <- getConvFromGroupId imsg.groupId - (events, unreachables) <- - first (map lcuEvent) + events <- + map lcuEvent <$> postMLSMessage lusr (tUntagged lusr) c ctype cnvOrSub (Just conn) imsg t <- toUTCTimeMillis <$> input - pure $ MLSMessageSendingStatus events t unreachables + pure $ MLSMessageSendingStatus events t postMLSCommitBundle :: ( HasProposalEffects r, @@ -188,7 +187,7 @@ postMLSCommitBundleFromLocalUser lusr c conn bundle = do map lcuEvent <$> postMLSCommitBundle lusr (tUntagged lusr) c ctype qConvOrSub (Just conn) ibundle t <- toUTCTimeMillis <$> input - pure $ MLSMessageSendingStatus events t mempty + pure $ MLSMessageSendingStatus events t postMLSCommitBundleToLocalConv :: ( HasProposalEffects r, @@ -259,7 +258,6 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do storeGroupInfo (tUnqualified lConvOrSub).id (GroupInfoData bundle.groupInfo.raw) propagateMessage qusr (Just c) lConvOrSub conn bundle.rawMessage (tUnqualified lConvOrSub).members - >>= mapM_ (throw . unreachableUsersToUnreachableBackends) for_ bundle.welcome $ \welcome -> sendWelcomes lConvOrSubId qusr conn newClients welcome @@ -342,7 +340,7 @@ postMLSMessage :: Qualified ConvOrSubConvId -> Maybe ConnId -> IncomingMessage -> - Sem r ([LocalConversationUpdate], Maybe UnreachableUsers) + Sem r [LocalConversationUpdate] postMLSMessage loc qusr c ctype qconvOrSub con msg = do foldQualified loc @@ -383,7 +381,7 @@ postMLSMessageToLocalConv :: IncomingMessage -> ConvType -> Local ConvOrSubConvId -> - Sem r ([LocalConversationUpdate], Maybe UnreachableUsers) + Sem r [LocalConversationUpdate] postMLSMessageToLocalConv qusr c con msg ctype convOrSubId = do lConvOrSub <- fetchConvOrSub qusr msg.groupId ctype convOrSubId let convOrSub = tUnqualified lConvOrSub @@ -413,8 +411,8 @@ postMLSMessageToLocalConv qusr c con msg ctype convOrSubId = do (epochInt msg.epoch < epochInt convOrSub.mlsMeta.cnvmlsEpoch - 2) $ throwS @'MLSStaleMessage - unreachables <- propagateMessage qusr (Just c) lConvOrSub con msg.rawMessage (tUnqualified lConvOrSub).members - pure ([], unreachables) + propagateMessage qusr (Just c) lConvOrSub con msg.rawMessage (tUnqualified lConvOrSub).members + pure [] postMLSMessageToRemoteConv :: ( Members MLSMessageStaticErrors r, @@ -427,7 +425,7 @@ postMLSMessageToRemoteConv :: Maybe ConnId -> IncomingMessage -> Remote ConvOrSubConvId -> - Sem r ([LocalConversationUpdate], Maybe UnreachableUsers) + Sem r [LocalConversationUpdate] postMLSMessageToRemoteConv loc qusr senderClient con msg rConvOrSubId = do -- only local users can send messages to remote conversations lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr @@ -459,7 +457,7 @@ postMLSMessageToRemoteConv loc qusr senderClient con msg rConvOrSubId = do for_ updates $ \update -> do me <- updateLocalStateOfRemoteConv (qualifyAs rConvOrSubId update) con for_ me $ \e -> output (LocalConversationUpdate e update) - pure (lcus, Nothing) + pure lcus MLSMessageResponseNonFederatingBackends e -> throw e storeGroupInfo :: diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 340d91fc4d0..4a4a50c7e04 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -18,8 +18,6 @@ module Galley.API.MLS.Propagate where import Control.Comonad -import Data.Aeson qualified as A -import Data.Domain import Data.Id import Data.Json.Util import Data.Map qualified as Map @@ -27,34 +25,31 @@ import Data.Qualified import Data.Time import Galley.API.MLS.Types import Galley.API.Push +import Galley.API.Util import Galley.Data.Services import Galley.Effects -import Galley.Effects.FederatorAccess +import Galley.Effects.BackendNotificationQueueAccess import Galley.Types.Conversations.Members import Imports -import Network.Wai.Utilities.JSONResponse +import Network.AMQP qualified as Q import Polysemy import Polysemy.Input import Polysemy.TinyLog hiding (trace) -import System.Logger.Class qualified as Logger -import Wire.API.Error import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley -import Wire.API.Federation.Error import Wire.API.MLS.Credential import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Message -import Wire.API.Unreachable -- | Propagate a message. -- The message will not be propagated to the sender client if provided. This is -- a requirement from Core Crypto and the clients. propagateMessage :: - ( Member ExternalAccess r, - Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, + Member ExternalAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member TinyLog r @@ -65,7 +60,7 @@ propagateMessage :: Maybe ConnId -> RawMLS Message -> ClientMap -> - Sem r (Maybe UnreachableUsers) + Sem r () propagateMessage qusr mSenderClient lConvOrSub con msg cm = do now <- input @UTCTime let mlsConv = (.conv) <$> lConvOrSub @@ -88,18 +83,17 @@ propagateMessage qusr mSenderClient lConvOrSub con msg cm = do newMessagePush botMap con mm (lmems >>= localMemberMLSClients mlsConv) e -- send to remotes - unreachableFromList . concat - <$$> traverse handleError - <=< runFederatedConcurrentlyEither (map remoteMemberQualify rmems) - $ \(tUnqualified -> rs) -> - fedClient @'Galley @"on-mls-message-sent" $ + (>>= either (logRemoteNotificationError @"on-mls-message-sent") (const (pure ()))) + . enqueueNotificationsConcurrently Q.Persistent (map remoteMemberQualify rmems) + $ \rs -> + fedQueueClient @'Galley @"on-mls-message-sent" $ RemoteMLSMessage { time = now, sender = qusr, metadata = mm, conversation = qUnqualified qcnv, subConversation = sconv, - recipients = rs >>= remoteMemberMLSClients, + recipients = tUnqualified rs >>= remoteMemberMLSClients, message = Base64ByteString msg.raw } where @@ -119,20 +113,3 @@ propagateMessage qusr mSenderClient lConvOrSub con msg cm = do in map (\(c, _) -> (remoteUserId, c)) (Map.assocs (Map.findWithDefault mempty remoteUserQId cmWithoutSender)) - - remotesToQIds = fmap (tUntagged . rmId) - - handleError :: - Member TinyLog r => - Either (Remote [RemoteMember], FederationError) (Remote x) -> - Sem r [Qualified UserId] - handleError (Right _) = pure [] - handleError (Left (r, e)) = do - logFedError r (toResponse e) - pure $ remotesToQIds (tUnqualified r) - logFedError :: Member TinyLog r => Remote x -> JSONResponse -> Sem r () - logFedError r e = - warn $ - Logger.msg ("A message could not be delivered to a remote backend" :: ByteString) - . Logger.field "remote_domain" (domainText (tDomain r)) - . Logger.field "error" (A.encode e.value) diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 31725dd731f..3a796a75c22 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -59,8 +59,8 @@ import Wire.API.MLS.SubConversation createAndSendRemoveProposals :: ( Member (Input UTCTime) r, Member TinyLog r, + Member BackendNotificationQueueAccess r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member ProposalStore r, Member (Input Env) r, @@ -104,18 +104,15 @@ createAndSendRemoveProposals lConvOrSubConv indices qusr cm = do propagateMessage qusr Nothing lConvOrSubConv Nothing msg cm removeClientsWithClientMapRecursively :: - ( Members - '[ Input UTCTime, - TinyLog, - ExternalAccess, - FederatorAccess, - GundeckAccess, - MemberStore, - ProposalStore, - SubConversationStore, - Input Env - ] - r, + ( Member (Input UTCTime) r, + Member TinyLog r, + Member BackendNotificationQueueAccess r, + Member ExternalAccess r, + Member GundeckAccess r, + Member MemberStore r, + Member ProposalStore r, + Member SubConversationStore r, + Member (Input Env) r, Functor f, Foldable f ) => @@ -152,8 +149,8 @@ removeClientsWithClientMapRecursively lMlsConv getClients qusr = do -- | Send remove proposals for a single client of a user to the local conversation. removeClient :: - ( Member ExternalAccess r, - Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, + Member ExternalAccess r, Member GundeckAccess r, Member (Input Env) r, Member (Input UTCTime) r, @@ -175,8 +172,8 @@ removeClient lc qusr c = do -- | Send remove proposals for all clients of the user to the local conversation. removeUser :: - ( Member ExternalAccess r, - Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, + Member ExternalAccess r, Member GundeckAccess r, Member (Input Env) r, Member (Input UTCTime) r, @@ -212,8 +209,8 @@ listSubConversations' cid = do -- | Send remove proposals for clients of users that are not part of a conversation removeExtraneousClients :: - ( Member ExternalAccess r, - Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, + Member ExternalAccess r, Member GundeckAccess r, Member (Input Env) r, Member (Input UTCTime) r, diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index cb849571ca6..9b5ed34274d 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -56,7 +56,7 @@ import Polysemy.Error import Polysemy.Input import Polysemy.Resource import Polysemy.TinyLog -import Wire.API.Conversation +import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley @@ -325,7 +325,8 @@ deleteRemoteSubConversation lusr rcnvId scnvId dsc = do type HasLeaveSubConversationEffects r = ( Members - '[ ConversationStore, + '[ BackendNotificationQueueAccess, + ConversationStore, ExternalAccess, FederatorAccess, GundeckAccess, @@ -348,14 +349,11 @@ type LeaveSubConversationStaticErrors = leaveSubConversation :: ( HasLeaveSubConversationEffects r, - Members - '[ Error MLSProtocolError, - Error FederationError, - ErrorS 'MLSStaleMessage, - ErrorS 'MLSNotEnabled, - Resource - ] - r, + Member (Error MLSProtocolError) r, + Member (Error FederationError) r, + Member (ErrorS 'MLSStaleMessage) r, + Member (ErrorS 'MLSNotEnabled) r, + Member Resource r, Members LeaveSubConversationStaticErrors r ) => Local UserId -> @@ -375,14 +373,10 @@ leaveSubConversation lusr cli qcnv sub = leaveLocalSubConversation :: ( HasLeaveSubConversationEffects r, - Members - '[ Error MLSProtocolError, - ErrorS 'MLSStaleMessage, - ErrorS 'MLSNotEnabled, - Resource, - MemberStore - ] - r, + Member (Error MLSProtocolError) r, + Member (ErrorS 'MLSStaleMessage) r, + Member (ErrorS 'MLSNotEnabled) r, + Member Resource r, Members LeaveSubConversationStaticErrors r ) => ClientIdentity -> diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 7fd56d2c497..15f75a71228 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -64,7 +64,6 @@ import Wire.API.MLS.SubConversation import Wire.API.Message import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.Version -import Wire.API.Unreachable tests :: IO TestSetup -> TestTree tests s = @@ -580,7 +579,7 @@ testRemoteAppMessage = do ((message, events), reqs) <- withTempMockFederator' mock $ do void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle message <- createApplicationMessage alice1 "hello" - (events, _) <- sendAndConsumeMessage message + events <- sendAndConsumeMessage message pure (message, events) liftIO $ do @@ -833,7 +832,7 @@ testAppMessage = do message <- createApplicationMessage alice1 "some text" mlsBracket clients $ \wss -> do - (events, _) <- sendAndConsumeMessage message + events <- sendAndConsumeMessage message liftIO $ events @?= [] liftIO $ do WS.assertMatchN_ (5 # WS.Second) (tail wss) $ @@ -862,7 +861,7 @@ testAppMessage2 = do message <- createApplicationMessage bob1 "some text" mlsBracket (alice1 : clients) $ \[wsAlice1, wsBob1, wsBob2, wsCharlie1] -> do - (events, _) <- sendAndConsumeMessage message + events <- sendAndConsumeMessage message liftIO $ events @?= [] -- check that the corresponding event is received by everyone except bob1 @@ -889,10 +888,9 @@ testAppMessageUnreachable = do sendAndConsumeCommitBundle commit message <- createApplicationMessage alice1 "hi, bob!" - (_, failed) <- sendAndConsumeMessage message + _ <- sendAndConsumeMessage message liftIO $ do assertBool "Event should be member join" $ is _EdMembersJoin (evtData event) - failed @?= unreachableFromList [bob] testRemoteToRemote :: TestM () testRemoteToRemote = do @@ -2068,7 +2066,7 @@ testSendMessageSubConv = do message <- createApplicationMessage alice1 "some text" mlsBracket [bob1, bob2] $ \wss -> do - (events, _) <- sendAndConsumeMessage message + events <- sendAndConsumeMessage message liftIO $ events @?= [] liftIO $ WS.assertMatchN_ (5 # WS.Second) wss $ \n -> do diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 97ddd4fc1de..435c4f0c6a8 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -50,7 +50,6 @@ import Data.Set qualified as Set import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Time -import Data.Tuple.Extra qualified as Tuple import Data.UUID qualified as UUID import Data.UUID.V4 qualified as UUIDV4 import Galley.Keys @@ -83,7 +82,6 @@ import Wire.API.MLS.LeafNode import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation -import Wire.API.Unreachable import Wire.API.User.Client import Wire.API.User.Client.Prekey @@ -866,11 +864,11 @@ consumeMessage1 cid msg = -- | Send an MLS message and simulate clients receiving it. If the message is a -- commit, the 'sendAndConsumeCommitBundle' function should be used instead. -sendAndConsumeMessage :: HasCallStack => MessagePackage -> MLSTest ([Event], Maybe UnreachableUsers) +sendAndConsumeMessage :: HasCallStack => MessagePackage -> MLSTest [Event] sendAndConsumeMessage mp = do for_ mp.mpWelcome $ \_ -> liftIO $ assertFailure "use sendAndConsumeCommitBundle" res <- - fmap (mmssEvents Tuple.&&& mmssFailedToSendTo) $ + fmap mmssEvents $ responseJsonError =<< postMessage (mpSender mp) (mpMessage mp) Date: Wed, 4 Oct 2023 16:36:24 +0200 Subject: [PATCH 187/225] Integration test fixes Some tests now have to wait for MLS messages to go through to the other side before continuing. Since message sending is asynchronous, this means waiting for a notification on a web socket. --- integration/test/MLS/Util.hs | 2 +- integration/test/Test/MLS.hs | 12 +++++++----- integration/test/Test/MLS/SubConversation.hs | 4 ++-- services/galley/test/integration/API/MLS.hs | 1 + 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index b525dc99739..d6bef3fc8a0 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -218,7 +218,7 @@ createGroup cid conv = do Nothing -> pure () resetGroup cid conv -createSubConv :: ClientIdentity -> String -> App () +createSubConv :: HasCallStack => ClientIdentity -> String -> App () createSubConv cid subId = do mls <- getMLSState sub <- getSubConversation cid mls.convId subId >>= getJSON 200 diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index b47355e4504..5a7cc4a9163 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -8,6 +8,7 @@ import Data.ByteString.Base64 qualified as Base64 import Data.ByteString.Char8 qualified as B8 import Data.Text.Encoding qualified as T import MLS.Util +import Notifications import SetupHelpers import Testlib.Prelude @@ -282,9 +283,11 @@ testMLSProtocolUpgrade secondDomain = do void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle - -- charlie is added to the group - void $ uploadNewKeyPackage charlie1 - void $ createAddCommit alice1 [charlie] >>= sendAndConsumeCommitBundle + void $ withWebSocket bob $ \ws -> do + -- charlie is added to the group + void $ uploadNewKeyPackage charlie1 + void $ createAddCommit alice1 [charlie] >>= sendAndConsumeCommitBundle + awaitMatch 10 isNewMLSMessageNotif ws supportMLS alice bindResponse (putConversationProtocol bob conv "mls") $ \resp -> do @@ -300,8 +303,7 @@ testMLSProtocolUpgrade secondDomain = do bindResponse (putConversationProtocol bob conv "mls") $ \resp -> do resp.status `shouldMatchInt` 200 for_ wss $ \ws -> do - let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" - n <- awaitMatch 3 isMessage ws + n <- awaitMatch 3 isNewMLSMessageNotif ws msg <- asByteString (nPayload n %. "data") >>= showMessage alice1 let leafIndexCharlie = 2 msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexCharlie diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 24ec1c354c0..ed5aa95c3d4 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -38,10 +38,10 @@ testDeleteParentOfSubConv secondDomain = do (_, qcnv) <- createNewGroup alice1 withWebSocket bob $ \ws -> do void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - void $ awaitMatch 3 isMemberJoinNotif ws + void $ awaitMatch 10 isMemberJoinNotif ws + -- bob creates a subconversation and adds his own client createSubConv bob1 "conference" - -- bob adds his client to the subconversation void $ createPendingProposalCommit bob1 >>= sendAndConsumeCommitBundle -- alice joins with her own client diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 15f75a71228..9fb3fe5f5b1 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -565,6 +565,7 @@ testUnknownProposalRefCommit = do Date: Mon, 9 Oct 2023 10:56:26 +0200 Subject: [PATCH 188/225] Remove failed_to_send assertion --- integration/test/Test/MLS/Message.hs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/integration/test/Test/MLS/Message.hs b/integration/test/Test/MLS/Message.hs index 28f68bbfaba..08714276a65 100644 --- a/integration/test/Test/MLS/Message.hs +++ b/integration/test/Test/MLS/Message.hs @@ -1,6 +1,5 @@ module Test.MLS.Message where -import API.Galley import MLS.Util import Notifications import SetupHelpers @@ -8,7 +7,7 @@ import Testlib.Prelude testAppMessageSomeReachable :: HasCallStack => App () testAppMessageSomeReachable = do - (alice1, charlie) <- startDynamicBackends [mempty] $ \[thirdDomain] -> do + alice1 <- startDynamicBackends [mempty] $ \[thirdDomain] -> do ownDomain <- make OwnDomain & asString otherDomain <- make OtherDomain & asString [alice, bob, charlie] <- createAndConnectUsers [ownDomain, otherDomain, thirdDomain] @@ -19,11 +18,6 @@ testAppMessageSomeReachable = do void $ withWebSocket charlie $ \ws -> do void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle awaitMatch 10 isMemberJoinNotif ws - pure (alice1, charlie) + pure alice1 - mp <- createApplicationMessage alice1 "hi, bob!" - bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do - resp.status `shouldMatchInt` 201 - - charlieId <- charlie %. "qualified_id" - resp.json %. "failed_to_send" `shouldMatchSet` [charlieId] + void $ createApplicationMessage alice1 "hi, bob!" >>= sendAndConsumeMessage From 89be3e0d05e098dfda5eb19ed7df5f05a6dcae22 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 9 Oct 2023 14:16:10 +0200 Subject: [PATCH 189/225] Add happy path test of MLS messages Also remove unnecessary mock tests from galley integration --- integration/test/Test/MLS/Message.hs | 29 +++++++ services/galley/test/integration/API/MLS.hs | 93 +-------------------- 2 files changed, 30 insertions(+), 92 deletions(-) diff --git a/integration/test/Test/MLS/Message.hs b/integration/test/Test/MLS/Message.hs index 08714276a65..88dce5a28d1 100644 --- a/integration/test/Test/MLS/Message.hs +++ b/integration/test/Test/MLS/Message.hs @@ -5,6 +5,35 @@ import Notifications import SetupHelpers import Testlib.Prelude +-- | Test happy case of federated MLS message sending in both directions. +testApplicationMessage :: HasCallStack => App () +testApplicationMessage = do + -- local alice and alex, remote bob + [alice, alex, bob, betty] <- + createUsers + [OwnDomain, OwnDomain, OtherDomain, OtherDomain] + for_ [alex, bob, betty] $ \user -> connectTwoUsers alice user + + clients@[alice1, _alice2, alex1, _alex2, bob1, _bob2, _, _] <- + traverse + (createMLSClient def) + [alice, alice, alex, alex, bob, bob, betty, betty] + traverse_ uploadNewKeyPackage clients + void $ createNewGroup alice1 + + withWebSockets [alice, alex, bob, betty] $ \wss -> do + -- alice adds all other users (including her own client) + void $ createAddCommit alice1 [alice, alex, bob, betty] >>= sendAndConsumeCommitBundle + traverse_ (awaitMatch 10 isMemberJoinNotif) wss + + -- alex sends a message + void $ createApplicationMessage alex1 "hello" >>= sendAndConsumeMessage + traverse_ (awaitMatch 10 isNewMLSMessageNotif) wss + + -- bob sends a message + void $ createApplicationMessage bob1 "hey" >>= sendAndConsumeMessage + traverse_ (awaitMatch 10 isNewMLSMessageNotif) wss + testAppMessageSomeReachable :: HasCallStack => App () testAppMessageSomeReachable = do alice1 <- startDynamicBackends [mempty] $ \[thirdDomain] -> do diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 9fb3fe5f5b1..6bb5d767a76 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -108,7 +108,6 @@ tests s = [ testGroup "Local Sender/Local Conversation" [ test s "send application message" testAppMessage, - test s "send remote application message" testRemoteAppMessage, test s "another participant sends an application message" testAppMessage2, test s "send message, remote users are unreachable" testAppMessageUnreachable ], @@ -122,11 +121,7 @@ tests s = [ test s "POST /federation/send-mls-message" testRemoteToLocal, test s "POST /federation/send-mls-message, remote user is not a conversation member" testRemoteNonMemberToLocal, test s "POST /federation/send-mls-message, remote user sends to wrong conversation" testRemoteToLocalWrongConversation - ], - testGroup - "Remote Sender/Remote Conversation" - [ test s "POST /federation/on-mls-message-sent" testRemoteToRemote - ] -- all is mocked + ] ], testGroup "Proposal" @@ -565,37 +560,6 @@ testUnknownProposalRefCommit = do messageSentMock <|> welcomeMock - - ((message, events), reqs) <- withTempMockFederator' mock $ do - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - message <- createApplicationMessage alice1 "hello" - events <- sendAndConsumeMessage message - pure (message, events) - - liftIO $ do - req <- assertOne $ filter ((== "on-mls-message-sent") . frRPC) reqs - frTargetDomain req @?= qDomain bob - bdy :: RemoteMLSMessage <- case Aeson.eitherDecode (frBody req) of - Right b -> pure b - Left e -> assertFailure $ "Could not parse on-mls-message-sent request body: " <> e - bdy.sender @?= alice - bdy.conversation @?= qUnqualified qcnv - bdy.recipients @?= [(ciUser bob1, ciClient bob1)] - bdy.message @?= Base64ByteString (mpMessage message) - - liftIO $ assertBool "Unexpected events returned" (null events) - -- The following test happens within backend B -- Alice@A is remote and Bob@B is local -- Alice creates a remote conversation and invites Bob @@ -893,61 +857,6 @@ testAppMessageUnreachable = do liftIO $ do assertBool "Event should be member join" $ is _EdMembersJoin (evtData event) -testRemoteToRemote :: TestM () -testRemoteToRemote = do - localDomain <- viewFederationDomain - c <- view tsCannon - alice <- randomUser - eve <- randomUser - bob <- randomId - conv <- randomId - let aliceC1 = newClientId 0 - aliceC2 = newClientId 1 - eveC = newClientId 0 - bdom = Domain "bob.example.com" - qconv = Qualified conv bdom - qbob = Qualified bob bdom - qalice = Qualified alice localDomain - now <- liftIO getCurrentTime - fedGalleyClient <- view tsFedGalleyClient - - -- only add alice to the remote conversation - connectWithRemoteUser alice qbob - let cu = - ConversationUpdate - { cuTime = now, - cuOrigUserId = qbob, - cuConvId = conv, - cuAlreadyPresentUsers = [], - cuAction = - SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qalice) roleNameWireMember) - } - void $ runFedClient @"on-conversation-updated" fedGalleyClient bdom cu - - let txt = "Hello from another backend" - rcpts = [(alice, aliceC1), (alice, aliceC2), (eve, eveC)] - rm = - RemoteMLSMessage - { time = now, - metadata = defMessageMetadata, - sender = qbob, - conversation = conv, - subConversation = Nothing, - recipients = rcpts, - message = Base64ByteString txt - } - - -- send message to alice and check reception - WS.bracketAsClientRN c [(alice, aliceC1), (alice, aliceC2), (eve, eveC)] $ \[wsA1, wsA2, wsE] -> do - void $ runFedClient @"on-mls-message-sent" fedGalleyClient bdom rm - liftIO $ do - -- alice should receive the message on her first client - WS.assertMatch_ (5 # Second) wsA1 $ \n -> wsAssertMLSMessage (fmap Conv qconv) qbob txt n - WS.assertMatch_ (5 # Second) wsA2 $ \n -> wsAssertMLSMessage (fmap Conv qconv) qbob txt n - - -- eve should not receive the message - WS.assertNoEvent (1 # Second) [wsE] - testRemoteToRemoteInSub :: TestM () testRemoteToRemoteInSub = do localDomain <- viewFederationDomain From 205c88597934da442495c1f8694746f28ee1ce6b Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 9 Oct 2023 15:10:36 +0200 Subject: [PATCH 190/225] Linter --- services/galley/src/Galley/API/MLS/Message.hs | 3 +-- services/galley/src/Galley/API/MLS/Propagate.hs | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index cd2920d87f9..6e25da04853 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -453,11 +453,10 @@ postMLSMessageToRemoteConv loc qusr senderClient con msg rConvOrSubId = do \sent to. The remote end returned: " <> LT.pack (intercalate ", " (show <$> Set.toList (Set.map domainText ds))) MLSMessageResponseUpdates updates -> do - lcus <- fmap fst . runOutputList $ + fmap fst . runOutputList $ for_ updates $ \update -> do me <- updateLocalStateOfRemoteConv (qualifyAs rConvOrSubId update) con for_ me $ \e -> output (LocalConversationUpdate e update) - pure lcus MLSMessageResponseNonFederatingBackends e -> throw e storeGroupInfo :: diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 4a4a50c7e04..da90671702c 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -83,9 +83,8 @@ propagateMessage qusr mSenderClient lConvOrSub con msg cm = do newMessagePush botMap con mm (lmems >>= localMemberMLSClients mlsConv) e -- send to remotes - (>>= either (logRemoteNotificationError @"on-mls-message-sent") (const (pure ()))) - . enqueueNotificationsConcurrently Q.Persistent (map remoteMemberQualify rmems) - $ \rs -> + (either (logRemoteNotificationError @"on-mls-message-sent") (const (pure ())) <=< enqueueNotificationsConcurrently Q.Persistent (map remoteMemberQualify rmems)) $ + \rs -> fedQueueClient @'Galley @"on-mls-message-sent" $ RemoteMLSMessage { time = now, From 315032f4b23d93dc80687959bf2d6d1bfe00e19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Mon, 9 Oct 2023 15:43:33 +0200 Subject: [PATCH 191/225] Add a changelog --- changelog.d/6-federation/wpb-4984-queueing | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6-federation/wpb-4984-queueing diff --git a/changelog.d/6-federation/wpb-4984-queueing b/changelog.d/6-federation/wpb-4984-queueing new file mode 100644 index 00000000000..c258c8eabda --- /dev/null +++ b/changelog.d/6-federation/wpb-4984-queueing @@ -0,0 +1 @@ +Remote MLS messages get queued via RabbitMQ From 4048b4609ec2e7b2d90154f3f86ee4cb473c12cc Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 10 Oct 2023 11:28:57 +0200 Subject: [PATCH 192/225] Update golden tests --- .../golden/testObject_MLSMessageSendingStatus2.json | 8 +------- .../golden/testObject_MLSMessageSendingStatus3.json | 12 +----------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus2.json b/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus2.json index fb932f00593..5d03a97ba11 100644 --- a/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus2.json +++ b/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus2.json @@ -1,10 +1,4 @@ { "events": [], - "time": "2001-04-12T12:22:43.673Z", - "failed_to_send": [ - { - "domain": "offline.example.com", - "id": "00000000-0000-0000-0000-000200000008" - } - ] + "time": "2001-04-12T12:22:43.673Z" } diff --git a/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus3.json b/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus3.json index 87c92624480..47e408103f9 100644 --- a/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus3.json +++ b/libs/wire-api-federation/test/golden/testObject_MLSMessageSendingStatus3.json @@ -1,14 +1,4 @@ { "events": [], - "time": "1999-04-12T12:22:43.673Z", - "failed_to_send": [ - { - "domain": "golden.example.com", - "id": "00000000-0000-0000-0000-000200000008" - }, - { - "domain": "golden.example.com", - "id": "00000000-0000-0000-0000-000100000007" - } - ] + "time": "1999-04-12T12:22:43.673Z" } From ee9c3b70204fda64ee3bbd637dd2d38a3ecce854 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Wed, 11 Oct 2023 03:23:26 +0200 Subject: [PATCH 193/225] Add documentation of team/backend distinction, as per WPB-4344 --- docs/src/understand/classified-domains.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/src/understand/classified-domains.md b/docs/src/understand/classified-domains.md index 5d27945abbb..bb1df193b28 100644 --- a/docs/src/understand/classified-domains.md +++ b/docs/src/understand/classified-domains.md @@ -17,10 +17,7 @@ galley: domains: ["domain-that-is-classified.link"] ... ``` - -This is not only a `backend` configuration, but also a `team` configuration/feature. - -This means that different combinations of configurations will have different results. +Note: This is only a `backend` level configuration option, the `team` configuration mentionned below only exists for technical reasons and is not actually accessible in any way. Here is a table to navigate the possible configurations: From e6b2ed44bfb072699d5f79d71efe344eb40e0f2d Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:53:04 +0200 Subject: [PATCH 194/225] [WPB-3138] Split spar-schema into exec and lib. (#3642) * Split spar-schema into exec and lib. * Update services/spar/schema/src/Run.hs Co-authored-by: Akshay Mankar * Cleaned up exec stanza. * Merged schema-lib and spar. --------- Co-authored-by: Akshay Mankar --- .../schema/{exe/spar-schema.hs => Main.hs} | 2 +- services/spar/schema/src/Run.hs | 79 ------------------ services/spar/spar.cabal | 54 +++++------- services/spar/src/Spar/Data.hs | 3 +- services/spar/src/Spar/Schema/Run.hs | 83 +++++++++++++++++++ .../{schema/src => src/Spar/Schema}/V0.hs | 2 +- .../{schema/src => src/Spar/Schema}/V1.hs | 2 +- .../{schema/src => src/Spar/Schema}/V10.hs | 2 +- .../{schema/src => src/Spar/Schema}/V11.hs | 2 +- .../{schema/src => src/Spar/Schema}/V12.hs | 2 +- .../{schema/src => src/Spar/Schema}/V13.hs | 2 +- .../{schema/src => src/Spar/Schema}/V14.hs | 2 +- .../{schema/src => src/Spar/Schema}/V15.hs | 2 +- .../{schema/src => src/Spar/Schema}/V16.hs | 2 +- .../{schema/src => src/Spar/Schema}/V17.hs | 2 +- .../{schema/src => src/Spar/Schema}/V2.hs | 2 +- .../{schema/src => src/Spar/Schema}/V3.hs | 2 +- .../{schema/src => src/Spar/Schema}/V4.hs | 2 +- .../{schema/src => src/Spar/Schema}/V5.hs | 2 +- .../{schema/src => src/Spar/Schema}/V6.hs | 2 +- .../{schema/src => src/Spar/Schema}/V7.hs | 2 +- .../{schema/src => src/Spar/Schema}/V8.hs | 2 +- .../{schema/src => src/Spar/Schema}/V9.hs | 2 +- 23 files changed, 127 insertions(+), 130 deletions(-) rename services/spar/schema/{exe/spar-schema.hs => Main.hs} (61%) delete mode 100644 services/spar/schema/src/Run.hs create mode 100644 services/spar/src/Spar/Schema/Run.hs rename services/spar/{schema/src => src/Spar/Schema}/V0.hs (99%) rename services/spar/{schema/src => src/Spar/Schema}/V1.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V10.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V11.hs (97%) rename services/spar/{schema/src => src/Spar/Schema}/V12.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V13.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V14.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V15.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V16.hs (97%) rename services/spar/{schema/src => src/Spar/Schema}/V17.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V2.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V3.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V4.hs (99%) rename services/spar/{schema/src => src/Spar/Schema}/V5.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V6.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V7.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V8.hs (98%) rename services/spar/{schema/src => src/Spar/Schema}/V9.hs (98%) diff --git a/services/spar/schema/exe/spar-schema.hs b/services/spar/schema/Main.hs similarity index 61% rename from services/spar/schema/exe/spar-schema.hs rename to services/spar/schema/Main.hs index d862e71bcdc..014014bde4f 100644 --- a/services/spar/schema/exe/spar-schema.hs +++ b/services/spar/schema/Main.hs @@ -1,7 +1,7 @@ module Main where import Imports -import qualified Run +import qualified Spar.Schema.Run as Run main :: IO () main = Run.main diff --git a/services/spar/schema/src/Run.hs b/services/spar/schema/src/Run.hs deleted file mode 100644 index e82ba618bd5..00000000000 --- a/services/spar/schema/src/Run.hs +++ /dev/null @@ -1,79 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 Run where - -import Cassandra.Schema -import Control.Exception (finally) -import Imports -import qualified System.Logger.Extended as Log -import Util.Options -import qualified V0 -import qualified V1 -import qualified V10 -import qualified V11 -import qualified V12 -import qualified V13 -import qualified V14 -import qualified V15 -import qualified V16 -import qualified V17 -import qualified V2 -import qualified V3 -import qualified V4 -import qualified V5 -import qualified V6 -import qualified V7 -import qualified V8 -import qualified V9 - -main :: IO () -main = do - let desc = "Spar Cassandra Schema Migrations" - defaultPath = "/etc/wire/spar/conf/spar-schema.yaml" - o <- getOptions desc (Just migrationOptsParser) defaultPath - l <- Log.mkLogger' - migrateSchema - l - o - [ V0.migration, - V1.migration, - V2.migration, - V3.migration, - V4.migration, - V5.migration, - V6.migration, - V7.migration, - V8.migration, - V9.migration, - V10.migration, - V11.migration, - V12.migration, - V13.migration, - V14.migration, - V15.migration, - V16.migration, - V17.migration - -- When adding migrations here, don't forget to update - -- 'schemaVersion' in Spar.Data - - -- TODO: Add a migration that removes unused fields - -- (we don't want to risk running a migration which would - -- effectively break the currently deployed spar service) - -- see https://github.com/wireapp/wire-server/pull/476. - ] - `finally` Log.close l diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 428b2c8e6f1..cf40efc54d2 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -28,6 +28,25 @@ library Spar.Options Spar.Orphans Spar.Run + Spar.Schema.Run + Spar.Schema.V0 + Spar.Schema.V1 + Spar.Schema.V10 + Spar.Schema.V11 + Spar.Schema.V12 + Spar.Schema.V13 + Spar.Schema.V14 + Spar.Schema.V15 + Spar.Schema.V16 + Spar.Schema.V17 + Spar.Schema.V2 + Spar.Schema.V3 + Spar.Schema.V4 + Spar.Schema.V5 + Spar.Schema.V6 + Spar.Schema.V7 + Spar.Schema.V8 + Spar.Schema.V9 Spar.Scim Spar.Scim.Auth Spar.Scim.Types @@ -455,31 +474,8 @@ executable spar-migrate-data default-language: Haskell2010 executable spar-schema - main-is: spar-schema.hs - - -- cabal-fmt: expand schema/src - other-modules: - Run - V0 - V1 - V10 - V11 - V12 - V13 - V14 - V15 - V16 - V17 - V2 - V3 - V4 - V5 - V6 - V7 - V8 - V9 - - hs-source-dirs: schema/src schema/exe + main-is: Main.hs + hs-source-dirs: schema/ default-extensions: NoImplicitPrelude AllowAmbiguousTypes @@ -529,12 +525,8 @@ executable spar-schema -with-rtsopts=-N -Wredundant-constraints -Wunused-packages build-depends: - base - , cassandra-util - , extended - , imports - , raw-strings-qq - , types-common + imports + , spar default-language: Haskell2010 diff --git a/services/spar/src/Spar/Data.hs b/services/spar/src/Spar/Data.hs index 683fc05a3ee..ad8915c45c1 100644 --- a/services/spar/src/Spar/Data.hs +++ b/services/spar/src/Spar/Data.hs @@ -44,11 +44,12 @@ import SAML2.Util (renderURI) import qualified SAML2.WebSSO as SAML import qualified SAML2.WebSSO.Types.Email as SAMLEmail import Spar.Options +import qualified Spar.Schema.Run as Migrations import Wire.API.User.Saml -- | A lower bound: @schemaVersion <= whatWeFoundOnCassandra@, not @==@. schemaVersion :: Int32 -schemaVersion = 17 +schemaVersion = Migrations.lastSchemaVersion ---------------------------------------------------------------------- -- helpers diff --git a/services/spar/src/Spar/Schema/Run.hs b/services/spar/src/Spar/Schema/Run.hs new file mode 100644 index 00000000000..4fef2264a34 --- /dev/null +++ b/services/spar/src/Spar/Schema/Run.hs @@ -0,0 +1,83 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Spar.Schema.Run where + +import Cassandra.Schema +import Control.Exception (finally) +import Imports +import qualified Spar.Schema.V0 as V0 +import qualified Spar.Schema.V1 as V1 +import qualified Spar.Schema.V10 as V10 +import qualified Spar.Schema.V11 as V11 +import qualified Spar.Schema.V12 as V12 +import qualified Spar.Schema.V13 as V13 +import qualified Spar.Schema.V14 as V14 +import qualified Spar.Schema.V15 as V15 +import qualified Spar.Schema.V16 as V16 +import qualified Spar.Schema.V17 as V17 +import qualified Spar.Schema.V2 as V2 +import qualified Spar.Schema.V3 as V3 +import qualified Spar.Schema.V4 as V4 +import qualified Spar.Schema.V5 as V5 +import qualified Spar.Schema.V6 as V6 +import qualified Spar.Schema.V7 as V7 +import qualified Spar.Schema.V8 as V8 +import qualified Spar.Schema.V9 as V9 +import qualified System.Logger.Extended as Log +import Util.Options + +main :: IO () +main = do + let desc = "Spar Cassandra Schema Migrations" + defaultPath = "/etc/wire/spar/conf/spar-schema.yaml" + o <- getOptions desc (Just migrationOptsParser) defaultPath + l <- Log.mkLogger' + migrateSchema + l + o + migrations + `finally` Log.close l + +lastSchemaVersion :: Int32 +lastSchemaVersion = migVersion $ last migrations + +migrations :: [Migration] +migrations = + [ V0.migration, + V1.migration, + V2.migration, + V3.migration, + V4.migration, + V5.migration, + V6.migration, + V7.migration, + V8.migration, + V9.migration, + V10.migration, + V11.migration, + V12.migration, + V13.migration, + V14.migration, + V15.migration, + V16.migration, + V17.migration + -- TODO: Add a migration that removes unused fields + -- (we don't want to risk running a migration which would + -- effectively break the currently deployed spar service) + -- see https://github.com/wireapp/wire-server/pull/476. + ] diff --git a/services/spar/schema/src/V0.hs b/services/spar/src/Spar/Schema/V0.hs similarity index 99% rename from services/spar/schema/src/V0.hs rename to services/spar/src/Spar/Schema/V0.hs index b537c16ffa0..104d68b0b78 100644 --- a/services/spar/schema/src/V0.hs +++ b/services/spar/src/Spar/Schema/V0.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V0 +module Spar.Schema.V0 ( migration, ) where diff --git a/services/spar/schema/src/V1.hs b/services/spar/src/Spar/Schema/V1.hs similarity index 98% rename from services/spar/schema/src/V1.hs rename to services/spar/src/Spar/Schema/V1.hs index d0dab374e17..1b1c44ca753 100644 --- a/services/spar/schema/src/V1.hs +++ b/services/spar/src/Spar/Schema/V1.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V1 +module Spar.Schema.V1 ( migration, ) where diff --git a/services/spar/schema/src/V10.hs b/services/spar/src/Spar/Schema/V10.hs similarity index 98% rename from services/spar/schema/src/V10.hs rename to services/spar/src/Spar/Schema/V10.hs index 532102f426a..88513ec0e82 100644 --- a/services/spar/schema/src/V10.hs +++ b/services/spar/src/Spar/Schema/V10.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V10 +module Spar.Schema.V10 ( migration, ) where diff --git a/services/spar/schema/src/V11.hs b/services/spar/src/Spar/Schema/V11.hs similarity index 97% rename from services/spar/schema/src/V11.hs rename to services/spar/src/Spar/Schema/V11.hs index 8e893da948e..6cf2892f882 100644 --- a/services/spar/schema/src/V11.hs +++ b/services/spar/src/Spar/Schema/V11.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V11 +module Spar.Schema.V11 ( migration, ) where diff --git a/services/spar/schema/src/V12.hs b/services/spar/src/Spar/Schema/V12.hs similarity index 98% rename from services/spar/schema/src/V12.hs rename to services/spar/src/Spar/Schema/V12.hs index 59ef5ed0d12..b09d1491371 100644 --- a/services/spar/schema/src/V12.hs +++ b/services/spar/src/Spar/Schema/V12.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V12 +module Spar.Schema.V12 ( migration, ) where diff --git a/services/spar/schema/src/V13.hs b/services/spar/src/Spar/Schema/V13.hs similarity index 98% rename from services/spar/schema/src/V13.hs rename to services/spar/src/Spar/Schema/V13.hs index c0f0248221e..26148eb3af1 100644 --- a/services/spar/schema/src/V13.hs +++ b/services/spar/src/Spar/Schema/V13.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V13 +module Spar.Schema.V13 ( migration, ) where diff --git a/services/spar/schema/src/V14.hs b/services/spar/src/Spar/Schema/V14.hs similarity index 98% rename from services/spar/schema/src/V14.hs rename to services/spar/src/Spar/Schema/V14.hs index 322ee66a3e5..2a682243640 100644 --- a/services/spar/schema/src/V14.hs +++ b/services/spar/src/Spar/Schema/V14.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V14 +module Spar.Schema.V14 ( migration, ) where diff --git a/services/spar/schema/src/V15.hs b/services/spar/src/Spar/Schema/V15.hs similarity index 98% rename from services/spar/schema/src/V15.hs rename to services/spar/src/Spar/Schema/V15.hs index c2d7d43e243..574c8df5b12 100644 --- a/services/spar/schema/src/V15.hs +++ b/services/spar/src/Spar/Schema/V15.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V15 +module Spar.Schema.V15 ( migration, ) where diff --git a/services/spar/schema/src/V16.hs b/services/spar/src/Spar/Schema/V16.hs similarity index 97% rename from services/spar/schema/src/V16.hs rename to services/spar/src/Spar/Schema/V16.hs index 289776c13db..ed52742af2c 100644 --- a/services/spar/schema/src/V16.hs +++ b/services/spar/src/Spar/Schema/V16.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V16 +module Spar.Schema.V16 ( migration, ) where diff --git a/services/spar/schema/src/V17.hs b/services/spar/src/Spar/Schema/V17.hs similarity index 98% rename from services/spar/schema/src/V17.hs rename to services/spar/src/Spar/Schema/V17.hs index bccc4aab7de..10465ee3251 100644 --- a/services/spar/schema/src/V17.hs +++ b/services/spar/src/Spar/Schema/V17.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V17 +module Spar.Schema.V17 ( migration, ) where diff --git a/services/spar/schema/src/V2.hs b/services/spar/src/Spar/Schema/V2.hs similarity index 98% rename from services/spar/schema/src/V2.hs rename to services/spar/src/Spar/Schema/V2.hs index 5ab0a0525ad..ffb4d97416e 100644 --- a/services/spar/schema/src/V2.hs +++ b/services/spar/src/Spar/Schema/V2.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V2 +module Spar.Schema.V2 ( migration, ) where diff --git a/services/spar/schema/src/V3.hs b/services/spar/src/Spar/Schema/V3.hs similarity index 98% rename from services/spar/schema/src/V3.hs rename to services/spar/src/Spar/Schema/V3.hs index 7a7e5441090..0bb754452b8 100644 --- a/services/spar/schema/src/V3.hs +++ b/services/spar/src/Spar/Schema/V3.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V3 +module Spar.Schema.V3 ( migration, ) where diff --git a/services/spar/schema/src/V4.hs b/services/spar/src/Spar/Schema/V4.hs similarity index 99% rename from services/spar/schema/src/V4.hs rename to services/spar/src/Spar/Schema/V4.hs index 9760d377c3a..41f2b5bde20 100644 --- a/services/spar/schema/src/V4.hs +++ b/services/spar/src/Spar/Schema/V4.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V4 +module Spar.Schema.V4 ( migration, ) where diff --git a/services/spar/schema/src/V5.hs b/services/spar/src/Spar/Schema/V5.hs similarity index 98% rename from services/spar/schema/src/V5.hs rename to services/spar/src/Spar/Schema/V5.hs index 7a635cc5283..66f7b22fcc7 100644 --- a/services/spar/schema/src/V5.hs +++ b/services/spar/src/Spar/Schema/V5.hs @@ -18,7 +18,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V5 +module Spar.Schema.V5 ( migration, ) where diff --git a/services/spar/schema/src/V6.hs b/services/spar/src/Spar/Schema/V6.hs similarity index 98% rename from services/spar/schema/src/V6.hs rename to services/spar/src/Spar/Schema/V6.hs index 71e13821fdc..7b5d0471b67 100644 --- a/services/spar/schema/src/V6.hs +++ b/services/spar/src/Spar/Schema/V6.hs @@ -18,7 +18,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V6 +module Spar.Schema.V6 ( migration, ) where diff --git a/services/spar/schema/src/V7.hs b/services/spar/src/Spar/Schema/V7.hs similarity index 98% rename from services/spar/schema/src/V7.hs rename to services/spar/src/Spar/Schema/V7.hs index 2a28aea17bb..01a353b2711 100644 --- a/services/spar/schema/src/V7.hs +++ b/services/spar/src/Spar/Schema/V7.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V7 +module Spar.Schema.V7 ( migration, ) where diff --git a/services/spar/schema/src/V8.hs b/services/spar/src/Spar/Schema/V8.hs similarity index 98% rename from services/spar/schema/src/V8.hs rename to services/spar/src/Spar/Schema/V8.hs index d8b795ccc12..f32e3d48d3c 100644 --- a/services/spar/schema/src/V8.hs +++ b/services/spar/src/Spar/Schema/V8.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V8 +module Spar.Schema.V8 ( migration, ) where diff --git a/services/spar/schema/src/V9.hs b/services/spar/src/Spar/Schema/V9.hs similarity index 98% rename from services/spar/schema/src/V9.hs rename to services/spar/src/Spar/Schema/V9.hs index 990d1a15ef2..e0751ce1851 100644 --- a/services/spar/schema/src/V9.hs +++ b/services/spar/src/Spar/Schema/V9.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V9 +module Spar.Schema.V9 ( migration, ) where From f35c9ae8a033082b74dafb0bc5cf0f263afe7591 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Wed, 11 Oct 2023 14:44:33 +0200 Subject: [PATCH 195/225] [WPB-3138] Split galley-schema into lib and exec (#3639) * Split galley-schema into lib and exec * Removed galley-schema-lib, merged into galley. * Removed dead comment --- services/galley/default.nix | 2 +- services/galley/galley.cabal | 199 ++++++++---------- services/galley/schema/{main.hs => Main.hs} | 2 +- services/galley/schema/src/Run.hs | 182 ---------------- services/galley/src/Galley/Cassandra.hs | 3 +- services/galley/src/Galley/Schema/Run.hs | 185 ++++++++++++++++ .../{schema/src => src/Galley/Schema}/V20.hs | 2 +- .../{schema/src => src/Galley/Schema}/V21.hs | 2 +- .../{schema/src => src/Galley/Schema}/V22.hs | 2 +- .../{schema/src => src/Galley/Schema}/V23.hs | 2 +- .../{schema/src => src/Galley/Schema}/V24.hs | 2 +- .../{schema/src => src/Galley/Schema}/V25.hs | 2 +- .../{schema/src => src/Galley/Schema}/V26.hs | 2 +- .../{schema/src => src/Galley/Schema}/V27.hs | 2 +- .../{schema/src => src/Galley/Schema}/V28.hs | 2 +- .../{schema/src => src/Galley/Schema}/V29.hs | 2 +- .../{schema/src => src/Galley/Schema}/V30.hs | 2 +- .../{schema/src => src/Galley/Schema}/V31.hs | 2 +- .../{schema/src => src/Galley/Schema}/V32.hs | 2 +- .../{schema/src => src/Galley/Schema}/V33.hs | 2 +- .../{schema/src => src/Galley/Schema}/V34.hs | 2 +- .../{schema/src => src/Galley/Schema}/V35.hs | 2 +- .../{schema/src => src/Galley/Schema}/V36.hs | 2 +- .../{schema/src => src/Galley/Schema}/V37.hs | 2 +- .../V38_CreateTableBillingTeamMember.hs | 2 +- .../{schema/src => src/Galley/Schema}/V39.hs | 2 +- .../Schema}/V40_CreateTableDataMigration.hs | 2 +- .../Schema}/V41_TeamNotificationQueue.hs | 2 +- .../V42_TeamFeatureValidateSamlEmails.hs | 2 +- .../V43_TeamFeatureDigitalSignatures.hs | 2 +- .../Schema}/V44_AddRemoteIdentifiers.hs | 2 +- .../Schema}/V45_AddFederationIdMapping.hs | 2 +- .../Galley/Schema}/V46_TeamFeatureAppLock.hs | 2 +- .../Schema}/V47_RemoveFederationIdMapping.hs | 2 +- .../Schema}/V48_DeleteRemoteIdentifiers.hs | 2 +- .../Schema}/V49_ReAddRemoteIdentifiers.hs | 2 +- .../Schema}/V50_AddLegalholdWhitelisted.hs | 2 +- .../Galley/Schema}/V51_FeatureFileSharing.hs | 2 +- .../Schema}/V52_FeatureConferenceCalling.hs | 2 +- .../Galley/Schema}/V53_AddRemoteConvStatus.hs | 2 +- .../V54_TeamFeatureSelfDeletingMessages.hs | 2 +- .../V55_SelfDeletingMessagesLockStatus.hs | 2 +- .../V56_GuestLinksTeamFeatureStatus.hs | 2 +- .../Schema}/V57_GuestLinksLockStatus.hs | 2 +- .../Schema}/V58_ConversationAccessRoleV2.hs | 2 +- .../Schema}/V59_FileSharingLockStatus.hs | 2 +- ...0_TeamFeatureSndFactorPasswordChallenge.hs | 2 +- .../Galley/Schema}/V61_MLSConversation.hs | 2 +- .../V62_TeamFeatureSearchVisibilityInbound.hs | 2 +- .../Schema}/V63_MLSConversationClients.hs | 2 +- .../src => src/Galley/Schema}/V64_Epoch.hs | 2 +- .../Galley/Schema}/V65_MLSRemoteClients.hs | 2 +- .../Galley/Schema}/V66_AddSplashScreen.hs | 2 +- .../Galley/Schema}/V67_MLSFeature.hs | 2 +- .../Galley/Schema}/V68_MLSCommitLock.hs | 2 +- .../Galley/Schema}/V69_MLSProposal.hs | 2 +- .../Galley/Schema}/V70_MLSCipherSuite.hs | 2 +- .../Schema}/V71_MemberClientKeypackage.hs | 2 +- .../Schema}/V72_DropManagedConversations.hs | 2 +- .../Galley/Schema}/V73_MemberClientTable.hs | 2 +- .../V74_ExposeInvitationsToTeamAdmin.hs | 2 +- .../Galley/Schema}/V75_MLSGroupInfo.hs | 2 +- .../Galley/Schema}/V76_ProposalOrigin.hs | 2 +- .../Schema}/V77_MLSGroupMemberClient.hs | 2 +- .../V78_TeamFeatureOutlookCalIntegration.hs | 2 +- .../Galley/Schema}/V79_TeamFeatureMlsE2EId.hs | 2 +- .../V80_AddConversationCodePassword.hs | 2 +- .../Schema}/V81_TeamFeatureMlsE2EIdUpdate.hs | 2 +- .../Galley/Schema}/V82_RemoteDomainIndexes.hs | 2 +- .../Schema}/V83_CreateTableTeamAdmin.hs | 2 +- .../Galley/Schema}/V84_MLSSubconversation.hs | 2 +- .../Galley/Schema}/V85_MLSDraft17.hs | 2 +- .../Schema}/V86_TeamFeatureMlsMigration.hs | 2 +- .../V87_TeamFeatureSupportedProtocols.hs | 2 +- 74 files changed, 347 insertions(+), 362 deletions(-) rename services/galley/schema/{main.hs => Main.hs} (52%) delete mode 100644 services/galley/schema/src/Run.hs create mode 100644 services/galley/src/Galley/Schema/Run.hs rename services/galley/{schema/src => src/Galley/Schema}/V20.hs (99%) rename services/galley/{schema/src => src/Galley/Schema}/V21.hs (99%) rename services/galley/{schema/src => src/Galley/Schema}/V22.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V23.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V24.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V25.hs (98%) rename services/galley/{schema/src => src/Galley/Schema}/V26.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V27.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V28.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V29.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V30.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V31.hs (98%) rename services/galley/{schema/src => src/Galley/Schema}/V32.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V33.hs (98%) rename services/galley/{schema/src => src/Galley/Schema}/V34.hs (98%) rename services/galley/{schema/src => src/Galley/Schema}/V35.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V36.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V37.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V38_CreateTableBillingTeamMember.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V39.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V40_CreateTableDataMigration.hs (94%) rename services/galley/{schema/src => src/Galley/Schema}/V41_TeamNotificationQueue.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V42_TeamFeatureValidateSamlEmails.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V43_TeamFeatureDigitalSignatures.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V44_AddRemoteIdentifiers.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V45_AddFederationIdMapping.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V46_TeamFeatureAppLock.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V47_RemoveFederationIdMapping.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V48_DeleteRemoteIdentifiers.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V49_ReAddRemoteIdentifiers.hs (98%) rename services/galley/{schema/src => src/Galley/Schema}/V50_AddLegalholdWhitelisted.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V51_FeatureFileSharing.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V52_FeatureConferenceCalling.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V53_AddRemoteConvStatus.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V54_TeamFeatureSelfDeletingMessages.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V55_SelfDeletingMessagesLockStatus.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V56_GuestLinksTeamFeatureStatus.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V57_GuestLinksLockStatus.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V58_ConversationAccessRoleV2.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V59_FileSharingLockStatus.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V60_TeamFeatureSndFactorPasswordChallenge.hs (94%) rename services/galley/{schema/src => src/Galley/Schema}/V61_MLSConversation.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V62_TeamFeatureSearchVisibilityInbound.hs (94%) rename services/galley/{schema/src => src/Galley/Schema}/V63_MLSConversationClients.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V64_Epoch.hs (97%) rename services/galley/{schema/src => src/Galley/Schema}/V65_MLSRemoteClients.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V66_AddSplashScreen.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V67_MLSFeature.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V68_MLSCommitLock.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V69_MLSProposal.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V70_MLSCipherSuite.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V71_MemberClientKeypackage.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V72_DropManagedConversations.hs (94%) rename services/galley/{schema/src => src/Galley/Schema}/V73_MemberClientTable.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V74_ExposeInvitationsToTeamAdmin.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V75_MLSGroupInfo.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V76_ProposalOrigin.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V77_MLSGroupMemberClient.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V78_TeamFeatureOutlookCalIntegration.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V79_TeamFeatureMlsE2EId.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V80_AddConversationCodePassword.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V81_TeamFeatureMlsE2EIdUpdate.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V82_RemoteDomainIndexes.hs (91%) rename services/galley/{schema/src => src/Galley/Schema}/V83_CreateTableTeamAdmin.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V84_MLSSubconversation.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V85_MLSDraft17.hs (95%) rename services/galley/{schema/src => src/Galley/Schema}/V86_TeamFeatureMlsMigration.hs (96%) rename services/galley/{schema/src => src/Galley/Schema}/V87_TeamFeatureSupportedProtocols.hs (95%) diff --git a/services/galley/default.nix b/services/galley/default.nix index 10d94f415e3..68e29faedeb 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -174,6 +174,7 @@ mkDerivation { metrics-core metrics-wai mtl + optparse-applicative pem polysemy polysemy-wire-zoo @@ -269,7 +270,6 @@ mkDerivation { QuickCheck quickcheck-instances random - raw-strings-qq retry saml2-web-sso schema-profunctor diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index a35b727e888..e6ede83b48e 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -16,7 +16,7 @@ flag static default: False common common-all - default-language: Haskell2010 + default-language: GHC2021 ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path @@ -68,7 +68,7 @@ common common-all ViewPatterns library - import: common-all + import: common-all -- cabal-fmt: expand src exposed-modules: @@ -203,14 +203,83 @@ library Galley.Options Galley.Queue Galley.Run + Galley.Schema.Run + Galley.Schema.V20 + Galley.Schema.V21 + Galley.Schema.V22 + Galley.Schema.V23 + Galley.Schema.V24 + Galley.Schema.V25 + Galley.Schema.V26 + Galley.Schema.V27 + Galley.Schema.V28 + Galley.Schema.V29 + Galley.Schema.V30 + Galley.Schema.V31 + Galley.Schema.V32 + Galley.Schema.V33 + Galley.Schema.V34 + Galley.Schema.V35 + Galley.Schema.V36 + Galley.Schema.V37 + Galley.Schema.V38_CreateTableBillingTeamMember + Galley.Schema.V39 + Galley.Schema.V40_CreateTableDataMigration + Galley.Schema.V41_TeamNotificationQueue + Galley.Schema.V42_TeamFeatureValidateSamlEmails + Galley.Schema.V43_TeamFeatureDigitalSignatures + Galley.Schema.V44_AddRemoteIdentifiers + Galley.Schema.V45_AddFederationIdMapping + Galley.Schema.V46_TeamFeatureAppLock + Galley.Schema.V47_RemoveFederationIdMapping + Galley.Schema.V48_DeleteRemoteIdentifiers + Galley.Schema.V49_ReAddRemoteIdentifiers + Galley.Schema.V50_AddLegalholdWhitelisted + Galley.Schema.V51_FeatureFileSharing + Galley.Schema.V52_FeatureConferenceCalling + Galley.Schema.V53_AddRemoteConvStatus + Galley.Schema.V54_TeamFeatureSelfDeletingMessages + Galley.Schema.V55_SelfDeletingMessagesLockStatus + Galley.Schema.V56_GuestLinksTeamFeatureStatus + Galley.Schema.V57_GuestLinksLockStatus + Galley.Schema.V58_ConversationAccessRoleV2 + Galley.Schema.V59_FileSharingLockStatus + Galley.Schema.V60_TeamFeatureSndFactorPasswordChallenge + Galley.Schema.V61_MLSConversation + Galley.Schema.V62_TeamFeatureSearchVisibilityInbound + Galley.Schema.V63_MLSConversationClients + Galley.Schema.V64_Epoch + Galley.Schema.V65_MLSRemoteClients + Galley.Schema.V66_AddSplashScreen + Galley.Schema.V67_MLSFeature + Galley.Schema.V68_MLSCommitLock + Galley.Schema.V69_MLSProposal + Galley.Schema.V70_MLSCipherSuite + Galley.Schema.V71_MemberClientKeypackage + Galley.Schema.V72_DropManagedConversations + Galley.Schema.V73_MemberClientTable + Galley.Schema.V74_ExposeInvitationsToTeamAdmin + Galley.Schema.V75_MLSGroupInfo + Galley.Schema.V76_ProposalOrigin + Galley.Schema.V77_MLSGroupMemberClient + Galley.Schema.V78_TeamFeatureOutlookCalIntegration + Galley.Schema.V79_TeamFeatureMlsE2EId + Galley.Schema.V80_AddConversationCodePassword + Galley.Schema.V81_TeamFeatureMlsE2EIdUpdate + Galley.Schema.V82_RemoteDomainIndexes + Galley.Schema.V83_CreateTableTeamAdmin + Galley.Schema.V84_MLSSubconversation + Galley.Schema.V85_MLSDraft17 + Galley.Schema.V86_TeamFeatureMlsMigration + Galley.Schema.V87_TeamFeatureSupportedProtocols Galley.Types.Clients Galley.Types.ToUserRole Galley.Types.UserList Galley.Validation - ghc-options: -fplugin=TransitiveAnns.Plugin - other-modules: Paths_galley - hs-source-dirs: src + ghc-options: -fplugin=TransitiveAnns.Plugin + other-modules: Paths_galley + hs-source-dirs: src build-depends: , aeson >=2.0.1.0 , amazonka >=1.4.5 @@ -255,6 +324,7 @@ library , metrics-core , metrics-wai >=0.4 , mtl >=2.2 + , optparse-applicative , pem , polysemy , polysemy-wire-zoo @@ -296,13 +366,11 @@ library , wire-api-federation , x509 - default-language: GHC2021 - executable galley - import: common-all - main-is: exec/Main.hs - other-modules: Paths_galley - ghc-options: -threaded -with-rtsopts=-T -rtsopts + import: common-all + main-is: exec/Main.hs + other-modules: Paths_galley + ghc-options: -threaded -with-rtsopts=-T -rtsopts build-depends: , base , galley @@ -313,8 +381,6 @@ executable galley if flag(static) ld-options: -static - default-language: GHC2021 - executable galley-integration import: common-all main-is: ../integration.hs @@ -486,11 +552,9 @@ executable galley-integration , wire-api-federation , yaml - default-language: GHC2021 - executable galley-migrate-data - import: common-all - main-is: ../main.hs + import: common-all + main-is: ../main.hs -- cabal-fmt: expand migrate-data/src other-modules: @@ -502,7 +566,7 @@ executable galley-migrate-data V2_MigrateMLSMembers V3_BackfillTeamAdmins - hs-source-dirs: migrate-data/src + hs-source-dirs: migrate-data/src build-depends: , base , cassandra-util @@ -525,103 +589,22 @@ executable galley-migrate-data if flag(static) ld-options: -static - default-language: GHC2021 - executable galley-schema import: common-all - main-is: ../main.hs - - -- cabal-fmt: expand schema/src - other-modules: - Run - V20 - V21 - V22 - V23 - V24 - V25 - V26 - V27 - V28 - V29 - V30 - V31 - V32 - V33 - V34 - V35 - V36 - V37 - V38_CreateTableBillingTeamMember - V39 - V40_CreateTableDataMigration - V41_TeamNotificationQueue - V42_TeamFeatureValidateSamlEmails - V43_TeamFeatureDigitalSignatures - V44_AddRemoteIdentifiers - V45_AddFederationIdMapping - V46_TeamFeatureAppLock - V47_RemoveFederationIdMapping - V48_DeleteRemoteIdentifiers - V49_ReAddRemoteIdentifiers - V50_AddLegalholdWhitelisted - V51_FeatureFileSharing - V52_FeatureConferenceCalling - V53_AddRemoteConvStatus - V54_TeamFeatureSelfDeletingMessages - V55_SelfDeletingMessagesLockStatus - V56_GuestLinksTeamFeatureStatus - V57_GuestLinksLockStatus - V58_ConversationAccessRoleV2 - V59_FileSharingLockStatus - V60_TeamFeatureSndFactorPasswordChallenge - V61_MLSConversation - V62_TeamFeatureSearchVisibilityInbound - V63_MLSConversationClients - V64_Epoch - V65_MLSRemoteClients - V66_AddSplashScreen - V67_MLSFeature - V68_MLSCommitLock - V69_MLSProposal - V70_MLSCipherSuite - V71_MemberClientKeypackage - V72_DropManagedConversations - V73_MemberClientTable - V74_ExposeInvitationsToTeamAdmin - V75_MLSGroupInfo - V76_ProposalOrigin - V77_MLSGroupMemberClient - V78_TeamFeatureOutlookCalIntegration - V79_TeamFeatureMlsE2EId - V80_AddConversationCodePassword - V81_TeamFeatureMlsE2EIdUpdate - V82_RemoteDomainIndexes - V83_CreateTableTeamAdmin - V84_MLSSubconversation - V85_MLSDraft17 - V86_TeamFeatureMlsMigration - V87_TeamFeatureSupportedProtocols - - hs-source-dirs: schema/src + main-is: Main.hs + hs-source-dirs: schema default-extensions: TemplateHaskell build-depends: - , base - , cassandra-util - , extended + , galley , imports - , optparse-applicative - , raw-strings-qq >=1.0 if flag(static) ld-options: -static - default-language: GHC2021 - test-suite galley-tests - import: common-all - type: exitcode-stdio-1.0 - main-is: ../unit.hs + import: common-all + type: exitcode-stdio-1.0 + main-is: ../unit.hs other-modules: Paths_galley Run @@ -631,8 +614,8 @@ test-suite galley-tests Test.Galley.Intra.User Test.Galley.Mapping - ghc-options: -threaded -with-rtsopts=-N - hs-source-dirs: test/unit + ghc-options: -threaded -with-rtsopts=-N + hs-source-dirs: test/unit build-depends: , base , containers @@ -651,5 +634,3 @@ test-suite galley-tests , uuid-types , wire-api , wire-api-federation - - default-language: GHC2021 diff --git a/services/galley/schema/main.hs b/services/galley/schema/Main.hs similarity index 52% rename from services/galley/schema/main.hs rename to services/galley/schema/Main.hs index d4037ab9cfa..6a175771138 100644 --- a/services/galley/schema/main.hs +++ b/services/galley/schema/Main.hs @@ -1,5 +1,5 @@ +import Galley.Schema.Run qualified as Run import Imports -import qualified Run main :: IO () main = Run.main diff --git a/services/galley/schema/src/Run.hs b/services/galley/schema/src/Run.hs deleted file mode 100644 index 6c134abcb33..00000000000 --- a/services/galley/schema/src/Run.hs +++ /dev/null @@ -1,182 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 Run where - -import Cassandra.Schema -import Control.Exception (finally) -import Imports -import Options.Applicative -import System.Logger.Extended qualified as Log -import V20 qualified -import V21 qualified -import V22 qualified -import V23 qualified -import V24 qualified -import V25 qualified -import V26 qualified -import V27 qualified -import V28 qualified -import V29 qualified -import V30 qualified -import V31 qualified -import V32 qualified -import V33 qualified -import V34 qualified -import V35 qualified -import V36 qualified -import V37 qualified -import V38_CreateTableBillingTeamMember qualified -import V39 qualified -import V40_CreateTableDataMigration qualified -import V41_TeamNotificationQueue qualified -import V42_TeamFeatureValidateSamlEmails qualified -import V43_TeamFeatureDigitalSignatures qualified -import V44_AddRemoteIdentifiers qualified -import V45_AddFederationIdMapping qualified -import V46_TeamFeatureAppLock qualified -import V47_RemoveFederationIdMapping qualified -import V48_DeleteRemoteIdentifiers qualified -import V49_ReAddRemoteIdentifiers qualified -import V50_AddLegalholdWhitelisted qualified -import V51_FeatureFileSharing qualified -import V52_FeatureConferenceCalling qualified -import V53_AddRemoteConvStatus qualified -import V54_TeamFeatureSelfDeletingMessages qualified -import V55_SelfDeletingMessagesLockStatus qualified -import V56_GuestLinksTeamFeatureStatus qualified -import V57_GuestLinksLockStatus qualified -import V58_ConversationAccessRoleV2 qualified -import V59_FileSharingLockStatus qualified -import V60_TeamFeatureSndFactorPasswordChallenge qualified -import V61_MLSConversation qualified -import V62_TeamFeatureSearchVisibilityInbound qualified -import V63_MLSConversationClients qualified -import V64_Epoch qualified -import V65_MLSRemoteClients qualified -import V66_AddSplashScreen qualified -import V67_MLSFeature qualified -import V68_MLSCommitLock qualified -import V69_MLSProposal qualified -import V70_MLSCipherSuite qualified -import V71_MemberClientKeypackage qualified -import V72_DropManagedConversations qualified -import V73_MemberClientTable qualified -import V74_ExposeInvitationsToTeamAdmin qualified -import V75_MLSGroupInfo qualified -import V76_ProposalOrigin qualified -import V77_MLSGroupMemberClient qualified -import V78_TeamFeatureOutlookCalIntegration qualified -import V79_TeamFeatureMlsE2EId qualified -import V80_AddConversationCodePassword qualified -import V81_TeamFeatureMlsE2EIdUpdate qualified -import V82_RemoteDomainIndexes qualified -import V83_CreateTableTeamAdmin qualified -import V84_MLSSubconversation qualified -import V85_MLSDraft17 qualified -import V86_TeamFeatureMlsMigration qualified -import V87_TeamFeatureSupportedProtocols qualified - -main :: IO () -main = do - o <- execParser (info (helper <*> migrationOptsParser) desc) - l <- Log.mkLogger' - migrateSchema - l - o - [ V20.migration, - V21.migration, - V22.migration, - V23.migration, - V24.migration, - V25.migration, - V26.migration, - V27.migration, - V28.migration, - V29.migration, - V30.migration, - V31.migration, - V32.migration, - V33.migration, - V34.migration, - V35.migration, - V36.migration, - V37.migration, - V38_CreateTableBillingTeamMember.migration, - V39.migration, - V40_CreateTableDataMigration.migration, - V41_TeamNotificationQueue.migration, - V42_TeamFeatureValidateSamlEmails.migration, - V43_TeamFeatureDigitalSignatures.migration, - V44_AddRemoteIdentifiers.migration, - V45_AddFederationIdMapping.migration, - V46_TeamFeatureAppLock.migration, - V47_RemoveFederationIdMapping.migration, - V48_DeleteRemoteIdentifiers.migration, - V49_ReAddRemoteIdentifiers.migration, - V50_AddLegalholdWhitelisted.migration, - V51_FeatureFileSharing.migration, - V52_FeatureConferenceCalling.migration, - V53_AddRemoteConvStatus.migration, - V54_TeamFeatureSelfDeletingMessages.migration, - V55_SelfDeletingMessagesLockStatus.migration, - V56_GuestLinksTeamFeatureStatus.migration, - V57_GuestLinksLockStatus.migration, - V58_ConversationAccessRoleV2.migration, - V59_FileSharingLockStatus.migration, - V60_TeamFeatureSndFactorPasswordChallenge.migration, - V61_MLSConversation.migration, - V62_TeamFeatureSearchVisibilityInbound.migration, - V63_MLSConversationClients.migration, - V64_Epoch.migration, - V65_MLSRemoteClients.migration, - V66_AddSplashScreen.migration, - V67_MLSFeature.migration, - V68_MLSCommitLock.migration, - V69_MLSProposal.migration, - V70_MLSCipherSuite.migration, - V71_MemberClientKeypackage.migration, - V72_DropManagedConversations.migration, - V73_MemberClientTable.migration, - V74_ExposeInvitationsToTeamAdmin.migration, - V75_MLSGroupInfo.migration, - V76_ProposalOrigin.migration, - V77_MLSGroupMemberClient.migration, - V78_TeamFeatureOutlookCalIntegration.migration, - V79_TeamFeatureMlsE2EId.migration, - V80_AddConversationCodePassword.migration, - V81_TeamFeatureMlsE2EIdUpdate.migration, - V82_RemoteDomainIndexes.migration, - V83_CreateTableTeamAdmin.migration, - V84_MLSSubconversation.migration, - V85_MLSDraft17.migration, - V86_TeamFeatureMlsMigration.migration, - V87_TeamFeatureSupportedProtocols.migration - -- When adding migrations here, don't forget to update - -- 'schemaVersion' in Galley.Cassandra - -- (see also docs/developer/cassandra-interaction.md) - -- - -- FUTUREWORK: once #1726 has made its way to master/production, - -- the 'message' field in connections table can be dropped. - -- See also https://github.com/wireapp/wire-server/pull/1747/files - -- for an explanation - -- FUTUREWORK: once #1751 has made its way to master/production, - -- the 'otr_muted' field in the member table can be dropped. - ] - `finally` Log.close l - where - desc = header "Galley Cassandra Schema" <> fullDesc diff --git a/services/galley/src/Galley/Cassandra.hs b/services/galley/src/Galley/Cassandra.hs index b3a7812e932..ab948beca05 100644 --- a/services/galley/src/Galley/Cassandra.hs +++ b/services/galley/src/Galley/Cassandra.hs @@ -17,7 +17,8 @@ module Galley.Cassandra (schemaVersion) where +import Galley.Schema.Run qualified as Migrations import Imports schemaVersion :: Int32 -schemaVersion = 87 +schemaVersion = Migrations.lastSchemaVersion diff --git a/services/galley/src/Galley/Schema/Run.hs b/services/galley/src/Galley/Schema/Run.hs new file mode 100644 index 00000000000..c24f6bb78d4 --- /dev/null +++ b/services/galley/src/Galley/Schema/Run.hs @@ -0,0 +1,185 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Galley.Schema.Run where + +import Cassandra.Schema +import Control.Exception (finally) +import Galley.Schema.V20 qualified as V20 +import Galley.Schema.V21 qualified as V21 +import Galley.Schema.V22 qualified as V22 +import Galley.Schema.V23 qualified as V23 +import Galley.Schema.V24 qualified as V24 +import Galley.Schema.V25 qualified as V25 +import Galley.Schema.V26 qualified as V26 +import Galley.Schema.V27 qualified as V27 +import Galley.Schema.V28 qualified as V28 +import Galley.Schema.V29 qualified as V29 +import Galley.Schema.V30 qualified as V30 +import Galley.Schema.V31 qualified as V31 +import Galley.Schema.V32 qualified as V32 +import Galley.Schema.V33 qualified as V33 +import Galley.Schema.V34 qualified as V34 +import Galley.Schema.V35 qualified as V35 +import Galley.Schema.V36 qualified as V36 +import Galley.Schema.V37 qualified as V37 +import Galley.Schema.V38_CreateTableBillingTeamMember qualified as V38_CreateTableBillingTeamMember +import Galley.Schema.V39 qualified as V39 +import Galley.Schema.V40_CreateTableDataMigration qualified as V40_CreateTableDataMigration +import Galley.Schema.V41_TeamNotificationQueue qualified as V41_TeamNotificationQueue +import Galley.Schema.V42_TeamFeatureValidateSamlEmails qualified as V42_TeamFeatureValidateSamlEmails +import Galley.Schema.V43_TeamFeatureDigitalSignatures qualified as V43_TeamFeatureDigitalSignatures +import Galley.Schema.V44_AddRemoteIdentifiers qualified as V44_AddRemoteIdentifiers +import Galley.Schema.V45_AddFederationIdMapping qualified as V45_AddFederationIdMapping +import Galley.Schema.V46_TeamFeatureAppLock qualified as V46_TeamFeatureAppLock +import Galley.Schema.V47_RemoveFederationIdMapping qualified as V47_RemoveFederationIdMapping +import Galley.Schema.V48_DeleteRemoteIdentifiers qualified as V48_DeleteRemoteIdentifiers +import Galley.Schema.V49_ReAddRemoteIdentifiers qualified as V49_ReAddRemoteIdentifiers +import Galley.Schema.V50_AddLegalholdWhitelisted qualified as V50_AddLegalholdWhitelisted +import Galley.Schema.V51_FeatureFileSharing qualified as V51_FeatureFileSharing +import Galley.Schema.V52_FeatureConferenceCalling qualified as V52_FeatureConferenceCalling +import Galley.Schema.V53_AddRemoteConvStatus qualified as V53_AddRemoteConvStatus +import Galley.Schema.V54_TeamFeatureSelfDeletingMessages qualified as V54_TeamFeatureSelfDeletingMessages +import Galley.Schema.V55_SelfDeletingMessagesLockStatus qualified as V55_SelfDeletingMessagesLockStatus +import Galley.Schema.V56_GuestLinksTeamFeatureStatus qualified as V56_GuestLinksTeamFeatureStatus +import Galley.Schema.V57_GuestLinksLockStatus qualified as V57_GuestLinksLockStatus +import Galley.Schema.V58_ConversationAccessRoleV2 qualified as V58_ConversationAccessRoleV2 +import Galley.Schema.V59_FileSharingLockStatus qualified as V59_FileSharingLockStatus +import Galley.Schema.V60_TeamFeatureSndFactorPasswordChallenge qualified as V60_TeamFeatureSndFactorPasswordChallenge +import Galley.Schema.V61_MLSConversation qualified as V61_MLSConversation +import Galley.Schema.V62_TeamFeatureSearchVisibilityInbound qualified as V62_TeamFeatureSearchVisibilityInbound +import Galley.Schema.V63_MLSConversationClients qualified as V63_MLSConversationClients +import Galley.Schema.V64_Epoch qualified as V64_Epoch +import Galley.Schema.V65_MLSRemoteClients qualified as V65_MLSRemoteClients +import Galley.Schema.V66_AddSplashScreen qualified as V66_AddSplashScreen +import Galley.Schema.V67_MLSFeature qualified as V67_MLSFeature +import Galley.Schema.V68_MLSCommitLock qualified as V68_MLSCommitLock +import Galley.Schema.V69_MLSProposal qualified as V69_MLSProposal +import Galley.Schema.V70_MLSCipherSuite qualified as V70_MLSCipherSuite +import Galley.Schema.V71_MemberClientKeypackage qualified as V71_MemberClientKeypackage +import Galley.Schema.V72_DropManagedConversations qualified as V72_DropManagedConversations +import Galley.Schema.V73_MemberClientTable qualified as V73_MemberClientTable +import Galley.Schema.V74_ExposeInvitationsToTeamAdmin qualified as V74_ExposeInvitationsToTeamAdmin +import Galley.Schema.V75_MLSGroupInfo qualified as V75_MLSGroupInfo +import Galley.Schema.V76_ProposalOrigin qualified as V76_ProposalOrigin +import Galley.Schema.V77_MLSGroupMemberClient qualified as V77_MLSGroupMemberClient +import Galley.Schema.V78_TeamFeatureOutlookCalIntegration qualified as V78_TeamFeatureOutlookCalIntegration +import Galley.Schema.V79_TeamFeatureMlsE2EId qualified as V79_TeamFeatureMlsE2EId +import Galley.Schema.V80_AddConversationCodePassword qualified as V80_AddConversationCodePassword +import Galley.Schema.V81_TeamFeatureMlsE2EIdUpdate qualified as V81_TeamFeatureMlsE2EIdUpdate +import Galley.Schema.V82_RemoteDomainIndexes qualified as V82_RemoteDomainIndexes +import Galley.Schema.V83_CreateTableTeamAdmin qualified as V83_CreateTableTeamAdmin +import Galley.Schema.V84_MLSSubconversation qualified as V84_MLSSubconversation +import Galley.Schema.V85_MLSDraft17 qualified as V85_MLSDraft17 +import Galley.Schema.V86_TeamFeatureMlsMigration qualified as V86_TeamFeatureMlsMigration +import Galley.Schema.V87_TeamFeatureSupportedProtocols qualified as V87_TeamFeatureSupportedProtocols +import Imports +import Options.Applicative +import System.Logger.Extended qualified as Log + +main :: IO () +main = do + o <- execParser (info (helper <*> migrationOptsParser) desc) + l <- Log.mkLogger' + migrateSchema + l + o + migrations + `finally` Log.close l + where + desc = header "Galley Cassandra Schema" <> fullDesc + +lastSchemaVersion :: Int32 +lastSchemaVersion = migVersion $ last migrations + +migrations :: [Migration] +migrations = + [ V20.migration, + V21.migration, + V22.migration, + V23.migration, + V24.migration, + V25.migration, + V26.migration, + V27.migration, + V28.migration, + V29.migration, + V30.migration, + V31.migration, + V32.migration, + V33.migration, + V34.migration, + V35.migration, + V36.migration, + V37.migration, + V38_CreateTableBillingTeamMember.migration, + V39.migration, + V40_CreateTableDataMigration.migration, + V41_TeamNotificationQueue.migration, + V42_TeamFeatureValidateSamlEmails.migration, + V43_TeamFeatureDigitalSignatures.migration, + V44_AddRemoteIdentifiers.migration, + V45_AddFederationIdMapping.migration, + V46_TeamFeatureAppLock.migration, + V47_RemoveFederationIdMapping.migration, + V48_DeleteRemoteIdentifiers.migration, + V49_ReAddRemoteIdentifiers.migration, + V50_AddLegalholdWhitelisted.migration, + V51_FeatureFileSharing.migration, + V52_FeatureConferenceCalling.migration, + V53_AddRemoteConvStatus.migration, + V54_TeamFeatureSelfDeletingMessages.migration, + V55_SelfDeletingMessagesLockStatus.migration, + V56_GuestLinksTeamFeatureStatus.migration, + V57_GuestLinksLockStatus.migration, + V58_ConversationAccessRoleV2.migration, + V59_FileSharingLockStatus.migration, + V60_TeamFeatureSndFactorPasswordChallenge.migration, + V61_MLSConversation.migration, + V62_TeamFeatureSearchVisibilityInbound.migration, + V63_MLSConversationClients.migration, + V64_Epoch.migration, + V65_MLSRemoteClients.migration, + V66_AddSplashScreen.migration, + V67_MLSFeature.migration, + V68_MLSCommitLock.migration, + V69_MLSProposal.migration, + V70_MLSCipherSuite.migration, + V71_MemberClientKeypackage.migration, + V72_DropManagedConversations.migration, + V73_MemberClientTable.migration, + V74_ExposeInvitationsToTeamAdmin.migration, + V75_MLSGroupInfo.migration, + V76_ProposalOrigin.migration, + V77_MLSGroupMemberClient.migration, + V78_TeamFeatureOutlookCalIntegration.migration, + V79_TeamFeatureMlsE2EId.migration, + V80_AddConversationCodePassword.migration, + V81_TeamFeatureMlsE2EIdUpdate.migration, + V82_RemoteDomainIndexes.migration, + V83_CreateTableTeamAdmin.migration, + V84_MLSSubconversation.migration, + V85_MLSDraft17.migration, + V86_TeamFeatureMlsMigration.migration, + V87_TeamFeatureSupportedProtocols.migration + -- FUTUREWORK: once #1726 has made its way to master/production, + -- the 'message' field in connections table can be dropped. + -- See also https://github.com/wireapp/wire-server/pull/1747/files + -- for an explanation + -- FUTUREWORK: once #1751 has made its way to master/production, + -- the 'otr_muted' field in the member table can be dropped. + ] diff --git a/services/galley/schema/src/V20.hs b/services/galley/src/Galley/Schema/V20.hs similarity index 99% rename from services/galley/schema/src/V20.hs rename to services/galley/src/Galley/Schema/V20.hs index a7a0432e58d..2e7db8dd359 100644 --- a/services/galley/schema/src/V20.hs +++ b/services/galley/src/Galley/Schema/V20.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V20 +module Galley.Schema.V20 ( migration, ) where diff --git a/services/galley/schema/src/V21.hs b/services/galley/src/Galley/Schema/V21.hs similarity index 99% rename from services/galley/schema/src/V21.hs rename to services/galley/src/Galley/Schema/V21.hs index 73c38cd9c58..0e9160fa550 100644 --- a/services/galley/schema/src/V21.hs +++ b/services/galley/src/Galley/Schema/V21.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V21 +module Galley.Schema.V21 ( migration, ) where diff --git a/services/galley/schema/src/V22.hs b/services/galley/src/Galley/Schema/V22.hs similarity index 97% rename from services/galley/schema/src/V22.hs rename to services/galley/src/Galley/Schema/V22.hs index fcf8b5a7a7f..313de020a1a 100644 --- a/services/galley/schema/src/V22.hs +++ b/services/galley/src/Galley/Schema/V22.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V22 +module Galley.Schema.V22 ( migration, ) where diff --git a/services/galley/schema/src/V23.hs b/services/galley/src/Galley/Schema/V23.hs similarity index 97% rename from services/galley/schema/src/V23.hs rename to services/galley/src/Galley/Schema/V23.hs index 364f1e801de..2eed13f3c30 100644 --- a/services/galley/schema/src/V23.hs +++ b/services/galley/src/Galley/Schema/V23.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V23 +module Galley.Schema.V23 ( migration, ) where diff --git a/services/galley/schema/src/V24.hs b/services/galley/src/Galley/Schema/V24.hs similarity index 97% rename from services/galley/schema/src/V24.hs rename to services/galley/src/Galley/Schema/V24.hs index c07a7732b83..7ace99b3b71 100644 --- a/services/galley/schema/src/V24.hs +++ b/services/galley/src/Galley/Schema/V24.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V24 +module Galley.Schema.V24 ( migration, ) where diff --git a/services/galley/schema/src/V25.hs b/services/galley/src/Galley/Schema/V25.hs similarity index 98% rename from services/galley/schema/src/V25.hs rename to services/galley/src/Galley/Schema/V25.hs index 2dbec4d4268..420afffec0a 100644 --- a/services/galley/schema/src/V25.hs +++ b/services/galley/src/Galley/Schema/V25.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V25 +module Galley.Schema.V25 ( migration, ) where diff --git a/services/galley/schema/src/V26.hs b/services/galley/src/Galley/Schema/V26.hs similarity index 97% rename from services/galley/schema/src/V26.hs rename to services/galley/src/Galley/Schema/V26.hs index 0e3e89b0ff4..8dc98b98fa1 100644 --- a/services/galley/schema/src/V26.hs +++ b/services/galley/src/Galley/Schema/V26.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V26 +module Galley.Schema.V26 ( migration, ) where diff --git a/services/galley/schema/src/V27.hs b/services/galley/src/Galley/Schema/V27.hs similarity index 97% rename from services/galley/schema/src/V27.hs rename to services/galley/src/Galley/Schema/V27.hs index 178c774cd94..096f210ed6e 100644 --- a/services/galley/schema/src/V27.hs +++ b/services/galley/src/Galley/Schema/V27.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V27 +module Galley.Schema.V27 ( migration, ) where diff --git a/services/galley/schema/src/V28.hs b/services/galley/src/Galley/Schema/V28.hs similarity index 97% rename from services/galley/schema/src/V28.hs rename to services/galley/src/Galley/Schema/V28.hs index 5e125e44d0a..959dee20c7f 100644 --- a/services/galley/schema/src/V28.hs +++ b/services/galley/src/Galley/Schema/V28.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V28 +module Galley.Schema.V28 ( migration, ) where diff --git a/services/galley/schema/src/V29.hs b/services/galley/src/Galley/Schema/V29.hs similarity index 97% rename from services/galley/schema/src/V29.hs rename to services/galley/src/Galley/Schema/V29.hs index b3bbfebac3b..a30664fdc2f 100644 --- a/services/galley/schema/src/V29.hs +++ b/services/galley/src/Galley/Schema/V29.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V29 +module Galley.Schema.V29 ( migration, ) where diff --git a/services/galley/schema/src/V30.hs b/services/galley/src/Galley/Schema/V30.hs similarity index 97% rename from services/galley/schema/src/V30.hs rename to services/galley/src/Galley/Schema/V30.hs index b11c8d3ee24..83f0ce3e372 100644 --- a/services/galley/schema/src/V30.hs +++ b/services/galley/src/Galley/Schema/V30.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V30 +module Galley.Schema.V30 ( migration, ) where diff --git a/services/galley/schema/src/V31.hs b/services/galley/src/Galley/Schema/V31.hs similarity index 98% rename from services/galley/schema/src/V31.hs rename to services/galley/src/Galley/Schema/V31.hs index b41a320f609..6c63e4ee175 100644 --- a/services/galley/schema/src/V31.hs +++ b/services/galley/src/Galley/Schema/V31.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V31 +module Galley.Schema.V31 ( migration, ) where diff --git a/services/galley/schema/src/V32.hs b/services/galley/src/Galley/Schema/V32.hs similarity index 97% rename from services/galley/schema/src/V32.hs rename to services/galley/src/Galley/Schema/V32.hs index 9ecccedcde0..21c23ac466e 100644 --- a/services/galley/schema/src/V32.hs +++ b/services/galley/src/Galley/Schema/V32.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V32 +module Galley.Schema.V32 ( migration, ) where diff --git a/services/galley/schema/src/V33.hs b/services/galley/src/Galley/Schema/V33.hs similarity index 98% rename from services/galley/schema/src/V33.hs rename to services/galley/src/Galley/Schema/V33.hs index 9f97a7af4a0..1445f5c4039 100644 --- a/services/galley/schema/src/V33.hs +++ b/services/galley/src/Galley/Schema/V33.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V33 +module Galley.Schema.V33 ( migration, ) where diff --git a/services/galley/schema/src/V34.hs b/services/galley/src/Galley/Schema/V34.hs similarity index 98% rename from services/galley/schema/src/V34.hs rename to services/galley/src/Galley/Schema/V34.hs index 57d98b84773..b87b5427898 100644 --- a/services/galley/schema/src/V34.hs +++ b/services/galley/src/Galley/Schema/V34.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V34 +module Galley.Schema.V34 ( migration, ) where diff --git a/services/galley/schema/src/V35.hs b/services/galley/src/Galley/Schema/V35.hs similarity index 97% rename from services/galley/schema/src/V35.hs rename to services/galley/src/Galley/Schema/V35.hs index 5b42b57fa2e..6b397865f69 100644 --- a/services/galley/schema/src/V35.hs +++ b/services/galley/src/Galley/Schema/V35.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V35 +module Galley.Schema.V35 ( migration, ) where diff --git a/services/galley/schema/src/V36.hs b/services/galley/src/Galley/Schema/V36.hs similarity index 97% rename from services/galley/schema/src/V36.hs rename to services/galley/src/Galley/Schema/V36.hs index 82c9c046993..19f893134e5 100644 --- a/services/galley/schema/src/V36.hs +++ b/services/galley/src/Galley/Schema/V36.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V36 +module Galley.Schema.V36 ( migration, ) where diff --git a/services/galley/schema/src/V37.hs b/services/galley/src/Galley/Schema/V37.hs similarity index 97% rename from services/galley/schema/src/V37.hs rename to services/galley/src/Galley/Schema/V37.hs index 214f1ef4e79..295b507bd29 100644 --- a/services/galley/schema/src/V37.hs +++ b/services/galley/src/Galley/Schema/V37.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V37 +module Galley.Schema.V37 ( migration, ) where diff --git a/services/galley/schema/src/V38_CreateTableBillingTeamMember.hs b/services/galley/src/Galley/Schema/V38_CreateTableBillingTeamMember.hs similarity index 95% rename from services/galley/schema/src/V38_CreateTableBillingTeamMember.hs rename to services/galley/src/Galley/Schema/V38_CreateTableBillingTeamMember.hs index eb03f182c34..2b28f6b418c 100644 --- a/services/galley/schema/src/V38_CreateTableBillingTeamMember.hs +++ b/services/galley/src/Galley/Schema/V38_CreateTableBillingTeamMember.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V38_CreateTableBillingTeamMember +module Galley.Schema.V38_CreateTableBillingTeamMember ( migration, ) where diff --git a/services/galley/schema/src/V39.hs b/services/galley/src/Galley/Schema/V39.hs similarity index 97% rename from services/galley/schema/src/V39.hs rename to services/galley/src/Galley/Schema/V39.hs index a0e6d85bb2c..66ff7bd5ed0 100644 --- a/services/galley/schema/src/V39.hs +++ b/services/galley/src/Galley/Schema/V39.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V39 +module Galley.Schema.V39 ( migration, ) where diff --git a/services/galley/schema/src/V40_CreateTableDataMigration.hs b/services/galley/src/Galley/Schema/V40_CreateTableDataMigration.hs similarity index 94% rename from services/galley/schema/src/V40_CreateTableDataMigration.hs rename to services/galley/src/Galley/Schema/V40_CreateTableDataMigration.hs index 7d3c0a1f6bc..cef66944912 100644 --- a/services/galley/schema/src/V40_CreateTableDataMigration.hs +++ b/services/galley/src/Galley/Schema/V40_CreateTableDataMigration.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V40_CreateTableDataMigration (migration) where +module Galley.Schema.V40_CreateTableDataMigration (migration) where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V41_TeamNotificationQueue.hs b/services/galley/src/Galley/Schema/V41_TeamNotificationQueue.hs similarity index 96% rename from services/galley/schema/src/V41_TeamNotificationQueue.hs rename to services/galley/src/Galley/Schema/V41_TeamNotificationQueue.hs index cde9f8c0935..4a195d1a90e 100644 --- a/services/galley/schema/src/V41_TeamNotificationQueue.hs +++ b/services/galley/src/Galley/Schema/V41_TeamNotificationQueue.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V41_TeamNotificationQueue +module Galley.Schema.V41_TeamNotificationQueue ( migration, ) where diff --git a/services/galley/schema/src/V42_TeamFeatureValidateSamlEmails.hs b/services/galley/src/Galley/Schema/V42_TeamFeatureValidateSamlEmails.hs similarity index 95% rename from services/galley/schema/src/V42_TeamFeatureValidateSamlEmails.hs rename to services/galley/src/Galley/Schema/V42_TeamFeatureValidateSamlEmails.hs index 2232fdeffef..3c5da6ca73a 100644 --- a/services/galley/schema/src/V42_TeamFeatureValidateSamlEmails.hs +++ b/services/galley/src/Galley/Schema/V42_TeamFeatureValidateSamlEmails.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V42_TeamFeatureValidateSamlEmails +module Galley.Schema.V42_TeamFeatureValidateSamlEmails ( migration, ) where diff --git a/services/galley/schema/src/V43_TeamFeatureDigitalSignatures.hs b/services/galley/src/Galley/Schema/V43_TeamFeatureDigitalSignatures.hs similarity index 95% rename from services/galley/schema/src/V43_TeamFeatureDigitalSignatures.hs rename to services/galley/src/Galley/Schema/V43_TeamFeatureDigitalSignatures.hs index 701396655aa..d938a803c66 100644 --- a/services/galley/schema/src/V43_TeamFeatureDigitalSignatures.hs +++ b/services/galley/src/Galley/Schema/V43_TeamFeatureDigitalSignatures.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V43_TeamFeatureDigitalSignatures +module Galley.Schema.V43_TeamFeatureDigitalSignatures ( migration, ) where diff --git a/services/galley/schema/src/V44_AddRemoteIdentifiers.hs b/services/galley/src/Galley/Schema/V44_AddRemoteIdentifiers.hs similarity index 96% rename from services/galley/schema/src/V44_AddRemoteIdentifiers.hs rename to services/galley/src/Galley/Schema/V44_AddRemoteIdentifiers.hs index 07eae794270..b4130f9129b 100644 --- a/services/galley/schema/src/V44_AddRemoteIdentifiers.hs +++ b/services/galley/src/Galley/Schema/V44_AddRemoteIdentifiers.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V44_AddRemoteIdentifiers (migration) where +module Galley.Schema.V44_AddRemoteIdentifiers (migration) where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V45_AddFederationIdMapping.hs b/services/galley/src/Galley/Schema/V45_AddFederationIdMapping.hs similarity index 96% rename from services/galley/schema/src/V45_AddFederationIdMapping.hs rename to services/galley/src/Galley/Schema/V45_AddFederationIdMapping.hs index 842aaae4144..0dac30d1ab7 100644 --- a/services/galley/schema/src/V45_AddFederationIdMapping.hs +++ b/services/galley/src/Galley/Schema/V45_AddFederationIdMapping.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V45_AddFederationIdMapping +module Galley.Schema.V45_AddFederationIdMapping ( migration, ) where diff --git a/services/galley/schema/src/V46_TeamFeatureAppLock.hs b/services/galley/src/Galley/Schema/V46_TeamFeatureAppLock.hs similarity index 96% rename from services/galley/schema/src/V46_TeamFeatureAppLock.hs rename to services/galley/src/Galley/Schema/V46_TeamFeatureAppLock.hs index b8ec3c03f81..20e7f98bc22 100644 --- a/services/galley/schema/src/V46_TeamFeatureAppLock.hs +++ b/services/galley/src/Galley/Schema/V46_TeamFeatureAppLock.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V46_TeamFeatureAppLock +module Galley.Schema.V46_TeamFeatureAppLock ( migration, ) where diff --git a/services/galley/schema/src/V47_RemoveFederationIdMapping.hs b/services/galley/src/Galley/Schema/V47_RemoveFederationIdMapping.hs similarity index 95% rename from services/galley/schema/src/V47_RemoveFederationIdMapping.hs rename to services/galley/src/Galley/Schema/V47_RemoveFederationIdMapping.hs index 4f8e0b5a9e0..2cc5a6f530a 100644 --- a/services/galley/schema/src/V47_RemoveFederationIdMapping.hs +++ b/services/galley/src/Galley/Schema/V47_RemoveFederationIdMapping.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V47_RemoveFederationIdMapping +module Galley.Schema.V47_RemoveFederationIdMapping ( migration, ) where diff --git a/services/galley/schema/src/V48_DeleteRemoteIdentifiers.hs b/services/galley/src/Galley/Schema/V48_DeleteRemoteIdentifiers.hs similarity index 96% rename from services/galley/schema/src/V48_DeleteRemoteIdentifiers.hs rename to services/galley/src/Galley/Schema/V48_DeleteRemoteIdentifiers.hs index 3268147ffe8..22ff43ec591 100644 --- a/services/galley/schema/src/V48_DeleteRemoteIdentifiers.hs +++ b/services/galley/src/Galley/Schema/V48_DeleteRemoteIdentifiers.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V48_DeleteRemoteIdentifiers +module Galley.Schema.V48_DeleteRemoteIdentifiers ( migration, ) where diff --git a/services/galley/schema/src/V49_ReAddRemoteIdentifiers.hs b/services/galley/src/Galley/Schema/V49_ReAddRemoteIdentifiers.hs similarity index 98% rename from services/galley/schema/src/V49_ReAddRemoteIdentifiers.hs rename to services/galley/src/Galley/Schema/V49_ReAddRemoteIdentifiers.hs index 91748953c54..946904ccad9 100644 --- a/services/galley/schema/src/V49_ReAddRemoteIdentifiers.hs +++ b/services/galley/src/Galley/Schema/V49_ReAddRemoteIdentifiers.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V49_ReAddRemoteIdentifiers +module Galley.Schema.V49_ReAddRemoteIdentifiers ( migration, ) where diff --git a/services/galley/schema/src/V50_AddLegalholdWhitelisted.hs b/services/galley/src/Galley/Schema/V50_AddLegalholdWhitelisted.hs similarity index 95% rename from services/galley/schema/src/V50_AddLegalholdWhitelisted.hs rename to services/galley/src/Galley/Schema/V50_AddLegalholdWhitelisted.hs index a7111849a06..dae85e18b5d 100644 --- a/services/galley/schema/src/V50_AddLegalholdWhitelisted.hs +++ b/services/galley/src/Galley/Schema/V50_AddLegalholdWhitelisted.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V50_AddLegalholdWhitelisted +module Galley.Schema.V50_AddLegalholdWhitelisted ( migration, ) where diff --git a/services/galley/schema/src/V51_FeatureFileSharing.hs b/services/galley/src/Galley/Schema/V51_FeatureFileSharing.hs similarity index 95% rename from services/galley/schema/src/V51_FeatureFileSharing.hs rename to services/galley/src/Galley/Schema/V51_FeatureFileSharing.hs index 734ef96280a..ae46aeda94d 100644 --- a/services/galley/schema/src/V51_FeatureFileSharing.hs +++ b/services/galley/src/Galley/Schema/V51_FeatureFileSharing.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V51_FeatureFileSharing +module Galley.Schema.V51_FeatureFileSharing ( migration, ) where diff --git a/services/galley/schema/src/V52_FeatureConferenceCalling.hs b/services/galley/src/Galley/Schema/V52_FeatureConferenceCalling.hs similarity index 95% rename from services/galley/schema/src/V52_FeatureConferenceCalling.hs rename to services/galley/src/Galley/Schema/V52_FeatureConferenceCalling.hs index 78e4d9d5f1a..a3c37a8eb52 100644 --- a/services/galley/schema/src/V52_FeatureConferenceCalling.hs +++ b/services/galley/src/Galley/Schema/V52_FeatureConferenceCalling.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V52_FeatureConferenceCalling +module Galley.Schema.V52_FeatureConferenceCalling ( migration, ) where diff --git a/services/galley/schema/src/V53_AddRemoteConvStatus.hs b/services/galley/src/Galley/Schema/V53_AddRemoteConvStatus.hs similarity index 95% rename from services/galley/schema/src/V53_AddRemoteConvStatus.hs rename to services/galley/src/Galley/Schema/V53_AddRemoteConvStatus.hs index 2db8a204b00..674a14273df 100644 --- a/services/galley/schema/src/V53_AddRemoteConvStatus.hs +++ b/services/galley/src/Galley/Schema/V53_AddRemoteConvStatus.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V53_AddRemoteConvStatus (migration) where +module Galley.Schema.V53_AddRemoteConvStatus (migration) where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V54_TeamFeatureSelfDeletingMessages.hs b/services/galley/src/Galley/Schema/V54_TeamFeatureSelfDeletingMessages.hs similarity index 95% rename from services/galley/schema/src/V54_TeamFeatureSelfDeletingMessages.hs rename to services/galley/src/Galley/Schema/V54_TeamFeatureSelfDeletingMessages.hs index ab36ce3cb99..17bfa279795 100644 --- a/services/galley/schema/src/V54_TeamFeatureSelfDeletingMessages.hs +++ b/services/galley/src/Galley/Schema/V54_TeamFeatureSelfDeletingMessages.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V54_TeamFeatureSelfDeletingMessages +module Galley.Schema.V54_TeamFeatureSelfDeletingMessages ( migration, ) where diff --git a/services/galley/schema/src/V55_SelfDeletingMessagesLockStatus.hs b/services/galley/src/Galley/Schema/V55_SelfDeletingMessagesLockStatus.hs similarity index 95% rename from services/galley/schema/src/V55_SelfDeletingMessagesLockStatus.hs rename to services/galley/src/Galley/Schema/V55_SelfDeletingMessagesLockStatus.hs index 888fde95d81..12094c97e11 100644 --- a/services/galley/schema/src/V55_SelfDeletingMessagesLockStatus.hs +++ b/services/galley/src/Galley/Schema/V55_SelfDeletingMessagesLockStatus.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V55_SelfDeletingMessagesLockStatus +module Galley.Schema.V55_SelfDeletingMessagesLockStatus ( migration, ) where diff --git a/services/galley/schema/src/V56_GuestLinksTeamFeatureStatus.hs b/services/galley/src/Galley/Schema/V56_GuestLinksTeamFeatureStatus.hs similarity index 95% rename from services/galley/schema/src/V56_GuestLinksTeamFeatureStatus.hs rename to services/galley/src/Galley/Schema/V56_GuestLinksTeamFeatureStatus.hs index d8b07ed2764..d1341779ff3 100644 --- a/services/galley/schema/src/V56_GuestLinksTeamFeatureStatus.hs +++ b/services/galley/src/Galley/Schema/V56_GuestLinksTeamFeatureStatus.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V56_GuestLinksTeamFeatureStatus +module Galley.Schema.V56_GuestLinksTeamFeatureStatus ( migration, ) where diff --git a/services/galley/schema/src/V57_GuestLinksLockStatus.hs b/services/galley/src/Galley/Schema/V57_GuestLinksLockStatus.hs similarity index 95% rename from services/galley/schema/src/V57_GuestLinksLockStatus.hs rename to services/galley/src/Galley/Schema/V57_GuestLinksLockStatus.hs index f347e3de81e..385d4c31456 100644 --- a/services/galley/schema/src/V57_GuestLinksLockStatus.hs +++ b/services/galley/src/Galley/Schema/V57_GuestLinksLockStatus.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V57_GuestLinksLockStatus +module Galley.Schema.V57_GuestLinksLockStatus ( migration, ) where diff --git a/services/galley/schema/src/V58_ConversationAccessRoleV2.hs b/services/galley/src/Galley/Schema/V58_ConversationAccessRoleV2.hs similarity index 95% rename from services/galley/schema/src/V58_ConversationAccessRoleV2.hs rename to services/galley/src/Galley/Schema/V58_ConversationAccessRoleV2.hs index a477e9b152d..7c7e2f2c175 100644 --- a/services/galley/schema/src/V58_ConversationAccessRoleV2.hs +++ b/services/galley/src/Galley/Schema/V58_ConversationAccessRoleV2.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V58_ConversationAccessRoleV2 +module Galley.Schema.V58_ConversationAccessRoleV2 ( migration, ) where diff --git a/services/galley/schema/src/V59_FileSharingLockStatus.hs b/services/galley/src/Galley/Schema/V59_FileSharingLockStatus.hs similarity index 95% rename from services/galley/schema/src/V59_FileSharingLockStatus.hs rename to services/galley/src/Galley/Schema/V59_FileSharingLockStatus.hs index d1b83924828..2064df5c938 100644 --- a/services/galley/schema/src/V59_FileSharingLockStatus.hs +++ b/services/galley/src/Galley/Schema/V59_FileSharingLockStatus.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V59_FileSharingLockStatus +module Galley.Schema.V59_FileSharingLockStatus ( migration, ) where diff --git a/services/galley/schema/src/V60_TeamFeatureSndFactorPasswordChallenge.hs b/services/galley/src/Galley/Schema/V60_TeamFeatureSndFactorPasswordChallenge.hs similarity index 94% rename from services/galley/schema/src/V60_TeamFeatureSndFactorPasswordChallenge.hs rename to services/galley/src/Galley/Schema/V60_TeamFeatureSndFactorPasswordChallenge.hs index f71ab44871f..bcfdce48328 100644 --- a/services/galley/schema/src/V60_TeamFeatureSndFactorPasswordChallenge.hs +++ b/services/galley/src/Galley/Schema/V60_TeamFeatureSndFactorPasswordChallenge.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V60_TeamFeatureSndFactorPasswordChallenge +module Galley.Schema.V60_TeamFeatureSndFactorPasswordChallenge ( migration, ) where diff --git a/services/galley/schema/src/V61_MLSConversation.hs b/services/galley/src/Galley/Schema/V61_MLSConversation.hs similarity index 96% rename from services/galley/schema/src/V61_MLSConversation.hs rename to services/galley/src/Galley/Schema/V61_MLSConversation.hs index 7d7c06af66a..97673bdf309 100644 --- a/services/galley/schema/src/V61_MLSConversation.hs +++ b/services/galley/src/Galley/Schema/V61_MLSConversation.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V61_MLSConversation +module Galley.Schema.V61_MLSConversation ( migration, ) where diff --git a/services/galley/schema/src/V62_TeamFeatureSearchVisibilityInbound.hs b/services/galley/src/Galley/Schema/V62_TeamFeatureSearchVisibilityInbound.hs similarity index 94% rename from services/galley/schema/src/V62_TeamFeatureSearchVisibilityInbound.hs rename to services/galley/src/Galley/Schema/V62_TeamFeatureSearchVisibilityInbound.hs index c2112899ca7..6bc46dd5cbf 100644 --- a/services/galley/schema/src/V62_TeamFeatureSearchVisibilityInbound.hs +++ b/services/galley/src/Galley/Schema/V62_TeamFeatureSearchVisibilityInbound.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V62_TeamFeatureSearchVisibilityInbound +module Galley.Schema.V62_TeamFeatureSearchVisibilityInbound ( migration, ) where diff --git a/services/galley/schema/src/V63_MLSConversationClients.hs b/services/galley/src/Galley/Schema/V63_MLSConversationClients.hs similarity index 95% rename from services/galley/schema/src/V63_MLSConversationClients.hs rename to services/galley/src/Galley/Schema/V63_MLSConversationClients.hs index 4b3a80c350b..1a82ab231b6 100644 --- a/services/galley/schema/src/V63_MLSConversationClients.hs +++ b/services/galley/src/Galley/Schema/V63_MLSConversationClients.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V63_MLSConversationClients where +module Galley.Schema.V63_MLSConversationClients where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V64_Epoch.hs b/services/galley/src/Galley/Schema/V64_Epoch.hs similarity index 97% rename from services/galley/schema/src/V64_Epoch.hs rename to services/galley/src/Galley/Schema/V64_Epoch.hs index 7bec37d3d2b..70cd8e8e617 100644 --- a/services/galley/schema/src/V64_Epoch.hs +++ b/services/galley/src/Galley/Schema/V64_Epoch.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V64_Epoch +module Galley.Schema.V64_Epoch ( migration, ) where diff --git a/services/galley/schema/src/V65_MLSRemoteClients.hs b/services/galley/src/Galley/Schema/V65_MLSRemoteClients.hs similarity index 95% rename from services/galley/schema/src/V65_MLSRemoteClients.hs rename to services/galley/src/Galley/Schema/V65_MLSRemoteClients.hs index c772b84ec45..c2a7deb9795 100644 --- a/services/galley/schema/src/V65_MLSRemoteClients.hs +++ b/services/galley/src/Galley/Schema/V65_MLSRemoteClients.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V65_MLSRemoteClients where +module Galley.Schema.V65_MLSRemoteClients where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V66_AddSplashScreen.hs b/services/galley/src/Galley/Schema/V66_AddSplashScreen.hs similarity index 95% rename from services/galley/schema/src/V66_AddSplashScreen.hs rename to services/galley/src/Galley/Schema/V66_AddSplashScreen.hs index 04fc5bb90c0..b5e9c31c327 100644 --- a/services/galley/schema/src/V66_AddSplashScreen.hs +++ b/services/galley/src/Galley/Schema/V66_AddSplashScreen.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V66_AddSplashScreen where +module Galley.Schema.V66_AddSplashScreen where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V67_MLSFeature.hs b/services/galley/src/Galley/Schema/V67_MLSFeature.hs similarity index 96% rename from services/galley/schema/src/V67_MLSFeature.hs rename to services/galley/src/Galley/Schema/V67_MLSFeature.hs index b3c5a3066a0..5391d1967ed 100644 --- a/services/galley/schema/src/V67_MLSFeature.hs +++ b/services/galley/src/Galley/Schema/V67_MLSFeature.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V67_MLSFeature where +module Galley.Schema.V67_MLSFeature where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V68_MLSCommitLock.hs b/services/galley/src/Galley/Schema/V68_MLSCommitLock.hs similarity index 96% rename from services/galley/schema/src/V68_MLSCommitLock.hs rename to services/galley/src/Galley/Schema/V68_MLSCommitLock.hs index 33edb236735..24380e5d914 100644 --- a/services/galley/schema/src/V68_MLSCommitLock.hs +++ b/services/galley/src/Galley/Schema/V68_MLSCommitLock.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V68_MLSCommitLock where +module Galley.Schema.V68_MLSCommitLock where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V69_MLSProposal.hs b/services/galley/src/Galley/Schema/V69_MLSProposal.hs similarity index 96% rename from services/galley/schema/src/V69_MLSProposal.hs rename to services/galley/src/Galley/Schema/V69_MLSProposal.hs index 5b0e0c9ab1f..e0730ce253a 100644 --- a/services/galley/schema/src/V69_MLSProposal.hs +++ b/services/galley/src/Galley/Schema/V69_MLSProposal.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V69_MLSProposal +module Galley.Schema.V69_MLSProposal ( migration, ) where diff --git a/services/galley/schema/src/V70_MLSCipherSuite.hs b/services/galley/src/Galley/Schema/V70_MLSCipherSuite.hs similarity index 96% rename from services/galley/schema/src/V70_MLSCipherSuite.hs rename to services/galley/src/Galley/Schema/V70_MLSCipherSuite.hs index 637b8ea7c4d..d445270b99d 100644 --- a/services/galley/schema/src/V70_MLSCipherSuite.hs +++ b/services/galley/src/Galley/Schema/V70_MLSCipherSuite.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V70_MLSCipherSuite +module Galley.Schema.V70_MLSCipherSuite ( migration, ) where diff --git a/services/galley/schema/src/V71_MemberClientKeypackage.hs b/services/galley/src/Galley/Schema/V71_MemberClientKeypackage.hs similarity index 96% rename from services/galley/schema/src/V71_MemberClientKeypackage.hs rename to services/galley/src/Galley/Schema/V71_MemberClientKeypackage.hs index 1695957905c..21e9903ca77 100644 --- a/services/galley/schema/src/V71_MemberClientKeypackage.hs +++ b/services/galley/src/Galley/Schema/V71_MemberClientKeypackage.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V71_MemberClientKeypackage where +module Galley.Schema.V71_MemberClientKeypackage where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V72_DropManagedConversations.hs b/services/galley/src/Galley/Schema/V72_DropManagedConversations.hs similarity index 94% rename from services/galley/schema/src/V72_DropManagedConversations.hs rename to services/galley/src/Galley/Schema/V72_DropManagedConversations.hs index acb633fe5e9..92d25c6928a 100644 --- a/services/galley/schema/src/V72_DropManagedConversations.hs +++ b/services/galley/src/Galley/Schema/V72_DropManagedConversations.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V72_DropManagedConversations where +module Galley.Schema.V72_DropManagedConversations where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V73_MemberClientTable.hs b/services/galley/src/Galley/Schema/V73_MemberClientTable.hs similarity index 96% rename from services/galley/schema/src/V73_MemberClientTable.hs rename to services/galley/src/Galley/Schema/V73_MemberClientTable.hs index 15f642018b9..9f18e360520 100644 --- a/services/galley/schema/src/V73_MemberClientTable.hs +++ b/services/galley/src/Galley/Schema/V73_MemberClientTable.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V73_MemberClientTable where +module Galley.Schema.V73_MemberClientTable where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V74_ExposeInvitationsToTeamAdmin.hs b/services/galley/src/Galley/Schema/V74_ExposeInvitationsToTeamAdmin.hs similarity index 95% rename from services/galley/schema/src/V74_ExposeInvitationsToTeamAdmin.hs rename to services/galley/src/Galley/Schema/V74_ExposeInvitationsToTeamAdmin.hs index f3df5766d9c..d6b5c56037b 100644 --- a/services/galley/schema/src/V74_ExposeInvitationsToTeamAdmin.hs +++ b/services/galley/src/Galley/Schema/V74_ExposeInvitationsToTeamAdmin.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V74_ExposeInvitationsToTeamAdmin +module Galley.Schema.V74_ExposeInvitationsToTeamAdmin ( migration, ) where diff --git a/services/galley/schema/src/V75_MLSGroupInfo.hs b/services/galley/src/Galley/Schema/V75_MLSGroupInfo.hs similarity index 96% rename from services/galley/schema/src/V75_MLSGroupInfo.hs rename to services/galley/src/Galley/Schema/V75_MLSGroupInfo.hs index 4615c73954e..84b6df1504b 100644 --- a/services/galley/schema/src/V75_MLSGroupInfo.hs +++ b/services/galley/src/Galley/Schema/V75_MLSGroupInfo.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V75_MLSGroupInfo +module Galley.Schema.V75_MLSGroupInfo ( migration, ) where diff --git a/services/galley/schema/src/V76_ProposalOrigin.hs b/services/galley/src/Galley/Schema/V76_ProposalOrigin.hs similarity index 96% rename from services/galley/schema/src/V76_ProposalOrigin.hs rename to services/galley/src/Galley/Schema/V76_ProposalOrigin.hs index c47ffc4d490..3324af00cb8 100644 --- a/services/galley/schema/src/V76_ProposalOrigin.hs +++ b/services/galley/src/Galley/Schema/V76_ProposalOrigin.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V76_ProposalOrigin +module Galley.Schema.V76_ProposalOrigin ( migration, ) where diff --git a/services/galley/schema/src/V77_MLSGroupMemberClient.hs b/services/galley/src/Galley/Schema/V77_MLSGroupMemberClient.hs similarity index 95% rename from services/galley/schema/src/V77_MLSGroupMemberClient.hs rename to services/galley/src/Galley/Schema/V77_MLSGroupMemberClient.hs index 8847b53c22c..ed22adf2768 100644 --- a/services/galley/schema/src/V77_MLSGroupMemberClient.hs +++ b/services/galley/src/Galley/Schema/V77_MLSGroupMemberClient.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V77_MLSGroupMemberClient (migration) where +module Galley.Schema.V77_MLSGroupMemberClient (migration) where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V78_TeamFeatureOutlookCalIntegration.hs b/services/galley/src/Galley/Schema/V78_TeamFeatureOutlookCalIntegration.hs similarity index 95% rename from services/galley/schema/src/V78_TeamFeatureOutlookCalIntegration.hs rename to services/galley/src/Galley/Schema/V78_TeamFeatureOutlookCalIntegration.hs index cd52a49ec43..808f49a1407 100644 --- a/services/galley/schema/src/V78_TeamFeatureOutlookCalIntegration.hs +++ b/services/galley/src/Galley/Schema/V78_TeamFeatureOutlookCalIntegration.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V78_TeamFeatureOutlookCalIntegration +module Galley.Schema.V78_TeamFeatureOutlookCalIntegration ( migration, ) where diff --git a/services/galley/schema/src/V79_TeamFeatureMlsE2EId.hs b/services/galley/src/Galley/Schema/V79_TeamFeatureMlsE2EId.hs similarity index 96% rename from services/galley/schema/src/V79_TeamFeatureMlsE2EId.hs rename to services/galley/src/Galley/Schema/V79_TeamFeatureMlsE2EId.hs index 9a544ab9b15..704b3ea6a3a 100644 --- a/services/galley/schema/src/V79_TeamFeatureMlsE2EId.hs +++ b/services/galley/src/Galley/Schema/V79_TeamFeatureMlsE2EId.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V79_TeamFeatureMlsE2EId +module Galley.Schema.V79_TeamFeatureMlsE2EId ( migration, ) where diff --git a/services/galley/schema/src/V80_AddConversationCodePassword.hs b/services/galley/src/Galley/Schema/V80_AddConversationCodePassword.hs similarity index 95% rename from services/galley/schema/src/V80_AddConversationCodePassword.hs rename to services/galley/src/Galley/Schema/V80_AddConversationCodePassword.hs index 34c67a42538..24ae8e2faf4 100644 --- a/services/galley/schema/src/V80_AddConversationCodePassword.hs +++ b/services/galley/src/Galley/Schema/V80_AddConversationCodePassword.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V80_AddConversationCodePassword +module Galley.Schema.V80_AddConversationCodePassword ( migration, ) where diff --git a/services/galley/schema/src/V81_TeamFeatureMlsE2EIdUpdate.hs b/services/galley/src/Galley/Schema/V81_TeamFeatureMlsE2EIdUpdate.hs similarity index 95% rename from services/galley/schema/src/V81_TeamFeatureMlsE2EIdUpdate.hs rename to services/galley/src/Galley/Schema/V81_TeamFeatureMlsE2EIdUpdate.hs index d5b22b2adba..c7804d59c50 100644 --- a/services/galley/schema/src/V81_TeamFeatureMlsE2EIdUpdate.hs +++ b/services/galley/src/Galley/Schema/V81_TeamFeatureMlsE2EIdUpdate.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V81_TeamFeatureMlsE2EIdUpdate +module Galley.Schema.V81_TeamFeatureMlsE2EIdUpdate ( migration, ) where diff --git a/services/galley/schema/src/V82_RemoteDomainIndexes.hs b/services/galley/src/Galley/Schema/V82_RemoteDomainIndexes.hs similarity index 91% rename from services/galley/schema/src/V82_RemoteDomainIndexes.hs rename to services/galley/src/Galley/Schema/V82_RemoteDomainIndexes.hs index fcc5fca6128..25b8f9f6f07 100644 --- a/services/galley/schema/src/V82_RemoteDomainIndexes.hs +++ b/services/galley/src/Galley/Schema/V82_RemoteDomainIndexes.hs @@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} -module V82_RemoteDomainIndexes +module Galley.Schema.V82_RemoteDomainIndexes ( migration, ) where diff --git a/services/galley/schema/src/V83_CreateTableTeamAdmin.hs b/services/galley/src/Galley/Schema/V83_CreateTableTeamAdmin.hs similarity index 96% rename from services/galley/schema/src/V83_CreateTableTeamAdmin.hs rename to services/galley/src/Galley/Schema/V83_CreateTableTeamAdmin.hs index f52cd0cebc4..e5fd7a4a5cf 100644 --- a/services/galley/schema/src/V83_CreateTableTeamAdmin.hs +++ b/services/galley/src/Galley/Schema/V83_CreateTableTeamAdmin.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V83_CreateTableTeamAdmin +module Galley.Schema.V83_CreateTableTeamAdmin ( migration, ) where diff --git a/services/galley/schema/src/V84_MLSSubconversation.hs b/services/galley/src/Galley/Schema/V84_MLSSubconversation.hs similarity index 95% rename from services/galley/schema/src/V84_MLSSubconversation.hs rename to services/galley/src/Galley/Schema/V84_MLSSubconversation.hs index 73dc617091b..e9e70252c22 100644 --- a/services/galley/schema/src/V84_MLSSubconversation.hs +++ b/services/galley/src/Galley/Schema/V84_MLSSubconversation.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V84_MLSSubconversation (migration) where +module Galley.Schema.V84_MLSSubconversation (migration) where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V85_MLSDraft17.hs b/services/galley/src/Galley/Schema/V85_MLSDraft17.hs similarity index 95% rename from services/galley/schema/src/V85_MLSDraft17.hs rename to services/galley/src/Galley/Schema/V85_MLSDraft17.hs index 75ca5200eeb..958c8c629d9 100644 --- a/services/galley/schema/src/V85_MLSDraft17.hs +++ b/services/galley/src/Galley/Schema/V85_MLSDraft17.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V85_MLSDraft17 (migration) where +module Galley.Schema.V85_MLSDraft17 (migration) where import Cassandra.Schema import Imports diff --git a/services/galley/schema/src/V86_TeamFeatureMlsMigration.hs b/services/galley/src/Galley/Schema/V86_TeamFeatureMlsMigration.hs similarity index 96% rename from services/galley/schema/src/V86_TeamFeatureMlsMigration.hs rename to services/galley/src/Galley/Schema/V86_TeamFeatureMlsMigration.hs index 00a1e04f6fc..431a93b4d38 100644 --- a/services/galley/schema/src/V86_TeamFeatureMlsMigration.hs +++ b/services/galley/src/Galley/Schema/V86_TeamFeatureMlsMigration.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V86_TeamFeatureMlsMigration +module Galley.Schema.V86_TeamFeatureMlsMigration ( migration, ) where diff --git a/services/galley/schema/src/V87_TeamFeatureSupportedProtocols.hs b/services/galley/src/Galley/Schema/V87_TeamFeatureSupportedProtocols.hs similarity index 95% rename from services/galley/schema/src/V87_TeamFeatureSupportedProtocols.hs rename to services/galley/src/Galley/Schema/V87_TeamFeatureSupportedProtocols.hs index 4b8d447734c..03f57315705 100644 --- a/services/galley/schema/src/V87_TeamFeatureSupportedProtocols.hs +++ b/services/galley/src/Galley/Schema/V87_TeamFeatureSupportedProtocols.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V87_TeamFeatureSupportedProtocols +module Galley.Schema.V87_TeamFeatureSupportedProtocols ( migration, ) where From 42283cb6f2198d422141f0930a387abffcdc5711 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Wed, 11 Oct 2023 14:48:18 +0200 Subject: [PATCH 196/225] [WPB-3138] Split brig-schema into lib and exec. (#3638) * Split brig-schema into lib and exec. * Removed brig-schema-lib, merged into brig. --- services/brig/brig.cabal | 146 ++++++++---------- services/brig/default.nix | 1 + services/brig/schema/{main.hs => Main.hs} | 2 +- services/brig/schema/src/Run.hs | 119 -------------- services/brig/src/Brig/App.hs | 3 +- services/brig/src/Brig/Schema/Run.hs | 123 +++++++++++++++ .../{schema/src => src/Brig/Schema}/V43.hs | 2 +- .../{schema/src => src/Brig/Schema}/V44.hs | 2 +- .../{schema/src => src/Brig/Schema}/V45.hs | 2 +- .../{schema/src => src/Brig/Schema}/V46.hs | 2 +- .../{schema/src => src/Brig/Schema}/V47.hs | 2 +- .../{schema/src => src/Brig/Schema}/V48.hs | 2 +- .../{schema/src => src/Brig/Schema}/V49.hs | 2 +- .../{schema/src => src/Brig/Schema}/V50.hs | 2 +- .../{schema/src => src/Brig/Schema}/V51.hs | 2 +- .../{schema/src => src/Brig/Schema}/V52.hs | 2 +- .../{schema/src => src/Brig/Schema}/V53.hs | 2 +- .../{schema/src => src/Brig/Schema}/V54.hs | 2 +- .../{schema/src => src/Brig/Schema}/V55.hs | 2 +- .../{schema/src => src/Brig/Schema}/V56.hs | 2 +- .../{schema/src => src/Brig/Schema}/V57.hs | 2 +- .../{schema/src => src/Brig/Schema}/V58.hs | 2 +- .../{schema/src => src/Brig/Schema}/V59.hs | 2 +- .../Schema}/V60_AddFederationIdMapping.hs | 2 +- .../Brig/Schema}/V61_team_invitation_email.hs | 2 +- .../Schema}/V62_RemoveFederationIdMapping.hs | 2 +- .../Schema}/V63_AddUsersPendingActivation.hs | 2 +- .../Brig/Schema}/V64_ClientCapabilities.hs | 2 +- .../Brig/Schema}/V65_FederatedConnections.hs | 2 +- .../V66_PersonalFeatureConfCallInit.hs | 2 +- .../Brig/Schema}/V67_MLSKeyPackages.hs | 2 +- .../Brig/Schema}/V68_AddMLSPublicKeys.hs | 2 +- .../Schema}/V69_MLSKeyPackageRefMapping.hs | 2 +- .../Brig/Schema}/V70_UserEmailUnvalidated.hs | 2 +- .../Schema}/V71_AddTableVCodesThrottle.hs | 2 +- .../Brig/Schema}/V72_AddNonceTable.hs | 2 +- .../Brig/Schema}/V73_ReplaceNonceTable.hs | 2 +- .../Brig/Schema}/V74_AddOAuthTables.hs | 2 +- .../Brig/Schema}/V75_AddOAuthCodeChallenge.hs | 2 +- .../Brig/Schema}/V76_AddSupportedProtocols.hs | 2 +- .../Brig/Schema}/V77_FederationRemotes.hs | 2 +- .../Brig/Schema}/V78_ClientLastActive.hs | 2 +- .../Brig/Schema}/V79_ConnectionRemoteIndex.hs | 2 +- .../Brig/Schema}/V80_KeyPackageCiphersuite.hs | 2 +- .../src => src/Brig/Schema}/V_FUTUREWORK.hs | 2 +- 45 files changed, 232 insertions(+), 240 deletions(-) rename services/brig/schema/{main.hs => Main.hs} (53%) delete mode 100644 services/brig/schema/src/Run.hs create mode 100644 services/brig/src/Brig/Schema/Run.hs rename services/brig/{schema/src => src/Brig/Schema}/V43.hs (99%) rename services/brig/{schema/src => src/Brig/Schema}/V44.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V45.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V46.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V47.hs (98%) rename services/brig/{schema/src => src/Brig/Schema}/V48.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V49.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V50.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V51.hs (98%) rename services/brig/{schema/src => src/Brig/Schema}/V52.hs (98%) rename services/brig/{schema/src => src/Brig/Schema}/V53.hs (98%) rename services/brig/{schema/src => src/Brig/Schema}/V54.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V55.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V56.hs (98%) rename services/brig/{schema/src => src/Brig/Schema}/V57.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V58.hs (98%) rename services/brig/{schema/src => src/Brig/Schema}/V59.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V60_AddFederationIdMapping.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V61_team_invitation_email.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V62_RemoveFederationIdMapping.hs (95%) rename services/brig/{schema/src => src/Brig/Schema}/V63_AddUsersPendingActivation.hs (95%) rename services/brig/{schema/src => src/Brig/Schema}/V64_ClientCapabilities.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V65_FederatedConnections.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V66_PersonalFeatureConfCallInit.hs (95%) rename services/brig/{schema/src => src/Brig/Schema}/V67_MLSKeyPackages.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V68_AddMLSPublicKeys.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V69_MLSKeyPackageRefMapping.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V70_UserEmailUnvalidated.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V71_AddTableVCodesThrottle.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V72_AddNonceTable.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V73_ReplaceNonceTable.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V74_AddOAuthTables.hs (98%) rename services/brig/{schema/src => src/Brig/Schema}/V75_AddOAuthCodeChallenge.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V76_AddSupportedProtocols.hs (94%) rename services/brig/{schema/src => src/Brig/Schema}/V77_FederationRemotes.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V78_ClientLastActive.hs (96%) rename services/brig/{schema/src => src/Brig/Schema}/V79_ConnectionRemoteIndex.hs (88%) rename services/brig/{schema/src => src/Brig/Schema}/V80_KeyPackageCiphersuite.hs (97%) rename services/brig/{schema/src => src/Brig/Schema}/V_FUTUREWORK.hs (98%) diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 772882d4ae6..9351b4f65b9 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -18,7 +18,7 @@ extra-source-files: docs/swagger.md common common-all - default-language: Haskell2010 + default-language: GHC2021 ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path @@ -70,7 +70,7 @@ common common-all ViewPatterns library - import: common-all + import: common-all -- cabal-fmt: expand src exposed-modules: @@ -162,6 +162,46 @@ library Brig.Queue.Types Brig.RPC Brig.Run + Brig.Schema.Run + Brig.Schema.V43 + Brig.Schema.V44 + Brig.Schema.V45 + Brig.Schema.V46 + Brig.Schema.V47 + Brig.Schema.V48 + Brig.Schema.V49 + Brig.Schema.V50 + Brig.Schema.V51 + Brig.Schema.V52 + Brig.Schema.V53 + Brig.Schema.V54 + Brig.Schema.V55 + Brig.Schema.V56 + Brig.Schema.V57 + Brig.Schema.V58 + Brig.Schema.V59 + Brig.Schema.V60_AddFederationIdMapping + Brig.Schema.V61_team_invitation_email + Brig.Schema.V62_RemoveFederationIdMapping + Brig.Schema.V63_AddUsersPendingActivation + Brig.Schema.V64_ClientCapabilities + Brig.Schema.V65_FederatedConnections + Brig.Schema.V66_PersonalFeatureConfCallInit + Brig.Schema.V67_MLSKeyPackages + Brig.Schema.V68_AddMLSPublicKeys + Brig.Schema.V69_MLSKeyPackageRefMapping + Brig.Schema.V70_UserEmailUnvalidated + Brig.Schema.V71_AddTableVCodesThrottle + Brig.Schema.V72_AddNonceTable + Brig.Schema.V73_ReplaceNonceTable + Brig.Schema.V74_AddOAuthTables + Brig.Schema.V75_AddOAuthCodeChallenge + Brig.Schema.V76_AddSupportedProtocols + Brig.Schema.V77_FederationRemotes + Brig.Schema.V78_ClientLastActive + Brig.Schema.V79_ConnectionRemoteIndex + Brig.Schema.V80_KeyPackageCiphersuite + Brig.Schema.V_FUTUREWORK Brig.SMTP Brig.Team.API Brig.Team.DB @@ -192,8 +232,8 @@ library Brig.Version Brig.ZAuth - other-modules: Paths_brig - hs-source-dirs: src + other-modules: Paths_brig + hs-source-dirs: src ghc-options: -funbox-strict-fields -fplugin=Polysemy.Plugin -fplugin=TransitiveAnns.Plugin -Wredundant-constraints @@ -278,6 +318,7 @@ library , proto-lens >=0.1 , random , random-shuffle >=0.0.3 + , raw-strings-qq , resource-pool >=0.2 , resourcet >=1.1 , retry >=0.7 @@ -324,12 +365,10 @@ library , yaml >=0.8.22 , zauth >=0.10.3 - default-language: GHC2021 - executable brig - import: common-all - main-is: exec/Main.hs - other-modules: Paths_brig + import: common-all + main-is: exec/Main.hs + other-modules: Paths_brig ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T -rtsopts -Wredundant-constraints -Wunused-packages @@ -341,13 +380,11 @@ executable brig , imports , types-common - default-language: GHC2021 - executable brig-index - import: common-all - main-is: index/src/Main.hs - other-modules: Paths_brig - ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N + import: common-all + main-is: index/src/Main.hs + other-modules: Paths_brig + ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N build-depends: , base , brig @@ -355,11 +392,9 @@ executable brig-index , optparse-applicative , tinylog - default-language: GHC2021 - executable brig-integration - import: common-all - main-is: ../integration.hs + import: common-all + main-is: ../integration.hs -- cabal-fmt: expand test/integration other-modules: @@ -401,8 +436,8 @@ executable brig-integration Util Util.AWS - hs-source-dirs: test/integration - ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N + hs-source-dirs: test/integration + ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N build-depends: , aeson , async @@ -495,72 +530,25 @@ executable brig-integration , yaml , zauth - default-language: GHC2021 - executable brig-schema import: common-all - main-is: ../main.hs - - -- cabal-fmt: expand schema/src - other-modules: - Run - V43 - V44 - V45 - V46 - V47 - V48 - V49 - V50 - V51 - V52 - V53 - V54 - V55 - V56 - V57 - V58 - V59 - V60_AddFederationIdMapping - V61_team_invitation_email - V62_RemoveFederationIdMapping - V63_AddUsersPendingActivation - V64_ClientCapabilities - V65_FederatedConnections - V66_PersonalFeatureConfCallInit - V67_MLSKeyPackages - V68_AddMLSPublicKeys - V69_MLSKeyPackageRefMapping - V70_UserEmailUnvalidated - V71_AddTableVCodesThrottle - V72_AddNonceTable - V73_ReplaceNonceTable - V74_AddOAuthTables - V75_AddOAuthCodeChallenge - V76_AddSupportedProtocols - V77_FederationRemotes - V78_ClientLastActive - V79_ConnectionRemoteIndex - V80_KeyPackageCiphersuite - V_FUTUREWORK - - hs-source-dirs: schema/src + main-is: Main.hs + hs-source-dirs: schema ghc-options: -funbox-strict-fields -Wredundant-constraints default-extensions: TemplateHaskell build-depends: , base - , cassandra-util >=0.12 + , brig + , cassandra-util , extended , imports - , raw-strings-qq >=1.0 + , raw-strings-qq , types-common - default-language: GHC2021 - test-suite brig-tests - import: common-all - type: exitcode-stdio-1.0 - main-is: ../unit.hs + import: common-all + type: exitcode-stdio-1.0 + main-is: ../unit.hs other-modules: Run Test.Brig.Calling @@ -571,8 +559,8 @@ test-suite brig-tests Test.Brig.Roundtrip Test.Brig.User.Search.Index.Types - hs-source-dirs: test/unit - ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N + hs-source-dirs: test/unit + ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N build-depends: , aeson , base @@ -600,5 +588,3 @@ test-suite brig-tests , uri-bytestring , uuid , wire-api - - default-language: GHC2021 diff --git a/services/brig/default.nix b/services/brig/default.nix index fb9c011a9ac..6887c802f38 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -244,6 +244,7 @@ mkDerivation { proto-lens random random-shuffle + raw-strings-qq resource-pool resourcet retry diff --git a/services/brig/schema/main.hs b/services/brig/schema/Main.hs similarity index 53% rename from services/brig/schema/main.hs rename to services/brig/schema/Main.hs index d4037ab9cfa..11d6e144194 100644 --- a/services/brig/schema/main.hs +++ b/services/brig/schema/Main.hs @@ -1,5 +1,5 @@ +import Brig.Schema.Run qualified as Run import Imports -import qualified Run main :: IO () main = Run.main diff --git a/services/brig/schema/src/Run.hs b/services/brig/schema/src/Run.hs deleted file mode 100644 index 0f1b71127b3..00000000000 --- a/services/brig/schema/src/Run.hs +++ /dev/null @@ -1,119 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 Run where - -import Cassandra.Schema -import Control.Exception (finally) -import Imports -import System.Logger.Extended qualified as Log -import Util.Options -import V43 qualified -import V44 qualified -import V45 qualified -import V46 qualified -import V47 qualified -import V48 qualified -import V49 qualified -import V50 qualified -import V51 qualified -import V52 qualified -import V53 qualified -import V54 qualified -import V55 qualified -import V56 qualified -import V57 qualified -import V58 qualified -import V59 qualified -import V60_AddFederationIdMapping qualified -import V61_team_invitation_email qualified -import V62_RemoveFederationIdMapping qualified -import V63_AddUsersPendingActivation qualified -import V64_ClientCapabilities qualified -import V65_FederatedConnections qualified -import V66_PersonalFeatureConfCallInit qualified -import V67_MLSKeyPackages qualified -import V68_AddMLSPublicKeys qualified -import V69_MLSKeyPackageRefMapping qualified -import V70_UserEmailUnvalidated qualified -import V71_AddTableVCodesThrottle qualified -import V72_AddNonceTable qualified -import V73_ReplaceNonceTable qualified -import V74_AddOAuthTables qualified -import V75_AddOAuthCodeChallenge qualified -import V76_AddSupportedProtocols qualified -import V77_FederationRemotes qualified -import V78_ClientLastActive qualified -import V79_ConnectionRemoteIndex qualified -import V80_KeyPackageCiphersuite qualified - -main :: IO () -main = do - let desc = "Brig Cassandra Schema Migrations" - defaultPath = "/etc/wire/brig/conf/brig-schema.yaml" - o <- getOptions desc (Just migrationOptsParser) defaultPath - l <- Log.mkLogger' - migrateSchema - l - o - [ V43.migration, - V44.migration, - V45.migration, - V46.migration, - V47.migration, - V48.migration, - V49.migration, - V50.migration, - V51.migration, - V52.migration, - V53.migration, - V54.migration, - V55.migration, - V56.migration, - V57.migration, - V58.migration, - V59.migration, - V60_AddFederationIdMapping.migration, - V61_team_invitation_email.migration, - V62_RemoveFederationIdMapping.migration, - V63_AddUsersPendingActivation.migration, - V64_ClientCapabilities.migration, - V65_FederatedConnections.migration, - V66_PersonalFeatureConfCallInit.migration, - V67_MLSKeyPackages.migration, - V68_AddMLSPublicKeys.migration, - V69_MLSKeyPackageRefMapping.migration, - V70_UserEmailUnvalidated.migration, - V71_AddTableVCodesThrottle.migration, - V72_AddNonceTable.migration, - V73_ReplaceNonceTable.migration, - V74_AddOAuthTables.migration, - V75_AddOAuthCodeChallenge.migration, - V76_AddSupportedProtocols.migration, - V77_FederationRemotes.migration, - V78_ClientLastActive.migration, - V79_ConnectionRemoteIndex.migration, - V80_KeyPackageCiphersuite.migration - -- When adding migrations here, don't forget to update - -- 'schemaVersion' in Brig.App - - -- FUTUREWORK: undo V41 (searchable flag); we stopped using it in - -- https://github.com/wireapp/wire-server/pull/964 - -- - -- FUTUREWORK after July 2023: integrate V_FUTUREWORK here. - ] - `finally` Log.close l diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 1131a971830..f084f528a89 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -99,6 +99,7 @@ import Brig.Provider.Template import Brig.Queue.Stomp qualified as Stomp import Brig.Queue.Types (Queue (..)) import Brig.SMTP qualified as SMTP +import Brig.Schema.Run qualified as Migrations import Brig.Team.Template import Brig.Template (Localised, TemplateBranding, forLocale, genTemplateBranding) import Brig.User.Search.Index (IndexEnv (..), MonadIndexIO (..), runIndexIO) @@ -157,7 +158,7 @@ import Wire.API.User.Identity (Email) import Wire.API.User.Profile (Locale) schemaVersion :: Int32 -schemaVersion = 79 +schemaVersion = Migrations.lastSchemaVersion ------------------------------------------------------------------------------- -- Environment diff --git a/services/brig/src/Brig/Schema/Run.hs b/services/brig/src/Brig/Schema/Run.hs new file mode 100644 index 00000000000..f18e340bc6b --- /dev/null +++ b/services/brig/src/Brig/Schema/Run.hs @@ -0,0 +1,123 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Brig.Schema.Run where + +import Brig.Schema.V43 qualified as V43 +import Brig.Schema.V44 qualified as V44 +import Brig.Schema.V45 qualified as V45 +import Brig.Schema.V46 qualified as V46 +import Brig.Schema.V47 qualified as V47 +import Brig.Schema.V48 qualified as V48 +import Brig.Schema.V49 qualified as V49 +import Brig.Schema.V50 qualified as V50 +import Brig.Schema.V51 qualified as V51 +import Brig.Schema.V52 qualified as V52 +import Brig.Schema.V53 qualified as V53 +import Brig.Schema.V54 qualified as V54 +import Brig.Schema.V55 qualified as V55 +import Brig.Schema.V56 qualified as V56 +import Brig.Schema.V57 qualified as V57 +import Brig.Schema.V58 qualified as V58 +import Brig.Schema.V59 qualified as V59 +import Brig.Schema.V60_AddFederationIdMapping qualified as V60_AddFederationIdMapping +import Brig.Schema.V61_team_invitation_email qualified as V61_team_invitation_email +import Brig.Schema.V62_RemoveFederationIdMapping qualified as V62_RemoveFederationIdMapping +import Brig.Schema.V63_AddUsersPendingActivation qualified as V63_AddUsersPendingActivation +import Brig.Schema.V64_ClientCapabilities qualified as V64_ClientCapabilities +import Brig.Schema.V65_FederatedConnections qualified as V65_FederatedConnections +import Brig.Schema.V66_PersonalFeatureConfCallInit qualified as V66_PersonalFeatureConfCallInit +import Brig.Schema.V67_MLSKeyPackages qualified as V67_MLSKeyPackages +import Brig.Schema.V68_AddMLSPublicKeys qualified as V68_AddMLSPublicKeys +import Brig.Schema.V69_MLSKeyPackageRefMapping qualified as V69_MLSKeyPackageRefMapping +import Brig.Schema.V70_UserEmailUnvalidated qualified as V70_UserEmailUnvalidated +import Brig.Schema.V71_AddTableVCodesThrottle qualified as V71_AddTableVCodesThrottle +import Brig.Schema.V72_AddNonceTable qualified as V72_AddNonceTable +import Brig.Schema.V73_ReplaceNonceTable qualified as V73_ReplaceNonceTable +import Brig.Schema.V74_AddOAuthTables qualified as V74_AddOAuthTables +import Brig.Schema.V75_AddOAuthCodeChallenge qualified as V75_AddOAuthCodeChallenge +import Brig.Schema.V76_AddSupportedProtocols qualified as V76_AddSupportedProtocols +import Brig.Schema.V77_FederationRemotes qualified as V77_FederationRemotes +import Brig.Schema.V78_ClientLastActive qualified as V78_ClientLastActive +import Brig.Schema.V79_ConnectionRemoteIndex qualified as V79_ConnectionRemoteIndex +import Brig.Schema.V80_KeyPackageCiphersuite qualified as V80_KeyPackageCiphersuite +import Cassandra.Schema +import Control.Exception (finally) +import Imports +import System.Logger.Extended qualified as Log +import Util.Options + +main :: IO () +main = do + let desc = "Brig Cassandra Schema Migrations" + defaultPath = "/etc/wire/brig/conf/brig-schema.yaml" + o <- getOptions desc (Just migrationOptsParser) defaultPath + l <- Log.mkLogger' + migrateSchema + l + o + migrations + `finally` Log.close l + +lastSchemaVersion :: Int32 +lastSchemaVersion = migVersion $ last migrations + +migrations :: [Migration] +migrations = + [ V43.migration, + V44.migration, + V45.migration, + V46.migration, + V47.migration, + V48.migration, + V49.migration, + V50.migration, + V51.migration, + V52.migration, + V53.migration, + V54.migration, + V55.migration, + V56.migration, + V57.migration, + V58.migration, + V59.migration, + V60_AddFederationIdMapping.migration, + V61_team_invitation_email.migration, + V62_RemoveFederationIdMapping.migration, + V63_AddUsersPendingActivation.migration, + V64_ClientCapabilities.migration, + V65_FederatedConnections.migration, + V66_PersonalFeatureConfCallInit.migration, + V67_MLSKeyPackages.migration, + V68_AddMLSPublicKeys.migration, + V69_MLSKeyPackageRefMapping.migration, + V70_UserEmailUnvalidated.migration, + V71_AddTableVCodesThrottle.migration, + V72_AddNonceTable.migration, + V73_ReplaceNonceTable.migration, + V74_AddOAuthTables.migration, + V75_AddOAuthCodeChallenge.migration, + V76_AddSupportedProtocols.migration, + V77_FederationRemotes.migration, + V78_ClientLastActive.migration, + V79_ConnectionRemoteIndex.migration, + V80_KeyPackageCiphersuite.migration + -- FUTUREWORK: undo V41 (searchable flag); we stopped using it in + -- https://github.com/wireapp/wire-server/pull/964 + -- + -- FUTUREWORK after July 2023: integrate V_FUTUREWORK here. + ] diff --git a/services/brig/schema/src/V43.hs b/services/brig/src/Brig/Schema/V43.hs similarity index 99% rename from services/brig/schema/src/V43.hs rename to services/brig/src/Brig/Schema/V43.hs index 288ea41b2e1..42038f4d0a0 100644 --- a/services/brig/schema/src/V43.hs +++ b/services/brig/src/Brig/Schema/V43.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V43 +module Brig.Schema.V43 ( migration, ) where diff --git a/services/brig/schema/src/V44.hs b/services/brig/src/Brig/Schema/V44.hs similarity index 97% rename from services/brig/schema/src/V44.hs rename to services/brig/src/Brig/Schema/V44.hs index f441fa08e45..b1222e53ff1 100644 --- a/services/brig/schema/src/V44.hs +++ b/services/brig/src/Brig/Schema/V44.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V44 +module Brig.Schema.V44 ( migration, ) where diff --git a/services/brig/schema/src/V45.hs b/services/brig/src/Brig/Schema/V45.hs similarity index 97% rename from services/brig/schema/src/V45.hs rename to services/brig/src/Brig/Schema/V45.hs index bd6317f5192..070801bd535 100644 --- a/services/brig/schema/src/V45.hs +++ b/services/brig/src/Brig/Schema/V45.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V45 +module Brig.Schema.V45 ( migration, ) where diff --git a/services/brig/schema/src/V46.hs b/services/brig/src/Brig/Schema/V46.hs similarity index 97% rename from services/brig/schema/src/V46.hs rename to services/brig/src/Brig/Schema/V46.hs index bbb06e2a9d2..c4d223582cf 100644 --- a/services/brig/schema/src/V46.hs +++ b/services/brig/src/Brig/Schema/V46.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V46 +module Brig.Schema.V46 ( migration, ) where diff --git a/services/brig/schema/src/V47.hs b/services/brig/src/Brig/Schema/V47.hs similarity index 98% rename from services/brig/schema/src/V47.hs rename to services/brig/src/Brig/Schema/V47.hs index 98a924ce891..e6f94b5e3b3 100644 --- a/services/brig/schema/src/V47.hs +++ b/services/brig/src/Brig/Schema/V47.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V47 +module Brig.Schema.V47 ( migration, ) where diff --git a/services/brig/schema/src/V48.hs b/services/brig/src/Brig/Schema/V48.hs similarity index 97% rename from services/brig/schema/src/V48.hs rename to services/brig/src/Brig/Schema/V48.hs index c8b984b1f6c..44927c581eb 100644 --- a/services/brig/schema/src/V48.hs +++ b/services/brig/src/Brig/Schema/V48.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V48 +module Brig.Schema.V48 ( migration, ) where diff --git a/services/brig/schema/src/V49.hs b/services/brig/src/Brig/Schema/V49.hs similarity index 97% rename from services/brig/schema/src/V49.hs rename to services/brig/src/Brig/Schema/V49.hs index 0d20e87f620..b319ce45295 100644 --- a/services/brig/schema/src/V49.hs +++ b/services/brig/src/Brig/Schema/V49.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V49 +module Brig.Schema.V49 ( migration, ) where diff --git a/services/brig/schema/src/V50.hs b/services/brig/src/Brig/Schema/V50.hs similarity index 97% rename from services/brig/schema/src/V50.hs rename to services/brig/src/Brig/Schema/V50.hs index f0c3914c454..ccfda0525e7 100644 --- a/services/brig/schema/src/V50.hs +++ b/services/brig/src/Brig/Schema/V50.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V50 +module Brig.Schema.V50 ( migration, ) where diff --git a/services/brig/schema/src/V51.hs b/services/brig/src/Brig/Schema/V51.hs similarity index 98% rename from services/brig/schema/src/V51.hs rename to services/brig/src/Brig/Schema/V51.hs index e8f4d1846c3..e0e2b3b7489 100644 --- a/services/brig/schema/src/V51.hs +++ b/services/brig/src/Brig/Schema/V51.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V51 +module Brig.Schema.V51 ( migration, ) where diff --git a/services/brig/schema/src/V52.hs b/services/brig/src/Brig/Schema/V52.hs similarity index 98% rename from services/brig/schema/src/V52.hs rename to services/brig/src/Brig/Schema/V52.hs index 6ec1beccf8c..e102ffceac4 100644 --- a/services/brig/schema/src/V52.hs +++ b/services/brig/src/Brig/Schema/V52.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V52 +module Brig.Schema.V52 ( migration, ) where diff --git a/services/brig/schema/src/V53.hs b/services/brig/src/Brig/Schema/V53.hs similarity index 98% rename from services/brig/schema/src/V53.hs rename to services/brig/src/Brig/Schema/V53.hs index a76f3d10936..475eb68c6f5 100644 --- a/services/brig/schema/src/V53.hs +++ b/services/brig/src/Brig/Schema/V53.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V53 +module Brig.Schema.V53 ( migration, ) where diff --git a/services/brig/schema/src/V54.hs b/services/brig/src/Brig/Schema/V54.hs similarity index 97% rename from services/brig/schema/src/V54.hs rename to services/brig/src/Brig/Schema/V54.hs index 245dd9efb99..e7a50904518 100644 --- a/services/brig/schema/src/V54.hs +++ b/services/brig/src/Brig/Schema/V54.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V54 +module Brig.Schema.V54 ( migration, ) where diff --git a/services/brig/schema/src/V55.hs b/services/brig/src/Brig/Schema/V55.hs similarity index 97% rename from services/brig/schema/src/V55.hs rename to services/brig/src/Brig/Schema/V55.hs index fb92b32cf3c..436b1548efd 100644 --- a/services/brig/schema/src/V55.hs +++ b/services/brig/src/Brig/Schema/V55.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V55 +module Brig.Schema.V55 ( migration, ) where diff --git a/services/brig/schema/src/V56.hs b/services/brig/src/Brig/Schema/V56.hs similarity index 98% rename from services/brig/schema/src/V56.hs rename to services/brig/src/Brig/Schema/V56.hs index f6b629cc144..052d86089ee 100644 --- a/services/brig/schema/src/V56.hs +++ b/services/brig/src/Brig/Schema/V56.hs @@ -18,7 +18,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V56 +module Brig.Schema.V56 ( migration, ) where diff --git a/services/brig/schema/src/V57.hs b/services/brig/src/Brig/Schema/V57.hs similarity index 97% rename from services/brig/schema/src/V57.hs rename to services/brig/src/Brig/Schema/V57.hs index d3a58275925..a08e75e416c 100644 --- a/services/brig/schema/src/V57.hs +++ b/services/brig/src/Brig/Schema/V57.hs @@ -18,7 +18,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V57 +module Brig.Schema.V57 ( migration, ) where diff --git a/services/brig/schema/src/V58.hs b/services/brig/src/Brig/Schema/V58.hs similarity index 98% rename from services/brig/schema/src/V58.hs rename to services/brig/src/Brig/Schema/V58.hs index 57b9c750434..a5e074efe80 100644 --- a/services/brig/schema/src/V58.hs +++ b/services/brig/src/Brig/Schema/V58.hs @@ -18,7 +18,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V58 +module Brig.Schema.V58 ( migration, ) where diff --git a/services/brig/schema/src/V59.hs b/services/brig/src/Brig/Schema/V59.hs similarity index 97% rename from services/brig/schema/src/V59.hs rename to services/brig/src/Brig/Schema/V59.hs index b84a2f7ef26..533bfb8fd8b 100644 --- a/services/brig/schema/src/V59.hs +++ b/services/brig/src/Brig/Schema/V59.hs @@ -18,7 +18,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V59 +module Brig.Schema.V59 ( migration, ) where diff --git a/services/brig/schema/src/V60_AddFederationIdMapping.hs b/services/brig/src/Brig/Schema/V60_AddFederationIdMapping.hs similarity index 96% rename from services/brig/schema/src/V60_AddFederationIdMapping.hs rename to services/brig/src/Brig/Schema/V60_AddFederationIdMapping.hs index 5fb5e21a358..c529d835d9f 100644 --- a/services/brig/schema/src/V60_AddFederationIdMapping.hs +++ b/services/brig/src/Brig/Schema/V60_AddFederationIdMapping.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V60_AddFederationIdMapping +module Brig.Schema.V60_AddFederationIdMapping ( migration, ) where diff --git a/services/brig/schema/src/V61_team_invitation_email.hs b/services/brig/src/Brig/Schema/V61_team_invitation_email.hs similarity index 96% rename from services/brig/schema/src/V61_team_invitation_email.hs rename to services/brig/src/Brig/Schema/V61_team_invitation_email.hs index 81b0b911de6..1f1a863942b 100644 --- a/services/brig/schema/src/V61_team_invitation_email.hs +++ b/services/brig/src/Brig/Schema/V61_team_invitation_email.hs @@ -18,7 +18,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V61_team_invitation_email +module Brig.Schema.V61_team_invitation_email ( migration, ) where diff --git a/services/brig/schema/src/V62_RemoveFederationIdMapping.hs b/services/brig/src/Brig/Schema/V62_RemoveFederationIdMapping.hs similarity index 95% rename from services/brig/schema/src/V62_RemoveFederationIdMapping.hs rename to services/brig/src/Brig/Schema/V62_RemoveFederationIdMapping.hs index 99670d3fed6..d28f09939ac 100644 --- a/services/brig/schema/src/V62_RemoveFederationIdMapping.hs +++ b/services/brig/src/Brig/Schema/V62_RemoveFederationIdMapping.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V62_RemoveFederationIdMapping +module Brig.Schema.V62_RemoveFederationIdMapping ( migration, ) where diff --git a/services/brig/schema/src/V63_AddUsersPendingActivation.hs b/services/brig/src/Brig/Schema/V63_AddUsersPendingActivation.hs similarity index 95% rename from services/brig/schema/src/V63_AddUsersPendingActivation.hs rename to services/brig/src/Brig/Schema/V63_AddUsersPendingActivation.hs index a4d1752a63f..540d6603ca6 100644 --- a/services/brig/schema/src/V63_AddUsersPendingActivation.hs +++ b/services/brig/src/Brig/Schema/V63_AddUsersPendingActivation.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V63_AddUsersPendingActivation (migration) where +module Brig.Schema.V63_AddUsersPendingActivation (migration) where import Cassandra.Schema import Imports diff --git a/services/brig/schema/src/V64_ClientCapabilities.hs b/services/brig/src/Brig/Schema/V64_ClientCapabilities.hs similarity index 96% rename from services/brig/schema/src/V64_ClientCapabilities.hs rename to services/brig/src/Brig/Schema/V64_ClientCapabilities.hs index eda30ee4e82..d6a8bcfc952 100644 --- a/services/brig/schema/src/V64_ClientCapabilities.hs +++ b/services/brig/src/Brig/Schema/V64_ClientCapabilities.hs @@ -18,7 +18,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V64_ClientCapabilities +module Brig.Schema.V64_ClientCapabilities ( migration, ) where diff --git a/services/brig/schema/src/V65_FederatedConnections.hs b/services/brig/src/Brig/Schema/V65_FederatedConnections.hs similarity index 97% rename from services/brig/schema/src/V65_FederatedConnections.hs rename to services/brig/src/Brig/Schema/V65_FederatedConnections.hs index 6d92a633170..ac8608b644a 100644 --- a/services/brig/schema/src/V65_FederatedConnections.hs +++ b/services/brig/src/Brig/Schema/V65_FederatedConnections.hs @@ -18,7 +18,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V65_FederatedConnections +module Brig.Schema.V65_FederatedConnections ( migration, ) where diff --git a/services/brig/schema/src/V66_PersonalFeatureConfCallInit.hs b/services/brig/src/Brig/Schema/V66_PersonalFeatureConfCallInit.hs similarity index 95% rename from services/brig/schema/src/V66_PersonalFeatureConfCallInit.hs rename to services/brig/src/Brig/Schema/V66_PersonalFeatureConfCallInit.hs index 53cb70ef706..edf40b2ed09 100644 --- a/services/brig/schema/src/V66_PersonalFeatureConfCallInit.hs +++ b/services/brig/src/Brig/Schema/V66_PersonalFeatureConfCallInit.hs @@ -18,7 +18,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V66_PersonalFeatureConfCallInit +module Brig.Schema.V66_PersonalFeatureConfCallInit ( migration, ) where diff --git a/services/brig/schema/src/V67_MLSKeyPackages.hs b/services/brig/src/Brig/Schema/V67_MLSKeyPackages.hs similarity index 97% rename from services/brig/schema/src/V67_MLSKeyPackages.hs rename to services/brig/src/Brig/Schema/V67_MLSKeyPackages.hs index 71a933f553c..21d14d97338 100644 --- a/services/brig/schema/src/V67_MLSKeyPackages.hs +++ b/services/brig/src/Brig/Schema/V67_MLSKeyPackages.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V67_MLSKeyPackages +module Brig.Schema.V67_MLSKeyPackages ( migration, ) where diff --git a/services/brig/schema/src/V68_AddMLSPublicKeys.hs b/services/brig/src/Brig/Schema/V68_AddMLSPublicKeys.hs similarity index 96% rename from services/brig/schema/src/V68_AddMLSPublicKeys.hs rename to services/brig/src/Brig/Schema/V68_AddMLSPublicKeys.hs index 599ea5163dc..cff6c189fc6 100644 --- a/services/brig/schema/src/V68_AddMLSPublicKeys.hs +++ b/services/brig/src/Brig/Schema/V68_AddMLSPublicKeys.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V68_AddMLSPublicKeys +module Brig.Schema.V68_AddMLSPublicKeys ( migration, ) where diff --git a/services/brig/schema/src/V69_MLSKeyPackageRefMapping.hs b/services/brig/src/Brig/Schema/V69_MLSKeyPackageRefMapping.hs similarity index 96% rename from services/brig/schema/src/V69_MLSKeyPackageRefMapping.hs rename to services/brig/src/Brig/Schema/V69_MLSKeyPackageRefMapping.hs index aae2b698ae2..a1a5933b107 100644 --- a/services/brig/schema/src/V69_MLSKeyPackageRefMapping.hs +++ b/services/brig/src/Brig/Schema/V69_MLSKeyPackageRefMapping.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V69_MLSKeyPackageRefMapping +module Brig.Schema.V69_MLSKeyPackageRefMapping ( migration, ) where diff --git a/services/brig/schema/src/V70_UserEmailUnvalidated.hs b/services/brig/src/Brig/Schema/V70_UserEmailUnvalidated.hs similarity index 96% rename from services/brig/schema/src/V70_UserEmailUnvalidated.hs rename to services/brig/src/Brig/Schema/V70_UserEmailUnvalidated.hs index 8a408dddee1..8373d8931fc 100644 --- a/services/brig/schema/src/V70_UserEmailUnvalidated.hs +++ b/services/brig/src/Brig/Schema/V70_UserEmailUnvalidated.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V70_UserEmailUnvalidated +module Brig.Schema.V70_UserEmailUnvalidated ( migration, ) where diff --git a/services/brig/schema/src/V71_AddTableVCodesThrottle.hs b/services/brig/src/Brig/Schema/V71_AddTableVCodesThrottle.hs similarity index 96% rename from services/brig/schema/src/V71_AddTableVCodesThrottle.hs rename to services/brig/src/Brig/Schema/V71_AddTableVCodesThrottle.hs index 7684653ec02..53b6d6baa86 100644 --- a/services/brig/schema/src/V71_AddTableVCodesThrottle.hs +++ b/services/brig/src/Brig/Schema/V71_AddTableVCodesThrottle.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V71_AddTableVCodesThrottle +module Brig.Schema.V71_AddTableVCodesThrottle ( migration, ) where diff --git a/services/brig/schema/src/V72_AddNonceTable.hs b/services/brig/src/Brig/Schema/V72_AddNonceTable.hs similarity index 96% rename from services/brig/schema/src/V72_AddNonceTable.hs rename to services/brig/src/Brig/Schema/V72_AddNonceTable.hs index 32c7b71012a..6d3f1d2fd34 100644 --- a/services/brig/schema/src/V72_AddNonceTable.hs +++ b/services/brig/src/Brig/Schema/V72_AddNonceTable.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V72_AddNonceTable +module Brig.Schema.V72_AddNonceTable ( migration, ) where diff --git a/services/brig/schema/src/V73_ReplaceNonceTable.hs b/services/brig/src/Brig/Schema/V73_ReplaceNonceTable.hs similarity index 96% rename from services/brig/schema/src/V73_ReplaceNonceTable.hs rename to services/brig/src/Brig/Schema/V73_ReplaceNonceTable.hs index 94b47f817a2..ad96cae9658 100644 --- a/services/brig/schema/src/V73_ReplaceNonceTable.hs +++ b/services/brig/src/Brig/Schema/V73_ReplaceNonceTable.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V73_ReplaceNonceTable +module Brig.Schema.V73_ReplaceNonceTable ( migration, ) where diff --git a/services/brig/schema/src/V74_AddOAuthTables.hs b/services/brig/src/Brig/Schema/V74_AddOAuthTables.hs similarity index 98% rename from services/brig/schema/src/V74_AddOAuthTables.hs rename to services/brig/src/Brig/Schema/V74_AddOAuthTables.hs index 45cdc018be7..5298b199f29 100644 --- a/services/brig/schema/src/V74_AddOAuthTables.hs +++ b/services/brig/src/Brig/Schema/V74_AddOAuthTables.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V74_AddOAuthTables +module Brig.Schema.V74_AddOAuthTables ( migration, ) where diff --git a/services/brig/schema/src/V75_AddOAuthCodeChallenge.hs b/services/brig/src/Brig/Schema/V75_AddOAuthCodeChallenge.hs similarity index 96% rename from services/brig/schema/src/V75_AddOAuthCodeChallenge.hs rename to services/brig/src/Brig/Schema/V75_AddOAuthCodeChallenge.hs index ebead11e8cd..62cb91e5dbe 100644 --- a/services/brig/schema/src/V75_AddOAuthCodeChallenge.hs +++ b/services/brig/src/Brig/Schema/V75_AddOAuthCodeChallenge.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V75_AddOAuthCodeChallenge +module Brig.Schema.V75_AddOAuthCodeChallenge ( migration, ) where diff --git a/services/brig/schema/src/V76_AddSupportedProtocols.hs b/services/brig/src/Brig/Schema/V76_AddSupportedProtocols.hs similarity index 94% rename from services/brig/schema/src/V76_AddSupportedProtocols.hs rename to services/brig/src/Brig/Schema/V76_AddSupportedProtocols.hs index 72365ff28ed..ddf5baa6361 100644 --- a/services/brig/schema/src/V76_AddSupportedProtocols.hs +++ b/services/brig/src/Brig/Schema/V76_AddSupportedProtocols.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V76_AddSupportedProtocols (migration) where +module Brig.Schema.V76_AddSupportedProtocols (migration) where import Cassandra.Schema import Imports diff --git a/services/brig/schema/src/V77_FederationRemotes.hs b/services/brig/src/Brig/Schema/V77_FederationRemotes.hs similarity index 96% rename from services/brig/schema/src/V77_FederationRemotes.hs rename to services/brig/src/Brig/Schema/V77_FederationRemotes.hs index 250164ceb81..1ab4ca90c6b 100644 --- a/services/brig/schema/src/V77_FederationRemotes.hs +++ b/services/brig/src/Brig/Schema/V77_FederationRemotes.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V77_FederationRemotes +module Brig.Schema.V77_FederationRemotes ( migration, ) where diff --git a/services/brig/schema/src/V78_ClientLastActive.hs b/services/brig/src/Brig/Schema/V78_ClientLastActive.hs similarity index 96% rename from services/brig/schema/src/V78_ClientLastActive.hs rename to services/brig/src/Brig/Schema/V78_ClientLastActive.hs index f0c7dec2bd0..2c80519700b 100644 --- a/services/brig/schema/src/V78_ClientLastActive.hs +++ b/services/brig/src/Brig/Schema/V78_ClientLastActive.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V78_ClientLastActive +module Brig.Schema.V78_ClientLastActive ( migration, ) where diff --git a/services/brig/schema/src/V79_ConnectionRemoteIndex.hs b/services/brig/src/Brig/Schema/V79_ConnectionRemoteIndex.hs similarity index 88% rename from services/brig/schema/src/V79_ConnectionRemoteIndex.hs rename to services/brig/src/Brig/Schema/V79_ConnectionRemoteIndex.hs index 729e81f72b6..cea8f360c1d 100644 --- a/services/brig/schema/src/V79_ConnectionRemoteIndex.hs +++ b/services/brig/src/Brig/Schema/V79_ConnectionRemoteIndex.hs @@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} -module V79_ConnectionRemoteIndex +module Brig.Schema.V79_ConnectionRemoteIndex ( migration, ) where diff --git a/services/brig/schema/src/V80_KeyPackageCiphersuite.hs b/services/brig/src/Brig/Schema/V80_KeyPackageCiphersuite.hs similarity index 97% rename from services/brig/schema/src/V80_KeyPackageCiphersuite.hs rename to services/brig/src/Brig/Schema/V80_KeyPackageCiphersuite.hs index 7e7ec8b21d8..43c5c5804fe 100644 --- a/services/brig/schema/src/V80_KeyPackageCiphersuite.hs +++ b/services/brig/src/Brig/Schema/V80_KeyPackageCiphersuite.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V80_KeyPackageCiphersuite +module Brig.Schema.V80_KeyPackageCiphersuite ( migration, ) where diff --git a/services/brig/schema/src/V_FUTUREWORK.hs b/services/brig/src/Brig/Schema/V_FUTUREWORK.hs similarity index 98% rename from services/brig/schema/src/V_FUTUREWORK.hs rename to services/brig/src/Brig/Schema/V_FUTUREWORK.hs index 1a9786fbb3f..d4e00c4ec19 100644 --- a/services/brig/schema/src/V_FUTUREWORK.hs +++ b/services/brig/src/Brig/Schema/V_FUTUREWORK.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V_FUTUREWORK +module Brig.Schema.V_FUTUREWORK ( migration, ) where From 9854a65c5baaf90a1c35ed8c58f517940c80748a Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:46:16 +0200 Subject: [PATCH 197/225] [WPB-3138] Moved gundeck schema migrations to gundeck lib. (#3643) * Moved gundeck migrations to gundeck. * Added a changelog. --- .../refactored-schema-version-tracking | 1 + services/gundeck/default.nix | 3 +- services/gundeck/gundeck.cabal | 34 ++++++------- services/gundeck/schema/Main.hs | 24 +++++++++ .../src/Main.hs => src/Gundeck/Schema/Run.hs} | 51 +++++++++++-------- .../{schema/src => src/Gundeck/Schema}/V1.hs | 2 +- .../{schema/src => src/Gundeck/Schema}/V10.hs | 2 +- .../{schema/src => src/Gundeck/Schema}/V2.hs | 2 +- .../{schema/src => src/Gundeck/Schema}/V3.hs | 2 +- .../{schema/src => src/Gundeck/Schema}/V4.hs | 2 +- .../{schema/src => src/Gundeck/Schema}/V5.hs | 2 +- .../{schema/src => src/Gundeck/Schema}/V6.hs | 2 +- .../{schema/src => src/Gundeck/Schema}/V7.hs | 2 +- .../{schema/src => src/Gundeck/Schema}/V8.hs | 2 +- .../{schema/src => src/Gundeck/Schema}/V9.hs | 2 +- 15 files changed, 80 insertions(+), 53 deletions(-) create mode 100644 changelog.d/5-internal/refactored-schema-version-tracking create mode 100644 services/gundeck/schema/Main.hs rename services/gundeck/{schema/src/Main.hs => src/Gundeck/Schema/Run.hs} (61%) rename services/gundeck/{schema/src => src/Gundeck/Schema}/V1.hs (98%) rename services/gundeck/{schema/src => src/Gundeck/Schema}/V10.hs (98%) rename services/gundeck/{schema/src => src/Gundeck/Schema}/V2.hs (97%) rename services/gundeck/{schema/src => src/Gundeck/Schema}/V3.hs (98%) rename services/gundeck/{schema/src => src/Gundeck/Schema}/V4.hs (97%) rename services/gundeck/{schema/src => src/Gundeck/Schema}/V5.hs (97%) rename services/gundeck/{schema/src => src/Gundeck/Schema}/V6.hs (98%) rename services/gundeck/{schema/src => src/Gundeck/Schema}/V7.hs (98%) rename services/gundeck/{schema/src => src/Gundeck/Schema}/V8.hs (97%) rename services/gundeck/{schema/src => src/Gundeck/Schema}/V9.hs (98%) diff --git a/changelog.d/5-internal/refactored-schema-version-tracking b/changelog.d/5-internal/refactored-schema-version-tracking new file mode 100644 index 00000000000..16d655ef96a --- /dev/null +++ b/changelog.d/5-internal/refactored-schema-version-tracking @@ -0,0 +1 @@ +Refactored schema version tracking from manually managed to automatic. diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index 18410fe9988..ddf49dc9faa 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -120,6 +120,7 @@ mkDerivation { mtl network-uri psqueues + raw-strings-qq resourcet retry safe-exceptions @@ -153,7 +154,6 @@ mkDerivation { cassandra-util containers exceptions - extended gundeck-types HsOpenSSL http-client @@ -167,7 +167,6 @@ mkDerivation { network-uri optparse-applicative random - raw-strings-qq retry safe tagged diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index 7a116433a4e..277c81fea78 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -16,6 +16,7 @@ flag static default: False library + -- cabal-fmt: expand src exposed-modules: Gundeck.API Gundeck.API.Internal @@ -42,6 +43,17 @@ library Gundeck.Redis Gundeck.Redis.HedisExtensions Gundeck.Run + Gundeck.Schema.Run + Gundeck.Schema.V1 + Gundeck.Schema.V10 + Gundeck.Schema.V2 + Gundeck.Schema.V3 + Gundeck.Schema.V4 + Gundeck.Schema.V5 + Gundeck.Schema.V6 + Gundeck.Schema.V7 + Gundeck.Schema.V8 + Gundeck.Schema.V9 Gundeck.ThreadBudget Gundeck.ThreadBudget.Internal Gundeck.Util @@ -130,6 +142,7 @@ library , mtl >=2.2 , network-uri >=2.6 , psqueues >=0.2.2 + , raw-strings-qq , resourcet >=1.1 , retry >=0.5 , safe-exceptions @@ -318,20 +331,7 @@ executable gundeck-integration executable gundeck-schema main-is: Main.hs - other-modules: - Paths_gundeck - V1 - V10 - V2 - V3 - V4 - V5 - V6 - V7 - V8 - V9 - - hs-source-dirs: schema/src + hs-source-dirs: schema default-extensions: NoImplicitPrelude AllowAmbiguousTypes @@ -380,12 +380,8 @@ executable gundeck-schema -threaded -Wredundant-constraints -Wunused-packages build-depends: - base - , cassandra-util - , extended + gundeck , imports - , raw-strings-qq - , types-common if flag(static) ld-options: -static diff --git a/services/gundeck/schema/Main.hs b/services/gundeck/schema/Main.hs new file mode 100644 index 00000000000..bf76183bd5e --- /dev/null +++ b/services/gundeck/schema/Main.hs @@ -0,0 +1,24 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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 Main where + +import Gundeck.Schema.Run qualified as Run +import Imports + +main :: IO () +main = Run.main diff --git a/services/gundeck/schema/src/Main.hs b/services/gundeck/src/Gundeck/Schema/Run.hs similarity index 61% rename from services/gundeck/schema/src/Main.hs rename to services/gundeck/src/Gundeck/Schema/Run.hs index 56ef98f095b..056363c2445 100644 --- a/services/gundeck/schema/src/Main.hs +++ b/services/gundeck/src/Gundeck/Schema/Run.hs @@ -15,23 +15,23 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main where +module Gundeck.Schema.Run where import Cassandra.Schema import Control.Exception (finally) +import Gundeck.Schema.V1 qualified as V1 +import Gundeck.Schema.V10 qualified as V10 +import Gundeck.Schema.V2 qualified as V2 +import Gundeck.Schema.V3 qualified as V3 +import Gundeck.Schema.V4 qualified as V4 +import Gundeck.Schema.V5 qualified as V5 +import Gundeck.Schema.V6 qualified as V6 +import Gundeck.Schema.V7 qualified as V7 +import Gundeck.Schema.V8 qualified as V8 +import Gundeck.Schema.V9 qualified as V9 import Imports import System.Logger.Extended qualified as Log import Util.Options -import V1 qualified -import V10 qualified -import V2 qualified -import V3 qualified -import V4 qualified -import V5 qualified -import V6 qualified -import V7 qualified -import V8 qualified -import V9 qualified main :: IO () main = do @@ -40,18 +40,25 @@ main = do migrateSchema l o - [ V1.migration, - V2.migration, - V3.migration, - V4.migration, - V5.migration, - V6.migration, - V7.migration, - V8.migration, - V9.migration, - V10.migration - ] + migrations `finally` Log.close l where desc = "Gundeck Cassandra Schema Migrations" defaultPath = "/etc/wire/gundeck/conf/gundeck-schema.yaml" + +lastSchemaVersion :: Int32 +lastSchemaVersion = migVersion $ last migrations + +migrations :: [Migration] +migrations = + [ V1.migration, + V2.migration, + V3.migration, + V4.migration, + V5.migration, + V6.migration, + V7.migration, + V8.migration, + V9.migration, + V10.migration + ] diff --git a/services/gundeck/schema/src/V1.hs b/services/gundeck/src/Gundeck/Schema/V1.hs similarity index 98% rename from services/gundeck/schema/src/V1.hs rename to services/gundeck/src/Gundeck/Schema/V1.hs index ae1a296ab99..f3b71a70ef1 100644 --- a/services/gundeck/schema/src/V1.hs +++ b/services/gundeck/src/Gundeck/Schema/V1.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V1 +module Gundeck.Schema.V1 ( migration, ) where diff --git a/services/gundeck/schema/src/V10.hs b/services/gundeck/src/Gundeck/Schema/V10.hs similarity index 98% rename from services/gundeck/schema/src/V10.hs rename to services/gundeck/src/Gundeck/Schema/V10.hs index d37ed269744..322c36cd7c6 100644 --- a/services/gundeck/schema/src/V10.hs +++ b/services/gundeck/src/Gundeck/Schema/V10.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V10 +module Gundeck.Schema.V10 ( migration, ) where diff --git a/services/gundeck/schema/src/V2.hs b/services/gundeck/src/Gundeck/Schema/V2.hs similarity index 97% rename from services/gundeck/schema/src/V2.hs rename to services/gundeck/src/Gundeck/Schema/V2.hs index a970bc2a2d8..175aa4d34b4 100644 --- a/services/gundeck/schema/src/V2.hs +++ b/services/gundeck/src/Gundeck/Schema/V2.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V2 +module Gundeck.Schema.V2 ( migration, ) where diff --git a/services/gundeck/schema/src/V3.hs b/services/gundeck/src/Gundeck/Schema/V3.hs similarity index 98% rename from services/gundeck/schema/src/V3.hs rename to services/gundeck/src/Gundeck/Schema/V3.hs index fb6055f8f49..09d94cfbfaf 100644 --- a/services/gundeck/schema/src/V3.hs +++ b/services/gundeck/src/Gundeck/Schema/V3.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V3 +module Gundeck.Schema.V3 ( migration, ) where diff --git a/services/gundeck/schema/src/V4.hs b/services/gundeck/src/Gundeck/Schema/V4.hs similarity index 97% rename from services/gundeck/schema/src/V4.hs rename to services/gundeck/src/Gundeck/Schema/V4.hs index e9f39f51b7d..ee2908219e2 100644 --- a/services/gundeck/schema/src/V4.hs +++ b/services/gundeck/src/Gundeck/Schema/V4.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V4 +module Gundeck.Schema.V4 ( migration, ) where diff --git a/services/gundeck/schema/src/V5.hs b/services/gundeck/src/Gundeck/Schema/V5.hs similarity index 97% rename from services/gundeck/schema/src/V5.hs rename to services/gundeck/src/Gundeck/Schema/V5.hs index 77e7907ea57..97abe2a4b32 100644 --- a/services/gundeck/schema/src/V5.hs +++ b/services/gundeck/src/Gundeck/Schema/V5.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V5 +module Gundeck.Schema.V5 ( migration, ) where diff --git a/services/gundeck/schema/src/V6.hs b/services/gundeck/src/Gundeck/Schema/V6.hs similarity index 98% rename from services/gundeck/schema/src/V6.hs rename to services/gundeck/src/Gundeck/Schema/V6.hs index d9b7ae9518d..9e2785244be 100644 --- a/services/gundeck/schema/src/V6.hs +++ b/services/gundeck/src/Gundeck/Schema/V6.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V6 +module Gundeck.Schema.V6 ( migration, ) where diff --git a/services/gundeck/schema/src/V7.hs b/services/gundeck/src/Gundeck/Schema/V7.hs similarity index 98% rename from services/gundeck/schema/src/V7.hs rename to services/gundeck/src/Gundeck/Schema/V7.hs index a5c1ac1ea90..c44dab3fbab 100644 --- a/services/gundeck/schema/src/V7.hs +++ b/services/gundeck/src/Gundeck/Schema/V7.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V7 +module Gundeck.Schema.V7 ( migration, ) where diff --git a/services/gundeck/schema/src/V8.hs b/services/gundeck/src/Gundeck/Schema/V8.hs similarity index 97% rename from services/gundeck/schema/src/V8.hs rename to services/gundeck/src/Gundeck/Schema/V8.hs index 50812186be3..914557eb6b0 100644 --- a/services/gundeck/schema/src/V8.hs +++ b/services/gundeck/src/Gundeck/Schema/V8.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V8 +module Gundeck.Schema.V8 ( migration, ) where diff --git a/services/gundeck/schema/src/V9.hs b/services/gundeck/src/Gundeck/Schema/V9.hs similarity index 98% rename from services/gundeck/schema/src/V9.hs rename to services/gundeck/src/Gundeck/Schema/V9.hs index 2583384eff0..6ba7b0d61bc 100644 --- a/services/gundeck/schema/src/V9.hs +++ b/services/gundeck/src/Gundeck/Schema/V9.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V9 +module Gundeck.Schema.V9 ( migration, ) where From 29fc8138b17df53607464da4da33d2f54225f4c4 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:54:53 +0200 Subject: [PATCH 198/225] Deflake federation offline notification test again. (#3644) --- integration/test/Test/Federation.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs index 3fc5b1d76c6..ba584b67bcc 100644 --- a/integration/test/Test/Federation.hs +++ b/integration/test/Test/Federation.hs @@ -130,5 +130,5 @@ testNotificationsForOfflineBackends = do -- void $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isOtherUser2LeaveUpConvNotif -- void $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDelUserLeaveDownConvNotif - delUserDeletedNotif <- nPayload $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isDeleteUserNotif + delUserDeletedNotif <- nPayload $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 5 isDeleteUserNotif objQid delUserDeletedNotif `shouldMatch` objQid delUser From d0ea46e87d5a6cf0d3b10512f60467d919d43711 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 12 Oct 2023 10:40:50 +0200 Subject: [PATCH 199/225] Fix empty case of push chunking Also add docs and unit tests --- .../src/Gundeck/Types/Push/V2.hs | 9 ++- libs/types-common/src/Data/Id.hs | 4 ++ services/cannon/cannon.cabal | 1 - services/cannon/test/Test/Cannon/Dict.hs | 4 -- services/galley/galley.cabal | 1 + .../galley/src/Galley/Intra/Push/Internal.hs | 55 +++++++++++-------- services/galley/test/unit/Run.hs | 2 + .../test/unit/Test/Galley/Intra/Push.hs | 42 ++++++++++++++ 8 files changed, 86 insertions(+), 32 deletions(-) create mode 100644 services/galley/test/unit/Test/Galley/Intra/Push.hs diff --git a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs index c82067837af..11fc40aad89 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs +++ b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs @@ -86,6 +86,7 @@ import Data.Set qualified as Set import Imports hiding (cs) import Wire.API.Message (Priority (..)) import Wire.API.Push.V2.Token +import Wire.Arbitrary ----------------------------------------------------------------------------- -- Route @@ -97,7 +98,8 @@ data Route RouteAny | -- | Avoids causing push notification for mobile clients. RouteDirect - deriving (Eq, Ord, Enum, Bounded, Show) + deriving (Eq, Ord, Enum, Bounded, Show, Generic) + deriving (Arbitrary) via GenericUniform Route instance FromJSON Route where parseJSON (String "any") = pure RouteAny @@ -116,14 +118,15 @@ data Recipient = Recipient _recipientRoute :: !Route, _recipientClients :: !RecipientClients } - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Generic) data RecipientClients = -- | All clients of some user RecipientClientsAll | -- | An explicit list of clients RecipientClientsSome (List1 ClientId) - deriving (Eq, Show, Ord) + deriving (Eq, Show, Ord, Generic) + deriving (Arbitrary) via GenericUniform RecipientClients makeLenses ''Recipient diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index 528725f888b..c96ae4dac2e 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -66,6 +66,7 @@ import Data.Attoparsec.ByteString.Char8 qualified as Atto import Data.Bifunctor (first) import Data.Binary import Data.ByteString.Builder (byteString) +import Data.ByteString.Char8 qualified as B8 import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as L import Data.Char qualified as Char @@ -298,6 +299,9 @@ instance S.ToParamSchema ConnId where instance FromHttpApiData ConnId where parseUrlPiece = Right . ConnId . encodeUtf8 +instance Arbitrary ConnId where + arbitrary = ConnId . B8.pack <$> resize 10 (listOf arbitraryPrintableChar) + -- ClientId -------------------------------------------------------------------- -- | Handle for a device. Corresponds to the device fingerprints exposed in the UI. It is unique diff --git a/services/cannon/cannon.cabal b/services/cannon/cannon.cabal index 89f6159ffba..27429695a74 100644 --- a/services/cannon/cannon.cabal +++ b/services/cannon/cannon.cabal @@ -249,7 +249,6 @@ test-suite cannon-tests , tasty >=0.8 , tasty-hunit >=0.8 , tasty-quickcheck >=0.8 - , types-common , uuid , wire-api diff --git a/services/cannon/test/Test/Cannon/Dict.hs b/services/cannon/test/Test/Cannon/Dict.hs index e3fe085c66b..114edea4a9b 100644 --- a/services/cannon/test/Test/Cannon/Dict.hs +++ b/services/cannon/test/Test/Cannon/Dict.hs @@ -24,7 +24,6 @@ import Cannon.Dict qualified as D import Cannon.WS (Key, mkKey) import Control.Concurrent.Async import Data.ByteString.Lazy qualified as Lazy -import Data.Id import Data.List qualified as List import Data.UUID hiding (fromString) import Data.UUID.V4 @@ -122,6 +121,3 @@ runProp = monadicIO . forAllM arbitrary instance Arbitrary Key where arbitrary = mkKey <$> arbitrary <*> arbitrary - -instance Arbitrary ConnId where - arbitrary = ConnId <$> arbitrary diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index a35b727e888..83618be89cf 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -628,6 +628,7 @@ test-suite galley-tests Test.Galley.API.Action Test.Galley.API.Message Test.Galley.API.One2One + Test.Galley.Intra.Push Test.Galley.Intra.User Test.Galley.Mapping diff --git a/services/galley/src/Galley/Intra/Push/Internal.hs b/services/galley/src/Galley/Intra/Push/Internal.hs index 272a4593493..78763164653 100644 --- a/services/galley/src/Galley/Intra/Push/Internal.hs +++ b/services/galley/src/Galley/Intra/Push/Internal.hs @@ -43,6 +43,7 @@ import Wire.API.Event.FeatureConfig qualified as FeatureConfig import Wire.API.Event.Federation qualified as Federation import Wire.API.Event.Team qualified as Teams import Wire.API.Team.Member +import Wire.Arbitrary data PushEvent = ConvEvent Event @@ -60,7 +61,8 @@ data RecipientBy user = Recipient { _recipientUserId :: user, _recipientClients :: RecipientClients } - deriving stock (Functor, Foldable, Traversable, Show, Ord, Eq) + deriving stock (Functor, Foldable, Traversable, Show, Ord, Eq, Generic) + deriving (Arbitrary) via GenericUniform (RecipientBy user) makeLenses ''RecipientBy @@ -77,7 +79,8 @@ data PushTo user = Push pushJson :: Object, pushRecipientListType :: ListType } - deriving stock (Functor, Foldable, Traversable, Show) + deriving stock (Eq, Generic, Functor, Foldable, Traversable, Show) + deriving (Arbitrary) via GenericUniform (PushTo user) makeLenses ''PushTo @@ -93,32 +96,24 @@ push ps = do nonEmpty (toList (_pushRecipients p)) <&> \nonEmptyRecipients -> p {_pushRecipients = List1 nonEmptyRecipients} --- | Asynchronously send multiple pushes, aggregating them into as --- few requests as possible, such that no single request targets --- more than 128 recipients. -pushLocal :: NonEmpty (PushTo UserId) -> App () -pushLocal ps = do - opts <- view options - let limit = currentFanoutLimit opts - -- Do not fan out for very large teams - let (asyncs, syncs) = partition _pushAsync (removeIfLargeFanout limit $ toList ps) - traverse_ (asyncCall Gundeck <=< jsonChunkedIO) (pushes asyncs) - mapConcurrently_ (call Gundeck <=< jsonChunkedIO) (pushes syncs) +-- | Split a list of pushes into chunks with the given maximum number of +-- recipients. maxRecipients must be strictly positive. Note that the order of +-- pushes within a chunk is reversed compared to the order of the input list. +chunkPushes :: Int -> [PushTo a] -> [[PushTo a]] +chunkPushes maxRecipients | maxRecipients <= 0 = error "maxRecipients must be positive" +chunkPushes maxRecipients = go 0 [] where - pushes :: [PushTo UserId] -> [[Gundeck.Push]] - pushes = map (map (\p -> toPush p (recipientList p))) . chunk 0 [] - - chunk :: Int -> [PushTo a] -> [PushTo a] -> [[PushTo a]] - chunk _ acc [] = [acc] - chunk n acc (y : ys) - | n >= maxRecipients = acc : chunk 0 [] (y : ys) + go _ [] [] = [] + go _ acc [] = [acc] + go n acc (y : ys) + | n >= maxRecipients = acc : go 0 [] (y : ys) | otherwise = let totalLength = (n + length (_pushRecipients y)) in if totalLength > maxRecipients then let (y1, y2) = splitPush (maxRecipients - n) y - in chunk maxRecipients (y1 : acc) (y2 : ys) - else chunk totalLength (y : acc) ys + in go maxRecipients (y1 : acc) (y2 : ys) + else go totalLength (y : acc) ys -- n must be strictly > 0 and < length (_pushRecipients p) splitPush :: Int -> PushTo a -> (PushTo a, PushTo a) @@ -126,8 +121,20 @@ pushLocal ps = do let (r1, r2) = splitAt n (toList (_pushRecipients p)) in (p {_pushRecipients = fromJust $ maybeList1 r1}, p {_pushRecipients = fromJust $ maybeList1 r2}) - maxRecipients :: Int - maxRecipients = 128 +-- | Asynchronously send multiple pushes, aggregating them into as +-- few requests as possible, such that no single request targets +-- more than 128 recipients. +pushLocal :: NonEmpty (PushTo UserId) -> App () +pushLocal ps = do + opts <- view options + let limit = currentFanoutLimit opts + -- Do not fan out for very large teams + let (asyncs, syncs) = partition _pushAsync (removeIfLargeFanout limit $ toList ps) + traverse_ (asyncCall Gundeck <=< jsonChunkedIO) (pushes asyncs) + mapConcurrently_ (call Gundeck <=< jsonChunkedIO) (pushes syncs) + where + pushes :: [PushTo UserId] -> [[Gundeck.Push]] + pushes = map (map (\p -> toPush p (recipientList p))) . chunkPushes 128 recipientList :: PushTo UserId -> [Gundeck.Recipient] recipientList p = map (toRecipient p) . toList $ _pushRecipients p diff --git a/services/galley/test/unit/Run.hs b/services/galley/test/unit/Run.hs index bcf3593c74c..4f28468fadc 100644 --- a/services/galley/test/unit/Run.hs +++ b/services/galley/test/unit/Run.hs @@ -24,6 +24,7 @@ import Imports import Test.Galley.API.Action qualified import Test.Galley.API.Message qualified import Test.Galley.API.One2One qualified +import Test.Galley.Intra.Push qualified import Test.Galley.Intra.User qualified import Test.Galley.Mapping qualified import Test.Tasty @@ -36,6 +37,7 @@ main = [ Test.Galley.API.Message.tests, Test.Galley.API.One2One.tests, Test.Galley.Intra.User.tests, + Test.Galley.Intra.Push.tests, Test.Galley.Mapping.tests, Test.Galley.API.Action.tests ] diff --git a/services/galley/test/unit/Test/Galley/Intra/Push.hs b/services/galley/test/unit/Test/Galley/Intra/Push.hs new file mode 100644 index 00000000000..5f5574f0ccd --- /dev/null +++ b/services/galley/test/unit/Test/Galley/Intra/Push.hs @@ -0,0 +1,42 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 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.Intra.Push where + +import Data.List1 qualified as List1 +import Galley.Intra.Push.Internal +import Imports +import Test.QuickCheck +import Test.Tasty +import Test.Tasty.QuickCheck + +normalisePush :: PushTo a -> [PushTo a] +normalisePush p = + map + (\r -> p {_pushRecipients = List1.singleton r}) + (toList (_pushRecipients p)) + +tests :: TestTree +tests = + testGroup + "chunkPushes" + [ testProperty "empty push" $ \(Positive limit) -> + chunkPushes limit [] === ([] :: [[PushTo ()]]), + testProperty "concatenation" $ \(Positive limit) (pushes :: [PushTo Int]) -> + (chunkPushes limit pushes >>= reverse >>= normalisePush) + === (pushes >>= normalisePush) + ] From f8f65e224e68ccdaf92112d61a8e14a8884b8d67 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 12 Oct 2023 11:13:05 +0200 Subject: [PATCH 200/225] Add one more chunking test --- services/galley/test/unit/Test/Galley/Intra/Push.hs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/galley/test/unit/Test/Galley/Intra/Push.hs b/services/galley/test/unit/Test/Galley/Intra/Push.hs index 5f5574f0ccd..c3dc1fd260c 100644 --- a/services/galley/test/unit/Test/Galley/Intra/Push.hs +++ b/services/galley/test/unit/Test/Galley/Intra/Push.hs @@ -18,6 +18,7 @@ module Test.Galley.Intra.Push where import Data.List1 qualified as List1 +import Data.Monoid import Galley.Intra.Push.Internal import Imports import Test.QuickCheck @@ -30,6 +31,9 @@ normalisePush p = (\r -> p {_pushRecipients = List1.singleton r}) (toList (_pushRecipients p)) +chunkSize :: [PushTo a] -> Int +chunkSize = getSum . foldMap (Sum . length . _pushRecipients) + tests :: TestTree tests = testGroup @@ -38,5 +42,7 @@ tests = chunkPushes limit [] === ([] :: [[PushTo ()]]), testProperty "concatenation" $ \(Positive limit) (pushes :: [PushTo Int]) -> (chunkPushes limit pushes >>= reverse >>= normalisePush) - === (pushes >>= normalisePush) + === (pushes >>= normalisePush), + testProperty "small chunks" $ \(Positive limit) (pushes :: [PushTo Int]) -> + all ((<= limit) . chunkSize) (chunkPushes limit pushes) ] From 3ce12a3c32042fd827c4bd6c67cdeec88c0db585 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 12 Oct 2023 11:14:01 +0200 Subject: [PATCH 201/225] Add CHANGELOG entry --- changelog.d/5-internal/empty-push | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5-internal/empty-push diff --git a/changelog.d/5-internal/empty-push b/changelog.d/5-internal/empty-push new file mode 100644 index 00000000000..f30fe164e8f --- /dev/null +++ b/changelog.d/5-internal/empty-push @@ -0,0 +1 @@ +Avoid empty pushes when chunking pushes in galley From 686f0d2110e03d45c6cb0535b2ffdf35410b2e9c Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 12 Oct 2023 11:33:59 +0200 Subject: [PATCH 202/225] Check that there are no empty chunks --- services/galley/test/unit/Test/Galley/Intra/Push.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/galley/test/unit/Test/Galley/Intra/Push.hs b/services/galley/test/unit/Test/Galley/Intra/Push.hs index c3dc1fd260c..81ad45f8aab 100644 --- a/services/galley/test/unit/Test/Galley/Intra/Push.hs +++ b/services/galley/test/unit/Test/Galley/Intra/Push.hs @@ -40,6 +40,8 @@ tests = "chunkPushes" [ testProperty "empty push" $ \(Positive limit) -> chunkPushes limit [] === ([] :: [[PushTo ()]]), + testProperty "no empty chunk" $ \(Positive limit) (pushes :: [PushTo Int]) -> + not (any null (chunkPushes limit pushes)), testProperty "concatenation" $ \(Positive limit) (pushes :: [PushTo Int]) -> (chunkPushes limit pushes >>= reverse >>= normalisePush) === (pushes >>= normalisePush), From 7c5db1907a4f366adef1bc3babfadec56e0b808a Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 12 Oct 2023 11:35:11 +0200 Subject: [PATCH 203/225] Linter --- services/cannon/default.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/services/cannon/default.nix b/services/cannon/default.nix index 1032b92eb15..b1ff1ab1d2c 100644 --- a/services/cannon/default.nix +++ b/services/cannon/default.nix @@ -109,7 +109,6 @@ mkDerivation { tasty tasty-hunit tasty-quickcheck - types-common uuid wire-api ]; From f6e9c72b18cb47c96e755ef1be2db772e0915200 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 12 Oct 2023 15:02:31 +0200 Subject: [PATCH 204/225] Fix copyright year MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marko Dimjašević --- services/galley/test/unit/Test/Galley/Intra/Push.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/galley/test/unit/Test/Galley/Intra/Push.hs b/services/galley/test/unit/Test/Galley/Intra/Push.hs index 81ad45f8aab..daf35389e63 100644 --- a/services/galley/test/unit/Test/Galley/Intra/Push.hs +++ b/services/galley/test/unit/Test/Galley/Intra/Push.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2023 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 From a38cd772f49289537b83d629243623205c72f451 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Thu, 12 Oct 2023 17:26:40 +0200 Subject: [PATCH 205/225] add DNS troubleshooting documentation --- docs/src/how-to/install/troubleshooting.md | 151 +++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/docs/src/how-to/install/troubleshooting.md b/docs/src/how-to/install/troubleshooting.md index de2d857e4be..62ebef70323 100644 --- a/docs/src/how-to/install/troubleshooting.md +++ b/docs/src/how-to/install/troubleshooting.md @@ -263,3 +263,154 @@ p: the expected ping (how many pings have not returned) Question: Are the connection values for bad networks/disconnect configurable on on-prem? Answer: The values are not currently configurable, they are built into the clients at compile time, we do have a mechanism for sending calling configs to the clients but these values are not currently there. + +## Verifying correct deployment of DNS / DNS troubleshooting. + +After installation, or if you meet some functionality problems, you should check that your DNS setup is correct. + +You'll do this from either your own computer (any public computer connected to the Internet), or from the Wire backend itself. + +### Testing public domains. + +From your own computer (not from the Wire backend), test that you can reach all sub-domains you setup during the Wire installation: + +* `assets.youdomain.com` +* `teams.yourdomain.com` +* `webapp.yourdomain.com` +* `accounts.yourdomain.com` +* etc... + +You can test if a domain is reachable by typing in your local terminal: + +``` +nslookup assets.yourdomain.com +``` + +If the domain is succesfully resolved, you should see something like: + +``` +Server: 127.0.0.53 +Address: 127.0.0.53#53 + +Non-authoritative answer: +Name: assets.yourdomain.com +Address: 388.114.97.2 +``` + +And if the domain can not be resolved, it will be something like this: + +``` +Server: 127.0.0.53 +Address: 127.0.0.53#53 + +** server can't find test.fra: NXDOMAIN +``` + +Do this for each and every of the domains you configured, make sure each of them is reachable from the open Internet. + +If a domain can not be reached, check your DNS configuration and make sure to solve the issue. + +### Testing internal domain resolution. + +Open a shell inside the SNS pod, and make sure you can resolve the following three domains: + +* `minio-external` +* `cassandra-external` +* `elasticsearch-external` + +First get a list of all pods: + +``` +kubectl get pods --all-namespaces +``` + +In here, find the sns pod (usually its name contains `fake-aws-sns`). + +Open a shell into that pod: + +``` +kubectl exec -it my-sns-pod-name -- /bin/sh +``` + +From inside the pod, you should now test each domain: + +``` +nslookup minio-external +``` + +If the domain is succesfully resolved, you should see something like: + +``` +Server: 127.0.0.53 +Address: 127.0.0.53#53 + +Non-authoritative answer: +Name: minio-external +Address: 173.188.1.14 +``` + +And if the domain can not be resolved, it will be something like this: + +``` +Server: 127.0.0.53 +Address: 127.0.0.53#53 + +** server can't find test.fra: NXDOMAIN +``` + +If you can not reach some of the domains from the SNS pod, you need to try those from one of the servers running kubernetes (kubernetes host): + +``` +ssh kubernetes-server +``` + +Then try the same thing using `nslookup`. + +If either of these steps fail, please request support. + +### Testing reachability of AWS. + +First off, use the Amazon AWS documentation to determine your region code: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html + +Here we will use `us-west-1` but please change this to whichever is closest to your server as needed. + +First list all pods: + +``` +kubectl get pods --all-namespaces +``` + +In here, find the sns pod (usually its name contains `fake-aws-sns`). + +Open a shell into that pod: + +``` +kubectl exec -it my-sns-pod-name -- /bin/sh +``` + +And test the reachability of the AWS services: + +``` +nslookup sqs.us-west-1.amazonaws.com +``` + +If it can be reached, you'll see something like this: + +``` +Server: 127.0.0.53 +Address: 127.0.0.53#53 + +Non-authoritative answer: +sqs.us-west-1.amazonaws.com canonical name = us-west-1.queue.amazonaws.com. +Name: us-west-1.queue.amazonaws.com +Address: 3.101.114.18 +``` + +And if it can't: + +``` +Server: 127.0.0.53 +Address: 127.0.0.53#53 + +** server can't find sqs.us-west-1.amazonaws.com: NXDOMAIN +``` \ No newline at end of file From f613b24ef1d5b24a27f555b2f3a426b4eef99395 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Thu, 12 Oct 2023 17:37:24 +0200 Subject: [PATCH 206/225] correct minor mistake --- docs/src/how-to/install/troubleshooting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/how-to/install/troubleshooting.md b/docs/src/how-to/install/troubleshooting.md index 62ebef70323..be526562241 100644 --- a/docs/src/how-to/install/troubleshooting.md +++ b/docs/src/how-to/install/troubleshooting.md @@ -303,7 +303,7 @@ And if the domain can not be resolved, it will be something like this: Server: 127.0.0.53 Address: 127.0.0.53#53 -** server can't find test.fra: NXDOMAIN +** server can't find assets.yourdomain.com: NXDOMAIN ``` Do this for each and every of the domains you configured, make sure each of them is reachable from the open Internet. @@ -355,7 +355,7 @@ And if the domain can not be resolved, it will be something like this: Server: 127.0.0.53 Address: 127.0.0.53#53 -** server can't find test.fra: NXDOMAIN +** server can't find minio-external: NXDOMAIN ``` If you can not reach some of the domains from the SNS pod, you need to try those from one of the servers running kubernetes (kubernetes host): From 3fdf6069322261df73c1d561b33443484c9d1425 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Thu, 12 Oct 2023 17:43:43 +0200 Subject: [PATCH 207/225] moving try-again-on-host instructions from extrenal- to aws- --- docs/src/how-to/install/troubleshooting.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/src/how-to/install/troubleshooting.md b/docs/src/how-to/install/troubleshooting.md index be526562241..92b1437b39c 100644 --- a/docs/src/how-to/install/troubleshooting.md +++ b/docs/src/how-to/install/troubleshooting.md @@ -358,15 +358,7 @@ Address: 127.0.0.53#53 ** server can't find minio-external: NXDOMAIN ``` -If you can not reach some of the domains from the SNS pod, you need to try those from one of the servers running kubernetes (kubernetes host): - -``` -ssh kubernetes-server -``` - -Then try the same thing using `nslookup`. - -If either of these steps fail, please request support. +If you can not resolve any of the three domains, please request support. ### Testing reachability of AWS. @@ -413,4 +405,14 @@ Server: 127.0.0.53 Address: 127.0.0.53#53 ** server can't find sqs.us-west-1.amazonaws.com: NXDOMAIN -``` \ No newline at end of file +``` + +If you can not reach the AWS domain from the SNS pod, you need to try those from one of the servers running kubernetes (kubernetes host): + +``` +ssh kubernetes-server +``` + +Then try the same thing using `nslookup`. + +If either of these steps fail, please request support. \ No newline at end of file From 0617c30593878d8d2c0dd34710175ec08ff3182a Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Thu, 12 Oct 2023 17:48:27 +0200 Subject: [PATCH 208/225] full list of domains` --- docs/src/how-to/install/troubleshooting.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/src/how-to/install/troubleshooting.md b/docs/src/how-to/install/troubleshooting.md index 92b1437b39c..0c3d4265743 100644 --- a/docs/src/how-to/install/troubleshooting.md +++ b/docs/src/how-to/install/troubleshooting.md @@ -274,11 +274,18 @@ You'll do this from either your own computer (any public computer connected to t From your own computer (not from the Wire backend), test that you can reach all sub-domains you setup during the Wire installation: -* `assets.youdomain.com` -* `teams.yourdomain.com` -* `webapp.yourdomain.com` -* `accounts.yourdomain.com` -* etc... +* `assets.` +* `teams.` +* `webapp.` +* `accounts.` +* `nginz-https.` +* `nginz-ssl.` +* `sftd.` +* `restund01.` +* `restund02.` +* `federator.` + +Some domains (such as the federator) might not apply to your setup. Refer to the domains you configured during installation, and act accordingly. You can test if a domain is reachable by typing in your local terminal: From 3433e249e9bf2d5e838d0a0444729f1abcf36228 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Thu, 12 Oct 2023 17:49:58 +0200 Subject: [PATCH 209/225] add note about values.yaml` --- docs/src/how-to/install/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/how-to/install/troubleshooting.md b/docs/src/how-to/install/troubleshooting.md index 0c3d4265743..85a2bc95e03 100644 --- a/docs/src/how-to/install/troubleshooting.md +++ b/docs/src/how-to/install/troubleshooting.md @@ -371,7 +371,7 @@ If you can not resolve any of the three domains, please request support. First off, use the Amazon AWS documentation to determine your region code: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html -Here we will use `us-west-1` but please change this to whichever is closest to your server as needed. +Here we will use `us-west-1` but please change this to whichever value you set in your `values.yaml` file during installation. First list all pods: From 43da11cbd860acc28f988df37e867d933c15a00c Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 13 Oct 2023 15:14:26 +0200 Subject: [PATCH 210/225] Fix MLS message notification bug (#3610) * Add test reproducing MLS notification bug * Collect recipients by user before pushing notif * Fix remote MLS message notifications Reorganise remote MLS message recipients by user, so that notifications can be more easily reconstructed on the receiving side. --- changelog.d/3-bug-fixes/mls-notification-bug | 1 + integration/test/Test/MLS/Message.hs | 33 +++++++++++++++++++ .../src/Gundeck/Types/Push/V2.hs | 6 ++++ .../src/Wire/API/Federation/API/Galley.hs | 3 +- services/galley/src/Galley/API/Federation.hs | 11 +++++-- .../galley/src/Galley/API/MLS/Propagate.hs | 32 +++++++++++------- services/galley/src/Galley/API/Push.hs | 28 +++++++++------- .../galley/src/Galley/Intra/Push/Internal.hs | 1 + services/galley/test/integration/API/MLS.hs | 3 +- 9 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 changelog.d/3-bug-fixes/mls-notification-bug diff --git a/changelog.d/3-bug-fixes/mls-notification-bug b/changelog.d/3-bug-fixes/mls-notification-bug new file mode 100644 index 00000000000..cfe1d68289d --- /dev/null +++ b/changelog.d/3-bug-fixes/mls-notification-bug @@ -0,0 +1 @@ +Fix bug where notifications for MLS messages were not showing up in all notification streams of clients diff --git a/integration/test/Test/MLS/Message.hs b/integration/test/Test/MLS/Message.hs index 88dce5a28d1..7282cfd700e 100644 --- a/integration/test/Test/MLS/Message.hs +++ b/integration/test/Test/MLS/Message.hs @@ -1,5 +1,8 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + module Test.MLS.Message where +import API.Gundeck import MLS.Util import Notifications import SetupHelpers @@ -50,3 +53,33 @@ testAppMessageSomeReachable = do pure alice1 void $ createApplicationMessage alice1 "hi, bob!" >>= sendAndConsumeMessage + +testMessageNotifications :: HasCallStack => Domain -> App () +testMessageNotifications bobDomain = do + [alice, bob] <- createAndConnectUsers [OwnDomain, bobDomain] + + [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def) [alice, alice, bob, bob] + bobClient <- bob1 %. "client_id" & asString + + traverse_ uploadNewKeyPackage [alice1, alice2, bob1, bob2] + + void $ createNewGroup alice1 + + void $ withWebSocket bob $ \ws -> do + void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle + awaitMatch 10 isMemberJoinNotif ws + + let get (opts :: GetNotifications) = do + notifs <- getNotifications bob opts {size = Just 10000} >>= getJSON 200 + notifs %. "has_more" `shouldMatch` False + length <$> (notifs %. "notifications" & asList) + + numNotifs <- get def + numNotifsClient <- get def {client = Just bobClient} + + void $ withWebSocket bob $ \ws -> do + void $ createApplicationMessage alice1 "hi bob" >>= sendAndConsumeMessage + awaitMatch 10 isNewMLSMessageNotif ws + + get def `shouldMatchInt` (numNotifs + 1) + get def {client = Just bobClient} `shouldMatchInt` (numNotifsClient + 1) diff --git a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs index 11fc40aad89..9b26ab71543 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs +++ b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs @@ -128,6 +128,12 @@ data RecipientClients deriving (Eq, Show, Ord, Generic) deriving (Arbitrary) via GenericUniform RecipientClients +instance Semigroup RecipientClients where + RecipientClientsAll <> _ = RecipientClientsAll + _ <> RecipientClientsAll = RecipientClientsAll + RecipientClientsSome cs1 <> RecipientClientsSome cs2 = + RecipientClientsSome (cs1 <> cs2) + makeLenses ''Recipient recipient :: UserId -> Route -> Recipient 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 7e042a0afa4..a635ee1cbfa 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 @@ -21,6 +21,7 @@ import Data.Aeson (FromJSON, ToJSON) import Data.Domain import Data.Id import Data.Json.Util +import Data.List.NonEmpty (NonEmpty) import Data.Misc (Milliseconds) import Data.Qualified import Data.Range @@ -348,7 +349,7 @@ data RemoteMLSMessage = RemoteMLSMessage sender :: Qualified UserId, conversation :: ConvId, subConversation :: Maybe SubConvId, - recipients :: [(UserId, ClientId)], + recipients :: Map UserId (NonEmpty ClientId), message :: Base64ByteString } deriving stock (Eq, Show, Generic) diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index cced908c9f6..d7f2e3539a1 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -27,6 +27,7 @@ import Data.ByteString.Conversion (toByteString') import Data.Domain (Domain) import Data.Id import Data.Json.Util +import Data.List1 (List1 (..)) import Data.Map qualified as Map import Data.Map.Lens (toMapOf) import Data.Qualified @@ -57,10 +58,12 @@ import Galley.Effects import Galley.Effects.ConversationStore qualified as E import Galley.Effects.FireAndForget qualified as E import Galley.Effects.MemberStore qualified as E +import Galley.Intra.Push.Internal hiding (push) import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.Conversations.One2One import Galley.Types.UserList (UserList (UserList)) +import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Polysemy import Polysemy.Error @@ -781,7 +784,7 @@ onMLSMessageSent domain rmm = assertMLSEnabled loc <- qualifyLocal () let rcnv = toRemoteUnsafe domain rmm.conversation - let users = Set.fromList (map fst rmm.recipients) + let users = Map.keys rmm.recipients (members, allMembers) <- first Set.fromList <$> E.selectRemoteMembers (toList users) rcnv @@ -794,7 +797,11 @@ onMLSMessageSent domain rmm = \ users not in the conversation" :: ByteString ) - let recipients = filter (\(u, _) -> Set.member u members) rmm.recipients + let recipients = + filter (\r -> Set.member (_recipientUserId r) members) + . map (\(u, clts) -> Recipient u (RecipientClientsSome (List1 clts))) + . Map.assocs + $ rmm.recipients -- FUTUREWORK: support local bots let e = Event (tUntagged rcnv) rmm.subConversation rmm.sender rmm.time $ diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index da90671702c..e9f6ac089d7 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -20,6 +20,8 @@ module Galley.API.MLS.Propagate where import Control.Comonad import Data.Id import Data.Json.Util +import Data.List.NonEmpty (NonEmpty, nonEmpty) +import Data.List1 import Data.Map qualified as Map import Data.Qualified import Data.Time @@ -29,7 +31,9 @@ import Galley.API.Util import Galley.Data.Services import Galley.Effects import Galley.Effects.BackendNotificationQueueAccess +import Galley.Intra.Push.Internal import Galley.Types.Conversations.Members +import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Network.AMQP qualified as Q import Polysemy @@ -80,7 +84,7 @@ propagateMessage qusr mSenderClient lConvOrSub con msg cm = do e = Event qcnv sconv qusr now $ EdMLSMessage msg.raw runMessagePush lConvOrSub (Just qcnv) $ - newMessagePush botMap con mm (lmems >>= localMemberMLSClients mlsConv) e + newMessagePush botMap con mm (lmems >>= toList . localMemberRecipient mlsConv) e -- send to remotes (either (logRemoteNotificationError @"on-mls-message-sent") (const (pure ())) <=< enqueueNotificationsConcurrently Q.Persistent (map remoteMemberQualify rmems)) $ @@ -92,23 +96,27 @@ propagateMessage qusr mSenderClient lConvOrSub con msg cm = do metadata = mm, conversation = qUnqualified qcnv, subConversation = sconv, - recipients = tUnqualified rs >>= remoteMemberMLSClients, + recipients = + Map.fromList $ + tUnqualified rs + >>= toList . remoteMemberMLSClients, message = Base64ByteString msg.raw } where cmWithoutSender = maybe cm (flip cmRemoveClient cm . mkClientIdentity qusr) mSenderClient - localMemberMLSClients :: Local x -> LocalMember -> [(UserId, ClientId)] - localMemberMLSClients loc lm = + + localMemberRecipient :: Local x -> LocalMember -> Maybe Recipient + localMemberRecipient loc lm = do let localUserQId = tUntagged (qualifyAs loc localUserId) localUserId = lmId lm - in map - (\(c, _) -> (localUserId, c)) - (Map.assocs (Map.findWithDefault mempty localUserQId cmWithoutSender)) + clients <- nonEmpty $ Map.keys (Map.findWithDefault mempty localUserQId cmWithoutSender) + pure $ Recipient localUserId (RecipientClientsSome (List1 clients)) - remoteMemberMLSClients :: RemoteMember -> [(UserId, ClientId)] - remoteMemberMLSClients rm = + remoteMemberMLSClients :: RemoteMember -> Maybe (UserId, NonEmpty ClientId) + remoteMemberMLSClients rm = do let remoteUserQId = tUntagged (rmId rm) remoteUserId = qUnqualified remoteUserQId - in map - (\(c, _) -> (remoteUserId, c)) - (Map.assocs (Map.findWithDefault mempty remoteUserQId cmWithoutSender)) + clients <- + nonEmpty . map fst $ + Map.assocs (Map.findWithDefault mempty remoteUserQId cmWithoutSender) + pure (remoteUserId, clients) diff --git a/services/galley/src/Galley/API/Push.hs b/services/galley/src/Galley/API/Push.hs index 51ba1db90f1..786a805a293 100644 --- a/services/galley/src/Galley/API/Push.hs +++ b/services/galley/src/Galley/API/Push.hs @@ -53,22 +53,28 @@ data MessagePush type BotMap = Map UserId BotMember +class ToRecipient a where + toRecipient :: a -> Recipient + +instance ToRecipient (UserId, ClientId) where + toRecipient (u, c) = Recipient u (RecipientClientsSome (List1.singleton c)) + +instance ToRecipient Recipient where + toRecipient = id + newMessagePush :: + ToRecipient r => BotMap -> Maybe ConnId -> MessageMetadata -> - [(UserId, ClientId)] -> + [r] -> Event -> MessagePush newMessagePush botMap mconn mm userOrBots event = - let (recipients, botMembers) = - foldMap - ( \(u, c) -> - case Map.lookup u botMap of - Just botMember -> ([], [botMember]) - Nothing -> ([Recipient u (RecipientClientsSome (List1.singleton c))], []) - ) - userOrBots + let toPair r = case Map.lookup (_recipientUserId r) botMap of + Just botMember -> ([], [botMember]) + Nothing -> ([r], []) + (recipients, botMembers) = foldMap (toPair . toRecipient) userOrBots in MessagePush mconn mm recipients botMembers event runMessagePush :: @@ -90,9 +96,9 @@ runMessagePush loc mqcnv mp@(MessagePush _ _ _ botMembers event) = do else deliverAndDeleteAsync (qUnqualified qcnv) (map (,event) botMembers) toPush :: MessagePush -> Maybe Push -toPush (MessagePush mconn mm userRecipients _ event) = +toPush (MessagePush mconn mm rs _ event) = let usr = qUnqualified (evtFrom event) - in newPush ListComplete (Just usr) (ConvEvent event) userRecipients + in newPush ListComplete (Just usr) (ConvEvent event) rs <&> set pushConn mconn . set pushNativePriority (mmNativePriority mm) . set pushRoute (bool RouteDirect RouteAny (mmNativePush mm)) diff --git a/services/galley/src/Galley/Intra/Push/Internal.hs b/services/galley/src/Galley/Intra/Push/Internal.hs index 78763164653..4adcf716f73 100644 --- a/services/galley/src/Galley/Intra/Push/Internal.hs +++ b/services/galley/src/Galley/Intra/Push/Internal.hs @@ -24,6 +24,7 @@ import Control.Lens (makeLenses, set, view, (.~)) import Data.Aeson (Object) import Data.Id (ConnId, UserId) import Data.Json.Util +import Data.List.Extra import Data.List.NonEmpty (NonEmpty, nonEmpty) import Data.List1 import Data.Qualified diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 6bb5d767a76..806a43b3b33 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -32,6 +32,7 @@ import Data.Aeson qualified as Aeson import Data.Domain import Data.Id import Data.Json.Util hiding ((#)) +import Data.List.NonEmpty (NonEmpty (..)) import Data.List1 hiding (head) import Data.Map qualified as Map import Data.Qualified @@ -890,7 +891,7 @@ testRemoteToRemoteInSub = do void $ runFedClient @"on-conversation-updated" fedGalleyClient bdom cu let txt = "Hello from another backend" - rcpts = [(alice, aliceC1), (alice, aliceC2), (eve, eveC)] + rcpts = Map.fromList [(alice, aliceC1 :| [aliceC2]), (eve, eveC :| [])] rm = RemoteMLSMessage { time = now, From 862ab9c48322849a29a3c623fd78906de28f5d95 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Fri, 13 Oct 2023 15:52:15 +0200 Subject: [PATCH 211/225] galley: Cleanup garbage data in mls_group_member_client table and drop unused member_client table (#3648) * galley: Truncate the mls_group_member_client table Pre MLS draft-17, the table didn't have a couple of fields. Nothing in the API blocked creation of MLS clients in prod. So, the table might now contain some nulls which the application code doesn't handle very well. This will get rid of all such clients as it is easier to do this than to expect the nulls. * galley: Drop table member_client The table is unused. * Changelog * Remove data migration from member_client That table doesn't exist anymore, so the data migration will fail. --------- Co-authored-by: Paolo Capriotti --- changelog.d/5-internal/wpb-5033 | 4 + hack/bin/cassandra_dump_schema | 1 - services/galley/galley.cabal | 5 +- services/galley/migrate-data/src/Run.hs | 2 - .../migrate-data/src/V2_MigrateMLSMembers.hs | 101 ------------------ services/galley/src/Galley/Schema/Run.hs | 6 +- .../V88_TruncateMLSGroupMemberClient.hs | 33 ++++++ .../Galley/Schema/V89_RemoveMemberClient.hs | 33 ++++++ 8 files changed, 77 insertions(+), 108 deletions(-) create mode 100644 changelog.d/5-internal/wpb-5033 delete mode 100644 services/galley/migrate-data/src/V2_MigrateMLSMembers.hs create mode 100644 services/galley/src/Galley/Schema/V88_TruncateMLSGroupMemberClient.hs create mode 100644 services/galley/src/Galley/Schema/V89_RemoveMemberClient.hs diff --git a/changelog.d/5-internal/wpb-5033 b/changelog.d/5-internal/wpb-5033 new file mode 100644 index 00000000000..4a16fe7e78f --- /dev/null +++ b/changelog.d/5-internal/wpb-5033 @@ -0,0 +1,4 @@ +Truncate `galley.mls_group_member_client` table and drop `galley.member_client` table. + +The data in `mls_group_member_client` could contain nulls from client testing in prod. So, its OK to truncate it. +The `member_client` table is unused. \ No newline at end of file diff --git a/hack/bin/cassandra_dump_schema b/hack/bin/cassandra_dump_schema index 624e4a0a180..74c657fe5a5 100755 --- a/hack/bin/cassandra_dump_schema +++ b/hack/bin/cassandra_dump_schema @@ -25,7 +25,6 @@ def main(): for keyspace in keyspaces: if keyspace.endswith('_test'): s = run_cqlsh(container, f'DESCRIBE keyspace {keyspace}') - print(s.replace('CREATE TABLE galley_test.member_client','-- NOTE: this table is unused. It was replaced by mls_group_member_client\nCREATE TABLE galley_test.member_client')) print() if __name__ == '__main__': diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index a5a25e84711..2fba1c3df8e 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -272,6 +272,8 @@ library Galley.Schema.V85_MLSDraft17 Galley.Schema.V86_TeamFeatureMlsMigration Galley.Schema.V87_TeamFeatureSupportedProtocols + Galley.Schema.V88_TruncateMLSGroupMemberClient + Galley.Schema.V89_RemoveMemberClient Galley.Types.Clients Galley.Types.ToUserRole Galley.Types.UserList @@ -563,7 +565,6 @@ executable galley-migrate-data Paths_galley Run V1_BackfillBillingTeamMembers - V2_MigrateMLSMembers V3_BackfillTeamAdmins hs-source-dirs: migrate-data/src @@ -574,7 +575,6 @@ executable galley-migrate-data , containers , exceptions , extended - , galley , galley-types , imports , lens @@ -583,7 +583,6 @@ executable galley-migrate-data , time , tinylog , types-common - , unliftio , wire-api if flag(static) diff --git a/services/galley/migrate-data/src/Run.hs b/services/galley/migrate-data/src/Run.hs index 319fe3075aa..bf85d0e97d2 100644 --- a/services/galley/migrate-data/src/Run.hs +++ b/services/galley/migrate-data/src/Run.hs @@ -22,7 +22,6 @@ import Imports import Options.Applicative import System.Logger.Extended qualified as Log import V1_BackfillBillingTeamMembers qualified -import V2_MigrateMLSMembers qualified import V3_BackfillTeamAdmins qualified main :: IO () @@ -33,7 +32,6 @@ main = do l o [ V1_BackfillBillingTeamMembers.migration, - V2_MigrateMLSMembers.migration, V3_BackfillTeamAdmins.migration ] where diff --git a/services/galley/migrate-data/src/V2_MigrateMLSMembers.hs b/services/galley/migrate-data/src/V2_MigrateMLSMembers.hs deleted file mode 100644 index 6fca23a7696..00000000000 --- a/services/galley/migrate-data/src/V2_MigrateMLSMembers.hs +++ /dev/null @@ -1,101 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 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 V2_MigrateMLSMembers where - -import Cassandra -import Conduit -import Data.Conduit.Internal (zipSources) -import Data.Conduit.List qualified as C -import Data.Domain -import Data.Id -import Data.Map.Strict (lookup) -import Data.Map.Strict qualified as Map -import Galley.Cassandra.Instances () -import Galley.DataMigration.Types -import Imports hiding (lookup) -import System.Logger.Class qualified as Log -import UnliftIO (pooledMapConcurrentlyN_) -import UnliftIO.Async (pooledMapConcurrentlyN) -import Wire.API.MLS.Group -import Wire.API.MLS.KeyPackage - -migration :: Migration -migration = - Migration - { version = MigrationVersion 2, - text = "Migrating from member_client to mls_group_member_client", - action = - runConduit $ - zipSources - (C.sourceList [(1 :: Int32) ..]) - getMemberClientsFromLegacy - .| C.mapM_ - ( \(i, rows) -> do - Log.info (Log.field "Entries " (show (i * pageSize))) - let convIds = map rowConvId rows - m <- lookupGroupIds convIds - let newRows = flip mapMaybe rows $ \(conv, domain, uid, client, ref) -> - conv `lookup` m >>= \groupId -> pure (groupId, domain, uid, client, ref) - insertMemberClients newRows - ) - } - -rowConvId :: (ConvId, Domain, UserId, ClientId, KeyPackageRef) -> ConvId -rowConvId (conv, _, _, _, _) = conv - -pageSize :: Int32 -pageSize = 1000 - -getMemberClientsFromLegacy :: MonadClient m => ConduitM () [(ConvId, Domain, UserId, ClientId, KeyPackageRef)] m () -getMemberClientsFromLegacy = paginateC cql (paramsP LocalQuorum () pageSize) x5 - where - cql :: PrepQuery R () (ConvId, Domain, UserId, ClientId, KeyPackageRef) - cql = "SELECT conv, user_domain, user, client, key_package_ref from member_client" - -lookupGroupIds :: [ConvId] -> MigrationActionT IO (Map ConvId GroupId) -lookupGroupIds convIds = do - rows <- pooledMapConcurrentlyN 8 (\convId -> retry x5 (query1 cql (params LocalQuorum (Identity convId)))) convIds - rows' <- - rows - & mapM - ( \case - (Just (c, mg)) -> do - case mg of - Nothing -> do - Log.warn (Log.msg ("No group found for conv " <> show c)) - pure Nothing - Just g -> pure (Just (c, g)) - Nothing -> do - Log.warn (Log.msg ("Conversation is missing for entry" :: Text)) - pure Nothing - ) - - rows' - & catMaybes - & Map.fromList - & pure - where - cql :: PrepQuery R (Identity ConvId) (ConvId, Maybe GroupId) - cql = "SELECT conv, group_id from conversation where conv = ?" - -insertMemberClients :: (MonadUnliftIO m, MonadClient m) => [(GroupId, Domain, UserId, ClientId, KeyPackageRef)] -> m () -insertMemberClients rows = do - pooledMapConcurrentlyN_ 8 (\row -> retry x5 (write cql (params LocalQuorum row))) rows - where - cql :: PrepQuery W (GroupId, Domain, UserId, ClientId, KeyPackageRef) () - cql = "INSERT INTO mls_group_member_client (group_id, user_domain, user, client, key_package_ref) VALUES (?, ?, ?, ?, ?)" diff --git a/services/galley/src/Galley/Schema/Run.hs b/services/galley/src/Galley/Schema/Run.hs index c24f6bb78d4..0c34f67d870 100644 --- a/services/galley/src/Galley/Schema/Run.hs +++ b/services/galley/src/Galley/Schema/Run.hs @@ -87,6 +87,8 @@ import Galley.Schema.V84_MLSSubconversation qualified as V84_MLSSubconversation import Galley.Schema.V85_MLSDraft17 qualified as V85_MLSDraft17 import Galley.Schema.V86_TeamFeatureMlsMigration qualified as V86_TeamFeatureMlsMigration import Galley.Schema.V87_TeamFeatureSupportedProtocols qualified as V87_TeamFeatureSupportedProtocols +import Galley.Schema.V88_TruncateMLSGroupMemberClient qualified as V88_TruncateMLSGroupMemberClient +import Galley.Schema.V89_RemoveMemberClient qualified as V89_RemoveMemberClient import Imports import Options.Applicative import System.Logger.Extended qualified as Log @@ -175,7 +177,9 @@ migrations = V84_MLSSubconversation.migration, V85_MLSDraft17.migration, V86_TeamFeatureMlsMigration.migration, - V87_TeamFeatureSupportedProtocols.migration + V87_TeamFeatureSupportedProtocols.migration, + V88_TruncateMLSGroupMemberClient.migration, + V89_RemoveMemberClient.migration -- FUTUREWORK: once #1726 has made its way to master/production, -- the 'message' field in connections table can be dropped. -- See also https://github.com/wireapp/wire-server/pull/1747/files diff --git a/services/galley/src/Galley/Schema/V88_TruncateMLSGroupMemberClient.hs b/services/galley/src/Galley/Schema/V88_TruncateMLSGroupMemberClient.hs new file mode 100644 index 00000000000..77e9d15a11f --- /dev/null +++ b/services/galley/src/Galley/Schema/V88_TruncateMLSGroupMemberClient.hs @@ -0,0 +1,33 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 Galley.Schema.V88_TruncateMLSGroupMemberClient + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +-- | This migration exists because the table could have some rogue data in it +-- before MLS Draft-17 was implemented. It was not supposed to be used, but it +-- could've been. This migration just deletes old data. This could break some +-- conversations/users in unknown ways. But those are most likely test users. +migration :: Migration +migration = Migration 88 "Truncate mls_group_member_client" $ do + schema' + [r|TRUNCATE TABLE mls_group_member_client|] diff --git a/services/galley/src/Galley/Schema/V89_RemoveMemberClient.hs b/services/galley/src/Galley/Schema/V89_RemoveMemberClient.hs new file mode 100644 index 00000000000..53f2d0df2d1 --- /dev/null +++ b/services/galley/src/Galley/Schema/V89_RemoveMemberClient.hs @@ -0,0 +1,33 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 Galley.Schema.V89_RemoveMemberClient + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +-- | This migration exists because the table could have some rogue data in it +-- before MLS Draft-17 was implemented. It was not supposed to be used, but it +-- could've been. This migration just deletes old data. This could break some +-- conversations/users in unknown ways. But those are most likely test users. +migration :: Migration +migration = Migration 88 "Remove member_client" $ do + schema' + [r|DROP TABLE IF EXISTS member_client|] From 80c7f7f48c34d92a9245bd85b0e3c307a6b791bf Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Fri, 13 Oct 2023 19:18:03 +0200 Subject: [PATCH 212/225] [WPB-4547] Attach the reason for a member to leave a conversation to the leave event (#3640) * [feat] attach the reason for a member to leave a conversation to the leave event --- .envrc | 2 +- changelog.d/2-features/WPB-4547 | 1 + .../src/Wire/API/Conversation/Action.hs | 4 +- .../src/Wire/API/Event/Conversation.hs | 40 ++++++++++++++++--- libs/wire-api/src/Wire/API/User.hs | 16 +++++--- .../Golden/Generated/Event_conversation.hs | 1 + .../Wire/API/Golden/Generated/Event_user.hs | 1 + .../Generated/RemoveBotResponse_user.hs | 1 + .../testObject_Event_conversation_9.json | 1 + .../test/golden/testObject_Event_user_11.json | 1 + .../testObject_RemoveBotResponse_user_1.json | 1 + nix/wire-server.nix | 1 + .../brig/test/integration/API/Provider.hs | 6 +-- .../brig/test/integration/API/User/Util.hs | 9 +++-- .../test/integration/Federation/End2end.hs | 4 +- services/galley/src/Galley/API/Internal.hs | 2 +- services/galley/src/Galley/API/Teams.hs | 2 +- services/galley/src/Galley/API/Update.hs | 13 ++++-- services/galley/test/integration/API.hs | 4 +- services/galley/test/integration/API/MLS.hs | 2 +- services/galley/test/integration/API/Util.hs | 12 +++--- 21 files changed, 86 insertions(+), 38 deletions(-) create mode 100644 changelog.d/2-features/WPB-4547 diff --git a/.envrc b/.envrc index c96c4e427df..6edee4dad4b 100644 --- a/.envrc +++ b/.envrc @@ -50,4 +50,4 @@ export INTEGRATION_DYNAMIC_BACKENDS_POOLSIZE=3 # Keep these in sync with deploy/dockerephmeral/init.sh export AWS_REGION="eu-west-1" export AWS_ACCESS_KEY_ID="dummykey" -export AWS_SECRET_ACCESS_KEY="dummysecret" \ No newline at end of file +export AWS_SECRET_ACCESS_KEY="dummysecret" diff --git a/changelog.d/2-features/WPB-4547 b/changelog.d/2-features/WPB-4547 new file mode 100644 index 00000000000..54a98f7352a --- /dev/null +++ b/changelog.d/2-features/WPB-4547 @@ -0,0 +1 @@ +Add reason field to conversation.member-leave diff --git a/libs/wire-api/src/Wire/API/Conversation/Action.hs b/libs/wire-api/src/Wire/API/Conversation/Action.hs index 4730ba2d47f..d6930d14488 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action.hs @@ -162,9 +162,9 @@ conversationActionToEvent tag now quid qcnv subconv action = let ConversationJoin newMembers role = action in EdMembersJoin $ SimpleMembers (map (`SimpleMember` role) (toList newMembers)) SConversationLeaveTag -> - EdMembersLeave (QualifiedUserIdList [quid]) + EdMembersLeave EdReasonLeft (QualifiedUserIdList [quid]) SConversationRemoveMembersTag -> - EdMembersLeave (QualifiedUserIdList (toList action)) + EdMembersLeave EdReasonRemoved (QualifiedUserIdList (toList action)) SConversationMemberUpdateTag -> let ConversationMemberUpdate target (OtherMemberUpdate role) = action update = MemberUpdateData target Nothing Nothing Nothing Nothing Nothing Nothing role diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index 6a15c287ced..cfac6ae07de 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -25,6 +25,7 @@ module Wire.API.Event.Conversation evtType, EventType (..), EventData (..), + EdMemberLeftReason (..), AddCodeResult (..), -- * Event lenses @@ -89,7 +90,7 @@ import Wire.API.Conversation.Typing import Wire.API.MLS.SubConversation import Wire.API.Routes.MultiVerb import Wire.API.Routes.Version -import Wire.API.User (QualifiedUserIdList (..)) +import Wire.API.User (QualifiedUserIdList (..), qualifiedUserIdListObjectSchema) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -------------------------------------------------------------------------------- @@ -164,9 +165,33 @@ instance ToSchema EventType where element "conversation.protocol-update" ProtocolUpdate ] +-- | The reason for a member to leave +-- There are three reasons +-- - the member has left on their own +-- - the member was removed from the team +-- - the member was removed by another member +data EdMemberLeftReason + = -- | The member has left on their own + EdReasonLeft + | -- | The member was removed from the team and/or deleted + EdReasonDeleted + | -- | The member was removed by another member + EdReasonRemoved + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via GenericUniform EdMemberLeftReason + +instance ToSchema EdMemberLeftReason where + schema = + enum @Text "EdMemberLeftReason" $ + mconcat + [ element "left" EdReasonLeft, + element "user-deleted" EdReasonDeleted, + element "removed" EdReasonRemoved + ] + data EventData = EdMembersJoin SimpleMembers - | EdMembersLeave QualifiedUserIdList + | EdMembersLeave EdMemberLeftReason QualifiedUserIdList | EdConnect Connect | EdConvReceiptModeUpdate ConversationReceiptModeUpdate | EdConvRename ConversationRename @@ -187,7 +212,7 @@ data EventData genEventData :: EventType -> QC.Gen EventData genEventData = \case MemberJoin -> EdMembersJoin <$> arbitrary - MemberLeave -> EdMembersLeave <$> arbitrary + MemberLeave -> EdMembersLeave <$> arbitrary <*> arbitrary MemberStateUpdate -> EdMemberUpdate <$> arbitrary ConvRename -> EdConvRename <$> arbitrary ConvAccessUpdate -> EdConvAccessUpdate <$> arbitrary @@ -206,7 +231,7 @@ genEventData = \case eventDataType :: EventData -> EventType eventDataType (EdMembersJoin _) = MemberJoin -eventDataType (EdMembersLeave _) = MemberLeave +eventDataType (EdMembersLeave _ _) = MemberLeave eventDataType (EdMemberUpdate _) = MemberStateUpdate eventDataType (EdConvRename _) = ConvRename eventDataType (EdConvAccessUpdate _) = ConvAccessUpdate @@ -383,7 +408,7 @@ taggedEventDataSchema = where edata = dispatch $ \case MemberJoin -> tag _EdMembersJoin (unnamed schema) - MemberLeave -> tag _EdMembersLeave (unnamed schema) + MemberLeave -> tag _EdMembersLeave (unnamed memberLeaveSchema) MemberStateUpdate -> tag _EdMemberUpdate (unnamed schema) ConvRename -> tag _EdConvRename (unnamed schema) -- FUTUREWORK: when V2 is dropped, it is fine to change this schema to @@ -406,6 +431,11 @@ taggedEventDataSchema = ConvDelete -> tag _EdConvDelete null_ ProtocolUpdate -> tag _EdProtocolUpdate (unnamed (unProtocolUpdate <$> P.ProtocolUpdate .= schema)) +memberLeaveSchema :: ValueSchema NamedSwaggerDoc (EdMemberLeftReason, QualifiedUserIdList) +memberLeaveSchema = + object "QualifiedUserIdList with EdMemberLeftReason" $ + (,) <$> fst .= field "reason" schema <*> snd .= qualifiedUserIdListObjectSchema + instance ToSchema Event where schema = object "Event" eventObjectSchema diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 98166f6c43e..c0095855963 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -25,6 +25,7 @@ module Wire.API.User UserIdList (..), UserIds (..), QualifiedUserIdList (..), + qualifiedUserIdListObjectSchema, LimitedQualifiedUserIdList (..), ScimUserInfo (..), ScimUserInfos (..), @@ -548,12 +549,15 @@ newtype QualifiedUserIdList = QualifiedUserIdList {qualifiedUserIdList :: [Quali instance ToSchema QualifiedUserIdList where schema = - object "QualifiedUserIdList" $ - QualifiedUserIdList - <$> qualifiedUserIdList - .= field "qualified_user_ids" (array schema) - <* (fmap qUnqualified . qualifiedUserIdList) - .= field "user_ids" (deprecatedSchema "qualified_user_ids" (array schema)) + object "QualifiedUserIdList" qualifiedUserIdListObjectSchema + +qualifiedUserIdListObjectSchema :: ObjectSchema SwaggerDoc QualifiedUserIdList +qualifiedUserIdListObjectSchema = + QualifiedUserIdList + <$> qualifiedUserIdList + .= field "qualified_user_ids" (array schema) + <* (fmap qUnqualified . qualifiedUserIdList) + .= field "user_ids" (deprecatedSchema "qualified_user_ids" (array schema)) -------------------------------------------------------------------------------- -- LimitedQualifiedUserIdList diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs index 12cc9c7b83e..fbe3ab3e5ae 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs @@ -147,6 +147,7 @@ testObject_Event_conversation_9 = evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, evtData = EdMembersLeave + EdReasonLeft ( QualifiedUserIdList { qualifiedUserIdList = [ Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "ow8i3fhr.v"}}, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs index a7309a7a1b3..b6ffdeea2ef 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs @@ -236,6 +236,7 @@ testObject_Event_user_11 = (Qualified (Id (fromJust (UUID.fromString "000043a6-0000-1627-0000-490300002017"))) (Domain "faraway.example.com")) (read "1864-04-12 01:28:25.705 UTC") ( EdMembersLeave + EdReasonLeft ( QualifiedUserIdList { qualifiedUserIdList = [ Qualified (Id (fromJust (UUID.fromString "00003fab-0000-40b8-0000-3b0c000014ef"))) (Domain "faraway.example.com"), diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RemoveBotResponse_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RemoveBotResponse_user.hs index 49b47d17aa8..7fd0d92a2f0 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RemoveBotResponse_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RemoveBotResponse_user.hs @@ -38,6 +38,7 @@ testObject_RemoveBotResponse_user_1 = (Qualified (Id (fromJust (UUID.fromString "00004166-0000-1e32-0000-52cb0000428d"))) (Domain "faraway.example.com")) (read "1864-05-07 01:13:35.741 UTC") ( EdMembersLeave + EdReasonRemoved ( QualifiedUserIdList { qualifiedUserIdList = [ Qualified (Id (fromJust (UUID.fromString "000038c1-0000-4a9c-0000-511300004c8b"))) (Domain "faraway.example.com"), diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_9.json b/libs/wire-api/test/golden/testObject_Event_conversation_9.json index 028aeb144dc..e83525ac019 100644 --- a/libs/wire-api/test/golden/testObject_Event_conversation_9.json +++ b/libs/wire-api/test/golden/testObject_Event_conversation_9.json @@ -63,6 +63,7 @@ "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" } ], + "reason": "left", "user_ids": [ "2126ea99-ca79-43ea-ad99-a59616468e8e", "2126ea99-ca79-43ea-ad99-a59616468e8e", diff --git a/libs/wire-api/test/golden/testObject_Event_user_11.json b/libs/wire-api/test/golden/testObject_Event_user_11.json index 8acfbce8fae..870249332d7 100644 --- a/libs/wire-api/test/golden/testObject_Event_user_11.json +++ b/libs/wire-api/test/golden/testObject_Event_user_11.json @@ -11,6 +11,7 @@ "id": "00001c48-0000-29ae-0000-62fc00001479" } ], + "reason": "left", "user_ids": [ "00003fab-0000-40b8-0000-3b0c000014ef", "00001c48-0000-29ae-0000-62fc00001479" diff --git a/libs/wire-api/test/golden/testObject_RemoveBotResponse_user_1.json b/libs/wire-api/test/golden/testObject_RemoveBotResponse_user_1.json index 2bd38208dc9..d5a64addc4a 100644 --- a/libs/wire-api/test/golden/testObject_RemoveBotResponse_user_1.json +++ b/libs/wire-api/test/golden/testObject_RemoveBotResponse_user_1.json @@ -12,6 +12,7 @@ "id": "00003111-0000-2620-0000-1c8800000ea0" } ], + "reason": "removed", "user_ids": [ "000038c1-0000-4a9c-0000-511300004c8b", "00003111-0000-2620-0000-1c8800000ea0" diff --git a/nix/wire-server.nix b/nix/wire-server.nix index d5b4935e346..12aafac3ddd 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -394,6 +394,7 @@ let pkgs.kubectl pkgs.kubelogin-oidc pkgs.nixpkgs-fmt + pkgs.openssl pkgs.ormolu pkgs.shellcheck pkgs.treefmt diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index f8dce17f04a..12749a533a3 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -789,7 +789,7 @@ testBotTeamOnlyConv config db brig galley cannon = withTestService config db bri let msg = QualifiedUserIdList gone assertEqual "conv" cnv (evtConv e) assertEqual "user" leaveFrom (evtFrom e) - assertEqual "event data" (EdMembersLeave msg) (evtData e) + assertEqual "event data" (EdMembersLeave EdReasonRemoved msg) (evtData e) _ -> assertFailure $ "expected event of type: ConvAccessUpdate or MemberLeave, got: " <> show e setAccessRole uid qcid role = @@ -2036,7 +2036,7 @@ wsAssertMemberLeave ws conv usr old = void $ evtConv e @?= conv evtType e @?= MemberLeave evtFrom e @?= usr - evtData e @?= EdMembersLeave (QualifiedUserIdList old) + evtData e @?= EdMembersLeave EdReasonRemoved (QualifiedUserIdList old) wsAssertConvDelete :: (HasCallStack, MonadIO m) => WS.WebSocket -> Qualified ConvId -> Qualified UserId -> m () wsAssertConvDelete ws conv from = void $ @@ -2083,7 +2083,7 @@ svcAssertMemberLeave buf usr gone cnv = liftIO $ do assertEqual "event type" MemberLeave (evtType e) assertEqual "conv" cnv (evtConv e) assertEqual "user" usr (evtFrom e) - assertEqual "event data" (EdMembersLeave msg) (evtData e) + assertEqual "event data" (EdMembersLeave EdReasonRemoved msg) (evtData e) _ -> assertFailure "Event timeout (TestBotMessage: member-leave)" svcAssertConvDelete :: (HasCallStack, MonadIO m) => Chan TestBotEvent -> Qualified UserId -> Qualified ConvId -> m () diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 96acc83396e..49dbacd7a90 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -60,6 +60,7 @@ import Test.Tasty.HUnit import Util import Wire.API.Asset import Wire.API.Connection +import Wire.API.Event.Conversation (EdMemberLeftReason) import Wire.API.Event.Conversation qualified as Conv import Wire.API.Federation.API.Brig qualified as F import Wire.API.Federation.Component @@ -512,16 +513,16 @@ matchDeleteUserNotification quid n = do eUnqualifiedId @?= Just (qUnqualified quid) eQualifiedId @?= Just quid -matchConvLeaveNotification :: Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> Notification -> IO () -matchConvLeaveNotification conv remover removeds n = do +matchConvLeaveNotification :: Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> EdMemberLeftReason -> Notification -> IO () +matchConvLeaveNotification conv remover removeds reason n = do let e = List1.head (WS.unpackPayload n) ntfTransient n @?= False Conv.evtConv e @?= conv Conv.evtType e @?= Conv.MemberLeave Conv.evtFrom e @?= remover - sorted (Conv.evtData e) @?= sorted (Conv.EdMembersLeave (Conv.QualifiedUserIdList removeds)) + sorted (Conv.evtData e) @?= sorted (Conv.EdMembersLeave reason (Conv.QualifiedUserIdList removeds)) where - sorted (Conv.EdMembersLeave (Conv.QualifiedUserIdList m)) = Conv.EdMembersLeave (Conv.QualifiedUserIdList (sort m)) + sorted (Conv.EdMembersLeave r (Conv.QualifiedUserIdList m)) = Conv.EdMembersLeave r (Conv.QualifiedUserIdList (sort m)) sorted x = x generateVerificationCode :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> Public.SendVerificationCode -> m () diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index d76f059fc65..7fe253c785a 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -548,8 +548,8 @@ testDeleteUser brig1 brig2 galley1 galley2 cannon1 = do WS.bracketR cannon1 (qUnqualified alice) $ \wsAlice -> do deleteUser (qUnqualified bobDel) (Just defPassword) brig2 !!! const 200 === statusCode WS.assertMatch_ (5 # Second) wsAlice $ matchDeleteUserNotification bobDel - WS.assertMatch_ (5 # Second) wsAlice $ matchConvLeaveNotification conv1 bobDel [bobDel] - WS.assertMatch_ (5 # Second) wsAlice $ matchConvLeaveNotification conv2 bobDel [bobDel] + WS.assertMatch_ (5 # Second) wsAlice $ matchConvLeaveNotification conv1 bobDel [bobDel] EdReasonLeft + WS.assertMatch_ (5 # Second) wsAlice $ matchConvLeaveNotification conv2 bobDel [bobDel] EdReasonLeft testRemoteAsset :: Brig -> Brig -> CargoHold -> CargoHold -> Http () testRemoteAsset brig1 brig2 ch1 ch2 = do diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 8c0586f5daa..2830fc16d43 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -386,7 +386,7 @@ rmUser lusr conn = do Nothing (tUntagged lusr) now - (EdMembersLeave (QualifiedUserIdList [qUser])) + (EdMembersLeave EdReasonDeleted (QualifiedUserIdList [qUser])) for_ (bucketRemote (fmap rmId (Data.convRemoteMembers c))) $ notifyRemoteMembers now qUser (Data.convId c) pure $ Intra.newPushLocal ListComplete (tUnqualified lusr) (Intra.ConvEvent e) (Intra.recipient <$> Data.convLocalMembers c) diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index d4395976b05..410c0152857 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -1027,7 +1027,7 @@ uncheckedDeleteTeamMember lusr zcon tid remove mems = do -- remove the user from conversations but never send out any events. We assume that clients -- handle nicely these missing events, regardless of whether they are in the same team or not let tmids = Set.fromList $ map (view userId) (mems ^. teamMembers) - let edata = Conv.EdMembersLeave (Conv.QualifiedUserIdList [tUntagged (qualifyAs lusr remove)]) + let edata = Conv.EdMembersLeave Conv.EdReasonDeleted (Conv.QualifiedUserIdList [tUntagged (qualifyAs lusr remove)]) cc <- E.getTeamConversations tid for_ cc $ \c -> E.getConversation (c ^. conversationId) >>= \conv -> diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index e39df03f31c..bbb54cd7b37 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -1170,6 +1170,11 @@ removeMemberQualified lusr con qcnv victim = qcnv victim +-- | if the public member leave api was called, we can assume that +-- it was called by a user +pattern EdMembersLeaveRemoved :: QualifiedUserIdList -> EventData +pattern EdMembersLeaveRemoved l = EdMembersLeave EdReasonRemoved l + removeMemberFromRemoteConv :: ( Member FederatorAccess r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, @@ -1184,8 +1189,8 @@ removeMemberFromRemoteConv cnv lusr victim | tUntagged lusr == victim = do let lc = LeaveConversationRequest (tUnqualified cnv) (qUnqualified victim) let rpc = fedClient @'Galley @"leave-conversation" lc - (either handleError handleSuccess . void . (.response) =<<) $ - E.runFederated cnv rpc + E.runFederated cnv rpc + >>= either handleError handleSuccess . void . (.response) | otherwise = throwS @('ActionDenied 'RemoveConversationMember) where handleError :: @@ -1204,7 +1209,7 @@ removeMemberFromRemoteConv cnv lusr victim t <- input pure . Just $ Event (tUntagged cnv) Nothing (tUntagged lusr) t $ - EdMembersLeave (QualifiedUserIdList [victim]) + EdMembersLeaveRemoved (QualifiedUserIdList [victim]) -- | Remove a member from a local conversation. removeMemberFromLocalConv :: @@ -1679,7 +1684,7 @@ rmBot lusr zcon b = do else do t <- input do - let evd = EdMembersLeave (QualifiedUserIdList [tUntagged (qualifyAs lusr (botUserId (b ^. rmBotId)))]) + let evd = EdMembersLeaveRemoved (QualifiedUserIdList [tUntagged (qualifyAs lusr (botUserId (b ^. rmBotId)))]) let e = Event (tUntagged lcnv) Nothing (tUntagged lusr) t evd for_ (newPushLocal ListComplete (tUnqualified lusr) (ConvEvent e) (recipient <$> users)) $ \p -> E.push1 $ p & pushConn .~ zcon diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index ffc437f2ad8..6f6ed15666a 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -1605,9 +1605,9 @@ postConvertTeamConv = do -- non-team members get kicked out liftIO $ do WS.assertMatchN_ (5 # Second) [wsA, wsB, wsE, wsM] $ - wsAssertMemberLeave qconv qalice (pure qeve) + wsAssertMemberLeave qconv qalice (pure qeve) EdReasonRemoved WS.assertMatchN_ (5 # Second) [wsA, wsB, wsE, wsM] $ - wsAssertMemberLeave qconv qalice (pure qmallory) + wsAssertMemberLeave qconv qalice (pure qmallory) EdReasonRemoved -- joining (for mallory) is no longer possible postJoinCodeConv mallory j !!! const 403 === statusCode -- team members (dave) can still join diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 806a43b3b33..54f6e4ae177 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -2393,7 +2393,7 @@ testCreatorRemovesUserFromParent = do liftIO $ assertOne events >>= assertLeaveEvent qcnv alice [bob] WS.assertMatchN_ (5 # Second) wss $ \n -> do - wsAssertMemberLeave qcnv alice [bob] n + wsAssertMemberLeave qcnv alice [bob] EdReasonRemoved n State.put stateSub -- Get client state for alice and fetch bob client identities diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 7486dac9363..4b4d4eb28a7 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -1830,7 +1830,7 @@ assertLeaveEvent conv usr leaving e = do evtConv e @?= conv evtType e @?= Conv.MemberLeave evtFrom e @?= usr - fmap (sort . qualifiedUserIdList) (evtData e ^? _EdMembersLeave) @?= Just (sort leaving) + fmap (sort . qualifiedUserIdList) (evtData e ^? _EdMembersLeave . _2) @?= Just (sort leaving) wsAssertMemberUpdateWithRole :: Qualified ConvId -> Qualified UserId -> UserId -> RoleName -> Notification -> IO () wsAssertMemberUpdateWithRole conv usr target role n = do @@ -1863,16 +1863,16 @@ wsAssertConvMessageTimerUpdate conv usr new n = do evtFrom e @?= usr evtData e @?= EdConvMessageTimerUpdate new -wsAssertMemberLeave :: Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> Notification -> IO () -wsAssertMemberLeave conv usr old n = do +wsAssertMemberLeave :: Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> EdMemberLeftReason -> Notification -> IO () +wsAssertMemberLeave conv usr old reason n = do let e = List1.head (WS.unpackPayload n) ntfTransient n @?= False evtConv e @?= conv evtType e @?= Conv.MemberLeave evtFrom e @?= usr - sorted (evtData e) @?= sorted (EdMembersLeave (QualifiedUserIdList old)) + sorted (evtData e) @?= sorted (EdMembersLeave reason (QualifiedUserIdList old)) where - sorted (EdMembersLeave (QualifiedUserIdList m)) = EdMembersLeave (QualifiedUserIdList (sort m)) + sorted (EdMembersLeave _ (QualifiedUserIdList m)) = EdMembersLeave reason (QualifiedUserIdList (sort m)) sorted x = x wsAssertTyping :: HasCallStack => Qualified ConvId -> Qualified UserId -> TypingStatus -> Notification -> IO () @@ -2843,7 +2843,7 @@ checkConvMemberLeaveEvent cid usr w = WS.assertMatch_ checkTimeout w $ \notif -> evtConv e @?= cid evtType e @?= Conv.MemberLeave case evtData e of - Conv.EdMembersLeave mm -> mm @?= Conv.QualifiedUserIdList [usr] + Conv.EdMembersLeave _ mm -> mm @?= Conv.QualifiedUserIdList [usr] other -> assertFailure $ "Unexpected event data: " <> show other checkTimeout :: WS.Timeout From 466a38464875c5bb740dab6d0aafb3a073694504 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Mon, 16 Oct 2023 00:25:23 +0200 Subject: [PATCH 213/225] s/mentionned/mentioned/ --- docs/src/understand/classified-domains.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/understand/classified-domains.md b/docs/src/understand/classified-domains.md index bb1df193b28..b1c6b25fa66 100644 --- a/docs/src/understand/classified-domains.md +++ b/docs/src/understand/classified-domains.md @@ -17,7 +17,7 @@ galley: domains: ["domain-that-is-classified.link"] ... ``` -Note: This is only a `backend` level configuration option, the `team` configuration mentionned below only exists for technical reasons and is not actually accessible in any way. +Note: This is only a `backend` level configuration option, the `team` configuration mentioned below only exists for technical reasons and is not actually accessible in any way. Here is a table to navigate the possible configurations: From c37d94f5a49c2afc0bb2e6620bdf4823121e4b89 Mon Sep 17 00:00:00 2001 From: Owen Harvey Date: Mon, 16 Oct 2023 18:12:17 +1000 Subject: [PATCH 214/225] WPB-5017: Adding a test config file to the PR guidelines. (#3624) --- changelog.d/4-docs/hotfix-pr-guidelines | 1 + docs/src/developer/developer/pr-guidelines.md | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 changelog.d/4-docs/hotfix-pr-guidelines diff --git a/changelog.d/4-docs/hotfix-pr-guidelines b/changelog.d/4-docs/hotfix-pr-guidelines new file mode 100644 index 00000000000..940c66b8fff --- /dev/null +++ b/changelog.d/4-docs/hotfix-pr-guidelines @@ -0,0 +1 @@ +Adding a testing config entry to the PR guidelines. \ No newline at end of file diff --git a/docs/src/developer/developer/pr-guidelines.md b/docs/src/developer/developer/pr-guidelines.md index be0bf1f01b1..09b7e37fd22 100644 --- a/docs/src/developer/developer/pr-guidelines.md +++ b/docs/src/developer/developer/pr-guidelines.md @@ -87,6 +87,9 @@ If a PR adds new configuration options for say brig, the following files need to * [ ] The values files for CI: `hack/helm_vars/wire-server/values.yaml.gotmpl` * [ ] The configuration docs: `docs/src/developer/reference/config-options.md` +Additional configuration may also exist for services in the following locations. +* [ ] `charts/$SERVICE/templates/tests/configmap.yaml` + If any new configuration value is required and has no default, then: * [ ] Write a changelog entry in `0-release-notes` advertising the new configuration value From 3f01001e4d7597d4ce55ba6579a9f73c62790872 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Mon, 16 Oct 2023 12:09:02 +0200 Subject: [PATCH 215/225] wire-api: Remove explicit dependency on the `pretty` package #3651 `proto-lens` already provides wrappers around the usage, so we should stick to those. --- libs/wire-api/default.nix | 2 -- libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs | 5 ++--- libs/wire-api/wire-api.cabal | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 92d2c3c8f3a..3793ac70e0b 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -63,7 +63,6 @@ , openapi3 , pem , polysemy -, pretty , process , proto-lens , protobuf @@ -235,7 +234,6 @@ mkDerivation { metrics-wai openapi3 pem - pretty process proto-lens QuickCheck diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs index 13fdf5510cd..f4c9a736005 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs @@ -32,12 +32,11 @@ import Data.ByteString qualified as ByteString import Data.ByteString.Lazy qualified as LBS import Data.ProtoLens.Encoding (decodeMessage, encodeMessage) import Data.ProtoLens.Message (Message) -import Data.ProtoLens.TextFormat (pprintMessage, readMessage) +import Data.ProtoLens.TextFormat (readMessage, showMessage) import Data.Text.Lazy.IO qualified as LText import Imports import Test.Tasty (TestTree) import Test.Tasty.HUnit -import Text.PrettyPrint (render) import Type.Reflection (typeRep) import Wire.API.ServantProto @@ -93,7 +92,7 @@ protoTestObject :: protoTestObject obj path = do let actual = toProto obj msg <- assertRight (decodeMessage @m actual) - let pretty = render (pprintMessage msg) + let pretty = showMessage msg dir = "test/golden" fullPath = dir <> "/" <> path createDirectoryIfMissing True dir diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 59a3672ae2b..0d06a7f9964 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -605,7 +605,6 @@ test-suite wire-api-golden-tests , iso639 , lens , pem - , pretty , proto-lens , tasty , tasty-hunit From a88d9b3dc30c2c7a2da3a17120b5601c502d6b15 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 16 Oct 2023 12:26:44 +0200 Subject: [PATCH 216/225] wire-server.nix: Add aws cli to all integration test docker images (#3653) Required for https://github.com/wireapp/wire-server/pull/3633 --- nix/wire-server.nix | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 12aafac3ddd..1290ec42ff1 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -254,8 +254,13 @@ let # extraContents :: Map Exe Derivation -> Map Text [Derivation] extraContents = exes: { brig = [ brig-templates ]; - brig-integration = [ brig-templates pkgs.mls-test-cli ]; - galley-integration = [ pkgs.mls-test-cli ]; + brig-integration = [brig-templates pkgs.mls-test-cli pkgs.awscli2]; + galley-integration = [pkgs.mls-test-cli pkgs.awscli2]; + stern-integration = [ pkgs.awscli2 ]; + gundeck-integration = [ pkgs.awscli2 ]; + cargohold-integration = [ pkgs.awscli2 ]; + spar-integration = [ pkgs.awscli2 ]; + federator-integration = [ pkgs.awscli2 ]; integration = with exes; [ brig brig-index @@ -275,6 +280,7 @@ let background-worker pkgs.nginz pkgs.mls-test-cli + pkgs.awscli2 integration-dynamic-backends-db-schemas integration-dynamic-backends-brig-index integration-dynamic-backends-sqs From d18bbd2f780237014b2076c7d8b4504732d786f4 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 17 Oct 2023 12:09:34 +0200 Subject: [PATCH 217/225] WPB-4748 create a way to inspect background notifications queue (#3589) --- cabal.project | 5 +- changelog.d/5-internal/WPB-4748 | 1 + nix/local-haskell-packages.nix | 1 + nix/wire-server.nix | 1 + tools/rabbitmq-consumer/README.md | 30 +++ tools/rabbitmq-consumer/app/Main.hs | 23 ++ tools/rabbitmq-consumer/default.nix | 45 ++++ .../rabbitmq-consumer/rabbitmq-consumer.cabal | 84 +++++++ .../src/RabbitMQConsumer/Lib.hs | 232 ++++++++++++++++++ 9 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 changelog.d/5-internal/WPB-4748 create mode 100644 tools/rabbitmq-consumer/README.md create mode 100644 tools/rabbitmq-consumer/app/Main.hs create mode 100644 tools/rabbitmq-consumer/default.nix create mode 100644 tools/rabbitmq-consumer/rabbitmq-consumer.cabal create mode 100644 tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs diff --git a/cabal.project b/cabal.project index e4fe242a3b4..2ee9da138e7 100644 --- a/cabal.project +++ b/cabal.project @@ -1,6 +1,6 @@ repository hackage.haskell.org url: https://hackage.haskell.org/ -index-state: 2023-10-03T15:17:00Z +index-state: 2023-10-03T15:17:00Z packages: integration , libs/bilge/ @@ -51,6 +51,7 @@ packages: , tools/db/repair-brig-clients-table/ , tools/db/service-backfill/ , tools/fedcalls/ + , tools/rabbitmq-consumer , tools/rex/ , tools/stern/ , tools/mlsstats/ @@ -120,6 +121,8 @@ package proxy ghc-options: -Werror package mlsstats ghc-options: -Werror +package rabbitmq-consumer + ghc-options: -Werror package repair-handles ghc-options: -Werror package rex diff --git a/changelog.d/5-internal/WPB-4748 b/changelog.d/5-internal/WPB-4748 new file mode 100644 index 00000000000..59a431e2b99 --- /dev/null +++ b/changelog.d/5-internal/WPB-4748 @@ -0,0 +1 @@ +CLI tool to consume messages from a RabbitMQ queue diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index 59d940f7cf2..39fe6a23e24 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -53,6 +53,7 @@ service-backfill = hself.callPackage ../tools/db/service-backfill/default.nix { inherit gitignoreSource; }; fedcalls = hself.callPackage ../tools/fedcalls/default.nix { inherit gitignoreSource; }; mlsstats = hself.callPackage ../tools/mlsstats/default.nix { inherit gitignoreSource; }; + rabbitmq-consumer = hself.callPackage ../tools/rabbitmq-consumer/default.nix { inherit gitignoreSource; }; rex = hself.callPackage ../tools/rex/default.nix { inherit gitignoreSource; }; stern = hself.callPackage ../tools/stern/default.nix { inherit gitignoreSource; }; } diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 1290ec42ff1..652cbcedd4b 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -84,6 +84,7 @@ let zauth = [ "zauth" ]; background-worker = [ "background-worker" ]; integration = [ "integration" ]; + rabbitmq-consumer = [ "rabbitmq-consumer" ]; }; attrsets = lib.attrsets; diff --git a/tools/rabbitmq-consumer/README.md b/tools/rabbitmq-consumer/README.md new file mode 100644 index 00000000000..387285946ec --- /dev/null +++ b/tools/rabbitmq-consumer/README.md @@ -0,0 +1,30 @@ +# RabbitMQ Consumer + +```txt +rabbitmq-consumer + +Usage: rabbitmq-consumer [-s|--host HOST] [-p|--port PORT] + [-u|--username USERNAME] [-w|--password PASSWORD] + [-v|--vhost VHOST] [-q|--queue QUEUE] + [-t|--timeout TIMEOUT] COMMAND + + CLI tool to consume messages from a RabbitMQ queue + +Available options: + -h,--help Show this help text + -s,--host HOST RabbitMQ host (default: "localhost") + -p,--port PORT RabbitMQ Port (default: 5672) + -u,--username USERNAME RabbitMQ Username (default: "guest") + -w,--password PASSWORD RabbitMQ Password (default: "alpaca-grapefruit") + -v,--vhost VHOST RabbitMQ VHost (default: "/") + -q,--queue QUEUE RabbitMQ Queue (default: "test") + -t,--timeout TIMEOUT Timeout in seconds. The command will timeout if no + messages are received within this time. This can + happen when the queue is empty, or when we lose the + single active consumer race. (default: 10) + +Available commands: + head Print the first message in the queue + drop-head Drop the first message in the queue + interactive Interactively drop the first message from the queue +``` diff --git a/tools/rabbitmq-consumer/app/Main.hs b/tools/rabbitmq-consumer/app/Main.hs new file mode 100644 index 00000000000..0379be52e30 --- /dev/null +++ b/tools/rabbitmq-consumer/app/Main.hs @@ -0,0 +1,23 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 Main where + +import qualified RabbitMQConsumer.Lib as Lib + +main :: IO () +main = Lib.main diff --git a/tools/rabbitmq-consumer/default.nix b/tools/rabbitmq-consumer/default.nix new file mode 100644 index 00000000000..1da708042e2 --- /dev/null +++ b/tools/rabbitmq-consumer/default.nix @@ -0,0 +1,45 @@ +# WARNING: GENERATED FILE, DO NOT EDIT. +# This file is generated by running hack/bin/generate-local-nix-packages.sh and +# must be regenerated whenever local packages are added or removed, or +# dependencies are added or removed. +{ mkDerivation +, aeson +, aeson-pretty +, amqp +, base +, bytestring +, gitignoreSource +, imports +, lib +, network +, optparse-applicative +, text +, types-common +, wire-api +, wire-api-federation +}: +mkDerivation { + pname = "rabbitmq-consumer"; + version = "1.0.0"; + src = gitignoreSource ./.; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + aeson + aeson-pretty + amqp + base + bytestring + imports + network + optparse-applicative + text + types-common + wire-api + wire-api-federation + ]; + executableHaskellDepends = [ base ]; + description = "CLI tool to consume messages from a RabbitMQ queue"; + license = lib.licenses.agpl3Only; + mainProgram = "rabbitmq-consumer"; +} diff --git a/tools/rabbitmq-consumer/rabbitmq-consumer.cabal b/tools/rabbitmq-consumer/rabbitmq-consumer.cabal new file mode 100644 index 00000000000..81eb049de12 --- /dev/null +++ b/tools/rabbitmq-consumer/rabbitmq-consumer.cabal @@ -0,0 +1,84 @@ +cabal-version: 3.0 +name: rabbitmq-consumer +version: 1.0.0 +synopsis: CLI tool to consume messages from a RabbitMQ queue +category: Network +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2023 Wire Swiss GmbH +license: AGPL-3.0-only +build-type: Simple + +executable rabbitmq-consumer + main-is: Main.hs + build-depends: + , base + , rabbitmq-consumer + + hs-source-dirs: app + +library + hs-source-dirs: src + exposed-modules: RabbitMQConsumer.Lib + default-language: GHC2021 + ghc-options: + -Wall -Wpartial-fields -fwarn-tabs + -optP-Wno-nonportable-include-path + + build-depends: + , aeson + , aeson-pretty + , amqp + , base + , bytestring + , imports + , network + , optparse-applicative + , text + , types-common + , wire-api + , wire-api-federation + + default-extensions: + NoImplicitPrelude + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + DuplicateRecordFields + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + OverloadedLabels + OverloadedRecordDot + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + RecordWildCards + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns diff --git a/tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs b/tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs new file mode 100644 index 00000000000..307d1d30039 --- /dev/null +++ b/tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs @@ -0,0 +1,232 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 . +{-# LANGUAGE OverloadedStrings #-} + +module RabbitMQConsumer.Lib where + +import Data.Aeson +import Data.Aeson.Encode.Pretty +import Data.ByteString.Lazy.Char8 qualified as BL +import Data.Domain (Domain) +import Data.Text.Lazy.Encoding qualified as TL +import Imports +import Network.AMQP +import Network.Socket +import Options.Applicative +import Wire.API.Federation.BackendNotifications (BackendNotification (..)) +import Wire.API.MakesFederatedCall (Component) + +main :: IO () +main = do + opts <- execParser (info (helper <*> optsParser) desc) + conn <- openConnection' opts.host opts.port opts.vhost opts.username opts.password + chan <- openChannel conn + qos chan 0 1 False + done <- newEmptyMVar + case opts.cmd of + Interactive -> void $ consumeMsgs chan opts.queue Ack (interactive done opts) + Head -> do + runTimerAsync done opts.timeoutSec + void $ consumeMsgs chan opts.queue Ack (printHead done opts) + DropHead dhOpts -> do + runTimerAsync done opts.timeoutSec + void $ consumeMsgs chan opts.queue Ack (dropHead done opts dhOpts) + takeMVar done + closeConnection conn + putStrLn "connection closed" + where + desc = header "rabbitmq-consumer" <> progDesc "CLI tool to consume messages from a RabbitMQ queue" <> fullDesc + + printHead :: MVar () -> Opts -> (Message, Envelope) -> IO () + printHead done opts (msg, _env) = do + putStrLn $ displayMessage opts msg + void $ tryPutMVar done () + + dropHead :: MVar () -> Opts -> DropHeadOpts -> (Message, Envelope) -> IO () + dropHead done opts dhOpts (msg, env) = do + putStrLn $ displayMessage opts msg + case decode @BackendNotification msg.msgBody of + Nothing -> putStrLn "failed to decode message body" + Just bn -> do + if bn.path == dhOpts.path + then do + putStrLn "dropping message" + nackEnv env + else do + putStrLn "path does not match. keeping message" + void $ tryPutMVar done () + + interactive :: MVar () -> Opts -> (Message, Envelope) -> IO () + interactive done opts (msg, env) = do + putStrLn $ displayMessage opts msg + putStrLn $ "type 'drop' to drop the message and terminate, or press enter to terminate without dropping the message" + input <- getLine + if input == "drop" + then do + ackEnv env + putStrLn "message dropped" + else putStrLn "message not dropped" + void $ tryPutMVar done () + + displayMessage :: Opts -> Message -> String + displayMessage opts msg = + intercalate + "\n" + [ "vhost: " <> cs opts.vhost, + "queue: " <> cs opts.queue, + "timestamp: " <> show msg.msgTimestamp, + "received message: \n" <> BL.unpack (maybe msg.msgBody encodePretty (decode @BackendNotification' msg.msgBody)) + ] + + runTimerAsync :: MVar () -> Int -> IO () + runTimerAsync done sec = void $ forkIO $ do + threadDelay (sec * 1000000) + putStrLn $ "timeout after " <> show sec <> " seconds" + void $ tryPutMVar done () + +data Opts = Opts + { host :: String, + port :: PortNumber, + username :: Text, + password :: Text, + vhost :: Text, + queue :: Text, + timeoutSec :: Int, + cmd :: Command + } + +data DropHeadOpts = DropHeadOpts + { path :: Text + } + +data Command = Head | DropHead DropHeadOpts | Interactive + +optsParser :: Parser Opts +optsParser = + Opts + <$> strOption + ( long "host" + <> short 's' + <> metavar "HOST" + <> help "RabbitMQ host" + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long "port" + <> short 'p' + <> metavar "PORT" + <> help "RabbitMQ Port" + <> value 5672 + <> showDefault + ) + <*> strOption + ( long "username" + <> short 'u' + <> metavar "USERNAME" + <> help "RabbitMQ Username" + <> value "guest" + <> showDefault + ) + <*> strOption + ( long "password" + <> short 'w' + <> metavar "PASSWORD" + <> help "RabbitMQ Password" + <> value "alpaca-grapefruit" + <> showDefault + ) + <*> strOption + ( long "vhost" + <> short 'v' + <> metavar "VHOST" + <> help "RabbitMQ VHost" + <> value "/" + <> showDefault + ) + <*> strOption + ( long "queue" + <> short 'q' + <> metavar "QUEUE" + <> help "RabbitMQ Queue" + <> value "test" + <> showDefault + ) + <*> option + auto + ( long "timeout" + <> short 't' + <> metavar "TIMEOUT" + <> help + "Timeout in seconds. The command will timeout if no messages are received within this time. \ + \This can happen when the queue is empty, \ + \or when we lose the single active consumer race." + <> value 10 + <> showDefault + ) + <*> hsubparser (headCommand <> dropHeadCommand <> interactiveCommand) + +headCommand :: Mod CommandFields Command +headCommand = + (command "head" (info (pure Head) (progDesc "Print the first message in the queue"))) + +dropHeadCommand :: Mod CommandFields Command +dropHeadCommand = + (command "drop-head" (info p (progDesc "Drop the first message in the queue"))) + where + p :: Parser Command + p = + DropHead + . DropHeadOpts + <$> strOption + ( long "path" + <> short 'a' + <> metavar "PATH" + <> help "only drop the first message if the path matches" + ) + +interactiveCommand :: Mod CommandFields Command +interactiveCommand = + (command "interactive" (info (pure Interactive) (progDesc "Interactively drop the first message from the queue"))) + +newtype Body = Body {unBody :: Value} + deriving (Show, Eq, Generic) + +instance ToJSON Body where + toJSON (Body v) = v + +instance FromJSON Body where + parseJSON v = + Body . bodyToValue . TL.encodeUtf8 <$> parseJSON v + where + bodyToValue :: BL.ByteString -> Value + bodyToValue bs = fromMaybe (String $ cs $ TL.decodeUtf8 bs) $ decode @Value bs + +-- | A variant of 'BackendNotification' with a FromJSON instance for the body field +-- that converts its BL.ByteString content to a JSON value so that it can be pretty printed +data BackendNotification' = BackendNotification' + { ownDomain :: Domain, + targetComponent :: Component, + path :: Text, + body :: Body + } + deriving (Show, Eq, Generic) + +instance ToJSON BackendNotification' + +instance FromJSON BackendNotification' From bc4afa0d48f262fe45e5ae6e2b7aff68e01418f2 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 17 Oct 2023 12:23:47 +0200 Subject: [PATCH 218/225] kube-integration-teardown: Ignore failures in `helmfile destroy` (#3645) This can happen due to some credentials being missing, the steps afterwards delete the whole namespace anyway. Also in this commit: Use 1 kubectl command to delete both namespaces, its faster like this because kubectl deletes them in parallel and kubeneretes can destroy resources inside in parallel. --- hack/bin/integration-teardown-federation.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hack/bin/integration-teardown-federation.sh b/hack/bin/integration-teardown-federation.sh index 5a5ca938e9c..fba96854904 100755 --- a/hack/bin/integration-teardown-federation.sh +++ b/hack/bin/integration-teardown-federation.sh @@ -22,7 +22,6 @@ else fi . "$DIR/helm_overrides.sh" -helmfile --file "${TOP_LEVEL}/hack/helmfile.yaml" destroy --skip-deps --skip-charts --concurrency 0 +helmfile --file "${TOP_LEVEL}/hack/helmfile.yaml" destroy --skip-deps --skip-charts --concurrency 0 || echo "Failed to delete helm deployments, ignoring this failure as next steps will the destroy namespaces anyway." -kubectl delete namespace "$NAMESPACE_1" -kubectl delete namespace "$NAMESPACE_2" +kubectl delete namespace "$NAMESPACE_1" "$NAMESPACE_2" From 05b5e66abfff1c8d6e2ef396ecfa3c1daa3e7996 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Wed, 18 Oct 2023 10:05:39 +0200 Subject: [PATCH 219/225] Pin rabbitmq docker image to 3.11.x (#3656) --- deploy/dockerephemeral/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index 17321961014..a988af62cae 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -205,7 +205,7 @@ services: rabbitmq: container_name: rabbitmq - image: rabbitmq:3-management-alpine + image: rabbitmq:3.11-management-alpine environment: - RABBITMQ_DEFAULT_USER=${RABBITMQ_USERNAME} - RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD} From 5bc7f9cf1358ecdb61c6393bd254d2856d721ffc Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 18 Oct 2023 14:02:05 +0200 Subject: [PATCH 220/225] integration-tests: Allow uploading XML results to S3 (#3633) --- changelog.d/5-internal/xml-reports | 2 + charts/backoffice/templates/tests/secret.yaml | 15 ++++ .../templates/tests/stern-integration.yaml | 46 +++++++++++ charts/backoffice/values.yaml | 9 +++ .../templates/tests/brig-integration.yaml | 50 +++++++++++- charts/brig/templates/tests/secret.yaml | 81 +++++-------------- charts/brig/values.yaml | 70 ++++++++++++++++ .../tests/cargohold-integration.yaml | 45 +++++++++++ charts/cargohold/templates/tests/secret.yaml | 15 ++++ charts/cargohold/values.yaml | 8 ++ .../tests/federator-integration.yaml | 50 +++++++++++- charts/federator/templates/tests/secret.yaml | 15 ++++ charts/federator/values.yaml | 9 +++ .../templates/tests/galley-integration.yaml | 49 ++++++++++- charts/galley/templates/tests/secret.yaml | 80 ++++-------------- charts/galley/values.yaml | 70 ++++++++++++++++ .../templates/tests/gundeck-integration.yaml | 45 +++++++++++ charts/gundeck/templates/tests/secret.yaml | 16 ++++ charts/gundeck/values.yaml | 8 ++ .../templates/integration-integration.yaml | 44 +++++++++- charts/integration/templates/secret.yaml | 15 ++++ charts/integration/values.yaml | 2 + charts/spar/templates/tests/secret.yaml | 15 ++++ .../templates/tests/spar-integration.yaml | 49 +++++++++++ charts/spar/values.yaml | 9 +++ hack/helm_vars/common.yaml.gotmpl | 11 ++- hack/helm_vars/wire-server/values.yaml.gotmpl | 76 ++++++++++++++++- 27 files changed, 769 insertions(+), 135 deletions(-) create mode 100644 charts/backoffice/templates/tests/secret.yaml create mode 100644 charts/cargohold/templates/tests/secret.yaml create mode 100644 charts/federator/templates/tests/secret.yaml create mode 100644 charts/gundeck/templates/tests/secret.yaml create mode 100644 charts/integration/templates/secret.yaml create mode 100644 charts/spar/templates/tests/secret.yaml diff --git a/changelog.d/5-internal/xml-reports b/changelog.d/5-internal/xml-reports index 5ab1e589deb..6e1a44781e6 100644 --- a/changelog.d/5-internal/xml-reports +++ b/changelog.d/5-internal/xml-reports @@ -7,3 +7,5 @@ integration suite pass `--xml=` to generate the XML file. For spar-integration and federator-integration pass `-f junit` and set `JUNIT_OUTPUT_DIRECTORY` and `JUNIT_SUITE_NAME` environment variables. The XML report will be generated at `$JUNIT_OUTPUT_DIRECTORY/junit.xml`. + +(#3568, #3633) diff --git a/charts/backoffice/templates/tests/secret.yaml b/charts/backoffice/templates/tests/secret.yaml new file mode 100644 index 00000000000..0104b5c9963 --- /dev/null +++ b/charts/backoffice/templates/tests/secret.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Secret +metadata: + name: stern-integration + labels: + app: stern-integration + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + {{- with .Values.tests.secrets }} + uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} + uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} + {{- end }} diff --git a/charts/backoffice/templates/tests/stern-integration.yaml b/charts/backoffice/templates/tests/stern-integration.yaml index bcdaa8bc630..cbe0da5f117 100644 --- a/charts/backoffice/templates/tests/stern-integration.yaml +++ b/charts/backoffice/templates/tests/stern-integration.yaml @@ -19,6 +19,33 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 6 }} {{- end }} + command: + - /bin/bash + - -c + - | + set -euo pipefail + + if stern-integration --xml "$TEST_XML"; then + exit_code=$? + else + exit_code=$? + fi + + {{- if .Values.tests.config.uploadXml }} + # In case a different S3 compliant storage is used to upload test result. + if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then + export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$UPLOAD_XML_AWS_SECRET_ACCESS_KEY" + fi + + # The `|| echo ..` ensures that the test isn't seen as failed even if the upload fails. + ts=$(date --utc '+%Y%m%d%H%M%S%N') + uploadUrl="$UPLOAD_XML_S3_BASE_URL/stern-integration/${ts}.xml" + echo "Uploading xml result to: $uploadUrl" + aws s3 cp "$TEST_XML" "$uploadUrl" || echo "failed to upload result" + {{- end }} + + exit $exit_code volumeMounts: - name: "stern-integration" mountPath: "/etc/wire/integration" @@ -26,4 +53,23 @@ spec: requests: memory: "128Mi" cpu: "1" + env: + - name: TEST_XML + value: /tmp/result.xml + {{- if .Values.tests.config.uploadXml }} + - name: UPLOAD_XML_S3_BASE_URL + value: {{ .Values.tests.config.uploadXml.baseUrl }} + {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + - name: UPLOAD_XML_AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: stern-integration + key: uploadXmlAwsAccessKeyId + - name: UPLOAD_XML_AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: stern-integration + key: uploadXmlAwsSecretAccessKey + {{- end }} + {{- end }} restartPolicy: Never diff --git a/charts/backoffice/values.yaml b/charts/backoffice/values.yaml index 50fbf711a4b..a7b9bc0a700 100644 --- a/charts/backoffice/values.yaml +++ b/charts/backoffice/values.yaml @@ -27,3 +27,12 @@ podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault + +tests: + config: {} +# config: +# uploadXml: +# baseUrl: s3://bucket/path/ +# secrets: +# uploadXmlAwsAccessKeyId: +# uploadXmlAwsSecretAccessKey: diff --git a/charts/brig/templates/tests/brig-integration.yaml b/charts/brig/templates/tests/brig-integration.yaml index 17632a48184..1599c3860b7 100644 --- a/charts/brig/templates/tests/brig-integration.yaml +++ b/charts/brig/templates/tests/brig-integration.yaml @@ -42,8 +42,8 @@ spec: configMap: name: "turn" - name: "brig-integration-secrets" - configMap: - name: "brig-integration-secrets" + secret: + secretName: "brig-integration" containers: - name: integration image: "{{ .Values.image.repository }}-integration:{{ .Values.image.tag }}" @@ -58,7 +58,33 @@ spec: # same file-system. # The other test, "user.auth.cookies.limit", is skipped as it is flaky. # This is tracked in https://github.com/zinfra/backend-issues/issues/1150. - command: [ "brig-integration", "--pattern", "!/turn/ && !/user.auth.cookies.limit/" ] + command: + - /bin/bash + - -c + - | + set -euo pipefail + + if brig-integration --xml "$TEST_XML" --pattern "!/turn/ && !/user.auth.cookies.limit/"; then + exit_code=$? + else + exit_code=$? + fi + + {{- if .Values.tests.config.uploadXml }} + # In case a different S3 compliant storage is used to upload test result. + if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then + export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$UPLOAD_XML_AWS_SECRET_ACCESS_KEY" + fi + + # The `|| echo ..` ensures that the test isn't seen as failed even if the upload fails. + ts=$(date --utc '+%Y%m%d%H%M%S%N') + uploadUrl="$UPLOAD_XML_S3_BASE_URL/brig-integration/${ts}.xml" + echo "Uploading xml result to: $uploadUrl" + aws s3 cp "$TEST_XML" "$uploadUrl" || echo "failed to upload result" + {{- end }} + + exit $exit_code volumeMounts: - name: "brig-integration" mountPath: "/etc/wire/integration" @@ -90,6 +116,24 @@ spec: - name: RABBITMQ_PASSWORD value: "guest" {{- end }} + - name: TEST_XML + value: /tmp/result.xml + {{- if .Values.tests.config.uploadXml }} + - name: UPLOAD_XML_S3_BASE_URL + value: {{ .Values.tests.config.uploadXml.baseUrl }} + {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + - name: UPLOAD_XML_AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: brig-integration + key: uploadXmlAwsAccessKeyId + - name: UPLOAD_XML_AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: brig-integration + key: uploadXmlAwsSecretAccessKey + {{- end }} + {{- end }} resources: requests: memory: "512Mi" diff --git a/charts/brig/templates/tests/secret.yaml b/charts/brig/templates/tests/secret.yaml index 69ce7e671e0..5c5459e609a 100644 --- a/charts/brig/templates/tests/secret.yaml +++ b/charts/brig/templates/tests/secret.yaml @@ -1,69 +1,24 @@ apiVersion: v1 -kind: ConfigMap +kind: Secret metadata: - name: brig-integration-secrets + name: brig-integration + labels: + app: brig-integration + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" annotations: "helm.sh/hook": post-install "helm.sh/hook-delete-policy": before-hook-creation +type: Opaque data: - # These "secrets" are only used in tests and are therefore safe to be stored unencrypted - provider-privatekey.pem: | - -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEAu+Kg/PHHU3atXrUbKnw0G06FliXcNt3lMwl2os5twEDcPPFw - /feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPHWvUBdiLfGrZqJO223DB6D8K2Su/o - dmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKVVPOaOzgtAB21XKRiQ4ermqgi3/nj - r03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiSbUKr/BeArYRcjzr/h5m1In6fG/if - 9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg87X883H+LA/d6X5CTiPv1VMxXdBUi - GPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7lanQIDAQABAoIBAQC0doVy7zgpLsBv - Sz0AnbPe1pjxEwRlntRbJSfSULySALqJvs5s4adSVGUBHX3z/LousAP1SRpCppuU - 8wrLBFgjQVlaAzyQB84EEl+lNtrG8Jrvd2es9R/4sJDkqy50+yuPN5wnzWPFIjhg - 3jP5CHDu29y0LMzsY5yjkzDe9B0bueXEZVU+guRjhpwHHKOFeAr9J9bugFUwgeAr - jF0TztzFAb0fsUNPiQAho1J5PyjSVgItaPfAPv/p30ROG+rz+Rd5NSSvBC5F+yOo - azb84zzwCg/knAfIz7SOMRrmBh2qhGZFZ8gXdq65UaYv+cpT/qo28mpAT2vOkyeD - aPZp0ysBAoGBAOQROoDipe/5BTHBcXYuUE1qa4RIj3wgql5I8igXr4K6ppYBmaOg - DL2rrnqD86chv0P4l/XOomKFwYhVGXtqRkeYnk6mQXwNVkgqcGbY5PSNyMg5+ekq - jSOOPHGzzTWKzYuUDUpB/Lf6jbTv8fq2GYW3ZYiqQ/xiugOvglZrTE7NAoGBANLl - irjByfxAWGhzCrDx0x5MBpsetadI9wUA8u1BDdymsRg73FDn3z7NipVUAMDXMGVj - lqbCRlHESO2yP4GaPEA4FM+MbTZSuhAYV+SY07mEPLHF64/nJas83Zp91r5rhaqJ - L9rWCl3KJ5OUnr3YizCnHIW72FxjwtpjxHJLupsRAoGAGIbhy8qUHeKh9F/hW9xP - NoQjW+6Rv7+jktA1eqpRbbW1BJzXcQldVWiJMxPNuEOg1iZ98SlvvTi1P3wnaWZc - eIapP7wRfs3QYaJuxCC/Pq2g0ieqALFazGAXkALOJtvujvw1Ea9XBlIjuzmyxEuh - Iwg+Gxx0g0f6yTquwax4YGECgYEAnpAK3qKFNO1ECzQDo8oNy0ep59MNDPtlDhQK - katJus5xdCD9oq7TQKrVOTTxZAvmzTQ1PqfuqueDVYOhD9Zg2n/P1cRlEGTek99Z - pfvppB/yak6+r3FA9yBKFS/r1zuMQg3nNweav62QV/tz5pT7AdeDMGFtaPlwtTYx - qyWY5aECgYBPySbPccNj+xxQzxcti2y/UXjC04RgOA/Hm1D0exa0vBqS9uxlOdG8 - F47rKenpBrslvdfTVsCDB1xyP2ebWVzp6EqMycw6OLPxgo3fBfZ4pi6P+rByh0Cc - Lhfh+ET0CPnKCxtop3lUrn4ZvqchS0j3J+M0pDuqoWF5hfKxFhkEIw== - -----END RSA PRIVATE KEY----- - provider-publickey.pem: | - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0 - G06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH - WvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV - VPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS - bUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8 - 7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la - nQIDAQAB - -----END PUBLIC KEY----- - provider-publiccert.pem: | - -----BEGIN CERTIFICATE----- - MIIDdjCCAl4CCQCm0AiwERR/qjANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJE - RTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xGDAWBgNVBAoMD1dp - cmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20xHzAdBgkqhkiG9w0BCQEW - EGJhY2tlbmRAd2lyZS5jb20wHhcNMTYwODA0MTMxNDQyWhcNMzYwNzMwMTMxNDQy - WjB9MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJs - aW4xGDAWBgNVBAoMD1dpcmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20x - HzAdBgkqhkiG9w0BCQEWEGJhY2tlbmRAd2lyZS5jb20wggEiMA0GCSqGSIb3DQEB - AQUAA4IBDwAwggEKAoIBAQC74qD88cdTdq1etRsqfDQbToWWJdw23eUzCXaizm3A - QNw88XD994aIArKbGn7smpkOux5LkP1Mcatb45BEg8da9QF2It8atmok7bbcMHoP - wrZK7+h2aeNknbPbeuFegQCtOmW74OD0r5zYtV5dMpVU85o7OC0AHbVcpGJDh6ua - qCLf+eOvTetfKr+o2S413q01yD4cB8bF8a+8JJgF+JJtQqv8F4CthFyPOv+HmbUi - fp8b+J/0YQjqbx3EdP0ltjnfCKSyjDLpqMK6qyQgWDztfzzcf4sD93pfkJOI+/VU - zFd0FSIY+4L0hP/oI1DX8sW3Q/ftrHnz4sZiVoWjuVqdAgMBAAEwDQYJKoZIhvcN - AQELBQADggEBAEuwlHElIGR56KVC1dJiw238mDGjMfQzSP76Wi4zWS6/zZwJUuog - BkC+vacfju8UAMvL+vdqkjOVUHor84/2wuq0qn91AjOITD7tRAZB+XLXxsikKv/v - OXE3A/lCiNi882NegPyXAfFPp/71CIiTQZps1eQkAvhD5t5WiFYPESxDlvEJrHFY - XP4+pp8fL8YPS7iZNIq+z+P8yVIw+B/Hs0ht7wFIYN0xACbU8m9+Rs08JMoT16c+ - hZMuK3BWD3fzkQVfW0yMwz6fWRXB483ZmekGkgndOTDoJQMdJXZxHpI3t2FcxQYj - T45GXxRd18neXtuYa/OoAw9UQFDN5XfXN0g= - -----END CERTIFICATE----- + {{- with .Values.tests.secrets }} + provider-privatekey.pem: {{ .providerPrivateKey | b64enc | quote }} + provider-publickey.pem: {{ .providerPublicKey | b64enc | quote }} + provider-publiccert.pem: {{ .providerPublicCert | b64enc | quote }} + {{- if .uploadXmlAwsAccessKeyId }} + uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} + uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} + {{- end }} + {{- end }} + diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index 7f756955226..818b4a55578 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -143,3 +143,73 @@ podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault +tests: + config: {} + # uploadXml: + # baseUrl: s3://bucket/path/ + + secrets: + # uploadXmlAwsAccessKeyId: + # uploadXmlAwsSecretAccessKey: + + # These "secrets" are only used in tests and are therefore safe to be stored unencrypted + providerPrivateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAu+Kg/PHHU3atXrUbKnw0G06FliXcNt3lMwl2os5twEDcPPFw + /feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPHWvUBdiLfGrZqJO223DB6D8K2Su/o + dmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKVVPOaOzgtAB21XKRiQ4ermqgi3/nj + r03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiSbUKr/BeArYRcjzr/h5m1In6fG/if + 9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg87X883H+LA/d6X5CTiPv1VMxXdBUi + GPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7lanQIDAQABAoIBAQC0doVy7zgpLsBv + Sz0AnbPe1pjxEwRlntRbJSfSULySALqJvs5s4adSVGUBHX3z/LousAP1SRpCppuU + 8wrLBFgjQVlaAzyQB84EEl+lNtrG8Jrvd2es9R/4sJDkqy50+yuPN5wnzWPFIjhg + 3jP5CHDu29y0LMzsY5yjkzDe9B0bueXEZVU+guRjhpwHHKOFeAr9J9bugFUwgeAr + jF0TztzFAb0fsUNPiQAho1J5PyjSVgItaPfAPv/p30ROG+rz+Rd5NSSvBC5F+yOo + azb84zzwCg/knAfIz7SOMRrmBh2qhGZFZ8gXdq65UaYv+cpT/qo28mpAT2vOkyeD + aPZp0ysBAoGBAOQROoDipe/5BTHBcXYuUE1qa4RIj3wgql5I8igXr4K6ppYBmaOg + DL2rrnqD86chv0P4l/XOomKFwYhVGXtqRkeYnk6mQXwNVkgqcGbY5PSNyMg5+ekq + jSOOPHGzzTWKzYuUDUpB/Lf6jbTv8fq2GYW3ZYiqQ/xiugOvglZrTE7NAoGBANLl + irjByfxAWGhzCrDx0x5MBpsetadI9wUA8u1BDdymsRg73FDn3z7NipVUAMDXMGVj + lqbCRlHESO2yP4GaPEA4FM+MbTZSuhAYV+SY07mEPLHF64/nJas83Zp91r5rhaqJ + L9rWCl3KJ5OUnr3YizCnHIW72FxjwtpjxHJLupsRAoGAGIbhy8qUHeKh9F/hW9xP + NoQjW+6Rv7+jktA1eqpRbbW1BJzXcQldVWiJMxPNuEOg1iZ98SlvvTi1P3wnaWZc + eIapP7wRfs3QYaJuxCC/Pq2g0ieqALFazGAXkALOJtvujvw1Ea9XBlIjuzmyxEuh + Iwg+Gxx0g0f6yTquwax4YGECgYEAnpAK3qKFNO1ECzQDo8oNy0ep59MNDPtlDhQK + katJus5xdCD9oq7TQKrVOTTxZAvmzTQ1PqfuqueDVYOhD9Zg2n/P1cRlEGTek99Z + pfvppB/yak6+r3FA9yBKFS/r1zuMQg3nNweav62QV/tz5pT7AdeDMGFtaPlwtTYx + qyWY5aECgYBPySbPccNj+xxQzxcti2y/UXjC04RgOA/Hm1D0exa0vBqS9uxlOdG8 + F47rKenpBrslvdfTVsCDB1xyP2ebWVzp6EqMycw6OLPxgo3fBfZ4pi6P+rByh0Cc + Lhfh+ET0CPnKCxtop3lUrn4ZvqchS0j3J+M0pDuqoWF5hfKxFhkEIw== + -----END RSA PRIVATE KEY----- + providerPublicKey: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0 + G06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH + WvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV + VPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS + bUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8 + 7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la + nQIDAQAB + -----END PUBLIC KEY----- + providerPublicCert: | + -----BEGIN CERTIFICATE----- + MIIDdjCCAl4CCQCm0AiwERR/qjANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJE + RTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xGDAWBgNVBAoMD1dp + cmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20xHzAdBgkqhkiG9w0BCQEW + EGJhY2tlbmRAd2lyZS5jb20wHhcNMTYwODA0MTMxNDQyWhcNMzYwNzMwMTMxNDQy + WjB9MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJs + aW4xGDAWBgNVBAoMD1dpcmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20x + HzAdBgkqhkiG9w0BCQEWEGJhY2tlbmRAd2lyZS5jb20wggEiMA0GCSqGSIb3DQEB + AQUAA4IBDwAwggEKAoIBAQC74qD88cdTdq1etRsqfDQbToWWJdw23eUzCXaizm3A + QNw88XD994aIArKbGn7smpkOux5LkP1Mcatb45BEg8da9QF2It8atmok7bbcMHoP + wrZK7+h2aeNknbPbeuFegQCtOmW74OD0r5zYtV5dMpVU85o7OC0AHbVcpGJDh6ua + qCLf+eOvTetfKr+o2S413q01yD4cB8bF8a+8JJgF+JJtQqv8F4CthFyPOv+HmbUi + fp8b+J/0YQjqbx3EdP0ltjnfCKSyjDLpqMK6qyQgWDztfzzcf4sD93pfkJOI+/VU + zFd0FSIY+4L0hP/oI1DX8sW3Q/ftrHnz4sZiVoWjuVqdAgMBAAEwDQYJKoZIhvcN + AQELBQADggEBAEuwlHElIGR56KVC1dJiw238mDGjMfQzSP76Wi4zWS6/zZwJUuog + BkC+vacfju8UAMvL+vdqkjOVUHor84/2wuq0qn91AjOITD7tRAZB+XLXxsikKv/v + OXE3A/lCiNi882NegPyXAfFPp/71CIiTQZps1eQkAvhD5t5WiFYPESxDlvEJrHFY + XP4+pp8fL8YPS7iZNIq+z+P8yVIw+B/Hs0ht7wFIYN0xACbU8m9+Rs08JMoT16c+ + hZMuK3BWD3fzkQVfW0yMwz6fWRXB483ZmekGkgndOTDoJQMdJXZxHpI3t2FcxQYj + T45GXxRd18neXtuYa/OoAw9UQFDN5XfXN0g= + -----END CERTIFICATE----- diff --git a/charts/cargohold/templates/tests/cargohold-integration.yaml b/charts/cargohold/templates/tests/cargohold-integration.yaml index 26e77778067..170e4e02595 100644 --- a/charts/cargohold/templates/tests/cargohold-integration.yaml +++ b/charts/cargohold/templates/tests/cargohold-integration.yaml @@ -21,6 +21,33 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 6 }} {{- end }} + command: + - /bin/bash + - -c + - | + set -euo pipefail + + if cargohold-integration --xml "$TEST_XML"; then + exit_code=$? + else + exit_code=$? + fi + + {{- if .Values.tests.config.uploadXml }} + # In case a different S3 compliant storage is used to upload test result. + if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then + export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$UPLOAD_XML_AWS_SECRET_ACCESS_KEY" + fi + + # The `|| echo ..` ensures that the test isn't seen as failed even if the upload fails. + ts=$(date --utc '+%Y%m%d%H%M%S%N') + uploadUrl="$UPLOAD_XML_S3_BASE_URL/cargohold-integration/${ts}.xml" + echo "Uploading xml result to: $uploadUrl" + aws s3 cp "$TEST_XML" "$uploadUrl" || echo "failed to upload result" + {{- end }} + + exit $exit_code volumeMounts: - name: "cargohold-integration" mountPath: "/etc/wire/integration" @@ -40,4 +67,22 @@ spec: key: awsSecretKey - name: AWS_REGION value: "{{ .Values.config.aws.region }}" + - name: TEST_XML + value: /tmp/result.xml + {{- if .Values.tests.config.uploadXml }} + - name: UPLOAD_XML_S3_BASE_URL + value: {{ .Values.tests.config.uploadXml.baseUrl }} + {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + - name: UPLOAD_XML_AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: cargohold-integration + key: uploadXmlAwsAccessKeyId + - name: UPLOAD_XML_AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: cargohold-integration + key: uploadXmlAwsSecretAccessKey + {{- end }} + {{- end }} restartPolicy: Never diff --git a/charts/cargohold/templates/tests/secret.yaml b/charts/cargohold/templates/tests/secret.yaml new file mode 100644 index 00000000000..a8bd1f0ae63 --- /dev/null +++ b/charts/cargohold/templates/tests/secret.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Secret +metadata: + name: cargohold-integration + labels: + app: cargohold-integration + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + {{- with .Values.tests.secrets }} + uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} + uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} + {{- end }} diff --git a/charts/cargohold/values.yaml b/charts/cargohold/values.yaml index 300e8b1472d..8ef51a263db 100644 --- a/charts/cargohold/values.yaml +++ b/charts/cargohold/values.yaml @@ -52,3 +52,11 @@ podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault +tests: + config: {} +# config: +# uploadXml: +# baseUrl: s3://bucket/path/ +# secrets: +# uploadXmlAwsAccessKeyId: +# uploadXmlAwsSecretAccessKey: diff --git a/charts/federator/templates/tests/federator-integration.yaml b/charts/federator/templates/tests/federator-integration.yaml index c4edb616f04..f30d7873798 100644 --- a/charts/federator/templates/tests/federator-integration.yaml +++ b/charts/federator/templates/tests/federator-integration.yaml @@ -23,7 +23,34 @@ spec: name: "federator-ca" containers: - name: integration - command: ["federator-integration"] + command: + - /bin/bash + - -c + - | + set -euo pipefail + + if federator-integration -f junit; then + exit_code=$? + else + exit_code=$? + fi + + {{- if .Values.tests.config.uploadXml }} + # In case a different S3 compliant storage is used to upload test result. + if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then + export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$UPLOAD_XML_AWS_SECRET_ACCESS_KEY" + fi + TEST_XML="$JUNIT_OUTPUT_DIRECTORY/junit.xml" + + # The `|| echo ..` ensures that the test isn't seen as failed even if the upload fails. + ts=$(date --utc '+%Y%m%d%H%M%S%N') + uploadUrl="$UPLOAD_XML_S3_BASE_URL/federator-integration/${ts}.xml" + echo "Uploading xml result to: $uploadUrl" + aws s3 cp "$TEST_XML" "$uploadUrl" || echo "failed to upload result" + {{- end }} + + exit $exit_code image: "{{ .Values.image.repository }}-integration:{{ .Values.image.tag }}" {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: @@ -38,4 +65,25 @@ spec: mountPath: "/etc/wire/federator/secrets" - name: "federator-ca" mountPath: "/etc/wire/federator/ca" + env: + - name: JUNIT_OUTPUT_DIRECTORY + value: /tmp/ + - name: JUNIT_SUITE_NAME + value: federator + {{- if .Values.tests.config.uploadXml }} + - name: UPLOAD_XML_S3_BASE_URL + value: {{ .Values.tests.config.uploadXml.baseUrl }} + {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + - name: UPLOAD_XML_AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: federator-integration + key: uploadXmlAwsAccessKeyId + - name: UPLOAD_XML_AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: federator-integration + key: uploadXmlAwsSecretAccessKey + {{- end }} + {{- end }} restartPolicy: Never diff --git a/charts/federator/templates/tests/secret.yaml b/charts/federator/templates/tests/secret.yaml new file mode 100644 index 00000000000..44edadffdae --- /dev/null +++ b/charts/federator/templates/tests/secret.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Secret +metadata: + name: federator-integration + labels: + app: federator-integration + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + {{- with .Values.tests.secrets }} + uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} + uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} + {{- end }} diff --git a/charts/federator/values.yaml b/charts/federator/values.yaml index b7bf4dd9990..531388b897c 100644 --- a/charts/federator/values.yaml +++ b/charts/federator/values.yaml @@ -56,3 +56,12 @@ podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault + +tests: + config: {} +# config: +# uploadXml: +# baseUrl: s3://bucket/path/ +# secrets: +# uploadXmlAwsAccessKeyId: +# uploadXmlAwsSecretAccessKey: diff --git a/charts/galley/templates/tests/galley-integration.yaml b/charts/galley/templates/tests/galley-integration.yaml index 9aebc3e7bd6..e1870228379 100644 --- a/charts/galley/templates/tests/galley-integration.yaml +++ b/charts/galley/templates/tests/galley-integration.yaml @@ -35,8 +35,8 @@ spec: configMap: name: "galley" - name: "galley-integration-secrets" - configMap: - name: "galley-integration-secrets" + secret: + secretName: "galley-integration" - name: "galley-secrets" secret: secretName: "galley" @@ -47,6 +47,33 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 6 }} {{- end }} + command: + - /bin/bash + - -c + - | + set -euo pipefail + + if galley-integration --xml "$TEST_XML"; then + exit_code=$? + else + exit_code=$? + fi + + {{- if .Values.tests.config.uploadXml }} + # In case a different S3 compliant storage is used to upload test result. + if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then + export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$UPLOAD_XML_AWS_SECRET_ACCESS_KEY" + fi + + # The `|| echo ..` ensures that the test isn't seen as failed even if the upload fails. + ts=$(date --utc '+%Y%m%d%H%M%S%N') + uploadUrl="$UPLOAD_XML_S3_BASE_URL/galley-integration/${ts}.xml" + echo "Uploading xml result to: $uploadUrl" + aws s3 cp "$TEST_XML" "$uploadUrl" || echo "failed to upload result" + {{- end }} + + exit $exit_code volumeMounts: - name: "galley-integration" mountPath: "/etc/wire/integration" @@ -71,6 +98,24 @@ spec: - name: RABBITMQ_PASSWORD value: "guest" {{- end }} + - name: TEST_XML + value: /tmp/result.xml + {{- if .Values.tests.config.uploadXml }} + - name: UPLOAD_XML_S3_BASE_URL + value: {{ .Values.tests.config.uploadXml.baseUrl }} + {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + - name: UPLOAD_XML_AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: galley-integration + key: uploadXmlAwsAccessKeyId + - name: UPLOAD_XML_AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: galley-integration + key: uploadXmlAwsSecretAccessKey + {{- end }} + {{- end }} resources: requests: memory: "512Mi" diff --git a/charts/galley/templates/tests/secret.yaml b/charts/galley/templates/tests/secret.yaml index d58a49c3601..d41373a7f21 100644 --- a/charts/galley/templates/tests/secret.yaml +++ b/charts/galley/templates/tests/secret.yaml @@ -1,69 +1,23 @@ apiVersion: v1 -kind: ConfigMap +kind: Secret metadata: - name: galley-integration-secrets + name: galley-integration + labels: + app: galley-integration + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" annotations: "helm.sh/hook": post-install "helm.sh/hook-delete-policy": before-hook-creation +type: Opaque data: - # These "secrets" are only used in tests and are therefore safe to be stored unencrypted - provider-privatekey.pem: | - -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEAu+Kg/PHHU3atXrUbKnw0G06FliXcNt3lMwl2os5twEDcPPFw - /feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPHWvUBdiLfGrZqJO223DB6D8K2Su/o - dmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKVVPOaOzgtAB21XKRiQ4ermqgi3/nj - r03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiSbUKr/BeArYRcjzr/h5m1In6fG/if - 9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg87X883H+LA/d6X5CTiPv1VMxXdBUi - GPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7lanQIDAQABAoIBAQC0doVy7zgpLsBv - Sz0AnbPe1pjxEwRlntRbJSfSULySALqJvs5s4adSVGUBHX3z/LousAP1SRpCppuU - 8wrLBFgjQVlaAzyQB84EEl+lNtrG8Jrvd2es9R/4sJDkqy50+yuPN5wnzWPFIjhg - 3jP5CHDu29y0LMzsY5yjkzDe9B0bueXEZVU+guRjhpwHHKOFeAr9J9bugFUwgeAr - jF0TztzFAb0fsUNPiQAho1J5PyjSVgItaPfAPv/p30ROG+rz+Rd5NSSvBC5F+yOo - azb84zzwCg/knAfIz7SOMRrmBh2qhGZFZ8gXdq65UaYv+cpT/qo28mpAT2vOkyeD - aPZp0ysBAoGBAOQROoDipe/5BTHBcXYuUE1qa4RIj3wgql5I8igXr4K6ppYBmaOg - DL2rrnqD86chv0P4l/XOomKFwYhVGXtqRkeYnk6mQXwNVkgqcGbY5PSNyMg5+ekq - jSOOPHGzzTWKzYuUDUpB/Lf6jbTv8fq2GYW3ZYiqQ/xiugOvglZrTE7NAoGBANLl - irjByfxAWGhzCrDx0x5MBpsetadI9wUA8u1BDdymsRg73FDn3z7NipVUAMDXMGVj - lqbCRlHESO2yP4GaPEA4FM+MbTZSuhAYV+SY07mEPLHF64/nJas83Zp91r5rhaqJ - L9rWCl3KJ5OUnr3YizCnHIW72FxjwtpjxHJLupsRAoGAGIbhy8qUHeKh9F/hW9xP - NoQjW+6Rv7+jktA1eqpRbbW1BJzXcQldVWiJMxPNuEOg1iZ98SlvvTi1P3wnaWZc - eIapP7wRfs3QYaJuxCC/Pq2g0ieqALFazGAXkALOJtvujvw1Ea9XBlIjuzmyxEuh - Iwg+Gxx0g0f6yTquwax4YGECgYEAnpAK3qKFNO1ECzQDo8oNy0ep59MNDPtlDhQK - katJus5xdCD9oq7TQKrVOTTxZAvmzTQ1PqfuqueDVYOhD9Zg2n/P1cRlEGTek99Z - pfvppB/yak6+r3FA9yBKFS/r1zuMQg3nNweav62QV/tz5pT7AdeDMGFtaPlwtTYx - qyWY5aECgYBPySbPccNj+xxQzxcti2y/UXjC04RgOA/Hm1D0exa0vBqS9uxlOdG8 - F47rKenpBrslvdfTVsCDB1xyP2ebWVzp6EqMycw6OLPxgo3fBfZ4pi6P+rByh0Cc - Lhfh+ET0CPnKCxtop3lUrn4ZvqchS0j3J+M0pDuqoWF5hfKxFhkEIw== - -----END RSA PRIVATE KEY----- - provider-publickey.pem: | - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0 - G06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH - WvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV - VPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS - bUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8 - 7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la - nQIDAQAB - -----END PUBLIC KEY----- - provider-publiccert.pem: | - -----BEGIN CERTIFICATE----- - MIIDdjCCAl4CCQCm0AiwERR/qjANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJE - RTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xGDAWBgNVBAoMD1dp - cmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20xHzAdBgkqhkiG9w0BCQEW - EGJhY2tlbmRAd2lyZS5jb20wHhcNMTYwODA0MTMxNDQyWhcNMzYwNzMwMTMxNDQy - WjB9MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJs - aW4xGDAWBgNVBAoMD1dpcmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20x - HzAdBgkqhkiG9w0BCQEWEGJhY2tlbmRAd2lyZS5jb20wggEiMA0GCSqGSIb3DQEB - AQUAA4IBDwAwggEKAoIBAQC74qD88cdTdq1etRsqfDQbToWWJdw23eUzCXaizm3A - QNw88XD994aIArKbGn7smpkOux5LkP1Mcatb45BEg8da9QF2It8atmok7bbcMHoP - wrZK7+h2aeNknbPbeuFegQCtOmW74OD0r5zYtV5dMpVU85o7OC0AHbVcpGJDh6ua - qCLf+eOvTetfKr+o2S413q01yD4cB8bF8a+8JJgF+JJtQqv8F4CthFyPOv+HmbUi - fp8b+J/0YQjqbx3EdP0ltjnfCKSyjDLpqMK6qyQgWDztfzzcf4sD93pfkJOI+/VU - zFd0FSIY+4L0hP/oI1DX8sW3Q/ftrHnz4sZiVoWjuVqdAgMBAAEwDQYJKoZIhvcN - AQELBQADggEBAEuwlHElIGR56KVC1dJiw238mDGjMfQzSP76Wi4zWS6/zZwJUuog - BkC+vacfju8UAMvL+vdqkjOVUHor84/2wuq0qn91AjOITD7tRAZB+XLXxsikKv/v - OXE3A/lCiNi882NegPyXAfFPp/71CIiTQZps1eQkAvhD5t5WiFYPESxDlvEJrHFY - XP4+pp8fL8YPS7iZNIq+z+P8yVIw+B/Hs0ht7wFIYN0xACbU8m9+Rs08JMoT16c+ - hZMuK3BWD3fzkQVfW0yMwz6fWRXB483ZmekGkgndOTDoJQMdJXZxHpI3t2FcxQYj - T45GXxRd18neXtuYa/OoAw9UQFDN5XfXN0g= - -----END CERTIFICATE----- + {{- with .Values.tests.secrets }} + provider-privatekey.pem: {{ .providerPrivateKey | b64enc | quote }} + provider-publickey.pem: {{ .providerPublicKey | b64enc | quote }} + provider-publiccert.pem: {{ .providerPublicCert | b64enc | quote }} + {{- if .uploadXmlAwsAccessKeyId }} + uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} + uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} + {{- end }} + {{- end }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 2783872f820..8bd2d28c37f 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -143,3 +143,73 @@ podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault +tests: + config: {} + # uploadXml: + # baseUrl: s3://bucket/path/ + + secrets: + # uploadXmlAwsAccessKeyId: + # uploadXmlAwsSecretAccessKey: + + # These "secrets" are only used in tests and are therefore safe to be stored unencrypted + providerPrivateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAu+Kg/PHHU3atXrUbKnw0G06FliXcNt3lMwl2os5twEDcPPFw + /feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPHWvUBdiLfGrZqJO223DB6D8K2Su/o + dmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKVVPOaOzgtAB21XKRiQ4ermqgi3/nj + r03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiSbUKr/BeArYRcjzr/h5m1In6fG/if + 9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg87X883H+LA/d6X5CTiPv1VMxXdBUi + GPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7lanQIDAQABAoIBAQC0doVy7zgpLsBv + Sz0AnbPe1pjxEwRlntRbJSfSULySALqJvs5s4adSVGUBHX3z/LousAP1SRpCppuU + 8wrLBFgjQVlaAzyQB84EEl+lNtrG8Jrvd2es9R/4sJDkqy50+yuPN5wnzWPFIjhg + 3jP5CHDu29y0LMzsY5yjkzDe9B0bueXEZVU+guRjhpwHHKOFeAr9J9bugFUwgeAr + jF0TztzFAb0fsUNPiQAho1J5PyjSVgItaPfAPv/p30ROG+rz+Rd5NSSvBC5F+yOo + azb84zzwCg/knAfIz7SOMRrmBh2qhGZFZ8gXdq65UaYv+cpT/qo28mpAT2vOkyeD + aPZp0ysBAoGBAOQROoDipe/5BTHBcXYuUE1qa4RIj3wgql5I8igXr4K6ppYBmaOg + DL2rrnqD86chv0P4l/XOomKFwYhVGXtqRkeYnk6mQXwNVkgqcGbY5PSNyMg5+ekq + jSOOPHGzzTWKzYuUDUpB/Lf6jbTv8fq2GYW3ZYiqQ/xiugOvglZrTE7NAoGBANLl + irjByfxAWGhzCrDx0x5MBpsetadI9wUA8u1BDdymsRg73FDn3z7NipVUAMDXMGVj + lqbCRlHESO2yP4GaPEA4FM+MbTZSuhAYV+SY07mEPLHF64/nJas83Zp91r5rhaqJ + L9rWCl3KJ5OUnr3YizCnHIW72FxjwtpjxHJLupsRAoGAGIbhy8qUHeKh9F/hW9xP + NoQjW+6Rv7+jktA1eqpRbbW1BJzXcQldVWiJMxPNuEOg1iZ98SlvvTi1P3wnaWZc + eIapP7wRfs3QYaJuxCC/Pq2g0ieqALFazGAXkALOJtvujvw1Ea9XBlIjuzmyxEuh + Iwg+Gxx0g0f6yTquwax4YGECgYEAnpAK3qKFNO1ECzQDo8oNy0ep59MNDPtlDhQK + katJus5xdCD9oq7TQKrVOTTxZAvmzTQ1PqfuqueDVYOhD9Zg2n/P1cRlEGTek99Z + pfvppB/yak6+r3FA9yBKFS/r1zuMQg3nNweav62QV/tz5pT7AdeDMGFtaPlwtTYx + qyWY5aECgYBPySbPccNj+xxQzxcti2y/UXjC04RgOA/Hm1D0exa0vBqS9uxlOdG8 + F47rKenpBrslvdfTVsCDB1xyP2ebWVzp6EqMycw6OLPxgo3fBfZ4pi6P+rByh0Cc + Lhfh+ET0CPnKCxtop3lUrn4ZvqchS0j3J+M0pDuqoWF5hfKxFhkEIw== + -----END RSA PRIVATE KEY----- + providerPublicKey: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0 + G06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH + WvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV + VPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS + bUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8 + 7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la + nQIDAQAB + -----END PUBLIC KEY----- + providerPublicCert: | + -----BEGIN CERTIFICATE----- + MIIDdjCCAl4CCQCm0AiwERR/qjANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJE + RTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xGDAWBgNVBAoMD1dp + cmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20xHzAdBgkqhkiG9w0BCQEW + EGJhY2tlbmRAd2lyZS5jb20wHhcNMTYwODA0MTMxNDQyWhcNMzYwNzMwMTMxNDQy + WjB9MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJs + aW4xGDAWBgNVBAoMD1dpcmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20x + HzAdBgkqhkiG9w0BCQEWEGJhY2tlbmRAd2lyZS5jb20wggEiMA0GCSqGSIb3DQEB + AQUAA4IBDwAwggEKAoIBAQC74qD88cdTdq1etRsqfDQbToWWJdw23eUzCXaizm3A + QNw88XD994aIArKbGn7smpkOux5LkP1Mcatb45BEg8da9QF2It8atmok7bbcMHoP + wrZK7+h2aeNknbPbeuFegQCtOmW74OD0r5zYtV5dMpVU85o7OC0AHbVcpGJDh6ua + qCLf+eOvTetfKr+o2S413q01yD4cB8bF8a+8JJgF+JJtQqv8F4CthFyPOv+HmbUi + fp8b+J/0YQjqbx3EdP0ltjnfCKSyjDLpqMK6qyQgWDztfzzcf4sD93pfkJOI+/VU + zFd0FSIY+4L0hP/oI1DX8sW3Q/ftrHnz4sZiVoWjuVqdAgMBAAEwDQYJKoZIhvcN + AQELBQADggEBAEuwlHElIGR56KVC1dJiw238mDGjMfQzSP76Wi4zWS6/zZwJUuog + BkC+vacfju8UAMvL+vdqkjOVUHor84/2wuq0qn91AjOITD7tRAZB+XLXxsikKv/v + OXE3A/lCiNi882NegPyXAfFPp/71CIiTQZps1eQkAvhD5t5WiFYPESxDlvEJrHFY + XP4+pp8fL8YPS7iZNIq+z+P8yVIw+B/Hs0ht7wFIYN0xACbU8m9+Rs08JMoT16c+ + hZMuK3BWD3fzkQVfW0yMwz6fWRXB483ZmekGkgndOTDoJQMdJXZxHpI3t2FcxQYj + T45GXxRd18neXtuYa/OoAw9UQFDN5XfXN0g= + -----END CERTIFICATE----- diff --git a/charts/gundeck/templates/tests/gundeck-integration.yaml b/charts/gundeck/templates/tests/gundeck-integration.yaml index 39fe64a2fc8..7f92351be5a 100644 --- a/charts/gundeck/templates/tests/gundeck-integration.yaml +++ b/charts/gundeck/templates/tests/gundeck-integration.yaml @@ -17,6 +17,33 @@ spec: - name: integration # TODO: When deployed to staging (or real AWS env), _all_ tests should be run command: ["gundeck-integration", "--pattern", "!/RealAWS/"] + command: + - /bin/bash + - -c + - | + set -euo pipefail + + if gundeck-integration --xml "$TEST_XML" --pattern "!/RealAWS/"; then + exit_code=$? + else + exit_code=$? + fi + + {{- if .Values.tests.config.uploadXml }} + # In case a different S3 compliant storage is used to upload test result. + if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then + export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$UPLOAD_XML_AWS_SECRET_ACCESS_KEY" + fi + + # The `|| echo ..` ensures that the test isn't seen as failed even if the upload fails. + ts=$(date --utc '+%Y%m%d%H%M%S%N') + uploadUrl="$UPLOAD_XML_S3_BASE_URL/gundeck-integration/${ts}.xml" + echo "Uploading xml result to: $uploadUrl" + aws s3 cp "$TEST_XML" "$uploadUrl" || echo "failed to upload result" + {{- end }} + + exit $exit_code image: "{{ .Values.image.repository }}-integration:{{ .Values.image.tag }}" {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: @@ -35,4 +62,22 @@ spec: value: "dummy" - name: AWS_REGION value: "eu-west-1" + - name: TEST_XML + value: /tmp/result.xml + {{- if .Values.tests.config.uploadXml }} + - name: UPLOAD_XML_S3_BASE_URL + value: {{ .Values.tests.config.uploadXml.baseUrl }} + {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + - name: UPLOAD_XML_AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: gundeck-integration + key: uploadXmlAwsAccessKeyId + - name: UPLOAD_XML_AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: gundeck-integration + key: uploadXmlAwsSecretAccessKey + {{- end }} + {{- end }} restartPolicy: Never diff --git a/charts/gundeck/templates/tests/secret.yaml b/charts/gundeck/templates/tests/secret.yaml new file mode 100644 index 00000000000..1af8959e4c3 --- /dev/null +++ b/charts/gundeck/templates/tests/secret.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Secret +metadata: + name: gundeck-integration + labels: + app: gundeck-integration + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + {{- with .Values.tests.secrets }} + uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} + uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} + {{- end }} + diff --git a/charts/gundeck/values.yaml b/charts/gundeck/values.yaml index 0ec2ab9efad..28416361448 100644 --- a/charts/gundeck/values.yaml +++ b/charts/gundeck/values.yaml @@ -68,3 +68,11 @@ podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault +tests: + config: {} +# config: +# uploadXml: +# baseUrl: s3://bucket/path/ +# secrets: +# uploadXmlAwsAccessKeyId: +# uploadXmlAwsSecretAccessKey: diff --git a/charts/integration/templates/integration-integration.yaml b/charts/integration/templates/integration-integration.yaml index f4ad967a975..044b63b3b9a 100644 --- a/charts/integration/templates/integration-integration.yaml +++ b/charts/integration/templates/integration-integration.yaml @@ -131,7 +131,31 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 6 }} {{- end }} - command: [ "integration", "--config", "/etc/wire/integration/integration.yaml" ] + command: + - /bin/bash + - -c + - | + set -euo pipefail + + if integration --config /etc/wire/integration/integration.yaml; then + exit_code=$? + else + exit_code=$? + fi + + {{- if .Values.config.uploadXml }} + # In case a different S3 compliant storage is used to upload test result. + if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then + export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$UPLOAD_XML_AWS_SECRET_ACCESS_KEY" + fi + + # The `|| echo ..` ensures that the test isn't seen as failed even if the upload fails. + ts=$(date --utc '+%Y%m%d%H%M%S%N') + aws s3 cp "$TEST_XML" "$UPLOAD_XML_S3_BASE_URL/integration/${ts}.xml" || echo "failed to upload result" + {{- end }} + + exit $exit_code resources: requests: memory: "512Mi" @@ -207,3 +231,21 @@ spec: secretKeyRef: name: brig key: rabbitmqPassword + - name: TEST_XML + value: /tmp/result.xml + {{- if .Values.config.uploadXml }} + - name: UPLOAD_XML_S3_BASE_URL + value: {{ .Values.config.uploadXml.baseUrl }} + {{- if .Values.secrets.uploadXmlAwsAccessKeyId }} + - name: UPLOAD_XML_AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: integration + key: uploadXmlAwsAccessKeyId + - name: UPLOAD_XML_AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: integration + key: uploadXmlAwsSecretAccessKey + {{- end }} + {{- end }} diff --git a/charts/integration/templates/secret.yaml b/charts/integration/templates/secret.yaml new file mode 100644 index 00000000000..52c3199b5f0 --- /dev/null +++ b/charts/integration/templates/secret.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Secret +metadata: + name: integration + labels: + app: integration + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + {{- with .Values.secrets }} + uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} + uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} + {{- end }} diff --git a/charts/integration/values.yaml b/charts/integration/values.yaml index 5b17cc91899..25de2d456e7 100644 --- a/charts/integration/values.yaml +++ b/charts/integration/values.yaml @@ -42,3 +42,5 @@ tls: ingress: class: nginx + +secrets: {} diff --git a/charts/spar/templates/tests/secret.yaml b/charts/spar/templates/tests/secret.yaml new file mode 100644 index 00000000000..1be597127b8 --- /dev/null +++ b/charts/spar/templates/tests/secret.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Secret +metadata: + name: spar-integration + labels: + app: spar-integration + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + {{- with .Values.tests.secrets }} + uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} + uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} + {{- end }} diff --git a/charts/spar/templates/tests/spar-integration.yaml b/charts/spar/templates/tests/spar-integration.yaml index af6aa3d420d..ff937f3d18e 100644 --- a/charts/spar/templates/tests/spar-integration.yaml +++ b/charts/spar/templates/tests/spar-integration.yaml @@ -23,6 +23,34 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 6 }} {{- end }} + command: + - /bin/bash + - -c + - | + set -euo pipefail + + if spar-integration -f junit; then + exit_code=$? + else + exit_code=$? + fi + + {{- if .Values.tests.config.uploadXml }} + # In case a different S3 compliant storage is used to upload test result. + if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then + export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$UPLOAD_XML_AWS_SECRET_ACCESS_KEY" + fi + TEST_XML="$JUNIT_OUTPUT_DIRECTORY/junit.xml" + + # The `|| echo ..` ensures that the test isn't seen as failed even if the upload fails. + ts=$(date --utc '+%Y%m%d%H%M%S%N') + uploadUrl="$UPLOAD_XML_S3_BASE_URL/spar-integration/${ts}.xml" + echo "Uploading xml result to: $uploadUrl" + aws s3 cp "$TEST_XML" "$uploadUrl" || echo "failed to upload result" + {{- end }} + + exit $exit_code volumeMounts: - name: "spar-integration" mountPath: "/etc/wire/integration" @@ -32,4 +60,25 @@ spec: requests: memory: "512Mi" cpu: "2" + env: + - name: JUNIT_OUTPUT_DIRECTORY + value: /tmp/ + - name: JUNIT_SUITE_NAME + value: spar + {{- if .Values.tests.config.uploadXml }} + - name: UPLOAD_XML_S3_BASE_URL + value: {{ .Values.tests.config.uploadXml.baseUrl }} + {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + - name: UPLOAD_XML_AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: spar-integration + key: uploadXmlAwsAccessKeyId + - name: UPLOAD_XML_AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: spar-integration + key: uploadXmlAwsSecretAccessKey + {{- end }} + {{- end }} restartPolicy: Never diff --git a/charts/spar/values.yaml b/charts/spar/values.yaml index 512bbf1b7bf..073fd5b0ee6 100644 --- a/charts/spar/values.yaml +++ b/charts/spar/values.yaml @@ -37,3 +37,12 @@ podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault + +tests: + config: {} +# config: +# uploadXml: +# baseUrl: s3://bucket/path/ +# secrets: +# uploadXmlAwsAccessKeyId: +# uploadXmlAwsSecretAccessKey: diff --git a/hack/helm_vars/common.yaml.gotmpl b/hack/helm_vars/common.yaml.gotmpl index 010aa42dadb..56f209fcce8 100644 --- a/hack/helm_vars/common.yaml.gotmpl +++ b/hack/helm_vars/common.yaml.gotmpl @@ -8,4 +8,13 @@ rabbitmqPassword: guest dynBackendDomain1: dynamic-backend-1.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster.local dynBackendDomain2: dynamic-backend-2.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster.local -dynBackendDomain3: dynamic-backend-3.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster.local \ No newline at end of file +dynBackendDomain3: dynamic-backend-3.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster.local + +{{- if (eq (env "UPLOAD_XML_S3_BASE_URL") "") }} +uploadXml: {} +{{- else }} +uploadXml: + awsAccessKeyId: {{ env "UPLOAD_XML_AWS_ACCESS_KEY_ID" }} + awsSecretAccessKey: {{ env "UPLOAD_XML_AWS_SECRET_ACCESS_KEY" }} + baseUrl: {{ env "UPLOAD_XML_S3_BASE_URL" }} +{{- end }} \ No newline at end of file diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index e126777a5c0..874bd5103f2 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -136,6 +136,15 @@ brig: password: {{ .Values.rabbitmqPassword }} tests: enableFederationTests: true + {{- if .Values.uploadXml }} + config: + uploadXml: + baseUrl: {{ .Values.uploadXml.baseUrl }} + secrets: + uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} + uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} + {{- end }} + cannon: replicaCount: 2 imagePullPolicy: {{ .Values.imagePullPolicy }} @@ -162,6 +171,16 @@ cargohold: secrets: awsKeyId: dummykey awsSecretKey: dummysecret + tests: + {{- if .Values.uploadXml }} + config: + uploadXml: + baseUrl: {{ .Values.uploadXml.baseUrl }} + secrets: + uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} + uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} + {{- end }} + galley: replicaCount: 1 imagePullPolicy: {{ .Values.imagePullPolicy }} @@ -210,6 +229,15 @@ galley: rabbitmq: username: {{ .Values.rabbitmqUsername }} password: {{ .Values.rabbitmqPassword }} + tests: + {{- if .Values.uploadXml }} + config: + uploadXml: + baseUrl: {{ .Values.uploadXml.baseUrl }} + secrets: + uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} + uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} + {{- end }} gundeck: replicaCount: 1 @@ -239,6 +267,16 @@ gundeck: secrets: awsKeyId: dummykey awsSecretKey: dummysecret + tests: + {{- if .Values.uploadXml }} + config: + uploadXml: + baseUrl: {{ .Values.uploadXml.baseUrl }} + secrets: + uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} + uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} + {{- end }} + nginz: replicaCount: 1 imagePullPolicy: {{ .Values.imagePullPolicy }} @@ -296,6 +334,15 @@ spar: - type: ContactSupport company: Example Company email: email:backend+spar@wire.com + tests: + {{- if .Values.uploadXml }} + config: + uploadXml: + baseUrl: {{ .Values.uploadXml.baseUrl }} + secrets: + uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} + uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} + {{- end }} federator: replicaCount: 1 @@ -305,6 +352,15 @@ federator: config: optSettings: useSystemCAStore: false + tests: + {{- if .Values.uploadXml }} + config: + uploadXml: + baseUrl: {{ .Values.uploadXml.baseUrl }} + secrets: + uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} + uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} + {{- end }} background-worker: replicaCount: 1 @@ -323,4 +379,22 @@ background-worker: integration: ingress: - class: "nginx-{{ .Release.Namespace }}" \ No newline at end of file + class: "nginx-{{ .Release.Namespace }}" + {{- if .Values.uploadXml }} + config: + uploadXml: + baseUrl: {{ .Values.uploadXml.baseUrl }} + secrets: + uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} + uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} + {{- end }} +backoffice: + tests: + {{- if .Values.uploadXml }} + config: + uploadXml: + baseUrl: {{ .Values.uploadXml.baseUrl }} + secrets: + uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} + uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} + {{- end }} From 4328659af82027f387ec099cf67fa224d876fe12 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 19 Oct 2023 10:43:16 +0200 Subject: [PATCH 221/225] WPB-5069 Docs for rabbitmq-consumer (#3655) --- changelog.d/5-internal/WPB-4748 | 2 +- .../developer/reference/rabbitmq-consumer.md | 116 ++++++++++++++++++ .../rabbitmq-consumer/rabbitmqadmin.png | Bin 0 -> 296326 bytes 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 docs/src/developer/reference/rabbitmq-consumer.md create mode 100644 docs/src/developer/reference/rabbitmq-consumer/rabbitmqadmin.png diff --git a/changelog.d/5-internal/WPB-4748 b/changelog.d/5-internal/WPB-4748 index 59a431e2b99..823b14baece 100644 --- a/changelog.d/5-internal/WPB-4748 +++ b/changelog.d/5-internal/WPB-4748 @@ -1 +1 @@ -CLI tool to consume messages from a RabbitMQ queue +CLI tool to consume messages from a RabbitMQ queue (#3589, #3655) diff --git a/docs/src/developer/reference/rabbitmq-consumer.md b/docs/src/developer/reference/rabbitmq-consumer.md new file mode 100644 index 00000000000..ec952f1f49c --- /dev/null +++ b/docs/src/developer/reference/rabbitmq-consumer.md @@ -0,0 +1,116 @@ +# RabbitMQ Consumer + +`rabbitmq-consumer` can be used to inspect and drop blocking messages from a RabbitMQ queue containing outgoing messages to another backend. + +E.g. in the following screen shot of the RabbitMQ management UI you can see that a message is stuck in the `backend-notifications.d1.example.com` queue: + +![rabbitmqadmin](rabbitmq-consumer/rabbitmqadmin.png) + +## Interactively inspect/drop messages + +Follow these steps to inspect (and/or drop) the message: + +1. Stop the background-worker because the queues are single active consumer queues. One way to do this is to set the background-worker's `replicas` count to 0 in the k8s deployment. The number of unacked messages should then switch to 0. + +2. Run: + +```shell +RABBITMQ_HOST= # default: "localhost" +RABBITMQ_PORT= # default: 5672 +RABBITMQ_USER= +RABBITMQ_PW= +RABBITMQ_VHOST= # default: "/" +RABBITMQ_QUEUE= +WIRE_VERSION= + +docker run -it --network=host "quay.io/wire/rabbitmq-consumer:$WIRE_VERSION" \ + --host "$RABBITMQ_HOST" \ + --port "$RABBITMQ_PORT" \ + --username "$RABBITMQ_USER" \ + --password "$RABBITMQ_PW" \ + --vhost "$RABBITMQ_VHOST" \ + --queue "$RABBITMQ_QUEUE" \ + interactive +``` + +The output will look similar to: + +``` +vhost: backendA +queue: backend-notifications.d1.example.com +timestamp: Nothing +received message: +{ + "body": { + "conversation": "7d86646e-1122-4979-8629-22dbd6e22afe", + "data": "", + "priority": null, + "push": true, + "recipients": { + "d0c931d3-ee12-43e5-8c97-26f5b9b1ee6d": { + "ea035ddd6d9647d5": "c3VjY2VzcyBtZXNzYWdlIGZvciBkb3duIHVzZXI=" + } + }, + "sender": { + "domain": "example.com", + "id": "83c78b82-545d-4f29-aec4-ae29ea5231d0" + }, + "sender_client": "550d8c614fd20299", + "time": "2023-10-17T10:39:46.38476388Z", + "transient": false + }, + "ownDomain": "example.com", + "path": "/on-message-sent", + "targetComponent": "galley" +} +type 'drop' to drop the message and terminate, or press enter to terminate without dropping the message +``` + +Now the message can be dropped by typing: `drop`. + +## Non-interactive commands + +There are 2 non-interactive commands: + +- `head`: prints the first message in the queue +- `drop-head (-a|--path PATH)`: drops the first message from the queue if the provided path argument matches the path field of the message + +These commands will time out (after 10 seconds per default) if no messages are received within this time. +This can happen when the queue is empty, or when we lose the single active consumer race. + +## Help + +```shell +WIRE_VERSION= + +docker run -it --network=host "quay.io/wire/rabbitmq-consumer:$WIRE_VERSION" --help +``` + +``` +rabbitmq-consumer + +Usage: rabbitmq-consumer [-s|--host HOST] [-p|--port PORT] + [-u|--username USERNAME] [-w|--password PASSWORD] + [-v|--vhost VHOST] [-q|--queue QUEUE] + [-t|--timeout TIMEOUT] COMMAND + + CLI tool to consume messages from a RabbitMQ queue + +Available options: + -h,--help Show this help text + -s,--host HOST RabbitMQ host (default: "localhost") + -p,--port PORT RabbitMQ Port (default: 5672) + -u,--username USERNAME RabbitMQ Username (default: "guest") + -w,--password PASSWORD RabbitMQ Password (default: "alpaca-grapefruit") + -v,--vhost VHOST RabbitMQ VHost (default: "/") + -q,--queue QUEUE RabbitMQ Queue (default: "test") + -t,--timeout TIMEOUT Timeout in seconds. The command will timeout if no + messages are received within this time. This can + happen when the queue is empty, or when we lose the + single active consumer race. (default: 10) + +Available commands: + head Print the first message in the queue + drop-head Drop the first message in the queue + interactive Interactively drop the first message from the queue +``` diff --git a/docs/src/developer/reference/rabbitmq-consumer/rabbitmqadmin.png b/docs/src/developer/reference/rabbitmq-consumer/rabbitmqadmin.png new file mode 100644 index 0000000000000000000000000000000000000000..2c304f66f7ef7375abf85035a1aebd61ce0725a0 GIT binary patch literal 296326 zcmeFYc{tSX+XgIK38iS0qLM8l29sS<29rJ6$vWAyuR}s9WDnW5Y(w@XCKO4GY-1l% zChKG!jG1|F{hsIeK7D`hzwdwV9CIA$bIkU+@9R3R^E$8dey)A*F6&Xwqck)$tT46P zx->K_5E>e~?IVofN^XhpG4Kz)$1Rxt5%35+V)G3A|D31l15Z6yJ5L|0$F?-~F0RhD zA|BR{ZEamV99%sKbZ7Jcy37ZXgPZFh>M9!@Z7i| zBOxsVxxu5Ns;4iFy4g!Z!$SkRt*r0+W_b$X!S&pGdY7MA#&90SW*Xq?g)_T|>GF9|) zx~0LTjUT$*!S0!I_sp=h;Me`@as5@m7{kA=(a?l&U!-OIud6fdCmjCkBF&i?CA$B* z+Tcdd`Ck`5gkS0ZuZuL-xk3+C{O^a`>=CdT|9XTl-_1VsuWK|krqKWYr~cQX{C~Bo zyD4TgZnGu2LK4No`AzYQ{zMY$T4Sq7rsz%nUh+^~U)8Iqp*rc_bJVV_0-h?xiYov2 zio=IU9>#-RZLz!%Z%Fwk%lxv*y&_&8OdWl@rP$OSsc~3FB zYko;dBEGRoSTEW3%Z90{&Rb!PB-@)z($JVIm<>J+Hu|F-ZdH&=hyUIoRMWL-80%MU zsYL(Mfe{JB+&f6%)l;dGMp_#xj#XLBmqc2w^x_hYnYTAd9T0XF#`6R7)`+D>XVLS^ zyE8iryKC=jeD4~+za}!7Y`ih@F=#kVyRXRHkC?u^Wffz&&7K^=^& z1K2c_O`^u*xw9VYcA69l6$IK~2??0|l{3w_tY*O}a%zn)CZ;%`ou|*QR7g;(e6@=z z?5v0*QzqdHeOmbi0jGJJN)lKHLTmOuw9e&;2x@tw33s=r%$xY9DDTV$U9+o%`Fl6P z{g;%eC=&cK1966I^y@*?FPcXj__#P!b0Ba7j!!vRu6!5hm9qt+e)!>iE{u4UgSUl- z&p(j1ih#hiPB}cwb$EO+?bC+btpoV|=Mi%4xe{G46s(pZ)xdw}+QO$4UF2|DnE`?F zW=YMDsC}yv_ZMbEwI8MQf2F#Vx$OxGJ}6D>`}ezTt|mYTOaY3|A+E4^kKD=_ z78=np%j@~=>6tW|j?DoF+N1Cz*8WT#+8Oumf~SXvN7USdRs#FK0NnJZVVMUf^llz} z;af#P1=ZM?bf44ZB4xL{=(Nj`iGeMj*kcfgRu*zCd~*sn0SOaNS)|K!S`FXkC5xx5 zl1+`99N{2+NyZ~d`n&^{z1u6_D(stsyWYWA*CwI;V#g$frxIM=i<$*=6+ZYnm1z7z zs%18EG|?nwj)|v2_*FGg4vNaiyL@vety$B{mR;vqiABtiNwpw{fuG>jiy8{?Qdy%4 zWeTq%&Nt&+MIL;%noUmRo|Z!bnR2qE?N70@w@mjyX0axZP*9;gJ3EADOVs*MKG+UM07;AA5{^ox?a2 zCDfyv_e~8vZT-Ax1r5tlO{gS%BiPiDk&&ULtwh_@bvZ*F4hbd+wK`nKtXow%h>)oU zca?)p%~i|9_gcFClPL#(L1k%KcV-17(!aP3*+krlgOR-)D@CiU%M|PRt7GeyVS9Jw zucA?9g-#CW!KXaI&lbmHR8z-pBz7j8H)X!bE6<#yAMUxNm;Qqd6RpBZfBv_MK7qav z^?{h3`t%iVcm55W`x}icdN%`FRxdNHm~WP1{qysMQqt0rXIb=~jCnv}#jw zs~VDUv7{vJDNiBx+k114@OWMwqvUltEdv8YaIm>MmqhWuR{JWO1I;mg4Fd#xQ)ZsM zdP!Z^YagRAl4zPz_vQ!j`qu_`e?~1<#W6caSQ2FP)kW%tZ~Uh#3oS@N@{Xd~?A`dK z8^wndHN9HcL)M>YP%r8m6v$zcY*V9hUqkvWygLFVif5|mSmCd&0s>4*OH0S6r|+i~ zMn;_D?e;QL)yd~M@`KbWhe=p!N~i=8lVE*)q1HlI=EWN16BFUnRNLd3?Mb+4Dn_vLPe-s=aQ=IC0)p-3lL)^fFe<;K7yc>4MMs zm*dv;zp#@`%`|pDCwuZl*A-N~YQmOhev*|c4i#keU5Z94`aRUMs|pQreUc3E(Th5^ zH*KcvbpVA-57O%_UY9V^((>TGRJiVB4B%yV-JIMzk*lZb z3JORAWW{6!GI1c9NVI?(7^z)4ubwAj5|KWaC0)>Vt(d4g9&DvCjw6%&E<4L7WCq$L zoV#N(5K9T;@H&3nl*Tp8U+zXBnlFf|H87$_B7MKGKvqO=DyifNYee6mKJF}PjX=WTEC7b<2hm2pLTCTN_ z><6Z@R;i6mRSQQ4LnFQwLMGWAMgxV*gReL%#QB#>r7F*LA46D-`#SYbnOjc0Y|;&Y z9sTYx=4F|ZeN|*S=|1IiyIlA7DlrdL<7Q&~o}Xc*+O*LJ$&tfoqJ+HxM>qiY zy88OsmX;QR%|T-MW#$U#77c!DZ+|jBKR>kMeJWzsQ8|MA!w)6K@{`oc)un^R8OHN| z*$BjLr)|vnWy)m4fP!2)+*AYm7cB?b=2l}ZKeqo21C9ByyUpvTm^l7I*Uv=gaq-sz z@q~5##g@g*3BBLAo#DNs#Re6f0|5l3`v0kB;Fn^Qv;%G%N&Yi0fbq%ut zZfUV8Q&Z`cHy`BXoxEgj= zdw##PUBA8yd#kXwj_e$LLJKg_)>p0MfQ2^P zgID|uu&MLjQ(1;-vuto4o*3)lgFNJ(Zm5vYEBDJqXEoDg>Cb zfk8#kbruy75fKD!{qc9kaPAT2Lb@PrWtn-_#r`#|<;Vx~g;$rZNF=cq7~(XRju*^Ez{4A83+Y%VCMFWFOCTm`)CTp9lHrAiO`kTQ z3=EP$Elw*0b-*yY&CBeQdl}D>gHQ~Ouv5HO%B0W2NM~zQSIUSWCcfC+_3Oo8u0`dt zYuSyKrbe8I$N}l9pIop3@5ZC)@)$%fb}a1pV{+v;-7r0;arsCqe8<-45rh)~sqB#sia8LuSeQzqT+21>v0Qab%X6NlDfXWj4)Qtf=MJN$k*S$;Q50@QE*IS#cUj#x@VLO))N*l zfblReP<{J%RdLG!ng#T{{rly8u?{a##B*u|YEScu`jREQ1b;pkkql*=$<@?4GoYQd8)Ct(4`26|+Hqd)yn!aaOB|I$#dAN^CkI|onNaovYr$0oGbiL5GUiB|{} zt2yVKIP*HS2^Gt7NmKf(mh`-IH2Hs$)gA26tXspi{{EqJUGD;MxZxq>#R)|RaaHy* z2R@UhR=(HUqVBL~IuttiX4Vs0Q*4WQVTrs<|B`S%gdY^xM;=_vk`0;LSu{Emx`uOE zhfFzeL}CU+8(RxPQ->n~!GIqVu)x-SshX6$VC5f>i5HBSgJs~)Ie?@?q^&liQAK3LvQ6XaB86JY6}qvJ3z0E>4OR^`g}Hx&!nxCLPR#G~SiqA69o>OvkSi$R>Z z7qEQN4WS?i+I=&Nyqn9tn?-QQpeO1&9H8)4z%^gj7G6?3*o>T#5^wm^*O~od7v4iL zGTQ*K+M+g4)}nwyO~?!PyG^&T|D6xp{w*Xn8dQ8q{QAda|EvfHY|@j_(|BRl_PKt* zyAc-ZhjpbYS5BwupVBp0+!ADqfye!(ExqKVVbw)>DYM-)=RjRJ$?rZSvmJnZuL!gF zvZpJRSQV$AKNq4VFL~%nr7>A){Yt4~W=ebV1#GIkDkX&c`u%%6`SO$=^75(fwWZ?^ z1CaNvQktrkveokQ?Yn%^xYfT+uGO2^d6)m}rsQOl+k4C3-(A(#F&JMv`#xTZTVvXt za?_^_HS&afldkt;s6Z4S`{BY4`u_!wlEDRmsN7U1p9(wiTL-o2q2cSCR9!LooZ0t) zP7C)OV`0=h%zRzEf98OS$avoBu=4tQBizgna-qG!3Tel>!moeowD4!fIilp9daCLOE`Kd}Sj^#i4mBiah1uLd$DGYI85h_|jHYA<~90pr{v7Az6Q2h2I8 z>o7Ar7wk_Zth}V-%yVA&X*e)0Jy1$0=;a^SW_IkXILdtOwzFe9cxT4dVh^*XZImaT zzfMb8_@=}TzTN#^^3q$!(v43kdRvMYu;>;&2Q3(7J28b7d_(jo^dc;EeWm?sDh48FK|k z?kxlKmA5uSU4lCjgNhO8Zp=Kg@Z0RSi2Ghf+)g zt+;a_kH}QQyen+wlY1EHCeC|lJZ9>xJ*j#xdh>^bW1t06z!}a!`=$L+tc>@*F|#fy zP4cG&Qj4=SK3TCU=$9!j73#h!RO(79sM%KX-%rg*XbF8OUcXla+A1=3x;BEEKDXB_ z-MJNMf35uDVa~ct4IK42bE{@EWx65RZ(>BJ>)HjiQk*)5YKgzLLI_5qU9gr4nLUr66gnp<6qOZ%Y2)dgySs*dI@hTa6~AjCk2qom}~N%7YO z9ki^2R?Ik}ee22{Gc4$)Rdoa~8+81Tr&e!yj$DWI&p>0I@Ib};``=voJA40tq@kXH zaW&DhgR_ZLI`VPKO-}HcH|N@1d(+iNP#Kt=_Gw;MnjwKFcjyVTA_3ySumtQ-B>N!~2=nXu!4WbgK_5(%gY zsfrdyJ<*r}O$T4dvAZnNK4^=4Z|9%gt4kGX1=A%h^=mt4u@q>(Htzs8`R67 zJ>i{?4c)@9isSZbh0);NraS9p#A+d2@fp7SIhps%Xw_71F(HAW-3Y1qdOmm%UU*HfpKc zGZX?(vK>P)(H)dRYru^BGNluoFY4cw$jA}yq3d8lEqx)9>B1QB1*!V({ZFIjp;DnQ z3xjX{-Moiv+yP6iW(HGrVeRTdXu5E|-1r8ich`h_cn&0#>~C(hI!B}r5UDn-yJ68- zmCHJYO}p9P z_w}*ILrIc9T@~V5caZrMjR3ToGp#c54J|8Kk~v9kH+pkNf?486p5Bm!2^kP}QEF+% zgp)vvbi51oL7Is|)*dn&1Zwlt{disyqwZWlwPR=#EG%BmD-kL{$fO>9=Of;} z`QzCJ3j|cFTmgNKPoY+S`sQhy#$QaVfNtea%_igu93u<=6`(ZLfIUFf42(9mEme%k z_ZO$R6jK!vA;Uk63`ZtgcZV`7Tgm%2T1uD*P+tz0$n8LG{~C$Dh+rgeOyMLK;O7O?FhQ#rra)&#h9)2j%R`{<>y-RS1J)wdo^ zb8}EqR)flhYjA2ny36u&d1-($lN_KU~)=r>2oc9E5KTc?Y|#rdq# zZylYGI+fE>^ zCpp~A3^VZ;WX|QLLUfXUHeM9{dy6>fIYYuATBA5$-k>#$GKBw6>*4_GNo@reH4zC% z8{~fK=C7>$GZseHyYA6-%ZSZ*j$R`=4>^0S^{nv9tF;wI?A6@reWzS>%e#8FVyQ;{ zzkLQeEMTR_%Tk+X8%DlPnI*51HJdgD6BszZOmc1;a;qVCFay#^{w?#>V-nC9xV0}n zWgvswgng&o*|G5W!_pj7dE0w6XK^J$LCs!tMvp5sJyG7p`^)_ai-gu)*O8jw6*Cju z^9uv?8T_Y97Rrs!{um~Di`33L=eCz~Yxo3T2I_b_k4{&NwSh5t38w)@1~KGn+?anM z>qvnswRwdUeLTOBZ$=^y#(E&Gr7%|H+aq6%psNW6a;~^$6UF#d%Ga1FPqLR}=F_zn zhMO13#{TOjvfP6a75M~88;bh#xX7CUg5+`fP^O`u(7hij-Qo(+i}{eTVk7dR?sL{(C#Wa)PFa z$IDr3lu!Y~Q4=KFT;*><`+u)HI>vLCWjV?EJ=99}337Z*XnB)He}hiBCsR!k*~3Tg ziJEYnzKSHLcc1oq6iw`{3EOflAW+=pBxlb%-p)GhonO#AG84REjaZ8bXNkFXevBg| z#X=nu>^f1ZZnUUWhw6)m2k*>SzzF-@_vixqtN8X128c_~DQPvEF$`*Zj^5@8nG6>p znH;~@uQ~F$PG|jm8j|md&9+H6SCu14wm@3=6D*7e_cO8{u|w z_K-!=_Uo(J_y5-m;6L;Zn_ft@(+#u1%e(fG8YoGzTHY&fBHuQ%vkB_To*B#}_pM&* zz3IG@GxZ`_F@Bor!Cb=Cd5+4zI36_5rFr-WkO@JT^-9^m01P=0X(a+j4Gbg^Wr{BV zWdf~K)a7t10UQzICDguY69%qTkKzZ@j!8fym&G$KwIo&r=I=RsNhmn;gnU)GVS^pUgNURQ^=$Ez_ue3C=8 zD-$zG;qdQ%%wK9!?DVN4`ec*Sb`9GxW)+Hi7RzsHj=jZgIX&!ZM%sm@G}U3`$WyB@ zRxc4&sr9&ORljMzpYi!wt zzpoz3x>nwe7G7rBGs30y)MsfiaJJShFm=GwEqRRC>z9o3$dl{$MrT$r&K8sG)16iB zO15N^^hcGYyQ6W&F9tHQ)Cv2#O}lXr^y8g9t}fbBRz1o4bPE(wCvK^%)u*N<1Igd& zTBIf`BQ}jBQ=}6VemwYW7p&Sul2~X_tnOyPPRlzScH+*B5s44?h#G2V>EPSc8GEN* z*U5e?xqyTe7MZH{n0fO~N0vK!DMdeJV3Jk#IDwo>_9H*r>!=fn&zQShE!N>?T@(yc zuM=?)_Zcj76`n%M3%kXc5`JrI3?;qqUrb-^_Tgeyp??>YGVua$zUU4KjQ}Emd4;rJ z3u*~Qa|w2`0O4QANVU6sD--`mE+af@&pLMBclzZmmAgY857@F(qxv-w4Vv%^q;SzR zvP0jQznA?$75l9c$J*HzO343t+`fKVyq=E?$Ps~}JRVqwiG1YvN{h5T))oPIa9&Gj zw1(%dRAq>=LY0@4DTZ^{^}T4&rfYc{nIc+tq6J~8_&m@?(nX;D1_@YeZx^JM%djErPu$`$>M-EpvYlsr&Y@BqNW>Ir<1ka*5y*nxB1nX z&eUlBAYYO6Ot(3eg^?nlGTf75xa=^-8~gJ(&>JTFru^&l98+O=lYEX&a&_7v0r=(6 z2pIVt!&#R=o+-*upK8C171?K*Sb-L8*_*z_ny}Y}UD{?YMS_-?gU#Xf39J_o008_R zX!NdzquERlmPi$^=EIsEHzR;!g!lQKytt^`(cusRGzRcl(A&=K+@fWT4tqk=`!vF? zQTXm~|I>5-_COuQeIZL&TZq8Yw0}N^K~4ll3k!+r%}U3 zGpo>UU4`OhftRPq&n4etD%8q9B}g_9PI-X{EsIz{u%TSxE7!6fgtW(&WhSiC7S~#m z*lL_w3~nf_R%H!_iWs=JCvnfcubnTDRg7DuXr?tIx(075tJKe_tvhia%)3|+xMbM5 z+F|r|xhL6-w2YwR3y@^LzDm(gGdaiIkuXA7MImcMuC`2NX&gQfY-)X$O*8B`51Y?H zV@$D01QL)u|HCV61-ofk|0pF|=CaWXu;YVEh_tOBoZ+9g9~BTqSQF&pKjL|P1K)y}VbS$7Y85nDgMwd#dfxB#T{k)dFGmsEz;YN2FZ7N3on zNM?OE!-{G#nC^9ttlT|q_sdk#h@A}Ss@L2P>3Ug6U;K z^6EGy;OFxivgx?Liy}sSD9I(*b9*nWuErCS9JYw_cVXwUGg9P*W#Ws6AAu*lojG2f zj26}as;O`fus}WGd4<2As00ReQMuajycKWJ&yT`{k#2H_&M?<5*OOxIvH;vkc+fNi43@8KY0F3NdMc>i;u{2S>mfarWI znp#P(r>REwdfenE?mOxjzrP|rY&Cqc}=_$#t@%a zq)RAhX|50@zoPtr88QRwRy#8l|@%DfW7WgkVy)RQz zVYh<{v51*1*%mMPS1n7naMPdv`zFtX9_-dypre9C6F*Q!y(+jVyAQb>&N{1;y$}t8 zpw+5%a<~*Zz1YW2lFk}^8WCuNM(^ivV2l#>lIBMhpsS94jKy`Gdq&$*q`0BR>t3$L zuV>2=giW=M%GRMOt(UHO6#H=oCuwxoiKyBo@N?ZIEibPv5zJHOlkGi%%`A>i=uXyF zMsrW_Ml@9tdv2-k&F$$G3w7b6_*RWT*JyS2Ui;BZ{K`>o&BL5v&=%7&Jx@#n!y>)b zIVfeK0X~+6OvB+$#CjUN==p1nIehUs5kQ%6!8~A?`jpE|YT4m~3o}bg0l6 z`E4-|ynPEx=;VlsWr8Z-6eVt`WigboJB@_t|A9Oi%-dU`wjo_$$=KEDEj_WSN!u(^dxk+*tLOqvhlW64guED@j5Nbe)tYVvNDxJ5dN&HE|BuJ$ zy%`93!*INYat|%7vQ6)7^UJJX7Iq`IGmA!MEG!;}mEkAYf8+1xCutQlE6Pt33h0W7 zw~YO}GQZ#&?MUw_+UfywMHH-5R?5X zWf7!sNe5}xdDpc0=lej4+l~RQLqR7K3DOJEf`>sw{NalHNGi{6DDY$6;ma@Sax#u2=3DOn2sdS)fg=0=XQtkza;x9mDq?&)<$0F^4v9Nrfi8btKgq8a(K~ zbl!NUP4=haHFG^`?n+Nt!*m0sk5UWggR?kuGWbORry0nTQXtI0;=7mNVaQB;g0>1E zG4I2)ls05Ze?a{|9ZM$O%y(|slYJD-E2@M=TXL!qGE)^Xc8b^M&bIf#+4pl!oZXpC zqqIsB>9Ag4a1Q#H-~H3>ZdBT7P1o<_f991RCV;ooZEU|kGX(8vanK!fk_IN>>31r7 zfPwJrVNUbltMa54hVv!s>->^$5|=b|B&KEy8O{x?-46Sf%y9Tipd6F(h-d5C1LGi` zmc4sT6x5IDx;r~YCF&C{3h{R)q%K_6@=lvh$P8Dn%2|HH#pd8}i{DSk;EbEgq*BzG z{E__y6$X;K9PJrVn~x0&{b(9Y2SY;^0*IUJ!=fbfUNW<^uG&O0HNR5;nwO1()1igHK7Yat8 zHs%}qZ)tOunN;Q=#4CVhZiTQ&#~e~Kzp~S-n>yhwebLC$X6rm+sd&huIz&Z^>##@^ z8HonArD~;4haS$Kq7QF%Z1RSuGICC?{Rr<5o-V!ad{i|qQ$&AmFH%bvD$-T^{J6QB z5;-ZOqIx7Y2)K2;o8_5=mnG%hFU43hv{9^*0pTqy{xkBgko-x==y&_Z)fGn|9ai{M z#{^jpdbDK=iC8JO8rNxKc@w+9pFYUKVyW1RzA}hJAJw{t2e}l4TKp?uK)>ZS_N1=H zSF363xhJt`U_G*n)FPg8npz5fBp^W|r3*W0U{@zL!%VW&CqxT|D@54(0ZzQGr1bOg zOz!V2Pn2I{zLXzSjK02Qu5N-UsC52yx7`Jf(WNl8<`OghbHD=WtCzB8c_FC>G^`dL_J!~)V^ zrXyrhywC?&(t;ZQ>;7X8?*F~gBT@BoRnl(5{u-h7jPs+=J9vFd=jdttRQ3;OylHVY z?-VHfEx2f%?J+T9)IKd;aU^nKhggxwUN0K6sm}R=z+k zc`Vl*>y+xn^TGWpR-T7Vd*Vjfmuj;sF^;u|zd6yJx5-h>2n?cG=dF(2LHqR zXCeV3aT^2d;i1XBfoH2Pk(k(WE0XgJR98;4Jn*63$yUAzPpqB2lz={EsaO7vQoA`q z7^#*SC|6e@n{OK>nuKq5&)C^P*B!+!`W%0q?`F=GvAWmxZiN+?+;1>l{WBj2;SOvh zM#&^8^$VvRjyzEU+=Nfb^iF~SJH108qyg3J>R}H=xeZgl%pRiXDzYt&=hO7U>F7e` zawA__!l&1nY045}A#>vagoc*^QlJ7y&I(Qbp9;J$c3^j*x8F~sFh4SVCId`d&d>3k z%Av(Y_%p@fZ2qOE9p0)lKY3Hr8feoN4&3}fl&k{&EF-mt4OWDeQwKsmB3N~C8aPgY zm#6f`v+@(j@zTfvL!?N{{`jIW=4L++;xWo@?2QHXbm!Y+uPtYwb%adCFw0&)Q~SBS zB|1Je%_lt7Z@CYz{Uc&&n!EBH1~$ZlDOPH`*ra4*_O9lE;%{S^ZROvRa>~vDk`n>K4wZouav5Ouw*E{_iasU@{)<&87sf$c|VN}sdnw7LA8bmq5;`>9pwf7s}B z@Im(@V$M^WlHZsP98nC61EKo!+rdWa!%Cdau| znL?%W&>W*pSM440TkAs+pPGS@wcwMiuFvi-rH%xg8m1Kp>M3u2Y{}gF`PQww!Oud% zNx0Ne_PW7!M3#2MoO-a^*KW4RaFItQ)G~gfp<4Y@QgPNer7*`@i!U}WxMLP(+P1g1 zQN*-`LvYKSR8JnGY8#{E=0d2Z^gXGi=(!zr$b&{y=^t;vmnu0(zqY}d6OYHoTqMpt zM6WafFYCjG>us4Y#HV<6_#9h{4#wCX;0@3iE&1?#fumryg?O_WgcRTX5s3oBLa!@` zlS)l*c?PCe(9hY;j>!a{c{gR3I$S3zq5ZpX3f)lgW_n`1CCfamTOZ$Y9KY6rE?YTi z`HyePZ5q(H24k)+zU#L$)^0yFk%HwgLEhF)6EDXxF4cj-YCiYfIAE0|neMG1<3U0p zNW0LlV#A5UPtPSQCMxVPFHB=Z`ukfh?Z*9xj6XA$KsM8=kX$5U^D8Br`TXwAiFF)` zHuQ3alZFogEG)gN(^ zJ@N$j6dUbL=Ue9BO-S&m!@~x)MuotX3iQRs#zyY_pq~Q0yUwe!1L?$;6z}$GTceVa z64&H*zBWmD}&hU|Uj97p2{@YLv4xo#%*2H(dOW16NxV{L!xp zjYW7+VRF>aEjqGSajnB_m`Zdk)12x%mcQZW??g6DPZBI z7I>2QPgg#R9m_sfw}L4da^mBA8FsNxVU`KO@2kz#-+(kWu+kH&`sQn~o&0m`X;il4 zF>kJHrjq3|lvnwWxa3~@k$*N&a#ngAz5MP6_}(T%6Bh1J3qzgu1S^2gWOsMBn~-$i z27_=$gChW_J3_#zd@vJg6+aNLIl9(D+d}M+@w}xxj6zXH`yQ~B$TIuLfpbxc}yW;vaH--@4BO_*c zc|9|`as-ZUk%C%$*KTQL%ITC25A5&n7t^tV0~N8{iX-4?LaigV=KXuQMg^_Qa6M;d zX9R?^xI_p{CkqO$f)P8+TkemYB%!FT-ng8OUjxz=4CMko%3_owc+ve*F9&N+kIcx9 zoCDu6@AJhbFy=BgHGMOQ-ZsazilZPi(>VPFvv(E>( z%%blcJdZ^tSPyFk#6cP>y4=e^h`7|tHd1@{rjvP}gQF8k_+6Xy^81dDF{7n7gMo=Q z#saW!H>~Gdo~T?H*_{hxV+{9D+`2`ysNw^1`G6B05 zb86m+!s=LS+`NkAA9nHqo#P`+mDA%DNh(q-h_LJ_WAC!3HRxG2wLpSb<1+DZ%G9qv zZqcaRUqjd}RaYXMN$j6~*;fL{a@S4D-Bp6T)1mGWW(Kq~>XHblvPj)sU&2JGm z^cTy=t2_z6wua}JXwtO`a8wIWg04Et2ap>g{~W&}oEa#h-fJRxFpJkDm$_3L7S+G7 zo5YOOc)jiBJ%psfXbo;oPakRv8!S00xB%cE9x`f>V7>M90<&8~8hPyI zkj4Ccl0(+s)l$VpW1h~rtdAQ5khi<7imCKsQ==ntv+^->X1y-(et?0?^{pE&BMQ&+ z`0fB})`C))Cq)bDlIDN>5uX$1RG&ZBi`;$?SZ+`-&Q2)A=RsI^fGiGt*(D|FOG`^Y z*;8hFJLPgDt8%+c$sb+xqxxczx)|GmxPD&fK2bm^@g3(DgG zp7g@v|C}%48{}D?Y|I0A6@&#Y>#g6jD)Pbm4-Rrkhr`(1e53{RC2L*WOM6>OB#QY) z{_+`7^IwMOy`)TqAOL4IRaLo)lsUzN*MUQ67LGQqt1mpxvJ-2e%3k z-v_>rh{5gezdykJSVhW%gVmgzoCJuQ2T{&C_+>9dTl)u#w0szV+^SzcF}yNSztzp3 z^=oyinSXlKz(H8Sg|(Ns_AT!&@ZggetG26aYVwaO>?jclIP8}%Uy8$Y!Ev(37L5T! z6j#^H;h>++DPUQ#*saY4y&&S0kN@s^0b^JjN+};4CnAu^B;wY$yW9(42}r?&fR@bG zy~S$>n-9(b0(gjYK{L%wHT!5G_ll7Ii%KdL*ugUvfjM_$FCv11f*xnNwvynl5$^6f zKC6@84nB-RA~=Ho4s=t|Wr$7O`1k-f<@e!(PdtDAJowEC*#-IC)cWh9qDx`N75c#r zf!%t%sMMkA@~@gR^=q2`x=E@=o(Q6$cEi)Z>szbVyOd6?@0bEl%jo$9ODaDGz3}97 zp~Tmz8n+Jf1SZzf?G2qT?$(rf_Jsukuv|RY2poJf>wo&}YxLEY4Sq1AHQt-C(2!QB z#Kw{~DJ3-<39KTP+I>%DfZR@0+kSxz?&); zM|$6Wwzuv@F4XOo%3$K;P*8GFJYfnwkLUw}7-wZV{}nC8xK^ejdSx-c62A(p9ynaX zMh{%E>o%+8Oq!`>`wjZvh3@YrR_>*-;~0C1RzX2#$1lqD_oRppFL?6d%Wq2c-kbtQ zvbMMD{97`=R1JZ6&S5{&rwb10j89LGEH0L;%PD$SuLpw^?d*&Ii3w4peR)pef4u;T zihj8y7?9d$-JpnvuJsemBZKsu04@MassS+rcsGd(@sfBnbvR?42^RakCq ztt5C)Qqn0AVd3QfQD6cvF)=wvkBf4?*@RZoJ#Y#S%C*=+oE`+xj3U@>TyO}+gK+oX z`JDs8m>)Mg-#j=en_fgMLde`S2iOJmeA1>f02v1Nw)LZ%T^>m>Jvb^sa`SL?M0KyM zIFqT%Em)5$u(TCF+ME;p@!7Ph_mh92(o!$Jv#zVd_z)>AB=429^4{^judA9`Ue8fq z$}}I*^3)vRq0I>ias#JS*oVG8v`eCX%}szaw#!e-%W2!;1G~J;1frgdIc*Ejv97Qy zv$}4u_S!Jzi%e_-g~|#htv+yu2%; zbr0r+!dQQj#BC1)Y&iZ=6!O|R%5;M`N0K_ zs9Z~htct)@d5lW)ROiBiU|KJq97aX%B6?dM(0B-3<$6lM&_4JQrs}i+EmLSKDB#52nwp*GwC##KU%k z{=feDJ#<7ROzHO|{jq+Oe=SewGNvxq*_O#S$kqsm_~CxC;d zkLL9zu6B*%lt{r@2|82zBiY_lhe*5cUZu93mr(g&kat^?;U$M<;dJUb7vn3sEU&zj z8gr=wVtilL!JG5 zbGxrwUm;WnA-8otR;X^Ajkwe<_M`JOd@xzzZmx`a71;P!yi6OrT+eUQrWKY$LbMqI zPx8gyx$@Oc;%&)Z`pL5E3jD1E6&(&I7nfXZ0VS;0$A|B(o~U=8aF9xOfN)L#mNkTPEgzhLNQpN-aNo0jqDkT8CdHr_GquMA~5>l+kvxtyw*t6R*Iq%PlhPUE*iVuMPt5hzEW?W)yy zPl~b7RgbYGbkk?tJ6u#$1QJ;FY=oQHnI;sPI62q~yHn;V6AfS_U$;1ErKgHTRp(-c z&~4%@NdbW3t9wOpntZ#&Hw&IoF7+?Go1^cSej**G0jNftmR=w)lN6+v!R}p7?`^LV zc1UA{#am8DX-(XOLO5jiX7P`Ux`b2xP?oDbv?NvyA)StYuE{tjBl(}`h(&WS2M*pw+>}rM~1I(7> zYuOD*58X~LdOgb_%Xuc8Uz#W5Oh%6Kx6~U-_mg{1<{XCxSq?O(x6Mi2$};O&6_Zf$ z{FxTT_|YRI$=<^&{rA222U(}c>lcT5D4664kIc8!6VZ1gdx^u2UpOso4_)&KX!_`} z&SjT-{Bv%O6@*iS9DUo>#`1xt^8*M2%~z=9Sw_vleM`HLNTqoi_+SH~!~MYl2Z@g4&3U!^TLPoEe7vbzTczFhNtsb25|1k>{BYVHGJ5E9ovbF(n273e~5 z-V6n4T|WVZLPLamz?J<;lEOqu$z%2qtFs_XKmU9j$kpBN20jG@8u&v0mSa6tzsKye zrb^3I9b!GX4o|t90gv}p6koYJau@NKZ8%2yDw_8slfKX?04e%HNko=Q<@cFExbyS# zJJcF$8=MMjqxtzb^*r|~oqX7e_j!AJ`(IoBfvXCnWr-gferoYggVD!Tb==(CLO}7M zQEHx=#|WHj4ZbFmF)Yv)VJ))2JM(M(SsqzLacVg?+~%PUFG=crHzC2M&03>#Jhx4yOKI}3=CWx1X6mgO%&a@Lib4}hisw4 z0q&CHC_}!kAf@G-ml*(>w5`0=%J}H0(z^#gFx9-U zv(b0J;zH%*K4KK9;Gl2l{nGG21q}OFg#vGe|EgyHNzxg?6A_G|GmT89$u(?HB|KX{ zyEO9x{#)~-?J!2tZrD*N{PG^UPC)-#sLlVw-kXP0*>~;Z<`5AwPZg464vCEnA!8`Y zoREx}Gs{rPOs32?LZ&pB$B-!`nQcR+44ce0dws;qrpOY$dBfYoCWUMe{V;*8+M&#pZ2cpLIeqiLO0ZucSocKeFb({Ly0)TVsJ&AadmAQBe3>}Fjvp#ZQ-+6z>RVXROvMd*NTrp~ zl}x&)Ehy{IamA?;$>Sk+PZD)`YchtG*{5b$Gqyc$4U;$-dRmLWa`e~_Ygk1Vo;3Cd zI>Q%L@^^J!X`K491W~U*&sNny>r7kGsY+Eb1nY_Y?NV~zB~(s7sW~HNo-ft(*wdhb z&dbTFyi0$4`Yvx!>jYGG!>KWUm+-;Aha`!Ple z61*f%6Id70XKb)pbTmpjeKuldDLnq^uaI0AZWjnKf#!^_25pBXQJ&ImsS=Ls^F1s8 zd5i?YWRI3Wzm2@cmvCDxFS*A`TIO_OmwYT2ti7z8MeqgAgC}-PjL8v{b?%w^?Q?4p zW^J9KG3#A$`D@@l*w)+I7ZJ$!_Gc+L9NyNY4AUx|R3A`06H}SE4}B@H_LB0Rs^t>k zNi&6h+prU#eQ-^O&LlZqU7)aVvgSt3f)}tBV2|Uxn?L8S`(XALz}m+@DLuGAAD-23 zV6>4(_r?toX~@2aIan(?DoI>~@W-s5M`NQmnQ zMp0FaJWs_gGM$U- zP(`=iYh~!t&FP#$+lhz!AGZ4|QWHt*TlFU;s^vy^SmNPV=R>#>JrIyS#xjd4ENXt+Qo%cSq{C zQS=aru&nfT*NivTVA=;(*so^voW~2+eUUDljX9Qhpg^# zmAFMAzIj>b_;7W5zHVSTk$rgc8>Q(hPhS*#Sub=YDO9mb;gKNlv5WrHx9j`7{Vuez zT6P+UIlz~%3Ll-5pEW*>zvn7tOcQ4s5}c{Ge^9QBmy)V~l(nB{yIWo2Vvcg_t135m zj=V0FWh~5GUE~>$x;P_oW&3N`t;R z!7}LRAhR@7E*)yI6p?2P?Q>&;gYWzJ6)QGg3gya^Lc{5Oaj* zm220omD#i46?tl$=~HStS1iozp|9V+D7W#c>3GepN|QnR3qMQu z&RXoCXV3^hyYOljQj|(-8y#ilOX&P^z}4j5fV>7yF=GjW4mmOqHqXR$)P6t_1k6gp z6@|@80`6^~H6Bv>FgAx~$UOz2H(re!hdA4yEps37Wyuvlt}uI4mHAorsIv)b8(;BT zgtQCvqI;~emJjM0ZoMk(48cgqNWgPU?8+Lk6G&Dmxhven7o)N+j-ZRzle0Iii`w@& zVLVi&^DI`H{U9y2q`}7a&L#JF_W{(dIU|YP*t}J9Y8&eAgZ~AKHuvEmspemd*SY#; zy1$y`8S)Y9qoM>NIM<@drU;{$PX5L6{@p(1ecBt(bC2tR4bL#<=t5yC0Zk8&{$FOD6JEhPIyH9lV_+~;@v?c|$`((KkipHrXg zD>6_6{uijYe%kqbvZIE4@Uyzi^&|gRS<`Q@tB)TXI2!LXJZiwm9>4I>GMprBB>$}9 z?bpqv%Tlh!3#TgadF0$#j{)M*p*u~{!kslZvq0HmV3=@pSMJ5VkF(Bu^;E_~)75LS z!jJi)jVa8OF~`uww?-}#57y(~A^H|Q)cS+-U(x$YY zzTm85!Wark1mtmX9ke{(eS)Gr<7qD5WRh0#^mTO&c5FDJ2Uvzz79TJFlp!S#xEMB5 z|D#Lq-@h+8^$8-#bLF##hnu~U2!wK-NxO~MZ4m*(7a=zaf`Op*+3bo)W3$ z(n$B-F%(XxV1xjF4C?)$KZnj1Y} zt%_`YUm_|B_9q~;>zi>r0R>Wh(qtw`6(LVFt-*b|U$EChL~Q&UmWr5r7RNk<>4<#H zHb|k7gAgi`{LxaR2~q@A(V)R-^B;?gq@^ZxSNZ8%hD`k_e*@E-Ma1pSnT33CSa%N( z)Z3k~meaV*k(G@tYiPM?Q`(^qf08K@PKji|(VKMSA-UTyx?|&L7nw6+2g%_X`tS`8 zI&N9W(|7ep8{FO9U+f41exI6}!UKl|9}=n)Qv|*=Q`ciZ#$D)~6Tx6m*z|L^$9E_E zG8X24RB`A^eX-w_N5(DIwE7?x3Mi>Gu2rGxi2_)x*jrrQUHG4<5geG*>S@n6p#|P# zN+d?kDtkwfz3icStww0*{&W4kT^{}db15PV!xt+~6Wf*{N4^Q>ffQs-be?ibChG0o z>V10`X#1m5P+w@%X$FbDoZ_q_8P>2`#52fla{V~E$|{=W=S~={yIX_dx16W0p>&N5 zz%bE%2HT9tO182JU7^pMY@yC%evMipzaGb#O`Q}Wi1y!F8=aR?im8Y$j&WvLWPNkl zP~9flf`7kB-ISU6J>sFlojjl12n6zS1EHG6K8F2@d;-xW$;Wt*1M@)3f@QnR(^qA zb6ZF@FyZU z^3zwa1PEdeFdxY47VO8AVZ@T#a|X)j_$x%0M0U1uwYSFy zVS0OEt9yKF;-2#)O9Y8ySZvbbWys+>RDLqjqp#jwLnAmUAWH_4$?!!???UCmb; zZC=sPlSK;=+JOfoWoldsLrfo#$vkVFKA#c0^QHl&7-B{fp_qL(BE4PZaM6mZ^_^d6 zOLkhA@D}{d3+Nva2(8_>A(gg@hnO`%Yb8WVAKWBN+;@>v$ISPW9p0!+aCmsf35L>} zyVw=MZYMJ?{LSagCP(3`@x=`8Qc`MiBy9zL zYQSuAoag!}MnPDDcJQMQ8RoibBqsLh4aI<%>}`CEJ}|Z7sdLTiN(zK{4v5UoHhF4( zeVh8yg1{0elmUz~8{6@=CkQAk1OmZGQFT5bic*an(tHSj=8P87>v_N2~Mb@Y5q{_v7%y6V!0| z1k{v(;)72EpL%=w`*)%@{5g7jfQBYhEX!(Yjw|U*dBJqNkXcDyI`X}m2J{sHOg>kV zr*x#HNXLL4sm4Hb?}Bh@50Gxq;|Z2h)i~nJic8Xlma~imTsAh{f_!cxXE&0<3v~%Q~0G*(qLmdhd9lRVIenrLws4cs`SB9LZ4&gjG%b}JM+6E0X*RrM4|#>E}K zi@d5igSH#ioN1byZr-Lg96P=lsqGH8qb&c*a?`NbNAf`cluFAsDTSm7PCnu3cP2)z z%E+W@0U{lMc2*9K90Ps}dwWW#IesT0H68+3Yi>wyH&&k);<382x|*fOCqE$O7hRT4 zK?;nE=k||NK)J#_Gc)|wc@$2QuRic`ZB2kLfxHyM5qg?Af}|e3b_EF3ml+w5{Kj|P zgOn0)$|@Atcve={MrT$ys5u1X+#6&s?p@qo-rHkDLbb1Me&)2cNceI!(8X}!;CGVM zNvcasQ@_fg$1IqeE!0%SQb4o_$g-?P9bHGkp$Bt3m1@W3?rG-nN2lIjlAKE2MQPn$a9thf_~&}oNF z=r!Twsp(v+H_Pna?L2#ko0+t-0C%P zjw9ylLI#R;EB=1RREo>pZ*sf)@PD)vpHHc(rOye7NSLr7Nl|6!iwpR9|4HnpB;XeS zaeYC4`dDUT$%%OP>H5Z>t}r+^LQbr9FwDvBO>x3tjOiwIKd+h{&!xyml z{TM<)t_HajBvq9{8Qm1`?Ps^P=peb^3Va8pbv+Z{Ddxf`JkhW0EQy=D0Qog;`v*03@+H)s@ zQK}7Llb|8y^)y_hqpkz`k<`GkV zA+E#B+nWWjFOW50S~_&_>%ap+K!nuQy@s;a+xk_yW zp0s@Xt}kS)2j9>>{PA!4?8LpWU2`JfPt8;2y&t4eT!CMYoaLlYNPj4gd;p1d{y>wH zp^yMCKs@DWf60w!0T z2S{pc`t-W4yGSxO!bSrQML5$tR+qxbnHi2x72C%MG= zp|zVzf{8gh;3pgVhJS=Z}Gfnc$Q2e-e7)B+WWp$WmML*NLE&Z$B6&q6Bvr<^-7L6QZJY?tyz3Em93VFNa-gEW(np9?IHjy#9eb;0UDr;lJV+`rMSo(A>p_ zD{weSPKWKraD@X68`DfWqflHp!^8}g!PcN&=Ii&FTAQuW48r*pC;TbZ91{&~#*#ck$B*n0+ zJGW%}u2ZS3@k#|L_euVnlkO0QlA#g+W_q1qUqAG0z|)G0UCon|>^r-=iNUSaCAg3R z>rb0E6I4}VF~!BjyCu2HQn28G5A!GpTGF}sqkL3 zgq2#4nuWL!Im@rFC80r^d*dk*?bheQ7JAx$?Nz6bo&KAzm--gOG-hWG@~J=RKKcyw z>a3)baUx3eErVBj7$+%$ERbKHUlxLVM@RX7d&xgq05TwM3kUg3YCVB!aw|GHE%hm0 zBBK{&X@l1oGVyl7WTM9ZfzhS!6GU}UH=()tVHGQ8ngfFm;W7B0;rNrQy%gL>;Qe)nKHh6|0QeKrZxnV!}Axc9Tx~;j>vuA1rwgUM$zeRbw4GnZ1#Q zVqdlMk05qyZH|nO8>xvrvP~~C{x5ZKPEXR`+=&An8;VR(^Mtq@ZZ+Dl| z1?HdEu569$H^uwX7VFHB^8z8WhsVE zB+m@jNztc=sI%+70xFd@j3f+dB=U_|?iik@kp&i#Y_6qZn&Cl1N(#VH%r$qVe(d_n zp&ARzgtdKoRWJ`>bV(e%psUo8T__2&LV*4dunyf`|{g4tRI|GiiP3M0<`M+)6`AZDF){;-o2Od1#(~jCjff zMMxle!*AE|sPX_)sd%PismG89}GCpQ&6A2qaluyJ&EIXY~#g^B1clA67kw@DtxP zmw*;v8lh7PT<}VQ+}ts9H~fAI;7^Ewz&CeT(o~mhZg`PHfmS40F-sd#6)@TnyB7@$ z5XXUZ0_R)6sfEw<63z@{79qh9|n^>X&de@8GS>m`a0++xFN zcPJ?NXdj-tF82s5FTnZOx?pR_=H&pU>(+Z1tYy$%aRVW__iOtJiy03`0t&%^NwPkl z78S%UOv%J#$v-RiR!s;@^S8(&M0#1I^qGPPDzjM|;Wb5oFiBioxmuijFX@)lawp<;vWYry0^?6HNFll8`P2wvtDnLw_grpuE-3e1H z(>pCqN&r)5VU+><0*T7uG?jlNsI#h)&2`q*NK&9C3net--=*dx&HO_zUHybxp6nnQ zsYNGk=HjrM(B6*=jCc;xvS~}~9nRnC{ zNJP0Jn9xSMM|5wMHa{yk8#8P_VuK69F|a`4qih~U(kV?VhqdaZhOLBcnZ(`mT1w`& zK~e_cg_<|Md3}Xj!-tnAjtpc08&HL;^+nS|dhYMfA!t%7mwTh?y+4@luYB&QfBPM= zb?qNrpojrUD~1rt8$j&Bk6m17_YViSqJS%gAO)1}uWIi8OYC|J6^K}f8)NSvu!o!H zM8&j46e~tPen9$#4-Duk>I8!Xp|t*hyr!n+Lfl2f;D0Z)3IEb=)m%N;yYR(SsY?B3 z8M%7!#a4lyV~N=uz}dd&qmCPi;TLkQogn!k`-Ts@`NY@-VcmQXev9ANT*~UqzGYx8 zp^9)L2QY`4hR+&bb$p!Jyu57c{+Mijn}{}<>T{sM?oyjVES@UKj3JNu?zz`&NaFG7 z8sT-G0RQeUOazJB6*Vni&d+ap5CBU|Avo-or(i|IsdEY@7!$n5(7(bE()OJDNo90B z@qtr0_OaCUr%uOKOarz{y$L*LLogXe16Up*TfE04qVC;ita$JzxkW7{l27IwP{}P3 zi@sIi@DK5~;aT>sCpR!OGsDwa|NMIT%F4Q(_{}31E1R;1-A>sycC}ZdibKZfMMI~o z@TW_gEM|h1t7{K7_$~CFFz9ceXoiwc`c8|-}CAyQwsXJAefG<9L2#e)aO(P%Vpf?5>)z5w~) zQk{EhR$#1~r6Fc2VJCy-_CkRk`P@S$P=0DXoCu3#N^XKca)OEqr+~rbMVot@3f`yd ztkXNWTy>?6ty;eoNH6R`Uwx_qR^*7RS1P{Q0$gpDb|c_lfNEfvd&5Q{-JC=6=oeh7 zziS4-P+Sj<%@2rWc!Co|-glg=m51V7ok=V)D5OA@ zzx~@cdh(FwxV4!7g}0C<#-X)ClV~oU#YBsY7X|X)lAcU-ikSPVo6wJkv0t~YyqFbO zM&5|Qb|O$)NzSLY0`_waJP_7F8_q0}XnLD^s}5>m;%L0?t31`aY*x&qd9P(}ztrzX z?lJEjG*oQ*V>r6|)=&4*swZ&}^WV2X9J7@zvm~)VSfAMmAChYzS;RKAfid|;`f#oX zICZW)zQIJVq#xg1w*8@z1AC?H=p3hEy^%tc!L@2)x?KYf9%J{q3RJ+9#eQu;&%Bv7 zp(bWCO%D6{s6pLo_%O7mYX@2J@v+w2QFSz;hfwKh)=`J`oSnROU;Gwj)x9^o=4+H^? z9c~S10oFo7q02k_0Mb?H7?_5#OPvW--u;=@a0X!8XA4-%4Jc+e7{T$uTa(n&t*{1S zU!<){7NvS4xKsf^fUkG@Rpx`GhlBr#T|yL(OGlP7xfr_Mz;y(1Hrj|a+lWEJAACPW z!q2aKv}#t8Gzbqu4SSS-nIem7ER>)n9s-yF3QsbU5LPMk0V9;mK@|niZ4ZR)!mKA^ zH1o@WDHLotq=D0TsC}u70C{AO2P7EvC}gGb`TN|@&ZH}@pXa9b{Xry;D`IbMy5KGn z_(rf8xSE#vc^;@2NW21~1$2zf@9m$r&r(&tXP~@;mwqoJwRZtzZJ+{c1iPQC_r-A@ zM@POZD=TKdrB=5^TZ}W0Oo!aS-XR|1@Lc`?)uY~6E|{tY1-Oz#3{kfjt*78;Q#z6Unk$vB=Vgg46MP*` zDbw&aSkCND6Vv+a_9j=I-a?Yi6#P7TXT(GKilk^KV+5}=gYXLWlj7ziBhU!$lXru$ zLs>Qfv4qcRUTdh>nOq6uzZcj|AJa=gHT!v@ghdxm!*6>%+>=d@*$OP-4KvERx#Ab0yY0%ME*KA; z44wWmSe2eu8WMF4-i&$Rgx*7p}O?s|KtqKd^F<*7M= zd~tR-a13r&Xk&yj02+Z@D7o4@IJmGX45T_xYVdB*rbELFP>%fEqENmDN$sTV+v?+6 z6s;EJxgXQ?cNKQ8qo);iPaZta<~^)2pe9@sgiH@k71FdQ-FUYs?p^I03T*IA%+<tHr2m<#`ja7#A_mI^3y*24=<7@5Aycs#ylCyX$?=fe2Ap0v+Gn3N!w^uG{F z5~NPSC7_i*P+M5FTn&B@ln#NI{OI)c2XaBz2E>5C#m57ADn8f7SLJ9e;z#KqhhEsLK!8Vs#u`3#UmMgmY_+PI9RHoEj8TE##op8GCYGMfrNf0u}! z{?SzP?IAbVza+AyMFA(lfOQVI1!4(K3|M7UuPZ`G7M{a9k;l-Eyt_MM47w=7S8opF-uwPDFKF|#Fwr?iX2>Zo2dwM>ps1SvsieUX70Z_`r!^55rbM)wxAgcXp;Bd;B z8LW?S@(0N>QFsv&e{D>FX=SlyWbna6{sd&>j1*3wwucUy6ObT0%g(%AJX2I!-we;h znn{3>c&vL;lRC&}=^ogEj4RI)&Ss^~17Q{5dYMl&&6kYACl_#C{cY`c%L zV*X@bu7*BX4JtcEt3{ZK6e4U{BuI)(#MZ-V>$3l7>Hb$|0Q)g9+9cwYgCiHShsj^5 z_Yp$mS+H^n3O5zXUuJaqg3J$ZkR5!;;ZhicT>=F!A4@q-LE10iWp-CNkCy3u;F%)4Pu*EE7DP^Qw(v8j3nZ`uc)3%|vtXlvRlmx!6TDXrC(1+?FrDXIRGnAh&Tf zYn3^)DWo@iM$dRC<8|6Hj8zN&@~eXcc%bT%7BLNhlH)DW;dG@pNwAl^R(u3Y&Z)*a zu$hXKX`gAy7J8(^UsuRRbM^L%)}lLD6HaG8pk&_NhBgJ@Dy54dG64?2bmOBG4{sGA z;-19bAOJ1}w%SsYU)9g%gIOjZpoWU;$kgV2O*gJf6Sm+5i86R$ z`fz9SLkJ8YWPsT4i;unjl+A-l5Q-wa%E&`;%rqu&#gs z^X@kbJ?ED9gVe-VEL9`@5mb;94^*HM1^R8kGb*tkY5BozbnY-*h|~2BZ#Z`se-+8~ z)%f*aTK4sjLIXC0D(5)$J0OaoF+^VGB4~v|I>CsKjFB9|+GiZ#5ba?4NvDfeLq2Kl$@MM)Ip}*zy zvomlRa8Ll5fZ48yfk5)b+CA}9XIhhrk>CZdal98Dme6Yv;DL@^@T&C> zU;R?gPJ|Ck4V2#iRzTYqfCQd6FYTJQ47+GwC|1t`++J8%c#=&DOyMg%K3pdb7Y&ei zC~hDA&lgr#d;9-ueOLNi14lxlTr@91#*w4?SCq!2{vNjHtg!rB2jQ&BWb-q7HCIoe zim;@X;(`}2f}BO_oc313+p=^8m!_z&l*31+NtM3z0@w)^;imV=R=(AghWJ}tw6&?{ z&Cpu>`dOxTRVm}xr?v!!)EZ0f=NZPz6&`A`rk9kxyEixN_7@`yYKwV249hl*UYNu1 zNi9E+Zb5A7y_%mpvJRLzZk4&-#{bUZE71!5l{PlB@q4>}CBGJYDjm&VfynRdrasv# z@;ESkuS_{X|46V7FHfAsJHPip#uk5;-t5DOj3Pezd9sB|EPr{G!4H3Oq4Y}Zo3?p- zw3eNQi7)#Wm#A10llYKU2$$%9vioBh$7O%ByTY~vkEK{xtk(r{iCo^BL~x19bCL5; zWNk5}qV8zp)Yco%rgi8x8e*8H&J!hupV|dgEhrV#n(FiJgJ`4A&9`7?SK;(VMaiGpGxAeiV974S{02?*$LKe{B$XG%D?0ull68kna~QAQ4%bcOQ@)Ji<`(al=ea zWm47HO6{W${1o*kAZqnkmA`uz0Q3}1jvfMx^gE1xO( zp~gS7JiW3)0Xn;8n4#cp@u^6gfH0X;MV&+GBqZ`U&I(vxC_2I_1uAKK+s&cOAo$}; zuzgU#0~t6V_B-=|h<5n1OAt^eCl6u;n(guI#T^oHJk?LboUxyIOvl!se1(52jc2My$Cb~kjLTr zC~_teaC(@Zi4A5V$!0)~L4oV|ebA2Gb$50BxF7~<4zEgE8*mVNn;F>Wkd;CH1*Pxl z4D5aeQdBIuOqxQ0NWKv%o4#;yHM|}LkfhnoxZXqXvkbQ=j-ivq?44E4ISJJ(T;o^Q z9JoZgvCw6Kb1pHQpy>i1TptF014_W4Jj4kq0b1jF1wfPt1@Qg!yk#lSy~BqDP!7k5 zjEsazlu5l$NBccV))_EDNHf7vvq&Rcv)f!0=J9A|kUOt!Q%Jhr>|Oa;kmVb#?rM2L z4O->s$bkq#-N$vSNlHSRFqgea6>>iX_E-e9q&(cC{r$_hY!BD(6xSh#)Aomh$PM~X z!IJ`w8m|_Gb}R=-o#A&t2E<7dkc)T#8E}+i-u;IyErT*46j_$tAa;f2Fk&|YP5O{4 z|E{Zz8%U(~;PeMJVwKdg6bd*6furMDc7N-}T&@rR>HgSVe;nSF0$zO>v77@WWwuPXe2}3%*IWG#3+WX=_x5U z_)}*sr;qfWo`OYb(({6E~+!+&~(b^yICpeP)D_A-5?C4fl!&*Um08$!O9>O6@QHzvvcX-g#hxU1EOT3tK$&@V86XA}T{ib$-kPfq{}lO+ zMt{=uQeML`n{HUhkZ^*$9*i2`D^A~M;4PjCC1z+i!8L}e?X*ls=hFjW4v`W5-mCX7 z3P=oI(^GwULmN~jpa+A6193${^mTEubN`Y4t22H@gU!>^r=fWh3OS*nRuvr~tsqo} zd{G(NVJbW`_?C2)pwAEb*eF2Rj*)v%ZorSQ5nvCxpJHHYx^U(z^kA8kwksW`X{*}kiO$PQ{u zFa!7?{$cz#JJ_x3LGZogSZob`-D$Cpwc*VTr~N*qRkV9DPV z1Wh^aHA5*bi@tGPb5I!Qp``pJq05YDPa)F(JAGpZ$5R4uiyyGJzc?*)xc2eXV9x zA#-7CW#zf-op?>1(jkByC994+aq@_a1te28Z!xX){<{mEXlRntcw0)`K2FvljJVIH9775mp-^lcOmCaxd%kcej71RDS_Ci+~DKRj<}^^8_fRr6tbx|mrf(_2@gu9FM{KTK=0rLS)g)G z(}fkmo8wT&1=%byju8?_^t!iI6s^4p&jT=`4;z$lI7#=UMuvwQVHihK$d*S4Q42tO zTmcF+UU2j$IL8h=6X-61g@Sx?w|edF>A-C`J>C=uFLB*i?lq7-_&B_}DjfkGcfbYW z%Hfdr7U(_FdqX61Epm1$(bNw_(+enKyz84Fbpy6%eejm)gg1N1)ecrTM(vDbBLzKB zwUFk+QGwgSiSr>j0qP#-`eEjS1B}KZEm?+0irE|)VH#cCiI_14MWhNB2r#0cSA3t`a)#IG02XNQRfH_U+fnD($Rp2sh(IX zo^OIW2ab*fU55UOKOZP$Va4aYf++?#q_el@*#VQec(fdaDQ4@4V)K=u9Z!S_0#?hW zbiKR%Sg8HjxRaVy5)ic1N(uD)kYO)%+nWeF&qjo_5DVWqm=(H4W{QQ++9E; zpeBuMKcyrOjR&N-Oka`(Hk|;g^7Ni z3YnI46m}f7J1u!4rDwjFCpRsP?U!b2$y)}iHL=#dKJnkgS%-f6q_E|eI5vNs$Xhjkl0n#VHC_KwYoJkkz>j!iNms9VX~> z0m&|s)~F5IO&lD4y2-V{{4EGP4BvE92W?LQ;qqx=^ZK&#fEeUokf=d3J+7Bp4|W<( zr3V2Nu%j@u=7uevK64`1({oaThk^!x` z3F1hvSRjq3&N%<@5KnCdju9t`0;Se9Zd;!f(15AsHeYD>PWQ;~HGI)Z{KJwSP74+X zHHmu@7@1FE8o0ajF$^S*5ilYKCO$yBEC*Z-L@Zn+=DME}3cW}-3f{)!=@sZrj?mcj!Y3qW6*PhaYJ1OI^6P&@C`(M^iRICMb>K7#IL-msqsb~4GBHZm079!-&}tR zcJ!Vf@!!RVAmn9OS%sY*`%;>xa@pEJ@%UR$TE-uKPWLtVg5SlNA-9v^t+1wGT|x=A zbWxf)LWn&IG^TV)P1xQ^Uzravc?-yfXcrPPJaOJT1F*EY+cF3!w8v* z9I@;qLQ(?7&o-Gz!n$5jJf(DSz8iFI<`1eI#RSWCXOw2ouxq ziGMI=hbQhSL14g{m?t+1;%S}+$9DX6HLFjZcYr8gBO11;=S$NZJ3}OjR)-Vi^aE*d z0|r1jNyaZ%lZRYY%e?`}UtGlz(F-%aUgRrH=5K0_If^8M&a_}?8HTJE z4J^Ha7i>Ka8<5{FhMuWUB}E|QK5F+r41+m4klKd9KrYzWHbPlp)qTtz4+Z-Bpn^K9$g`Y^4`QbJ98rY&2Dp{Qio29-dD$;xQtiPp^ z)xsl!xn1CBkG4zw^(9%qVbT6rEhsrgoQe5#=gOApyL8)kDX=5?MP+XWD|PW@K5A0f zHFNus4CE$PBB73!JoDB}=#>G#)sQ1pRvuQtyzL2xTZ6WPCCDXb>jNbG>ZMeTI}}5( zlQC-d5_^<=fBV+(sqog4aZS`gy_GwWOKro0rpS{K$_ZU_bm6U5RxwGtXv_`s7LCY% z8zov#LHBE$O&6=6Cp^Pmy6OxMb@`#~b8Z)Xih@K|k`Yy1LK#36rf@m(?v-b$YxAhl zlcPjxVJ*|@@{gBP{EVB_XyqivS{61(0`~0}P3(6VHgyFO-&IuPKUQrNI!p)+O^091gglEgr^SnN#y-(~PMUon#@yjHk-YjdtD-AO+nbjz z1>Jq(!@a|5hr9rAt)oGqOGiE?qF0iS+UO8Th7N~*4e&r@1RlIgf%uI(p>=Qi`83Vt z2F!B9)?$ASReJ99U7-pQim(>kd+2LXUemsl9@V@K)tOD*H@B*K*IbPq9@hSATHpG; z%y+oG;hhNc9>+_ZBH!_p{`sg*yllC;PG!k@%x4+Y-tU9(z`u;2vcDGYLDV5x>7y4_9#@+)IPe#R1_-;$U} zx-)vbWhGiEz=|Lf-!TxM_7|;DFE{vFVciP~B4{&=?_F5!rv0!~`H~&`xM}J^dsF-E zhJl5sCHsR1Q4NAU_Kg2FK-UMuYl9mqJKs&zT4z$)HoFla7~Z>3vqKQLym-RlYP?D5 z?x@vW&jIxKe91uKhki>Doh)tKuJTJu1p)#BuFlT5J*{oT_!^Lc?aq3-8BqsjUD0d6^tA_hsM%uM>;lH+5M49C4Lw_X)mwdhX%n zi1xdbB!p}nxI0|{E@)<^*_~U9izY*VinUaofo`0xtu(6Zmx-mNr~jCmGPAl#xVdZ9 z_5wzO`EC#1x@~HD3-ZN|);8-!@#p_$AqfA!EkvA+dh1c$L*qnkp-Etob;X$V5g9|^ zHN=<)M@EvFBSK+A8(5OyZbg!x2Q^J;2nv++&y~;SS=*Q$9!SEEu4YBUk8T&gfPt(q z|9iRG{aci$bV*1EF~scqFxPM2$0Sg0DvyCJBW6r!68CU^|Jf7qG>Sb2UpJ1!G~5l( z*RaF=hNpVIORYq^Gwf!*+6x9{DN-J|pu&}{$CspBqr?15QxDdpMaHqC`+rX7hB}Z)XReQ<|MB%f%>sOzhu3#i7ox zE!xLHA9JSNS&Ti3jF`|fXUVC&cOedmN*r3Q>%V7lm_nsHW%jL{tLEsuS!25sOZ&eK zanF#zY6#-4X+8HRSQq+8G*Itq6JDN1Soo12wsvAx>y zZyWzMDSsg%2>0DosJh3HxV(N;yC^L=4=aU_S9c{`?!d$s$Lsm7ZKeC>JS0Bq8O@&m zHZOZ~-dU9qcXEkNH)FVu9hTW}D?2PL{7Fd&O(TnfT6v7@s<)8u&92NctlvMN`;Sk4 zjvVJ30`4>i2MHbvsvPF#DGnHPL2x)zLx5ISzVyG}SRd_QHx~EV{`F`6v-iO9e|`JUSM}1r7vw+R|N1we zDkt+_j_jWwb+i5Y4F7!l`(LxQ-)8@x|Lby1{$F?MpC2V%`E||zeEa*~#_`{);6ML& zKJwRN`Op6p^7y}x`hR!zKi349(EqK~|NqP?4V%aC_XF5tttz>)iIrjRrMyVCF14fe z=fRAV{IS!aZnudcf?1j@=kXIO8GnAG^5P$-fr$PULm+g(Eb5&8PZ-I_v%iADTy+UV zjX>vd^1RHZ*$v3BJ5ak_s9l&vFq~l~#0w~)bg0Ykk4p=P3jN1tOc84E(8Bm=m`1K& zqz@n%z%7iW{ymu-uu3|m$shH+O7jh4rCONT3Cj0LVy1;5`+l4rXfi5`}_3*fH5%kIQNDsaAAB2YK3}yg*S_A zKa4?o19xb4V;GZII_1RTCC$F?gV;avLnGxlGa=^Ni5K}G01#xtVCqxk#I3W=Uef8Q zCU*av2+v2^rhxeOcePN^hWrU`H{<{Sm!Pvq>hfisDx;glH(?*>Z%&f6OG?C($?#jO z3-6c|Q+o2?1y=`uS5K6*&6d;C)6+#L1EL4b0I$QJb2OGJ|G7o<0Ky=l>$h3l+cSW| z(CFBhZtji#Un8D7Mw;d|-2C0q(UdPsZwUhN{w>Vx4gOgv zlNOS!g>b;FEj@27!R+( zbxOhWe?B6@?a$I|Xmm4%LG@`bU)p$h+yah~FR=@dj6|JHqY)GbPlYR8IsO!KS0VyP zU%S8jIYfYI$e(pa2@3(n*TRKOPiH~ipf8Y^oQ!Wu*Y3Q6;2GFMd z^Ab#-{reAsLM!F>mjMz`(vy;v9cpR{gi4#+qeq?H6nhr1N_#afgn`5-dLLxP}! zLMh1f&-?KZ`hPAYvGNy&gYoB3u!PHbb)!H?l(62$Dlb$mHyhd~m2z}Ih!oOd4r}Ju zvQYeU7$1vTiMqePYqS?-kAkd2uXISCpWe;SucXP$X&{_e@KiWY3Q(g`0InlbtwK` z*4vNL{&`67W*?z{JY>Licw%6*MT2KvQPwuuh`nhi6HWSa{3(S$$jL~=xKB=-rINXw z?9Y$=F8;@HV9)fL2_85O{{Q?t=V5DkXI6NU@B(dpstMQBO}KY(2Vsyj>$yLN-Q)ao zRajZr{G}RaV}SnQ#S_=@>jwVzXOv+Ik+gr_=0p9jPh&JRXd@OoEBVlPH;>_1WkRUAJRXEJfHWa!}AGPT62Nx28RtVNMQvcE7>DwZG;$s-x=8!P=)jXvR*&g7DhyU zm$BjH$p(!f=()SI&(Elx7x>!gar*nz?cIsyb!Mq_Q`E`jq9!Gu+kXt~; zUiZ1_FnPHLvX>^a%neU|)t~!5IC~``fG>VND$s3TZTBP`f7vxeI6wL5fSsi=AFjzp znjP2y7}GquSpbO_FWn7J>dI5dTa_hODqhY@LBsawvOxo~*X_?jjaHN=T%YVF^as0s ztU&8PN%9y@Ac5&ycM)NvfL!^j?K>FvzjC0-6L;ItPzN%}SAQzxJK}qpXt_sh4+?GFu=?waTmAY&@(HiQ2#R zE_FY*uXuAZ5hE6IR0?54qt`VGwIOoddJ{?4@@YeVqt9P}vuVF`p zix)2f`Nm80bbRYt(GTfqNArsrzz!*~UYjKo-0D`8o8y;}P=5Vtg+O3SP+QMLv`Jnh5^#%Jpt4*~3hb zw5ooA`vE)$tO}^oETn0@OpdYh_c!fLm#Ju zs-W?M{RUuRby|u@R6qac8BXnRnp4&WF2z3rb_*!XRTOF&WI}Ma%XM>WN*?&bIb&6U&v+oA1|9+HdnwGPtT{ZKIQD9?tWuHCrt=mJ2ZQtFcvE zk>uf_=ELL@qp)ouAl3gz)|v=vIW5Ru3Ne;qkhRUJ zRJ5Ez6LKml%UCj&F{Y?gwn_{!hLlNT4B27~|Lf8FzQ6zP|9?L|=X2hUX1>ew-1l`~ z*LB|yT#GjxWF0mvnyDWwYvfrYwF{%HZ*r?dZx?)tiz>^LB|9m0`75AQKveK!V*4#R zG$4#$98Yc;fF@ERQUHcBMQ&M+ouj9(zn|rhr@e2|&tnK5FzyG9jueCqcdUdcQ85yV zwLLss_@SdOcl~CJ*g`f1FabOnC|tHc8f%(+M|{9n(-09kAm~GC|FKu{RpS4?k!Sbr zO$_|4W;Hc9*cLNp;vvr=Cga&+qT1KOE{lo_h5z*en041u?H7|{W^i$OMn-j|7iRaz zut&BAh?)Q-Qog%HwmWjkLpW+HnDqL_{+8sd>;DypP3#5m+qOcV7 z$)E@U{F6Fof?@$xL*q4*po5d=g4%s^6c9c)Gm}&dhM~g;fYA%{^8-aa(|*naEC%-} z=MJ%n)-hqZ)TwT1iR0L8Q5`1 zW&pJ0;0|(<5HdlA;M3I6p1>ymP3yVuk>sFQAe0J?^pv7+Rk{EsKP@iSK%5$9(M}$l zdMvSeWpr7uO^i=XEv2yh4o7f{Bgl=HONq1iXDBXA6Ki$zQm1n>K`f_>_&tH0;<#gX z%~Z4#OV2N-_xb|xB53WrAv8re!*+3SUvWjTrtxLbz(D~uNbzu(*}arn_l7pfP%nQVa8N9X+;*S* zygY&(4^rP>e;~*Z)h1Fbk`M~ctK06Y^2$2*137~c^-4*mJ!x1%*GssXrx@e1ZGNwJ zMcCqLlRJyUYWce5I+8+_zN5_?wY<6`T!sDF#e-ww*hPgytNFB)OIPv|*!iCc&Qw~2qRM*jv12u55 z<6vP~t*&QYdE4`^YrE|oMfP!2@4B*sZ4Y>&JmUpT&1vO)XW!zPlPBTfVYg$ode4x= zIXNxAPNWO^+R#u*vS5D)zfEw`tY`(dDg^?_28s-^=dgL$6}_`pV+LmI1YsCD3%nXi1n<3 ziK>&uDDZ+BjuZkQ;IH!n_z@!#efJtjLQnVg7Ox8}iVOF^9fk^jgq9tP8KjpFevhg4 zjJcfOJQzwJHv0NwP{As$d5mte%{NrSnAKNP_VHk1z;Kw;u#I9~X`h)53MOP5a0I#h z6R3PJE^G_vcwit=(}csr!|#idal4g9&Fz1R22-k;HD%(yQ_n*ZdvI@+jcJNurZEV?RIg?a-9&wHDJuJtb5PT$JnOdMwN$pG5%2L`O@r$G_Fcb z66QreKnC*4^C;~=t$=XT>J#V^3PpygZthCSiJE`uy#31di9%#Fz(FUiYs!lsVHII5 z`K8db=kH+V=Vz7$BYiZgF1qTDgJY2CJ~cHR(1_W;bPi0Pgs_3)1mo&sF#?T6W?|;~$n{Vt6y>lw@ zYhQRhteIEoiVOQ#K}_)%pZ(W|xF;hh0l zgEWE)4@lkisgtQqObmVBU8Xp;Dk=#x+c z^>-S8H`s-W*xgP&E)^a{g%zFUP)}G`{-%)~&fa3XJUWNc6i3Fxv_u~di)mJpLtj#_M#3E{Rs+A`MrOuzwzFl8EgO&vJ1i%W* zl&9287t1Ki)MKvQXoLnNA1GV_5>6?4?DMIqx~g)QiF^frhK+**{0r#U065*g&`=w^P0zSx5MvLY_3u1XipYTo4=6q_;pLL;B`6pW0BRc2%Wl!Si!d$w5AzkDOeVsovT}@Hl82;5MW?JVngO&1Eo>g9p0+l~Ml^ zZ5Drp++NYKu1ly}4UO-Fer(d;E6?9yJT*>J{`c8ae#2`I=ADR}zOU>^7AI<|_BJ$+ z=)2g@qSAP?r<~J{D6JXPuaTM<#&+xpw0rLKPELk4r(lz#qf6Dp#|L)x1ctc$u2`1k z-0hb#sw=xJc5RK_Y_lbMlLJdb`0ETa_J8!zkIgEwOl??xDyP~kQ5%P30RxiEkIB9R z@eYj&nzwenlvCbGuad%hh3b~24Y2aM&&kxlc_2i504!qs}MFOWAuYOVUd4mndaM{E)iYLoc3jW^3=9OytVeh z5(a-mYr^sNiEbpw3aZ zfm%p74+>QYM5lIcVWE5JX_b8UfpU}) z$O}w1?X(G8LLgEVSDO1QT_D}y^$sWEA(UyGOr4)uoYC{TD4|2cmdJUAsAVKqkoUBotK~QZ1O%YW`vMp{?iqoV0M3!lz$DB*k6@i(= zan;q;o;bzB#8l;G-0w^7zyD_D^l|dGHMb(+3ZPv41Hk|%vLPvgECxiz4~#cny^y)V zbOYfve4aYLTdVQog4oyzQPX$EnbPnEai@9FhnBDpnEf5icaFA-0Y89Ut*E5*sk=qo z=?Y*5>Ki{;`%%F)If`aeuiSj|;AFvF!vPWu1%!aTd$-;hqw|R9fZ!BUj_{UxU`<1J z`t*ez&mXT@CwqqE_q7w)tuq2iE9Z(=nWKXO17?QTbCRwx!0dv0MEbnp7Qka`t+>lpxNtsyA^qjo*)P$c8*e$CN6fTvNsZX*~1Z;G3@Y?h%a8L1cRmAIZ$FJ;nyQ+zWsJ zKoR|A#-o4Xc*mUQi+2J*yyK0H29V7`RhpSgaGiLqbA9N@9;zPpn@L)Kx#Zu zb^axRY;1|p+g}2LPtp~@zFF1=d)}3${pV_5 z4IJ0Y7E^3(Y_fA^Z&^M!c*ah8iHyqTKHL(PGsx#g=Y2OqSzy$#-Eiu#VaQJl@?D+_ zdFNX-mc+DA3`e#BQ{m6l>4aC)v#c6y#)a8S+&w&YM=Kd-Cz;Ma`H9El@J0u86%`eC z2FYq!+t}RTiL3xc?JdkVm!9Wx@CNpd_3aJnZaD+2;3~Z+!`EP);$q-;CF3xPBY2nB zClmY*iLKewmT>D3hjbtVpJ8h=aJ+36M@ z&dZ1{7eActg`|y`j01r?aN_3twvRK1P(jj6Eud+lH&f5hFn)R;TM$-9(GdmfExkg(XZ|p5vQw$L9K>0%!?ioh8vlHl*HMc&1AyzU9d1QCHu}Ow;1S~9UNA+uIhn9i_grH$>RWqoC%|MsEU{@xI-qeMfm0L{s z&cRX_TQkS9*kjNhNbm?B<%i>N46WUzGqw0QT_qg=WXtlym#3O#;mmXfzM66-eYJtc ztz8J9s6s#t`Kv6{KG*d5^S{U+h5f5T zueWHhN?W-%0n9fgdGqQ;S3mt8Xl4>I{m5SS?cdA59NA~!zgoQW*&RD6A7jDv7hgay zfOifo2*j|So-EXr>IU%dA*fhY4PZPf;0Q|Eeh|7IfQt|Jy^*Wld1v)7%)|H!Wb#JolkXVRHx z+lL(fXA%gp`T6&f{G)}X{JsTUtv=RR9i)h{wvO%C+|d(=;0JmTp%Ugss792Pte!xM z{&!TJ$j%^Y(9{X70Qyv8)SLF_DTnn6{-aJ~wcL0B3p9Lvm6Kzjtzw)gIV=>r@6mfV zH!!kOA12Fqq}FEJ=hG^aQ8mt#LB%8v!X_ps=031GbAH?X!}|aF>nQ?PfZuEUtLgl- zj9%Y3A!au~F@Auu+gdFd>9cnZA{`h_BHWV|(aWzUNgvo1V8CZ1<B&drL6-mT3%)A7MgFLFQ}y4c{x zQT_E`1#DxF7KY85@*6lh+rVWky=Fmwq3{RA!5ix2glkwrDK%Nok81~CK2#-8h=$C8 zhfVShT(^X{Cmw-8F_PqfYvIkus}<101Sk7nOfEz4%FhFbG-RDI>^QjHeEv7^()9E} zGnEyn2||p#Ic2cBg0H}_B?$(n{#_D2@)u0Epn`SW+6Tp+-7n6AP5c+@TKL(3+ZtGC z+m7H3(*1$e^Qb*3D?oJu!Jtxvt7Wl`&aywtPEf9;TCA$49$Iq?)0vDLx(|_y&{`n< zM$iw6zQXT3>f*?Bd?}h z))Y6DEynW&IkWA45?uD=KyY%-JxGCu#KV0?&}dQm z5~&ap5hB3i1RdwILq^WWj%@`Ztbw*6q#SqIHbr~?{kTi&G35uD_?jJt5$CDYOckfZ@~>DMtFbmGo+}vKkTV^$GKcZ7 zz$zFqh@5SXr>lSSZJ(Lhhw8G-)oS@~nav_vXyC978V^z%D4%|RiLP?O(V$u4dd4a5 zoGXwyt42128u|=Lrk*(gBHybyM z<&5Cq&ot~U#CmL?)J=(~xH6&r`NNJDFS~wz^64XBBanMQAYze|q&yy3ySm^ts@J$o z&;b%8s8@)D{n%-kU3&n^7wV+p)(cCZ&LNmXCZ%^Kxa&n)rXCbKeVBYUnG+2nWuUrW z1DkHvtXUMFZe&LkozS33%7suwS;+A_v0gx?2Ee%di>I|h!~=mbadS-p7@Nl+Z6;is zz6s}7)OV$SsE+@EnixMYgL|;Ojl6+$(anR4o2Jrq8qp6m;F4#Hfy;3>upwmSL>67z z)Mfrmec_BxXX7!1O+xB|=4QB77+cqZv_WnH{f#h-J*YDo2VqbqgsqE1v%_}pCf$mB zaGu1zG%oBDUr@TGaP6lKKR+Lzdq@Smk#=43EhZOJW0yMO!a)}!|DwAGjwmpb6Wx4A zw%q>NGXKv0nLu21!_eS=9p#O znXVX>k{3Olf{7)FoZ}`OXQnqaxrRf^b{xcm4@K820}#mWRdne8v)=%yH*U zaV>{Y58_=uPa#zHp7v14#8@UE@|)`m`Z-al?Sr#t&+ccBiP=FtpCbUTkm8_aj~Cx< z908Rma4CslpsRBPOp8@$X!_p8Wr~^(Gf+qVOYewt!>tP~JzQ98+m1dy0251Q#6ksL zUUXc+y#s4HaNtQu+gc{-fp7^YJXd=rkiBFeZ@k7tcNWsznE@cE`bmEx_TeJNYS8vU zIR4BqBx3TK`YUyLtK#>-m4jpnT=I=6D$Wt-PG5E>?lPQpfUD{sagH2A>s!nKy*jjL zEx#7;C)jX*x~1ow^>UXR1he+Lp*{nP;Xpff00<#WMA(L6Ouq$TU*W6;%IV0Rj#>pLr^#% z23I-IypMNwktgB+83^Bc81ZpOXfQ!S zS69xbtcyc!+8F=Q&uP6e0tXnu8-D7Ef*9%T8Am1=l1zwS$HoB<4sL|JA-F~wYta(h zF!dP%CRhXPasmdxj3HJSC|T*nTduj4y>;5S3$#*hBQ`2*1aK>fC!8uwebP~cQ#&XY zFZ_7A@xG9KKoZ?}^F?1Oew@)W0dIKp5FlixD_9OUF|v8 zQqj%Z_g^mnpn-d?yMFBGzGpa|1oenJ2w0LeULIjEzce=|;3)~!yrO8}f5#g^LOk29 z4+Y@P;M*ZF7cBG388GOOb%@#twLc;Z(QIJ4LO^-#p;v#rP=6Qw?0FT7qlIQT0_E7c zxPVWiE|S~3{vf*#0wV0ua%!Lnztg;n|I(p>>)nUUn`0c?5Ddp4tfGWahaX||bKsmA zQ*OOEO6kF|nZ(nJH#)O8);Fr7!n4W{@fNU*v7g@e>G9o4@=ys9jn?`Qy#scfj|?M?B<-P*?$I65*$&0|g6uHi<76_op3k z&1!ALHLyuoUlx!aagm6$lgmZJSp39RXeEq$ zIG6~XZI7+hz;Z+JhSqHg;t)I&d`m&j&xY6eHg7_eUzWbwX|2dJ@+oXtiaF3uLOL+x&ToLZplUZx?@q`;W2YHxf*E0|Ec~P zP2-_RRiA(-2xVKmK~~V07%sft}?G@+Wpmx>#rW(;lM2I+FDW z`vN=;K+0RuAd~vMq{&-)T3DChDr9`f3)n>f3{6L;Fjv~476LA_e^%f?XFi@+cWHQ? zE_Dnl#Xn{$2jS9PbIUI%=t%>mU9ZG--S4QYA6cw|SX1ES1}oP%1$}SvfMRx6htA9w z_0-ZI{6*!R*4D+vnU|)u`uyA`eUpA<{yrp?6j;dI)_>Q_jloiba?vP+Qyp?AT%4Hs zT3|Y6r;%c(!B>fHS}$|QcG~mg0_*NU;2Ai_z2@;D#OlzfQuR=$u#BL7l6)q!*k);? z^e2c-sA;e_ApHQjH3fUr=#GSqO$|;6)Q50{JdGU!3SR00Mem}ylBW(0%Mj(2*PRRT z0czhq_XNsy-4DlMh=iTu`r6j~Lnt@UVo<)IqM(S6_vPul%M4*-I&ag4GC0RtCQGFlT}(oU3raBbtsk zBAmRb54spsmFlBx&FFLsyuZTDr4Xwyf!Z#!v}CU3={uD!_Rnht1F9hl>H5TY*;vN2 zM3Xk8IpA)&NEt-fFp0oX;A$+6Q@y?9>NadUqG;#Z-~mF11D9iobp?k~_I-DxVvsFL zhCvU;p`fE9I`(R@&+#Dy>jQ6YSadbw@$$N_eQOY39YcB|z;dP`Z>z@d@z9|ihn4S! z-Ll4!HZY!6bEx|T#DTa`!GCK&@8Kx||6z<+rh~>lvGp=z=B-^@YdYXJgx+Zfgp0?D zRTFX9Z29$f2)-z(vors0H0oAiBpl*6+wz4c_{;*Eo=mCDX%nzPhzMw>0YwhW*?ve2 ztHJ0{;@BZ1X-mEM+u&%Cz5#!lg7j&XO&*6MDCMGIp-bFZg>RW%t}54ee7me!{0=D6C8wc;dX0KIcTX3BRo6(+K_BNhLlJhm1kj3! zuJxrLCZM{5O(OLusR5AtAH3ZBS|F{@3$V|f)P@IUbp>uN`>A3OZ-WkpUcb!<#`|O{ zW%c(Ko|&RLP~7>G!2#=syMW85ysd}Ws*#tmO&487a{n1vzg8!HkE89vjS92uV~%!| z4RZA&!ePwJXn8*{hV}pn&N8^B3?a;F1i7^j(@;9(**_ytPSRT5qs17|9@;DCu-)+5 zM?5F_5yP)A`A0kAuiKPP8d$3na*=og}f{-r#0@obl(v&JsCu&r@g zgefWq1Z@x=UiNlI85c^EkP&5OG#D1!|7P9&F}X-By!mh$H_*U;zXip#azt_|BH1U8 zA~7&F4%i=_T=N*{8)+Hr^WzXBO(M5fxG+KI|Ku4 z2L2FT<{jPL@67wN-r*=}=Jll?nPD3LM?Ny2yy)~FeZ*P7 zSa57UFY_<~K{k8m$%tYdcOwYtDCZ%Z;d0wT3QK5Tvj%T8$;Tl)p+%<#nv`luBHSwxrWx9Na|3$0sjL;N3R@4gGspm{+KegZ zGVV8Fn~V4Pu8YgpG~_emKY5^F$J79O`68q@u(3dKFlnIZ;{3&xW`M*N$Z5WjUG}LL zoK%KZ>7eip|d|v)Uw2oJZ|12Oy8jyvEQeE2t}F?F^Z;TWSbB_Xkqh`m53N<<3 zGxjR$Lqa-7j=iK?*)Q>?P~gab=*{eZcJ>@rIBa2Ei|*sOkI&}VF?_-^OJf9o9@O{k zI+6==U{k?KqeqRRL3yPy7|erEoj&GVOr<`@)0#;*4#&^v4 z@ZU_|1`luX8^M53VDRd~5y3_C<98F69}Q2ebm6XbdXA~~3?I9C-l2tq$qiZF9(N^P zv_M$8WG`*8b9)hGuP@(N-9UXB8n#X;+VK#KH?b&mux)7uu7}H>JGIp1q$V?32I^nD zuc@ht$V`)>sNHp)ZwH3}od2r(6)D3Px!W6E_xVTdQJ#m;1B2N|*zK7bGcS~aPw8qJ zBcVV&(3`&maUTdR`8?`S(Dj5|(U|7JUm|m$0yBH3X7XzP{~O+ly78&ksb210P2Ynn zw+MXPqVh=p9(fMC>sKy!0fWAauB3 zb3gvAw^354IRU;Sz;+}(65t98zn3TuY)zpK7GL61?FSyFnww{GxYBgsH2lwQZicMC zACns~OITiQJnvHe^qF86S$v(DAox3e{;a1~?N*)_H1ab`xj^%=`a7rUEQAx&Cr8&6 zbpOm>+*>9NnHw#%s(U!#sUFZCZ)}wI&7)vWSz&XXfXcb*3uOVu9G=MWlwBaJ*-E-Q zJ=41&lV2ENZY7nUwshs9t9Y2KAA!0or9PNe- zZio}Nwp>I_yuJx$D+ z>7Yh)lviJmQ!mjVM5I6{v4wlygR%e~Lh)D_AW8rvDzdm36}=s$2v!hbfEWz_uP{~qYV>is(q&eZxx1YweO5lG&Qc5r_^dshxtMb(w>B10-z-0X*Y6PwP z%tF;2jAtT2Mu72P|EseT{qNG8w7w-bvCN&ilSL%kVZqB>J73xwvDqKc2OyF8_hne` z%i3g~1Lag0Rw`zNQik#~1w*>#)|op^%gZ^jf*g7#T#+4De0u!iJ+ z4%Ht(pJj=%wyh=vv8mH*e})wAj_w_Lqt#1iaeEuq9&Y^7JJi&dv+aKCB1O#T%X2_m z3|t^kijS{fRNys;CL}@*AfY81w|F5|BVxw1 z2UQ$cbd;8;k{l1YIfZ*WcB#uZ)W`gIrLsaze%(UV1(}m>gSqMGWk2Tk>!e})Y6p8{ z5eD&CMV<>aN(7)6iy+vVsk9Z($t~fbbl9Q197qZA68wRCE~w`RK#J&gK$3o9vC0kU z1~N0CWdT6W11+60rHYHn84x$-h5^))6I(G8og3eVlJONtam*L3(L z-CH$hTG5BC@1>WdEqxX#NZZ(9Zt~=7SEuESr%jJ-gF<5lIfm^cZB_L7z0mn+vv57HLLP+l@~~>Q=(bC^6)X8h>l)Sv}DY3)v*YVfrJWqYgjYM z$Pri9GTBF;4qWqR3Y&MUE(FH}2Z2n|#i7q?O$oDe9avS&PFG8c9i2n=VW?P`H$2ej zjdm{F^-|nE9xNat(qG++uWciGf{#CRJ_-C9hIQF{xL?|;j2VWbCtYo9t8wn-L$*FD z)6W&amN@o!S4)Sv+QLr|R_UCts3?5VP|FzRGk^-oRHgBjJv2rvdBEJz9B)2{ z_+!pZot$EPjXRGwq8PVUa#{$Y7-rIC&FLjSxu zb<~h5!PWGMDhwzIkn*`1-GoWY<|i*M^&-}#090o!2lM$q{FtN#lkDC+1F zho$us(K2Ec2wSqppfc6TkZZ5h8$0?g9 zU~k8{`<|Yjk_Wd3dkTfjOJ$+)<$48ECryO83P@3X!F)$D=>Uw5r>F33_qSu{rvvH3 zkJONfIE$1|1}~vbaiH_-%mp9APM-X1>IgSn$w<%Z4>StQG0#o%~1z&0uH`^xbLx*N_xfKrNQ?1 zTJ7nMbervz&$fts5M`Mz1-l?I;rXGX%}L98+RpITo@w{lcj62~{w|Kdd<(QQA5@pHl|tXa0L0Z<3-T-qkwID~4)R#qZ4!6e)_({P+04 zTl4oA`bxqpTvIDDskwQ^FunkpX51{Kw&d-(QnxlhYhk|ES<4ESt+Nguz~FGH z^>asXU1+cF6Y*uLDeOaGck_A*2b&XI;aNtcKnOuAC5#k!i@&<4foid9K()x6K*WLR z31%B}m7A)UJ-b^&P;l87c^T(SdX0ww_cORp@KWePU~Xt3#15br_6l~OA5s*4#Vb!- zx0OY2d`0u)QmLaG6W9q^*Ghoe5I3vOP8nEvYZvR_Uzzc`xQH$uTa=-#_6`U3Hs!iw zv0}-l)R6XFrd;rkFK_xC=QKqQoi(IccI#PoyNL;ggb4!-*y95yBVq;uZ`j@Y3bF$a z84(q%D3E^FBI>Att2ozu1!^QO^DJRb3GcnzmlBR>(d{9Hy2Z>{Hb!jQIn9Nqb{iM_ z!>r135Z$_D)yo#Ch{6H+fEhro0DXXCkqL+?KNevA4wiARyjuK|u(L~|%ODq_d$#Lr zYrbLcnTKI)QS!f)JXik7P4ZrguWq~XC3#!&zSYFEA5*U0c>!?(Y65_nRD7r12JfNa z+Ng%1H!@a=EBvn>xhX@rLsM+T9e_EAUmO!oH-ClxAg~e|frpI|Ln;}*>ldjjY^RGy zp4Ywp+I9l#zJc-`9m!X>IVbIA_YLL}@AU%dg6eq`8<4IaK#qgtq;jGWHLt zo?0zbYRPW-?|bGIUk=w3{5z@(vp$ys)?tgmRD%pfZT( zq&aaSCm_D#onXB}PSlY6+pJe!>7qRttJ@QSBc$kpJDT&59yJ1;q)O2X2n3966(ctt zJtLge?~CmiWw*lAHmfa=M$MG5inMEjYOZvtp%y=#U~oBu;n|MGo_ zI<~B=T9HTVx@O*w9xsu2M67W6C5{)!g}o7WCCD?86yROrlM_IDfzub23gSh^H|1Nq zU`ar~d*PgD>c^-5N~tbHm4qKi_HY?ytlG5bs>b;gBNtM=U9Uy;_Dn6Y+5_WlZ%qoO zJ#v0NSHJy1x&CU~_YY-HvpgS_OTW6kvdKE>$isbyzCAI7Jx?PB%g<5&4L`X-1gZ$s z8qga8FN7(4N5;ZV$ot_SsEK{ZCcWKsBba^=Q zD#(9$a6*K2*xpc0K&K6uHQW&H49x#Y{?qaS0SH21v3-xfB;Rc~FjvMyXVFa5iCA># zY8iaxn7H>CD{Gbj1#jz!*I=}cJx*T({RPvU9pDvruLiS2yhNbW)f0$2gH7c>PGOFD zF;7HZAdX~xhqq52SrDr$BZ;C|;2t^6NFi8)Wi6v_9W#dD4m1I}4fvGYSk)*hRH!F} z6U0T{oR@A<2qA7{B&7Aur{v*^#Kd;ag`rn7@mon2-`+YgxGUh)1-{MW%3bRo3V~ce zgjk=AAzu~HX9W)lSMrZ!R&W!?(-qaGpB@avr;aTz$xc;is_#{ZE+dV{4^MNuHi4!9 z6)m=V1Bn%Y15gdpB6@f%Qw!eRi737N@?Rp=q4)~mq3;wEDw_oE)v|eIXQ$OYD>GCcN1F#v*~iE#!8xNl7qb`1bP1@fzs4Rx`@k|d{2jgYmYBkUj!yi` zcq4tl)`?(ErQq>8tJ;!%1ZSmm7FOAoQB;j^*algNsurEOe@rO+YIMb5NRLDZt_ay9x@8SbI^?aB^{mLE$Au ztOFQbk@ca-k<|u_rfQdS)>Otk^y24%*+}s#Wc<+R2s}C%D7OGdh?5bc>mgB`kznhH zxL6SgRM4{U1eaafaQA2~ik3d#m-x`G)^`0d^hN zi(iA97qngaZ*gX? zJmKi*xMgx5Br9N`mm+rQO=ZI1gfE84>+;K{>T3B%jSJ>x%^O?`YE z8k|6+pBN|)yGi5Xyh~d!_@6YTf$@jU8;Z#!P52cxtetD-V-4X)za20GR#33;V71Uc z@}DMuEFL@^Owl;5)fm-)#0X>spxYI#mWGomnzmdUdQ8#jSB~Gq%*0dmH`ycexnc=l zZbrPB3SbcO06LYyxB3lEkHOF=@QGgW3bQVFMJ7ZnKu58yn6g;?E{@)U(~2n0S>7gY z9c6-Eeh>lWsan1-{bse`Q~fW zEmKjs9Gb=kbk_a-ft`k+rDZc5(fjinNuy4>rSITPWS%I8|BdN9P3u2?V8^69qQM{e zoH#bZzq2Q0xRE!S4Ev3F3aNcbPvbDoVuc$TBX;atpHLkz5~6E&CpM-gdt@JbUT&pZ-~ zUrLm8x*}VX&udz69UB2h`t1P%977`!Y6~@xJIRxAAT!7j>{#eJgZa>M;gNxcM*Nhd z7)3(Fofu0x_M+?}5em&5P;7wxh=D`Uk2)R~N;-qM>~J1ce{?}pU(i1w_t9wx`d@Gg zPXphKADja4h$q5u88qtxp)o&q;YFYbAR~J5Oz?Izd=9t%u|KtTyv7E^%gDhQRa8X} z-V13%KuN6KU(N-zf%S6(cm-ORb`O`_zK8ox#KW&xjJJUOMyOeb(>Kci7!DfAWkB*e2}VUww^&wXn=QfmASf|l2(`%iU?@cre#{9f2pX%VSu&x2c|JFCB zt0xkxi_~1QUJL?T^hVHp!r3IkUxO=*2){BdN7tROp9oT2aLqkpD@-&o{=*E@zr-ba z;9R51a~diP9nkQYW$kx!!tFd=WQNw#3+QD)a>#JDL%TPY19B0*08dgi0W~$eBiL-n z9QZBW0li{TZxDLc4#0R$>SC5d45$_n8G#7r9eitcXU@y&S+A@?Kmmhgr4+E~-csmV zPD9~|qGx)JAzF^zhvzFyTi?VQYLno*@!9xb;u{*nyd6{|8P?CGPo`~FfgL=iykPpu zx~nX-mYq`eVU zG8%DY=7#}V20IYJe$#e1qtnv9;o6BI5d9icRi`{X@)Vm5=te$yC+0?!wc&;`mR9NM z+O8!|8zQxusdRh^+ivtRh+5Mbi1TETp#DM)6T#HE^`L>WX5J-u@tBi}s~Xq%{Up|70FhItPg{SIeS!fb3EHWe-_gL`3(;To*=J%=hAa^rl&Lti6L!y1vU;z z^@$&c+EYrTkEhmt_Xw{#n_Xa6C2QR!5((e~t|243;3q-33dlut%{Mn!65UrBJR+v+ zb)c~WnOS2(H(DdWQW3u!%Hzap$3jQYb5={eImL1Z{(0c1$Fdj+xklSI?2#g2#-oiS>m?1wPT+pqaw-TvJNXAv4>-JHbE*=%DYft~Lh)@BpM{ zOhKyvruBbTs;)v~>_LcI(EA{xnjgs70KE&tpz^1$u*zC$0tAmW52TGqpwIqv{w_#Z zQ+YWzRQ+D&Mfv$Y`_0t;Ic#zb-~h(2v(h?eUPf|+VGrSDLTOObQPfYJ)EsU8TehG8 zAPTy5i9V1xy2aq-VEUQ3<**nkjc7|6^?-Ns=qhk6$*8B#RwL zu_%Seh%xXU#L0r?ePc99 zsqSm4uYboLDQDw&5z@n6i=`%biun+r6}f9&1y>N+Bd+3n_H-4MiVMd*kJN+x#sIz& z8-0wlTyzy7jrbbLY{GI)_DA%tLK2vI%G@WoQvpYR^piP8^l%B>SCOh}RbNjI*V4lt zI*xpn2cW(2fIwykI!CArhZ2ZL(i05%id$fyFpDWEv{*%~F=UJB%X8c}z*wHwUe7ki z+^2{8mOOYT*?-8pQ*T*fwIGizNyio>bPXa4^cW0B`p+P}oI`_Q9|crE_khZ$3ag49 ziMePDJRCSAa{1iKn(6+|mCfNQ|>sZCc=@*~CoX9L#$>Xh@K zQmM;g*DYDs9mLY0AUkIOT7&ZWjyf&kWF=vfgyh8{Ga;0r0jA5fvj>ThSergZfF}65 z2ctoL`OXO@($XlpL%{D92p+*QksO2Qi$Q{USN)r8TRzt9l92;s$77LS zcNH#yWm4!G0g`d(#f=f)37|8U^MWj1_R!-rV$Gb$H=don`g9CewP8y!8tMlNKAgOF z*xP$Ab~dqsDi>V4Y`!?FZSD98C6IpIwHBh~^yfC(&Ho0W2~Dz-WC@F zE=sOBeyjW#*eWdah_jmnTNYx>CEOr5@&yn+c@M#Kf!)sx!(c5P9v-%p{mgsO3*laHRCQT$1>Z!Cx-pex;Pud$Ma{!`WJz{M6)g- zu-oi!=r^a&e@vz4O_HK*zJV}<+>SSdMT)ta5j!V;;4w37UCYptLZ(>*kkS|AIS%H- za)3GqOOb<)8x(lTJMo`D7t2VwBDO=QA{JN!rItH z&V>~&ImD#~pD9v5T6y*5heM14)TW#>fewTPjUPam0JX|QK0VkFb3?d2By7{g*F^)p95j>ST0jU|07pNbq9)>on z@-c+wi$gSCIKUBb4{ygkS&_?7koL_tMYZg`DK(@gWV{ITtFc9;fI1SQ9n=|O$H$-t zba?>H?YIHv1SSHs-l0i^M7A(je(TV5GylpX``;iW3U(QVYcO)%eSK@ppX7KXC4LTa zHu(nF8PJ4|0HT7rWeRBESWpS)pS}p%0@g!O*+`i0{6XVhaV3@?cLSQ20=Of>P4)id zuCWjiy}FdX|7j+9-hb#!IL0oFH}&}*=7Kv7ko|Cj;mV5COrW&Bt!STLv*; za?7k+%8aJQ&Q>^d`kjg+j`bZZQorYeLzA#xK)NDY)v>>Fm^fMtZg6zSL5J5+xWqin zpGr$`=Cpw3w}7(v!^VBISp+oOrJyiAFRJOwQ;>+@oOKW4WbuN2yBoG_czgl*@T$QN zKxcd~$mZp}F|N~kCg=xcQPA7wr*w0b8crZV$}5#O$Aq!j@3PRn z%G|K=)-H9k;LNk+oG2_RhU=-&H!e9{JdB0&V0Bpr9;M$*4`o!M_bpzN5_CS@a2%++ zCEZV9>DIlhjeL@3jpNHSi;Q{&RVa!h5Iq=r^}>7V2-q3&CoECB4ywmU)C5_fX(C+j{O{mFqPcFBOZH4#v@7y|I;%Tj&^z` zNf_cCle(|a7~pE4q}l0cnICBH?~r8-wGgNfClD_?jfnwV>bH^HNZAU1V(a`i(96)Y zxiE%GW5B}}XKE)j0#mQ(ACqq3jdx-VR%uXg50QEXb zzFQ4fkB#gF%4KDb6;(^3BTn#FzOHawQsJ1>z>a4}Wwq4tgB*KN`l0ny5|dyU?9ZW{ zRF_-8*m)C{XRgPWlbc8z8@#YyyDppC1p;~#>;)!&FroUwv`(dZQ*)hD&sFjBu(iM&7aB;B{{$Rjs-?W%Gxtz(VKhUB$Yc~9i|9WJBs(X%Rv+->q>$s&w zF%DIggjs2sWKOU;)Es_W<~(m8Fz>9j>Kxjh6q&7aAR5S)heUY=8D{s_N8TZ(!715V z4Fc;djM6zNb=lI7IzQqRW6XN)O`)-6AoQcI&X5y_OzK1KEmN2k?#I_IYBr}^=^g_k z=S(TyxO(A?(Fav*%W1Ck(h%q?n{DSe^A()7rZ6;d#)H+QnZ)?w6_&wY+%!w@j~7Rm z;aI)*M1H0CN^~d-r7xjKP=4f(X0?T(`a#1sh=SqLC8PHMfic@cm-^j`J$!4l&3N=r z(4KUOp(uDvdrZ$k&h6)8u5qaQnZJ+e%zYt!V+q=h!iQ7BKY+VKc1I8<$M!*0!RJ8r zx2pH%WFhEDUAtm_I$N+!b%zopf_)|8%-*(;B(`#Z&?{W+nU5^Z=SVbQH21;zq3J53 zk|d{O-?4T23 z3MgY)?C_X2Zq#-`(JBe++RMlWBYs;C1T)*{^lNWNtC|yavz99dCkjPg?)HJ)QD^pG zMzgn|m=$7_6WZ-;kl5Pq6~roBEjRqMs+?hNoZBnbI$+#&;R_+@BIG%?|nY!zTMR4^ZvY+>v~?#>v>(*-f=}4iKz{) zJF*tnmAY0rb$I>kw)@9RVR|I91!F4uL$O}?=Z#jh+p;pmhLd?4>(Sls-$(F9>o!(s1 z{LL+0*Sd1cnNiR|0l+F6poqRC zx$Av0ru5D)`RVHN=Ua<6oI|)v_(&R(9Hd-tg|kBDOkN%NHi(nE>NF-`TGcn-K8fE` zc5`l)Yu5vgK|87fRM#G<>bS=B)!T%SlmVMQe>sJEm9n?eKuv|~ZEbzdL937vMbgC5 zpYCm}IPW?l39=r<5@L`5rIC`<;q~j+moXnpFRY2P4l5D0!Z1J($d}2yZ8B)Qr+5#i zAFQR__pi@UxW4{q!)-1u3ih2doL1d<(+}bHVL%Vc} z5S#FX#19d-b=by?WB<9Lc_XL%^$j7pc@YMApDy0;`lb@(0m>PgQ9vQ8A1Ly=+D&$z z?WAoWuX*SD3$AbnDAt68&RP#@{l0b_{@1tIDr?#D)5l!ByZ0_6&bK`W-rt(FtS)fZ zsyC%?l>hokg=biG`Tx6mcT9e(jr+^~>Kvu8HM%gij~dVvs;6Ky4&j)@w3UW#OS!`EralTLlP1j3lMmI(n+*Zt>fFK-|DQ(R2P?<_ z?1bTjz)XAH=#u@{&5{X^8q0PYmGzfT|EXR9+=f~LZ%5~-FSF0>nlR_{x%2DaMkn`*Dsbxy{E~uY>fTSxT{a^v32akPiOzHH{}n{ z#*|w9Z6509f2tNT)_?rxf6mtb-><>{7?=NhJ6PTHCOdQ8vw6N(o-2RR=TOC6m(HV- zz0aSrw90nt#4tNMA4)H%j(;9Ix%Tl_+*QI{WIz&+3q|t z;*wss@n2tblm^=>Y@PORrrPpfP4(k{JEMO8YN|K~UpKz^-@MBI`Hxz1 zPEVn}o1Omm;dI)<+d>|ZTyKAvWq&VUnO3;seQP}rTzBt z6FLtv=|aZd!dIxGc+Ebmk)Um>dS|ZV--Z^;f4@)5j@8po+npP<_jgOJ%a&RNb#Vr% zW2@e~*(_Eq+}0*d2Ym{jPvn-*&hd808ev*-&hZ}))L7hk#r1*1OwG^O z4asm4EgMCWl|f&)+(}m7&F2wQt+Cln**az9?G#;`Bu25K|p#61pz(zOk~S_pY9>pvX^P79ev4 zP=vLGCZdDR=jG_1`wtgYUorggRi|yO=G#YFl1L~MMxvh=sI)l`-q@+Ef zS%zrXy!qpsqoiV1EfymTMPJVg7ieB4dg`oI)J*^Z7{^3&pUpoGMD>DKf7}`PjY*fuvqX1Kk)suvdnjK~38rJZ)IS&o z8L2A;Qlc`LZAwp1-`HM<>6b#YBWY(=upO5c43hR;!-NLl{1%)vd|YUhM)U7l7Co6e z@$;a)IUoX0MA*S*!t=pTwAhbm6lhA;Fanrh0wkkugOml|pJ^8)w z(qh6_VuM*KLT{ZJAttgiENonN-{zmD;JQGOWA)375OHXQ3X80thDmvn66uzxEmfh7 z4Y^HLdaIu?;r5_|P$!hSHeer+d!W3QPw;A@Bgvz6k#kf%+b8g2-y=s5?KJt0LgQzz zYxKhEZ=g?lg6l>DTXwv*PP&&A9Ez_9U4FPE%3C8%hsbC{{+*85(HqV59CMyf%fdpH zjS046hzZNhwc(f{`Wost4Ptp{w+8r`J)cYO6?3GvAXZhyqvt}*_ zUs}l~aD~!7SO!?f`n{>`{zZswhN<@?EKXF*AIZ#%64(U7uMxrL$s9#J!IPHTGx(WbL-|$A(>TAbD75KHc!v=Mk5m zcW0~ep7E_hEjufs-2fO1O**-BRZo^*C@ZN-^Rsb3E4sKnT2Wtk#~zz|b`CSBLisBU zxsYlu)a>Sc`u2}Os1DFXz9y07l3$xu9T$cHOetF5-=rV*-NF}(#|`4Qb5_YPEL15} z)L43#Z$_*Iw6BU=t(I8sMo%J*uo?`dtD#YMC2do4tS{Pj^b|Cpq_P9qyo*nL^xd0E z{Bnc4k@9&JMgR=?igYxYYi1S))qU?hX|XCw$%1W%-$Y4;H;jq0y8Yw(i5V%AW??2p z6I>n*CXKWfd)j)!36A0N*+Y($E8It@f;0envVXB&Y?z%tTgIsouQp;}^3ZPk);~X} zd5fxN@=g4hoT=v=m2dqtI{5R-DwGWlO89F2gDN-T7g04eCV)3H5grOPVPT;d{3SQT z9u=EWobW;f!}({_kkDC>f6IA8!CSC+R>+aSkRLBqKW)?arpRo@43)`;FuH1qkD&I> zrN7?NR=Kc3qKElmd>f{nUBXyRJ+tn4Nt+3MGMuoJSjdVk2JA31LQA5;MkO^-w7~~p zTuJr9a50|1#%+K_*&>{aXcUh0`9oTT6^DS2nR@2&u2vn_suiV;5+?fjnn5Pi9Eh|j z*JkhS7h~J@3^(Z#SX3~7ZW4XZXD`hyF&AJ+y@-LAaq{c+24y)r+dhF z<8BLU>T;We*1?4NyN_wx^g{o^z%MK(sU>pF#C$!`J6|ZVyb*41sQc(}#_pVRbb#5^ zO@2M`VMvLDn3z_+LSsTY#UM2~Wd1Dy)5?>+witis3Ntfq8z_V$Eiz&rR+RJGKX=hg z&!;c_)I=@ED?o+IFa6rJ+_m^ueJ^ku9_jS@y5BUX`7SM3%q+T4bPCx~PEu?6FPgI^ zc|H`1%-34wTw1xgW<#6ekn1;C9x~Kycy7(kx{ZJZzBjZKf?zQamNtx&XSq*o1RWt5 zzBq5os&6*wK6;M!BDr8TW|+p^6e3UP98=X89u>paw)01IcDKTSB4O)SQGrEtKY2xx zQYt$kDJ}X&;TIko4H*8%m7!@m6%W7HwdmWfWy-P$*M5vLo-I6wc2g6DXPPA`O~-8~ zv;YT^|MVMXGqu((d@|*;g@wxu?ehACH}FgAGb#*D-f~ymWV9AKM#VZ>*-L&5_&;$6(juK?&NlF9!jUxmWQFTK()AeaXH_4$rAo zA82w?--|@GDDW53SRqzs`T5!M%|G~7-dX-t;#&2)E~DNs9EE4Ye2P+=3}oDf^xtL^=Rf3VP> z*Or7y6>OZ>8n>xNyTaXr@|3Wf*7)Q(w3PL-ujn@)BL-g`zY_YZyXO30|8v}Qi zolV;QC5WNcqp7s1m@#P&0dVk+2B|)56iv}kwE`6~O1Lhbi(lTLVc2mG*Id6M7jEpm z1PuwF(rJq`srT+1uO$FyHsZAqOjjl-8&OzPcteR@983{|F^TRg4%{+MCf)`b^Y)K@ zbE_(}6SLgoo2|^&^H8<(2aL^T5}j#CTFlxkZKWQbeVNWuyaAPZ?w^W{ef7Ogfw+0? z*s-I&PFtyBKxOKsFAXFVM}IH3riLm}ga8g4l3F3=7xSxco#_p84tt5Sp|c>i4fBDj zj2t~R7vhFuG8e1juW-8f9^8_Lv#e`s1!ZR+s^8}ue8Avom|*8gEr`-03k#Q3Cl*U` zN0A3W1WQxCiW&piyWjA!m@Sm*SL2fLm+~xu5NZg#KWMLQ))fK>=0}1|$?W*26h@v^ zozQv3r3EX-1hDRQhO(mP$LAZZRkK-WC8w?bTBNNu#$tIojMVJb0SAa{(f3-a>ZzGb z7fQeO+NHkM@-E?(Bp(TMTARV|GQTl zfZ7>jvZm;sWp_k7Yf;q*GYOqOEaLQED=GiO^|5^)ZvECrV~=#S336wtn0QZ5I}Eb( zO9tHajy1^%*A#`eD=*jI+G64|GQK2AmY(#HAllTN16uz(x#p09b@ZJ16=8}PIw5pF zJQD~Zux*bVjdm?T8)ZdedBXNXn#*YPsVnd|Z9LXt?=K+e%) zGoK##!Ajv`M0w}sRT%^)9`DC(u$s>i47Ni~hAHLJai6$%Gy!~#jFa%g#~~{$eM_X} zYpQwLwr$JCJvLiRw)E84jqhGQ_t_lVxUcT+fIxJgMP0u}7DxR7eFqK47c^U#t;#z< zm(#s9Sd+NYZ%w@Gw~bou zBntS6-;Z){h?05@5QqQ@XV8vD+Qq6`ouMPMc7u&H(8 z#E5n&`ZF3M?EDMsl0F`4?I7Yn+$U#T;HB&-x@&K(RRcg-#k*Uf$C2j#xgPwp*_~79 z-EvzS%cGhPJ1gFZ9B<6QWfHww(M9uChNtcO-Ac`vCU9%qm3X@7c#0;obn2C8gPCfH ztONMqY05D@r-oW54nn;7g3~hxp(e%B6oVf8Q;URU01`gdN$FKh%?ERiK3-6`3S_tWoBGcN#HKfGCw|{cDC|Hb*jjiu7eLz>j9B?v`4pw&d z*$r9``!p=+clXCjGrcuxDv7o*J3aT`8T&RrJy!;2!R1_i^;-c4yMOzaZs*fcDErc_ ze)ZbCq2brZt9ll-Y=s+0dFJ=!icMLYGf(f(Ryq<~JS8`AHv_on5iS}G;63?x9aF;R z4WctKcYz7JH+_bIb_F_r`n24xo9AM}ZCn!Hs`Z4jL+@TE2o%UXjpxr7t1kns;JuXH zV$2liH!8*?qDU?Zo@?twlq3AVpwcD)A-^um?miu9I@0lBs%^&tquksOInoF9x&#g^4l%Ez;z%$%NWa%b zx;`&i=-VG_cs^Yme`u?^xSQI@mI+$TRVMeYbvE?355L9L#&s^WU?tgeE+Xv^T*5Z~ z#zsSZ@FI%KogxJ>nfA_F4-XDjY47se0Iz)4?Mb{5M;JL3g7$p<5lZ<1tp;b_`cH~Q z{-yHF6Lz+-JbSfu>u}Gv`(}JuJv8U{dCr>~i&R#$+de+wxwh<=MXH|mS@Q@}&Zmpx zQR-70F}MxRmshae5U(X1BVM=H)^95<%>O=3C(K-Tco0vBXp9${dt_1>b;|{FGSyl( zT|%z7#-i7UV~c#3QiAm|ZCH2|6C?$m5G`9g6P_>E8~#_d@u$__)JrV7t&eKX6P-Yf zJ>D9Tzu$SQW14#{XRhM6?&s})yt!e_IK3|xVWrB-%3>3gj^ed}YzD6mNrt$zPB!^O z-(yNI>f$Ujj+8`!!GRAZT{sz{R5Y|CE~C;%W0tBXL$;p4%f%naUofFFmZHDOx_8Ur zv#4!Hvx<{FISAOuucH{kc_p>#1YrTV0n3*jJ7J>a1oxhNHM?a;#~;EZuE4^#(qh!^#I>%Wvy4pBVs12wUia*vM_Og~eY=R4 zFh(Z_dE5E7PbiBxs&;T!56(wvoy&gLPqRow$6g|{EmlSyT&{(lcaQgYSEqTptYZ8Q z4Lq<_w6iHs9g(T2JV(=SnxG>=$3oA_Y+9YVY5SADSMCB)$%^sC=@5p)VnwZ1??(AKL#bx{b>xDRX|wSYgV8T>J>lu)x?#Vt)| zsp_36nNysH;P{JLc9aMvv>&eXLb5{a{LQ8x6dbt3H4+=$D=2Skm8t`saSHvIvisWu zeLJ+RcRjgtsRd__1S@<7SN@^Xs>}W$dQd4etWDjPtSf}P*qiqw8E9l70`t#4VJp-ju6{#{H@Res5L>EONn zpmN}GEww7Xd-tZ)$P+|JFVTP*UwJNWjKyeCRhGU|thi6>@zx#H>JvTccN6;=y&40z z!B?hWHm%t8;VKU$oPccpXfGeLQKH0;@VTv(-m$=k2p?DI3KC^kj=E zl#{6P=aC0*rW*HnY?QN{eWJ_>0u_2rbg#E@uQ`KnFns0KY*!RYODakp-zoi0!it;; z@toXVtNGY*Va<|bT%xmAd;C6g0xVdk-Mrdw@6!|~Iadm9lCHe%U{hrpQe@+4-_KY# zy&v~)c|RfM+$ z2JCZ238hnjrJCgs--Zl;4WNHKRJiDFk1z!bL-6hmH!nOo#I5CF_l2Le0IM=Wsh@z& zt336rce8J{=1aw%YYK4lm|a$kSmR|>=xJHcO9#BX1Uix8t`y}zf9fsLNuuT_o};Z} zYg&YBLVA$Bx~hAtj-HE;XU&hmW2CcpJPrb8HAVP1bqSaERZS~a|=_3JMhEcWNj z3rRlQ+K`2I$RY@V+cKZPh2eWIwdmcydhH>%ZtYuVUZDq^zlW^#c+h=G6pCz7>88{M z@0R z?_N1VSz@*&fzNn2DKcHAFEdR(^8LK>>t9^9cKE|AO~>(I{NC}#&L=%+hHpC}X-6wA zGEpR*Ku`#8KJmOLtyk!|8JQJBIqs6D0(~nP6Ci<^2=_Eha$NjxF1`2BT(et6o+%#> zO(VupkPZ{WW5k3XdHV%T6PApyl)#M`ZN5u~W_z|(gB+cr?KV#|MTDWUUrOuZxB=o~ z@?OZfF#h;~djB^&>uOv>k=VZ?Nx%xi^?HjbM#UR`CozN^Y3iBsg(Dl!D8z>nQ6?nx(-- zjKb|l?R99LPAH0ro!0A`hFAUy`~g+J5Ym_wo_DQUvJ*t}xQ6Qd1ER>&GNU1-1dvIh zN3ByOl_;W>BQ>}+xzu)I*6A9+u-=CGX2p6+#_%9l9C~;j(tBMOH!Qd6tI3ac^(ys9 zzO`K*50d&izUt0KtNG{zQHM75adMrXIY}`&{6TBu7y3_syz~%i*tFxv(jUG}Tw6Xy z7}lSENm(X>89^4nedp9g{iJE@SBC|d{sSEVz-hRMJQ3Ro`zB1A>uA*TnC1^Ph zsbHws(;WtcoEEN&Q%F#a4KE$FtyQ<%-_dxsq3L3MHBs0FWmRradb0`|U+5G$yS$QYz0KbJk(+BYzjxwVTmQmcs~|46 z_koVzo39_XBVRxKf~>K|)LX;DhI1sb1~iUxAF5ccMc~EhganV2rEJ&pU23&yVY_C_ zS~gs8)ye+nd+qn6r2GUqZ4#ui)cy@R7I|C=@FiYyLg!b7DT41Va&OGA-CqW`R9j-x zbna967%|fkoq`HSqrS3HXN5+q7R zLJ~PG@l=PlGr}Kw0jmbs{1PnM2GJHEthq?gvVYXV_a@p(#*`Wh6vveTQ()_vafR1E zkT^ogm@0LVYilWHgho8X@gS_@_ za(Zzao(A7XztRg>OsE;CAu$V4Qe03HQ0R1jz#!X!bpuVjGuVlmIMzud!hu6H^#DdB zXp2J)bEs$dLxT28eJ_b0q|X5fk~7=;-0jsEvF8gg-{o;T7yl|xqW9b{K9015;G;Qo z|n7K2vi!EOHqMc>7)guw;CJW z8yj2=GY|z3wVxa$I+jSCrDz^^ci=2q#|s4s{49fA<@wWem=aab?x9Pr6yeweOI7oG zy9}q)^tC8`Y0S^#i9-m?V`o^toHF&vvvQ6b!LPE3n>YKH((@0Gz<)&ElV^9bK_VR{ zjxE`5|6mSmEs>%T4qfD~@k88wqOqxII?7v)2Ly=h>R+_9D9LxTfd{@e0}xIfIi5?D zwW@lG7Ujg48bF8QrpD4HK^0+AsK!G;X-?LQ<>T>4l{|~qm8DW-N1m=?j64+zs6MR|DNP00Se?W zhY$?U7c@-DGu>Z%lxUX}N_{4*15yIqVnRZKK{Xv3%9qy!k2n4p0UQOm&pwNY#9F_e z0bJyZ$c$}3sZ(#ArUP-YP|MF{Kj)O45`A{wI#-N#MBg3h1C?d*kb3*AqeXs3Tnlh{&J;%T4Y314q7j_fANgNJw09 zu=#afGq-g;QAoHb;0ylY>S{UE0>6vNdG`}q%u-2mvLYRWEf*6`7HA4BLi^SLncaO~ z=+9I2 zWy!Ljo!C!QRasY%MEB=>m^0Do;(iV6tN}boSZXh@`zk*3`5)R~&@H!&}yD*r$2@ z5-toRVc|PH&GoI-zLSH(W?jj(>99G*q}xjhIXEOZX_5qzKm$Z&u%_bmmczTu!i&Gb zLpWhJJO~B<)S^4=%Z|tqx2>tbns-xi2la>y+JL)V^LK;RSmw zAU470F+E37{u2fmJao5?4Yl(ZJyraJCD<$aG%#Ps$%?pw76uQ3zL2O4TIU*`S>)hg z%I(HU)Y$IUBmNYE5=tY4qtsR+G!QvYJFA zy4;Oi)(U+?!v_OLRmU|wDD_{giW!k|h$MHAxnZ~v>S5vvgSr)mn0(2%kSt8eGk=Rs zFVmeOO5%I2xNDeMK~x}lAghcp8&O@op`y?f>$ZMXbC)wesa{N2YKnSos^IveM9>g` zK)Q&^4oWR#-}lCH-)+zeA5cbm1lRHEd|2FLS>uO+Ztrh*Cq_&jIMR0ii|&U;dDANn zu(sZ(ue-y#@lQX*QH>H7S36Yc}RG3iKoTKT57Ysi>om9*|6^&vbpVdMsR(=Qub3^PVuSM|At;z$x)!mCh>Yc~9+ zldrmmj8pYwT;DlVU4mj4T_GxI%RzDh+X)0Oym5ekCFJIB#MLBL33v@fMJ&t;@C-nE zSSIofB_9O-g&hEwq7wgS)4WjProG|)Dyr&>tlgGb z{Bna7K=M*y-T}U~ltRvLcz?j-3Aqk{ABmSZt!5T!O}hTMUNTL{E_+>=QcgEdQnA$- z3_(DFd+e+TG!b$O@0g^qO2(0)$C})NITImI9UUAfv^hTZeWM$vg-c9YklZ3RnhXZb zr&k|%Y~=rkL)JWUpp5)NM)K}iBpjRraV#{zu(;}e%4=PJSGdsSM6_tg=R~WdsXMo)jf+&RLD|E2}nXTAz;M`5asVZ znUQl@^THw1jdugnQ&*9-CgJcPb``;e=VHd;6q7;)u8NCDwgJDqEW#r0U9^G7;~?!2 ztzh{HAtAWDu;F%bk7lcXeXjQ7jpK6Euk{0tdMyF_gHa3C4aVbeTh$L#loGRBY+B58 z6_SuUZuD_%M|=z1Nb_hc@_232SU}ZzItdVu^}qDZB1wQj=fTNV=D*i(vh7$$nk}sx zrm+!FBH)K#GUk!!4@Vzbg42t!Fzm3&QsYV4%CC2b*bMK;3pP)(fBlqyvNf%P;&Ob* zh;tDW%^4zcuu>Wpj0XlsCugp$A&Dpp)*a3GB;nLOmQ-PId#&s0@JZ3xhVxGW%-Z&| z%t$fr;axs+;>RYd-wzp?bV1eu)?T2=>!nMeWrk?fhW;b>@}RvMgO#lgCoe_JK$16x zc7B7y%r7l)Em1j0f=O{3v^@rP3#=mRRI%K5oXmd64u`Cw?W%H_K<<=}_P^Y3w%A+E zItnFja&V6YzGqfm9#R}q=*mI|%&7RzF}Et!bcsG}6UY>}sWRE6>$TJMIt_0&)GJ>$ zLt413iJTlieJn*s{lMh~nmmL?!8T8#fZdArS%w>9x!(8$_k*!Z>yls7Ia#A&P9hg` zj7y;3ZllLKF?EPHkXVHtDhUwrPsOD)WXs0a~SCcwyqHx6WgghyN1n@@_LU z9H22i(lG2@Dyiyw6T8hx3S9b8Ew`I!-&!+i=f3HUo6;hZT8<$5(oH|9Wk=to0UeIB zY%P_Vef4Th!sX>jSgw7OG42Q{NDAtE9nmmRU&v$OIG+%_wo>-yYXReVX(n$T`h8i@ zFTqD;UN&$B4JJ+P<{$z}4!2(QI9}7j*j2YjzpN!!QZMw))iEW#uXoSm*UmqEFu(TA zs$2AztQo1JWX#F&_-=msX<7DplJ_d@Ie{+w!#57iS~c&KeftE*v*sToxb|G8BU1~W zjSWnHl|CjmO1Ig=m!19Q;nvi7Pi>oYamhGZA>7(z@ZdAuNAd-s1*c<4gsZoPORJjm z9ZO38)NFh`Y3J;-AKdj^zK{ro7e_*6rjx4Y#P)n$y?jA)bbL(-Ic+rH6kA+A(|DO& z*1$7!yX6c?*r>|HwVD}D$W3!T$V@GpJ*9#|z3zr(`5Ou3MG`=?V@9>cO1{wdMW8c! zG^{Aw59c7>5t2*ESPDV$^EIwoh-X(`7Cyj#z(1Hyg=&RY_wSUFLlnFs$#jw$z+<9M z*eaj|(lqd_fN1Qiq!Ovn-ceeIDZnd>R?_$lOgk-|!fKy3f9kvliap|mq&3r?o*w21 zcd0B8D-0p5G8u}E7i_K+Zp%985aa68A?@;+%1S3N=eJ(@=i`Ppf75EKJ`cC%4waA# z;G?m=$cVJh>tP->`v8SntRc6D$^gE5pLVvYp0Gb^i2y!3f9v;F&EM>LDT6ZWL&>#s z#UQ*n#n?%h2_y+vOZZ98Aa9M)-L2_6%zp8#kh_3mo;cXf`g!(0->)t2_wJ~~ZV2JR z`MDzFA@mNFo8g?XmMk5bliBLF^f$!heHyieqqfhAdlB6{BeFHO?Kf_f3pVF%)RYq8 zJQNt2c!!CnPshV_5txD5gcFbg-M;^#*jM0@%pr(~0VGv3-)iz9K>x?{KKKG?*`QQn^m6k;QeKWSM|F^=W<75A=#`M|06)yc%9sKtd)Zh4T zg-enDR=D&a!0AzIh29JOgSqB4G;TV$)MA8E`Pjd|o%nOvCGLmW1BurlU&;&YkL`zA zJ~^#9XjZ8A5K4a&Nr)|}o+W_^h!y;ZWoy6d=43yYWiTmd7Ss~G<+)Fk@S<$T{ITpE ziU=OZ`>`_Y4|LTd$v5HcnYy)-Fi#6h7e@nzfTVxwL`(b4sgPb`{sKi%lsFvLb6%cO zVB*bGau7Cw(c?e%D}PGa0ne0s8xkLyqh_|6kH}TXk|&=wa`TEw3%GP~KaxIX&ClCH7~ z@R^@&AQ}b^96p6cl-Y+pc@xLD8Zk|E76My)U7ruxJ6i*{;4V#?RxZht^1ovOXQg+qO`I5|^cZmH1wHfF`Y&34%({S-EIG zFCgt!kD5@elM^I{5R+>wv9^VM_Yf;VN3V~c^517rLDoyqmYfN`I(Wu2=yYlDR|H?k zS5qrcs4F|KiT`!Xlv9VumGhnf=G)pNhXXHhks_9w5mIp{Dna{+$Th}n>T3e~1s4xl zx%cECk8IE%+G&J|LO?7_#bpu@@rTsLo9`{|4j6izA|)s`AykLIBq37%aJ=r^=LKim ztyM$tAe4f_;fjCjxu4}bIq2TNUu>VB##f7fafY`@3Od30zk~QbZgYRf}F}&|W$<%Y{$Feh(#lB3yz>qdgq15V)a% zuk6!KP|)}xc6i~%0f}q1eqWvE5TiEz8ha%w4q%zMoZrq5eI-<&n=8+u8#p^EAeM$A zb{-x^O>YL6APGJcuhl^jcMF$I1(>{JAQe%l#*h0q=D7rFSw-XlkS!?mG87qG}`8|t)pn__Yu;Hd@=3$J0 z5%82-pq5zks#V8*xBDp{pz?%jGF4B)WO&iVsv-VqOqQCScHhQl+_5+M!{W8Jq$KZu zow*805+b{Xk%HI=*!(sbq~WbWatH-_+fh={q<1XczlfWOFee+JpW~tn%uQb z_$rU|_cj}iB39jhx7#7hCBR76qR)93S}l0xn^%2%{h9Zn zY;>5Uh7uind7GM&h|ae@)nPC@N17;~QxetUm8bor&su>0z9(h*(s>C!OT|O46E>^2 z(xak#x6Y{2MAnnJyY{+Umj2RrOE&xwb_Q&-^75ap=hS@+KXHfS1ShMOh?(Ny$;lEk z#+4RzERo0|7=Y5BWx;lavHg^cO%PMLoJgoJuz0%B=nkYG#$Cvo`pDMIA{ky(cW$OJ znB1I3h)m%~cQy3;U{!$>Rw$;R1<=(`$m_G}99eoY^AlPpuAO1}`0j&VlMmr1$gLQw zvkFex^VORQ>PFM@l!O>Brcnx4a5q&f#MwFJeQut&EpZ32lQlqy!##5P!57&M*B8bb zmZ)6eMxCgXL)0ny`yHKbtNNY#jB7}rL#<G#521Mst ziun?~8_U{?S~rOqmZ$#VQlWwip8xD803=#iG_xc^sAfntHWQk#%L|Vbx%>kZ^SXrS9fjEJMt;nu1(j}=bwQns( zM$8R?-JPXEQjNR#a+_!I26B2Zer%*Xtxx$U`3<%bi50wr2HY~DT`G2l4joG1iq`-? zV;}PK^Xq#+rorE!iYyB9d{b#LzRxkQN0fmcoo?M@1+)4%#quzS6H!yCF16L8r7)sV zsjh|xWGBpabs#mta8Y4(u*Oo=6d?_js=j(T%S)W3A%UMWJEKP@iSj4RJ**9$XGVxm zM$NjPjB8g!7;W%1`I_bkuxiqDHqU6$D#q2Lu8&vbzx~CtZ7^; zm%5PVw5d57p*aorN(AllDu3=a3a(D_TvSY$h25Rlk>BM+FW&D*cHia)78#pQMu0CM zSZjLsrKF^6j9T{JcS<2f_?|b2BD9svOec{4gkIVIx~PW=@&MFtqRYHU2u50EAbD{) z<*u6)M2G9DhM2|!gP$A>KFcQv3XG(tNaw+cQ|F0tX@4;$LHpZz7gyJ zL|CgPW-N)KR?#EUWHy{2$wQH=W~)oz$+Bglh6?d90h_M|saCKAdRelENy^*=5Q&&k zCiVpRNJ>$K`6%)@2);GDUH<*|)8rru%v*<#Cs-7*swg65m;-}mrh123MZ-;sVSkL% zXxl;5#nKiRH;JrAM7S<47DS2=kmi>crj#yKnJj9?#F!$H@x;Qo=xdmG8$9_DrRQyt zLe{d0yAWS{KK^m~c~P-e@kJ|g^PFQ*z7V^mO7s!}kV$3EGcJnfUz7l6S!vev!HkPo z`uo%H+JQcMNP3AXb8RKAwNvrq{@@6R^Q zy{UJ3VG(6)I;ItcE7NprvQ{(%QSSk!!;w;05fc<8m$)&miq5iN0ggNW&5g}1EgGA& zCD6}o(6tqrFZa=)lOYl|V2G4sg+mcVDuEPDYdDTwd;0oeP11jli4o+CR3C-WUYVr1+WN&7?$Cq0X2k+N3Q$2$5J0%>?GxCs2&MI(Y3NlC738s zlN?rkE%uq@lHeD+6NK=_5EDRrY!1|n@n|ZWoU+Y*w2(m^YilTJz-q?adskzxn+C{o zHB2377qe>XfGz5Xq-{iHC`D+@b0xzOSQPtllf`c&nPNVwiOT8Y1~1FXPw&=ziAS*+ z-QpxteO{JYG&P<})Q(R5Ziq9J4@uureEQ?1O)Kv;+eZb%pAVes+Vh%1=4nsO1qab< zsk{{hFJsbX8b2*_BTtJUhhM+F_PAchCxp_0XYo&J(25#f91!8 zi;;VGOz!Wov6R}0`hLvf!i&z!oI33`78U6sHb z3?T*;j11f~^r_7`BDYYSJ)xsZvZaU*-dn^n)BLpT?Cb{1##{n@;?c-EymK<#@=;US zipmQJCUOx1anC|Z zNWMW>LzV{02r~*A7Mi$iuDN1_4{}tDBY6RpM3MXlgCn((nN5`|gZ;FuW^*PMtGcWR z1}yYFjE)qWkG4=>ZOGn!Qg)ZHMd)&n>Z@iAqCd3+;LE}k{2wW!`KecyIy>rymyL!0A4%g0#;@pvY11R&ee#vZe)VhMb zM6OW8WbJXKbeHgW+^M0I(E!+Wp-8~H8OlU4OJ#3$^&#k5FK22sWL9YuJf}jGz|3&d zev8gjp>yt%03(5@`5+o_xuWXaWlditV+!$Iy{AZ(QgEMvMM*j-^$3D68gjCPS`ZUa z%E-6CbL8ub#sSrNRvGi1>*YSXbx-7ay+Q`m|IzS8!b~oe=t@Doj!-W`@n#87D#C9e zShfypdexJNOt=Uu5kxiGnh*O&bD!fK1=f;hT0tU&&xn7dNY?#vr*cPc0+67^xG$t+ z66VQp!eJ79s7xkb8CXPN3=~yS*On|-MRs+>cW?F`e7mf-Cv%4pY9Kg)q=6(xNaFZw z{I3speFzzUy{d%cf=2;;LSZ5Hz9052?1g!!@cjEl>k(|SOv=_p$=mk$?NA{UpjAE) zo$?@;K7G7^SL=Bv z_&T%Z)S!EB$(IjPGUX>YhQ5bsBCod@$g06?j-% zThC+qED?N>3nxmD5Nr(rRKjrZIlv$+{fYwzDzcgc3^4^Ht7;+y%fl}*S zDGiQZ8I?)tos!-oWvMJ0$M~?`k>#do#Dhp?DJZP*s+Qt0ihXF{6EoTsxW{S4$bjXY zvX!k%vOuA->lgr-jk96OgU1#|>-sOhyws+LB%~5TOf(5AFW275Ka50g%-8T<_bOt{ z&A*JZDXP=C6@Tu#;^^x)aX)+otiSsQ7rkSq*utU~0f-~w>!Rb+U`0gNgBQ0uh{{X@ zby@p$8d6mWUh@4W&0Nv48qF2ipNcQKIp){3@RWrQ5J3*DkA=EI6b8!&BQE{xic(GN zC#r%OXuHK!F)0xBzsvRK#%D7B_uWT2EQ$FAFZu@EJaLY;a(RM6pTDym1w&^S8Bh)g zvGfOkKUlv>H|4qM{ipZ&hBMPj_plK>OOs-Wnk} zeiw@(>Rp;Gf7xh}6@8HG(}R9WEJs``x2LXdzO0()go1JJ(@w)J!@xlRxP^vCpVzS+ zNG(-8L%hZpMtS&!J8v@mrk<(4Y@ttdE${_q)z_&uammvK*jhN6Uhix&stvhBv`;Us zLd*wAXWYu7bN<{6*~J}rh25)!=mjE=tAp;bal$EgipXY!0PE?21zaKBz(tp|#1MAG zti@CdkupK3E;p+JZh%U}U}OqdVTR!qrP_L6Rqkf$e-E{tLK%>hZJ}IMc|%Nbgl|AJ z+C8eE#VjllAA|7$QE(C94J+#WjGAgvMNu?|r|oyv*2tOP&(VLmG;{Ou)S{4^11uWg zMh7?7MK9k;)3RO`<}nUgfOYNyT3s4Dc-UC9iY^tk24dbA8hFt~Xl$-soOM`yc?0pA zoHv>e?~z(0YFXrS{8T1~Is9oTq$aQ*(r^{9Hi~LF5xbIDTvDh(%!3z26O*{o`1ikL z#v;c;6yvo--h>NMQVaw`=ErOrA=YJ_B(a05p+~{+h~oC#6G5*61SxDwXpiS6G$!Om zrl72a@}H<2FF~blS+Z!&G0-+>*Uxe=u9*x=h~AF962zKlE|x&NMz1H)=78s=w^)@M z0Wuhbw?L_BZq9sziG~f74KQeUJ~`)w7tuJ%AqKU|CXXLKp6$LLdYR!da@kaioo{-? z7;JlCg=B-dFU)sq`)&Bl${Dmv{FW^z^qgCHU+YC=#D(yd|Y>_(TA?dr&fS;~;W z%Tfc0s13#55S%DD)-75>FNdx@!)qg!H`~9h$_KPeA?E@EcAL@yb{}y^MMeBG zRS$>+kVCNx`3$Y)sVQ_?25)6DGQP`r?B0)lV&;h>~N{ArA<(nahLbB>zvEAwkB}T1 z(Y!rQ7lHZXyP&gP(nkH@dj`s` zBqQuw5nS$a^}i)=lt>iEWl;Z~E5ox)=zQGY^AI>4tP9Xh8aRtmeQ3i3Z5{-ci0=y! zmL_)z2_lIh<(9pS26Rxg4ccS>r})mT#-T+(byVJ95ewT#cZC{T&-=+fUy(O+FKg6P zovqm|S9MKNAf1VW(8Lk|mr=3=RjnP=9De=$@XVf6&wPDIW}+QTkIVW77kLnY#l+3l zbZQ7KVI=lsvc+Kz?|i8zyG*EBP_R_QX-)2(6*=}P8{fna;-To!gB!yS%Y6fQ4NwqW z!YnT6K^pJS(&){T(CRQ&QnUej8C|!W!nm1RX|N*|+=3KvrmV{7^!rUCq@eh?SsyrX zaSEwjFwE|2(nX%-fzQw|LA|1um~`wz#CKJ1A|ZQ3y#z^7m<6Atmrq>iS|G>+9aABh zu@0-cv&fj9NJ5Z9-vWj*^S7b^=H*Kb19nc$O1eztBjtb?hBgM_B`7?Y=4A59mWJthSO7Im#3T-=(kfdPb^LaoacwhkjGqud3`n|dv8Q2*5|460Cg^nBwSEuix0(wn_4 zPTZ(45V@70ZPTx;j(BK_rhrB`NFF2C&%tTz_A01vpFYjoVKxS6rMo_WF? zWx<7@%qtE=C6Zk5a?GWMm;@z)wv^|%oVx?N(eH^RS{GS=r_VNIqi7^J*O2=tyH1yG zSL7z*op^cWzmm~svZBozDGN_A{@Ls@M!&mPp0|-E7D|+KN~K(_em+rvb! zKU^a@H8TStwNycbKO?(B^VKW_0YqYJ%;W_s-IO`~DSHhm0Dq8;g;6TR67dt zOT0vMq`G(P*3jhHsL+^yOQnQnN!ygPq8nEnh?*grMK4x&k8bVw$`})uwyGVW4?>BIIyk-@1G(4h@4$HdXH7#3I1_%v&C`Q=8qCW}t?j4I@S<{G-|J&3c+ zycVkhV^AzfAu2jcWW8p~^h=ACWT!NL%F~}=*l5#DI7#0N<7^KS)kZGzI_@jW8Xl{MxoM;Eawbp|#*li++CjHuSR=fJQ$^8o`Uo zN)(ofw}8nEEeW)1F4dGFj;nr2%N?Y;xnJd0#B9vD(c37cq-TDv-orkF9iy5$xA4^a zx-R6x5&Lrq7By!RJw~5i>+7^ko^KW0xMk!?awGs?mK0w`o*64B^3v8}GMAqgNff2% ze<^qxk{noSIeHQZTAcEQ7rVIrXDEeajP#cX30IQy`>R5%9^kTL^wx~z?ftOk9`Cz* z1aW(5oB??N!WPr#Ewz9GWN+=&5$$#AL>t>d{&qIa0?etwLZK_UkzP)^7y;)XCV1tXn#;47G@_J%I)T@>Xu7TF+ z1~ZInik|nAjVtN&-|v=}e`9C!G@F9Lns-0%)B5ip6#viF{U7`;BSHSR%QgSC&-}ap zdDLcFLpJdLi3s+8R`p-THvGSq7XSOIUf{OLWEv)t20rrD>}FTbaH>e43k}|*GYZ#_ zk_=qmUG%IAWTMbyzh+jqY!<*1;VPun$!RUNj-;z)k(Siqt0vBD-RGEk>w)RIb{1Vc z4xlQ}=@4(mC)Vo>Ycf2dk<@yAD~09z#<$Y?nGDg@EP6ulKA)ec31*$>-$$cSolHec zcXt{8Qf+75=+<7rkTE`M76V|hZ^+cF+JF9~O(s|NjgWs-_fH=+J+ft4q_$Sn=O6jc z|CZJKHMQoizbsq6TFO9^x!I>xaEfS6z~tY08Q}q5KckBdp2K_`h%E{}QwF06V@_d8jG6 zj*~!z;Is1DLNePyGq1fWZ}!B7ThNLKiQ^LJsH4GyMdz#E^;*fg8osZ7DaoElL3(S@ zNQskj#bH{Ppk^LmYX=J3MIkDj#$>uBr;#CM^7drEHhF4PpUYzVw4;S6*~f~e86l$e zECIAss7iRqi$IQix`dBf7*Q7GL?ReatYIP|T?7#;FK>Qy=JQ?{{`k2IJy;J~l)M2L zR>+2j9MNmcuD2Z_#&E+G0TU&(LPh0#Dcxj{c%C=u-nXvmHKaUhB9w}`Z3#_QdG!7% zpZ;#5jWzrsL_kexU*fPAxzX<@$Z1M_F9GZIy^v&2vEv5Z;A4%8Odd+9V@fQu%Kp!wD-itR;HexVZOBm14flVRx&~!@2Ul=_b!u19V;+HB}f1)*q5oOek+EAJ%jd zuH0mE!=wVTw+)PLj}{Vb=tUuta%?Pg{J=y6K^)YdqT))n?M?3acz%E!8eo`-oAHn5 z+qj*9ON!ThD3Ue2xe%0#VG?{0t#PE^&ux60D~Oci6-V;Sb?MC`P#2vcs3x+r+q1u4 z3x!QnU_C!IWhStqidtpzBWaBuCG9stSXx4AR4CO--K)k5`4R~>;%nG60y19cB|f*r zqJy!J+cO&BbvhCH!VyY4OgiQg{>5#1N`ytY?P?hRD*W@Q?j0#*x+^IJ{!Gr2mIXG& z)t65L7elP{*6Fv*=WAzY^h3E#)7C-$H=#nJr~kp=CG*nP2w21VzuhpeV21h8E&`!t&&`*c3N zA45;7PXot3Yxprv65rftlE|O}5-9ymuSMFvit8N#pFCy$H7ce5-3HJFxodzH{f81R7TSgY;$}JmAS5zIC>{ znzdof*R>6XWZSO4bzV=j(PhuA4*R;zJ3klB-le*XisM5wLUsh({hsB}X2*?;GSk!X zww0R$kY~c-L-}3HUcGGDvbc9?;vr8@J7d=~do|gviMq$&Kt~QAzAd99_F)~ZH)oic zB^9hASkp0GVsHQRo;_b2pHb95AXdX9S7k)dgxWi^<{oj5PI>=+I34(&KTp^_XUJC* zgF3_hTnG)_`=A5Vx^uLtp=|k2KH+sRW@W1oynVNN&-vUkOu0`6aC+vFe zxS_D)#@6(eZVo4R1P<0$>ool6^cSU>w|$Dkp@~Kz`vT(=p7&8du;gl1aaCP)L*x7zDj!-yP)>=@ zosy9*K>PsEYh!N2<+X;H0_~|Y0SGC&FQ9Tw~7$p zydP(KCHQX}$$C9*8t3LP-)!Pcm0dR$UC+r;51RRE*O4Q~QU<^mtEsD>JU3+C`6J6B zOyYBmBGem4I7ZKCFmMa7XiCfY({zB(g4B%M{1FE-GVaO7&B&dit*V5?c5S0$_`8b- zVy#Ednmbp4LF>P+`}F|rv|OLZ-H9lz)T-;{H#$^&_En#_-mA4$*F;)!l-5P6@sU(+ z9Q-H9W;D)ZoK=)IL;r9()+`E-`4 zbA0Og7hQBrjpxqY9IFo3dA}7;tD3RzT}iyD<*r97Zm~n!s<$G?ja^+ks{D;X!Q}X8 z_lwN#;9==BQ|)R9^xy(~cdCW!sEp0dovK`8Zbe%=MlBEP-q16{C~f8Cy{Yc$mbA|@ z@wT2acP{rO($Z_n0rka~7rw7??LI>ceGuO{xj~) z4bj;nmCw`;?>^4seEQTuDmRx!?D2@mv>)acw)*NcbkiR`9G-e+`#GAzZBH%wMy(SQ zbRQV3DRKA}%dr2uR;M0qDs+{ZK6vFE{UX&~SNv17<|F==Wb1 z$os>nw6QJQ?wEjTVV24+G=E>(hCq%RZ2s^Q*%?NGo;z3mGRa4;@%UA%5cj~i2bLG- z4fadM()|40sZGiU5_e?1buSLCjaD~ORYIQi;t!5^NZOYzT(tc5Ez%zJv7PnRcjNe{ zt69`(k2w1Cf*l(pEhqK+@{M!dQH4odkB{nsm7$#Eaq^RVjC-o8^mz4E-i1xV_5AeR zsp+Psd+K%@Dt{l6rq;W6+Ul!4di83Rhs5Zz?9_a9sBV^e<>j8|&ixpX*-o(~J5%r8 z^ljIkAGT}`n#m-N$84CY66V7|T{&oGyO6Y+dq3`W+!Et%8J!(G`|Ql~={!0!nRHAO zSU2{?^=|QoINfV*`fB{@-DMFx`BlDwA*Izt5}O%(*Rc-#G3l zD$%_=$;OdxI5Tt5Pn+7DPv1PCQ-0>AJ^L&jquBbhb7-RtFN&-9!(Ml|$A(Bt$0K*8 zog8*D*UbE^ir|0N=&M2Z9r|8F>qA`1G9N(Dh@(oF(@U3zyBg*m-Gaf&RbO{!h+96n z7Mu!XI?ML+ci&)^W11T4_~kG6OtRi|JnQX%&h79hk?V^(@k2D%>8jMm!rFR(N_&LU z40p$z{djqy_?w>lvA=VhW7Ll34OpKRRWy{?i8-j|m8#GvtxI?js#j%{Ds| z7N2}Hh)NGm0ylQSoJT(9A99Cx+ZT|jZ+Cg&=`_3Y8Ut?Z)vT0z_u3!&{^k9N(=N`- zhqPl$>)nl>uUYVavG*o^J?HEHfA&386jDh-64H<)q_RxPn$RX$#+Vi@DlL>$qNFiN z(kRO)B#lWsQdvtvLc69CDwWduy}$CA^Eu~yZomKFH#cXFq29fg=kvNA*W>ZHuBV&i ze{K|o+PX@ol+DJ*Oy>vO=5%*!aZ3FLqj{VV(BXbGOCA(QlpQN6+31?aBF$)apOtKI zEiOFq?JvYO{J%U1esf&U+bnLXS9i?#s|tuhWXp@ z`la`%miLGFD(&;nh7IefS?Y3eoj-pM`}x31RV{oqx)!@^iJaWfaYdh;*sPN$Pu7WE zYoa1~$ES6E^R146z*r$m^I}@g#9M)3S;jC$(HhzlK@CsXX5uHu6`DrVL zObUIB+YFH=>O;Dg><}MqE@fnN#uH2G>D|S7Tvb$5?LZ#0B`kJ zU?2~q(OI)vY_i&FGMnFl#BPr~MI(?%V-b&PiQFBUZSsmUTHZg>KEH&Qyk;8nSZm2X zViP6@52n4vR=v|;NTAJxO)OqxGqdcS3Bme`1P_o?@M$0fs=UM+mXGC==EI{7>P0hQ zR}{bV*>mTRM7b`4=|d|^PfuBq$h@5U_xrPYGfIOnL_KTy`mrA2ob4~)c%j%jKy-(z zSb$b~`+)~-LwKsSrZqkRGfoY?vSBETHl^63y7hq5qUwf*6K-0Dv!$#oE&B%LOcrlc zsNK4%J9L(NC89Ioj8cbGIGY1ZSbA)|+r^(lWp>3IH@K$pIBwp!feBG^=Jb|holF(a z&K}s)RoXN2q?q#9$64_8btrN2bzDU?8iZs*4D4r+WB_a2qYg#TaFQc<+R-ZNjEz>Y1=I`gf zlIdRDkfw9PL8Krt|NaCli`mXQ4^3gT6Qe87Aih6;F2)dysF;xdL|iAETUa?JN&5x_ zWZk1RfVKOvzFvqf^1*jVoz{IB$!Qjs^W@3!G_xRVqyaH^@=|IvZlznBDV=KTY|2aW z?9swZMF#M*P}{f>WJq2Er&4}a(vtL!bBg+ZPOEF=gd`rL(kU4AwgKpIx&>;4&3WM? z5FETlBaX8SM{2bl#L#Ak4tcM;_w*5T;kfs4Y*P1&t2UH|OXiu9TmSD*wJgPE7KOkq~hsBW8HQ1IZv;6qCe;-P#r zG=mt=?B4cv4XUu7WvXFVA1wIyxdYMpcj{VLNOz7;nBP_t9dutas<3XY(y7<)-n|bo z)Y`Udm#wJ##85T9!xWc?A zlbhr7#pf`=gZvHrn_JobQLBd|(x;zVwQJ~vn3z?U3%21h4bp1&qvoDFfBybU4=Td> z>DMZhIPU0BX?;$)ZA0zl$bGG!O`q4Q;gT^K1JN1rkgc(i|AG zoTS4CjbgKizwFf=2M$cgC^d{L1bl3tEkz{Nbop|aw&F&LS0--lUS8Aa{7mimz9ju? zM%(xA4+W)a%CLCE0#&C` zL*OW-mr<e+;eM{M#RoNdshAFMag6tJT?3&0X?f<^8eYY>m+m? zsz>-}gX!?h=;(HH6YQwAw$r>)5Qj__8Gf>-!8xj312vmqsPlYX@e7Vcw7 z{{tOe9&TE)%>8xyRu%g3)2HC4JIBe&3ThVH>dhPZ(8ofxpHn%yIG|bHCGUB^Oj^`< zxYh7kB);c+Zu#lRQcrX9MLpQ z+c@?m(liqB+~U(JuALUMe}DVws58%ivW}F_6N!nTXNb~4STJO50^cVmBB0XH{vVoR z7^-N`kO3U^x~kD^C(C$07k`gZmC(o2j%t%+D&N$#=-jGe9>UG|_@!4i2+OwV8=p;- zO3$Nsr->`mVs=?;hpzM?s?O-TWpPqnm+Z#CZz=5mka10GIZ=w@@kr*RPfp>y4@?Uy zKAt^&dQ|CB!$0#ww26|Gh}6}Y<)en25%ulYTP^-hG>Uzx;*bXQ1`dPnc$an{6yK(_ z=pdEQVUc;hP=+t}fHkjdP@+a`?1+tV9nzz_N+@-+@Cw9h*}1wtvY0J5Hehs6&W_-I zE8EA03>oo!eqi_PH@nsDUpicDy?Fh#o{f9wO*p^CZ;9MNgi}87ni0Ij3I%QXU^!{g z5QEDL)YavHJsKLW*ToK|-#D@AA~%05C}^&#s#jrMmX#{6`CD^=VbQyH{mUQ@-KrOs zZriu-i~bZJWY{mn5?(*^1+)}HR_^_v2^ZPG3_OxpK|z7?tXY*!->AAM52-~-U+!~)6!De@*^TGJ7dMgbwEH2#k)^MQ0S{m^Ixp%TU$n~E*R{adEaslCDzXwzW==w zFYizrPc3^`dlFc+sH9{k&_{Qzv5Cor()xt>_`7%S_Tia9>D(*7G1%ikSitr53#COV zC3sIlnkJEt^lV$ks1gFZeA)G)z3Zk)=*St%jT-fEl;i@l(gOax^HXW}Oj);vKKZ*h zTrHR|e4yIu%J=UNGg%|y)~zfM*qKXq5L9F}l81s4TjWbhX;&a?02C{HAI-K_Zf++1 z8{lS%=HG&P+D5`jgjpt={6044N|)Kq)B`KZovA}e*4ZqQc=t>p1W zshh(%)U}*^Tm|I%9U5-YqSeY{h4=~bluS3zD7|bH8=sh%BhZQz`H%oRJv`3QJU5Cu z-PRd;%HBSN0}~VX?%nH`lnpi%0swOv*{Gwu@7`h1avMW7A2c`rxkB@!gNFRzViLOX zfU0?cf{AlL&KNWO$R zVAS9S*y2_Jk@;pGD%z{J5oKcNLo2b>1yU0xEG%&62%D8B)2)dWtb0;Am0aj!-QHU8 zk@MnzZQ__1!abDAp(RVKVlq~$_B#m0SKUYhIP9x%r=Yjg<^z#1kF+kL#yHi zvL7-+*vM@Z!tpn5$R*F(edb8iv}4DPJ==3j#i2VcT%u;ec!vJLI>o1@9e=rJ$IhKc z&>Z4#-jtVVZqW&FO*2S*yJP!y!W^2;=$UgC$&6$M%9d+-NhXSsc?F-Gba%v(oRds0 z5y+M3p$!CWbF~anOl1nMMZ7UP7l4qT%@mLSs6X4=rH<>7kU1;j;>9T%5u&iX!u9k0 zADtEk$t;o&c{nO4XY-aVVs+WGhVD$c>iEG{{Yh=@bbg4wqIc2Woo;St0jP84%~R~# z$p_HFvSIS8ekCx*!ery1!f#z3I2IK(X4UlZhIfU{N-5qDyO9ErpEYXZA6u`DrQ|TU zyi74slnMt8|C~w+$mg}qKb9I|f0B_osjmn! zf0CEil)qe7crjn=S~^bQmSU5`s;s*rL7`8@4|oCav$CjvgI64;uYnkK-`#1p6i57w z8#iv-Y$-p3b*>^Ex`BlYx8eWEpG29!`Fy%lFYx9PitbnK!|w)3%AFj;scPTEIh8X- zi09m01qFd~ni&r}v;2gWDr`lxu`(5uy|u}N;{(awA3uJCqM`f+MxPR@0gKswndQO> zVxtH52fbnLwzaiopD=rq7hUql;L3nhmFxG)ZOfXziJT4(-|p%p^lRx z45;ZSD3z6!8#shA^2Z0KPoJhC$Kuu&-J5a3%1V0xG|FoihF@=rgtALA89QJIOX207 zQyk~=x1;M`hT+^Tw@qy>iO$1-Kb#tf%qchOPSw5$|2`WxZgiOSJVkj?9!1BkEp|yJ zH&=KmxM%hft;=n?)TM?qQ_?CdV>*XNz4;+rU(s&v=N4Bs)Z0%&kcIsu39wtEyjhxh z1!`<}f~&w0T-k8n`^MW(V~5G&_k&RZEtX;(odGESrMTWBl%4Bu z^xSo427En9Z^c_4zi6ejlF!zzw((@OUq?J4)1d#}uaQ|NBP1uy`&7E=!ZGgETVf)GC-SfA|N2 z21b2hF7}5HO8BNP*4uBcdvVPuz$jLin8MPPU(xPyh!JYQF;Ev#iDD19!Vbl+zECzU zGE&CxkDtb~MPUqK67zRIvrg2VB)8~+LH(b<@4tDyurBJrvdaaO-L`eFW-?R5<&sxI z$~ru^N%>c~&>>X{oh9FC(Ry|C5+EQF>K6BAX& zO1$5THI27yu~e64)9Yq+2FSopHFR{bM)G${)EDl#^+#cyd>E%wvT-@WuA+3q#xeE* zkRo5d&f_st6w_d9ARjy`B3i^QwV+m_aco{e^%StYv2ymkdwm6p!u#&r0Yi$d=iA%c z!*xO>!YRQcX+`FIatgTbo7j?kdnhY3cQRbWw@~=wtE?w%;|%7QtW$yz}_u06+WI zpP@flXmu?|UG4_})hL1I}iICGzhlSm(+L!;yiH3TTBx{_4i5@3#N3BEqyJhZLk&Ldo;KOh=ZHTjGkH{oNWNt)$pW_yg<##Q@v)CNr|yqDj>_N*m~;N z0RM-}RYJL&{~_~-4glZ#>v)BQjbl%v`Y42~%uk*ZnYUgg6hIpFGb!0QwyqZ>{M(*s zFL>TBmKaRtBiYeJe;6=NU&Xw6daOg4pp2XAdUjk}-|D((?X(ASVE%J+GB+I%cYSuSd($-|Ms`tW;t%@cuyI_W?p@k5XNdK0 zUX_+)B1$fsEu|n6L_Ottsh>BCvd=HG-fT^}+I9S~R4+U1dd_2{y}C4j$7g54L+}&b zWJp;g^mt=^2iNC8E@3<`rQ*abGlPF`y}86S?Zoe9nhn&hP{0~Gq!aQmQSKck00o&h zpPcOS?2}BQj;4E9OD-}qY`$7p$GbyfpeQFpp;hoO>su-HnW8(_-}lU=h_FpnO)khk z>^CqPwS-ej*SmTl&indF9;fgBZz^ok?5!=Z3>_vG7gSEI%HORfNJQ^Cy1MIT`GqF%6EHn5Pvvl6J|9>&j8lM7UeB zElgN%O$wFzbmxIsdB&5XNs<~s-9mv(b4j5KlY!R76>{x7aAac}oBDT)wi>Pg8BUfO ze(uNJ&(ub2+<3((77lUkl?}$5H&bbeaek$Bg!U{yH)rP+DxnMKM?PEVWuUJ=TUAwF ze7r*K1cO}Ti#F1%ODL-1G3WBsJSTQZrM)?r-heI$-*($X1s<}O<<*AZFDpBUu!qVV z6T=baIjnpjXk6jr7cV5l#lDobzi;?jI;*6}W%qg*T@k>!^Vr$xC8_%RbKMimv>lzkJdxA zBg%l)k^e}!s$mz_Oe0Bzojz^h@Gm;-~x%^7)IAIX(6L2J`c|si{upcCG1+hue zqTJCW|jmTST$ zsi%OBDqv2JdsRnGYZ@U#C3NzsRsPm}WRy90#(@#uns zM^lmBZq2H++qr8Osy`Y*@PqyNl`W65gyN?uZ?Rw}fQ10Yup(g{UhcVAw3qr1k`L{3 zPSBc-2=LQSF2ddn{gYAJWAN}u`)!$t6YbpGgsc+j+5PRukd3{Z{!tQpbpEKy_Uk2T z*Xi|bqT7~!9m=sj>{t{u5qe>y<}b=8rbdK$rd@lSTta>sHf$JMmToj2<~J(Cka*vWIkz>ND{RhwgCj=s+%*CT@c{Br=!sz9(qABZXy z6%|Q&`PCEioDK*@+@3ujd?rq4IeY{biwDa%#a-RSR;sLaF!I%F*G%uNSaGu1@#I{i z+6l|!K5~Q_z7Ud9qU-N3W5ubf^0n5im585Cgz(pq7xb5*KCIFU>!zas^HBeYdGz_S z3U@f|=)Kpkr3iW%rJk33wj=5OB3B;!6mx^^=krDA3Bv~t@y&!Y{JXl^yzN=cuG#6< zbC`o*UehU1TZGizW3KIg{H*7f*-~d0+JryNhP!6n0!jJStiGfO$!dXsIyD{o9yN^| zg47j%9`6}k^$O={ zH&5h~6=ocT$l0-P-$}q8NPoeC1?O2253O_xeH}DNmI@ID6u76)o>h<->OVB8-Zk~E zMH%rK+;3f){*<|vP*C!BNhjs|k^mu5#v$jfHtwAraqh+M^)ANOUL}6E9%1sPo28ca z>~`wP#wonk+P6Qhm4MnHq_F!b+Rj|!{ZQ8^hz)`I{aJkcxlbE@&zoYV7UcZZeyXA( zsxp72w|x0pi2*@5ndluv2&e)Ierjd1UVaO(+h61>cz5jBvG=>0z&?Fs}841-@>!2ZZJbP}_;Efxz&OX|$c2vi2zQ3&V%iC}E{#6*rSvxyv z@N)R@#Wk(^_EDWt3cW$&OX^fql%gEHvMat~51y)$%vM zFsD-3&sM7bFZYb`7vmzq5p;g7m=+cmHo7u4r)Y(c0-Bl+FVS5KrW4Xt&-EB3@GNXE zmkEq*aivzOF3H)gmfq}9^2rHM9aR?pbf@^k@Vxy#UU3yVrYe$P{GACdpMOvc8A9*3 z+^7?+>MPra$%&7@U?UC8$g9rehUaCcnbjW;D%`5b(N6=t`#7ac?DZv#y*6%)?j)(< zstXp7b|{Jx5WLVW0I}>I>ROT}PgGMTFX@9$`?4<(JUyLxJs|K8Gi1-UOmKw|246I_ z)r17}RuFL$$Biq`S=>DbE*v`Ly-(4}Jf7eEO1nA1&lajtD&#wYSCPPgAki=P*m2x8 z?~CAixv&mAz5CV{3Kf<#;f&YYD>i=g5tshY8bP5p&)aNs(gr1e(GmQhDy)CbX(jVL zA|D%H{>xaIpVr+d+Y`(w+_q#z>Pf!xXeuv@^VBO_I~HhazE5i{sT4{eH#cd{$IH#x zsytR8(JBIFabT(pY8dLt&D*!9X^bVG0Vdb4oQ+uHj5!CfQL z3cjWU2I%i*hS*87EMA*aioag;liXY=Z7ES-eM3X>fUFV?IkS2h&S7bLXy|dTxw(0U zxeZ03Mg&k2?i0ZX%!l;?L@s|K6$Q7K*G8-pf~r_4mH*9OMST~ z%R14Xl_;n+XrNF9zykZhKqN2diFeN&95FkHbB?m1kNnUbB63W za{ldoRYM_NsgqK_w21n86V54KD;rars?!5Y@+;8#J3sHwm>gj+_lF;{fCO+~aHhBg zYK}E9S_Nm~w7;C!N~V7X+BNPAvBd8Tz1R{gTxw4_6rp6Cxk-8X*dxM z!~0dti>z+?X1sMPjKCSFUmRntE_%_U;mXv;r`60R!2j@_?v@de9qPjbO^P#M1*8G^ z_IcKjMJZ`Uv33p)I_KBmRsfD#CvM#^=$YgjRCo3OW;}GoBFZ*?Xp9mo2a+8!I4m-f zv#9(g+&s4iR}xz;^l=sd0xH&RTWsB~+WDJt3MamGzucHq^DQF~VsGZymPexOB5GgO zdF~D~EyErU*Sw5nY;zacd6kd<-tDMlw{N^B3Cx7 z+WXss!vp_w8(dgy!)w!#ze|%P56D7Yn$a(iu=ghnNHG7Zam#F?3-Td`1enM0r$=KB zLH@wB%?sb*?mp2s^Dd)eUatq9)H|dWTMMc@`WF!sZkzI3tFvL*TF3E%D4;x+6Iaxw z1MN*?TXJ8qRw68s%3;a7ELV z7!w>hQ6MYelbJ@icOH7si@Fwjd(pPi+|{njeM65E?X&AzR6!ujL!b_Hsj5G}=?K=9 z(39#7s;)a~kZwuA1V%P$dTUxel%}@Y_(e?}`!yz}n$3E-r{z{*|I}3P%pGPnD2QM& zN)7UxzP|p6hgKjr3QArjJi-4cQIuvt9(0t_`i#SCg+0;zoJTNc>fnLi)!k`kaRKrJ zxP=xkc(%lL=|$&pksvW z&HUAoz=C~7X(64pSa@KdUVr*pxHB~)=28Az=fWn>iSFBT1b@ZR-R8#ABu+;}xL4L+ z_FSIkbp$=Fx=HX{v^P)3bMKlKSXwVRWTb1$A*2%!D9$GWgwiRoahN8D>#kK%bNUY^ zi1Zj4SmN*U%P+@m)ra4~%Qj&8y!4}CZ)*uR&M&gyj~AM6>iWW&Z5 zpHrc;4tQ2ZRdS@E!pb-?QQvQzIO|fM)=D%NKysY3c)NRE)jm60+dHtw01-0mLo3&9 zr!0z_><70gjl%tCIWHXHvf><5H>uc!vO=(lKsnj&+jkEKGm!1$%ZyKr?J`)F&KFw~ z+6b?}(>WAwViM-?b)-Y&5roJq8^o425|ygW;(iiRGXUYzPqQX&x?BJ$iQVV-n3x)o zMy0&G{B8+YLSP~+Nk-|^QG;ziz4-yg`bc4&`;L8gALni!+4rURa`Rtb*cKIrpZhg7 zFOqOw>y(08LWK-RUO~sf6nGM-Jf%~XJ9bpf)DtwtlPZO_>KC|p(&_m%7SNMdu3RZ} zDiU=5WxYB-y3E6eD^V<9^Pw)O^p8gU9;4*1)Aw=#5Qu|Nu@_QPLr3#P-;FTme;UtS zfl1LV@)rI2Au@XZFl-8fb_BPRP-I2)0`P+R@;11@jeYm5k!kka0)k(EVD`d1Zk7!7 zC9UqxH(o3pYtlTa%QUaY5m6O{gITGj=8p$=-o1HaNt)MPd%q1xIHwzkwdL9oCPqq& z1}KQJP|?<820UP9D2OYccSDP5>1S6x(xbYFL6)N#Eb1HI)Nm;ToRZEw0T z$UW`qqmsu;Z$GH~9I+)%`(3RA92lTs?P_E5%Y`Y@m;(Xd`D%<&@kNABG3BVLg|Z|4 ztcSF>KA%zl!G92FgBJ$W4%6qrhWMhryRw(9mB`M{_S1`o$Q|m_m+Floiq#J7yZekD zEGxQPQqpv;PN=d&Traer^zIQn(#^_;r9P3>LlBe(bY zBZfZ(ufdWau@Y=zUVaD@9qj5mBrTSCDpd$d@NDwIez!yG>;+1SP6**S7F%aD>_=Lo zs3+Iy&R0UmMnq=!l=VZZ-td!;J9?VW}_E8vaBp!AYl@45W)d43NHTc>Ed$0 z^9-!TuI1TTDO>dV_3vw@LAPZi9VXdTW)DlzzzA&_F9nweo1hp{jv3_so?BQ#vwG^e zmOnTD_4X6M3;#KV0@y-mKY>JNC1A&*3^cebzPbSMQSfV1i;X!_?_PhvX5*p?-I5ln z$2h@qkOY2%ecc-cE5o*5zka=CXUk9nk~~lUtie=4Ge_jx6*HR{qC8fhc6b+DJev+r z(77u*7iS$5(kAX9BqAD|_CBc@+5TTk{JHrhxRS4~Y2Q5^1T$C#pb-9I@uk}ceykB3 z#QD3+@cwpm;dKDL3+_RN78NwQZZyS@(nmJ!&CR@Oj3lr}MMXvB0khJ}D9X?gSmn9L zd?~dsEa9C(xIDG0QE;`v13_Y7Gm0AD2n!|1NA>lU`;v0||7H5B8@t5qXWyYl&j3Ie zo?1+08%_^nAgD_yv{g}7LH$4BI*Ls zp&n>J5jwZ56CYZsQiBi=F=k*o0u%4Kb@J)X(%QD~2>Fu}0E9S&kW6yWy)0PbuWYbH zH5Uv?bOfAtS?(SPf)awCMQbD(#IM`66H?y7>uxML5Ok zY`TnYf+?U4L1GwFxQWgqTqlR!uh4p_cAV#othj%_g#jo z&Xowr8fI$4GF;r<7zAeiuHe9%_7*7u{kTC1_J+p16~6{IDYK7i>zH&4tdJrSi z%%+Au#sss927lNMgUj~QE1OlI=v@QoXvqUIL;JAvSOVldj$I(w=l&v58zITQ6}%Pd z(&w&N{65tJo0s4Z+Iqiw?wm2Y+t7%?c0?ki*wzlR0`Anr5A!DdCp1@ZZ!&qMaVu&X zZfWwJAmgPhRUVtk+M(^sK?502;8^6|uyCytC-gQq)VrldO@rm&>scAx3x`(XGRC0M z*H7u8>obeE!iDo?TzC0^K+$b=8Y(!I2KN)D7@#pe1Ke*UO~|AnH@zRfErn8)9N){r zw`bt3{9cr@#OSI}h|IHRGC1E>>o7j9aE7HQxF5ZNTnd&2^7ZXc&d1cbWv5^(-tTQ;t-vZB~{^cW)X z1|^@&a%Abz!v~t5R=3@Zq)1T+PT;)}1K2qQ^{u+vnF&J%xXLpSc-F^fu&}7Ra+v7T z(w-7b%sswMxwvi!7gJEHfUc$R8%7NpxwOyU0avsYvu6O|gm7N7i3;IT;JYy}a?Uu_ zRhafkAx1XOYkseZi(f;j*7SVI;d8=b#jH<3&qmaWtiM*W{N#m9>N$8!lS`k zmMzLh3nc)?4A@d&4c`|$rL?XEfW(gkkGu4AwQlaMZQD=HH|8q8E5cMmXwdcO`wh>1 z9C6o>Cls|5;ydws=luw?3)w6g-h6QIA9KT>l5M^S&V;}I(wi-% zQ=qmsxYaa}c4@%}KtmrMPZEY84s>?6W=QG=&}@clA9DsGVwnEl@65?7bg*`9k7+!nA_&rfFCZ3-lF3{PKX9 zJQE4sZ5|#n0a^R5G>XlNl3ZhgSWLAfq#^hj!bcD&&wdO(35aU5ILXTe)Bj$Q*YwTJ zB*p1Cb4gCF@oWq*hv?=hP^UID=E&L;PEAw_ec}>(dmeVEOMk&)OWK|j{B!u_4SWALH^it$+j&yMF@um)d!c8UXRP?s+}wTC_5vjViNe=a zEd8>hS=YU5E*7%3hRl%y6yj*Ti%Xm76eFH_H6GWSj5?Vb**%J;U@9>(h3ewPd7Yi9 z4?1Nr+l?Oq+=6U>ciRpIJ2EzluRq0)I>Cn;uENMi5xq_;G)M z=P>s^@VH69<&iCI2*|l3u=~c1N zHZ#Z@`H|N6Ka8)&#Jpxj4dHCt{A(1p{n+_!JtyEs7?!xRCo zM|!54_O@f)*x(iY)wlj+oKauf@iMQZwHb!~*H+Rp6cO zTJ9bAOwt1cJ;`Ew;}=#QOImm9=_s8Vyy8yJuKIM>%2oAq4=ye^U~~3f$^DCeV!8da z_f@?3gusY_?-d7gw2@q-qPbks&=Ub7hqg<79sfG!=E-SC#gaZc9+BSXT0cedNyq2m zQ?3nN*wQJ|=SZmZxdryQVTql?cMiAUjITWK$dF0eR#ygH0yUc_eL@AT}K1f}0exi$7%CV>@+kJ%2xy-}X;5 z^k4AJ&T)~3bkMlL{b$da)5kZn(K};mx;RZmLTDj$;e;d@Z*z8*LQ~gI4}Y=emcH_o zsHSE49-9r-qQrmAtKKnSQodQ6DZ@dj{K1GsT_PO6;TM0hU^D3fpaQbPslzYT(G`vp zrZQD4-C85!$hs?sb1DxNTl-Sf9QrzI;a?;e{tQ(G!ye^=o7OBz1@G>wV`WG)obNn;r?k@C5yzZTj?)5QtA1?VmHy<|`BtwL zgZ=*br)GCxav%*JIOTR|dqHuTn@0|RcOc{`V+D_M-9U951cW-6G|71lK{pMqVFI|a z;Y40Ft%(8)tUOlur4rgldP5@z3$sRrhm%rFOD$iAMuMnW-`PP4!8`osCPND%O>4_F zWtPVqbGq^o#Nqn4kR~JT{p)&{SFVB=KN-Wn_juigbC`?R2#jgy9ucNL(6b}a zGr(|hmjv6%JFhyVx_Mg11R7{oR9CGQH}mtWaa;JoS-aCC$>fN^W$!G@Lj|=MOq`=j zB?o-DzyCCW8mGdrjUN_fEDm{ip;Krp==TESIxGxNle|sjK_TF=QW+9qz_(mEzbHL$ z21xrlWMgm6qj7g~mj^jqD-!MrZdF%47#q@Z?&qPE*ULRVkE^`sMvr}|&7WwW=M)yi ztpiovo_q~2_nJ{Z+fi-1W=j!jX3w4L&A^52Bu8&=?>6DgbGOdM9MkGsW^s5D=|w>5 zdfcePw?*e4>?MOv_MmG>kKCDMD!5RlYpAH-@<|F|uK6$W&NL6hmqUdB!3BZ@FfP z4^kNLAXLnev?UQ*?=>{s(7&qri{;*L*L`mL*I3Oyf5@QNPEpN4Y}iLjFH>fcMJ2-w zj_IGPlwh!70(Ao=?3dT~2YT(6bzbZL^||E|CRp@YVl7eYkUYQS5BY!n6O~}UuC8tB zr$6dmKsPx3Rx_M_=@y$mGi-k?mF#mE9}uz%-EbNa1_Pf=mpEE@JuaZ{atK#Ob~ss& z^%u{wgwpRTv`PrWFW8vnk=?bC4Iz=8{rmOn7x(d?+l#K_(dRbF%O{^!`QRMYbRc%_ zoleKntx+J zGD<$&ruqjrYxN}=xW ztoH0i&>A3!kE8J}y2R?dtjs<q#YJJ;*11wU}f!^n5oS!r{V4I7}ICdAa8GsmQZG_py&hM5Nm#TkonZ%{6 zUjD5_`eMHoVGKZ3Jw%cItmXTAZV%pj?zE2 z{JHLmW@x-vj=RaKi(jtg_N>iWS>2Kw z6#n-f*bHTCeA;1W^X7o4`;s`ijP@{E>8JFQUWQ~@oa}?djaK+5lIx9X-9(g0GZXr* zk%B0;29|Z)*PCNR*1+b6gX4aXNAqNWQ0AUn=c>+&j~<=BKtSLH1^uAJ0SeHkkQoVD z%JLeQPd{6t^Kbt++~p<09D&4mk7RJc-ZSXs0enJ~l-zM6wZnD?2MKt0?)O;GS+Zg7 z8D=r$?>@}?gZx8j?cIDLA}|VoKfnVBdjR@<7FKuCt@vb?Va-K885l;w;O(MI=qna% zmK{4fV!i(Ee|y8i)8PkuP`;F5a%a(!5^=+dh;3fAXhHijg_0FtD-5-&1%Wag(#fqK@8_GPhM; z+WUT{9vU*D_c#CiE7Wg=nZ^9xkCbG#zM>v$j(;N5u3LWp-M>EjpWozP+3qWR{r~wV zY8dF96nVB~+u5zu8WU&w=T1^o``?K4&nNKzrnanBB=~Dabyr7LnXK!Ls=M5!bQ}hY zkN=Oy_RoK)s9#+7P_(~}LwUm$-G8qBzyAMn;D6oS|NgzhhyPlm|NB=umkr!s4O&tk z$Af0r{wv?_kLvpuOoVs>Yj>6k|7jCYIfUDeiXL4_*RA6#Uu_+S3O;ui$E?Y7mhZ68 zf3B9LDb2|))M6rc9NqXH*QwHm!jqVIT*`3V@$3XG3Hd1ToWldzh%2h^oj15#d2f>N zd?Ka?8rG_oIqK{5jU1$-Q#~y5u|sfU`xJQ%^EMO3WM}76UW=)+{_fxBMq?P@Lie8G|51b)iceYiw_nHPtM?T+ir|z_0f31-its{akD%f$&vM2lZU@HJ?vK^`6xk4-l zN>UU+Fa7b-ru8)^3<$ySK7RGat~ugqPJWl(?4<_LeQegsW(IoxOlSVVNC^)h|k>DbImNSp9tJJIgDsc3ie6L z{rMK>eaVC8-zCtX@@&s7K<&H4B6s}D{o_7Jo2{^tqRn4l2wXm8f|4puIAosyr$8KXcP&|Z8- zMv!IvU=b=;fWvnLKV0Yhuxr3`6P3wf`toA_V1+)EXT`)`rtt@cQ-Z zC8}Wj89%=jWExYcUP#f0pM?-W*1B`I|0BzI*!8b~ur=ejsBRjUvYUBI@hMqmjiea%MJ#qE7O|JhK`i!+MRKrTAaO_ZP2@suN zmQa-Ic8|_y;5`itRwq|ZjklQcIV<WJdnm z(PQ2+R);6Rnq$Kte_bt8QTl;VjhkK2t?ydQzGE5ha4q3%Xv?LhebNpZW{H8$r?t=5 z#3b)k8^4YAq|lOpWV3D!%%FsH><@U~sMiOHK$nV~I2AZf&b{62uXESdT>;#4i)Q1? zl$ACC_H6$P(PlbcG$Ghn=X23DT|m zTQvs40V2&`zcLkS3GceDMP|XlbWR^qF2dS|mHV3(&AMJ|ru7eh>a=apd)|pqo-%u* zk6TYVZIbOB9hg!|KeX`9u}U^j-Qo4ebZ~FatqQ~g;ey)R+YKsyIU7b1PGN<<1D{1O z21SFCkv!5{zAd!=Ud{;bgZV3nKgrz(JUthCdZG|an_F;!T^$<7o@kSD_R6zz(9IO4DURjnO!e)z!$=Z&C*W*#@# zh)%)jZL@hiI1bUa=C?0mRu5)YxK}YYDY0s*+XWlF4K+V1h(QAI-UZ!t;Q0Klz-N{Y z)8B+4gB;E|n(n+cZRLjtM&}O>e>xCgA70kpQYR%Pb^FCB55flw%ACcEgAnJDGbV@}q<#EOt;4-fP7L#4c*5Dn z;B<4dTjyqqtwMope9U2UJ|eqD8~=IZ#zCPagFcr&w$JQnfvMn$wK1PXr8qsi&d)%B zZPX@=ox6Q|u!xkj^sl7@qCPa}o-nv9Oa&2KL=Kb@ngrzHf6Mrfj2i)xLfwm>c^=PZ5AiY-@^9m{N?=+kvoVs78W8zOAf7iw@Gt{ z{Aha)|JA9SUUO%&dueKI)9jFk7CUziM61z(y}Gr96n-~i;eNrBI9ycp+=E6_PWv1| zy(x5%SIYJXGJ%LNx{0wULwlpJUG{R9PL%_lFIFb)t^Ao64zhRH=J(qNRwpx1vJf1g zB}gZDb8zSk95|4+ShMyHQXQOj+t;@?bKt$8WvS4{-Kc1BMO#_JKnKEbRs?kn?KAU- z2CeE}ve(+Wg4QI8R*{2;Zq`aLBKrGWfJ1#SAfj2E*1t5lCn5n1PxDRupMUl*vzUZ4 zX}gt`&d&82I1!FAfD)4lY3HGZ-pAo2)Fh(q(v1q>*l+;Px2 zju}CCA>6xZkNfW3D~xK8oDfJ%#jzV5XHrsz`!0;gEgY!Aw0jCvo)Dh;^%`#q z#q0W_a}wX#ZD_AVvR&@Gb*j%9habzbM8C@3U+V75F$LRp>==L|O*zj__;PMjZw2$L zu7xdvl2B7$za0BcS=sNy8#HFst*)+8uW?AVp15|@&%VCm)l6IA5x!1l7<}lqOob31im2%JmASLw_j}xj+JiOvaTOmmV))3Bkt;&InpCyaKXv4pk1(W;(L; zohvW*5Qv}ynp;~Z_foT!v@)OYwj^++r)#ksc%YLrMy}m9lb+gdP1QHG4bQK+gQUq8 zP`}|8#0grkJHl)S(9)N7UOP`_m%B6I7r08r1{|IxyelRG+Fqji5n>?0V@>b=Rb1Ph>(6VKYaD(5lYgcG#krB=Jh*x_3L}56sECacf|ZCh){g8 z2x3T0T^(-4k7o`BWG#u`J#(y)&XnoASbEobv}|WToE7@`!OjGh8xBgSMDmCHTzE@J z|FLhP9-RSa+MbAbw|8EZ*ZH>ah*MWIW*A9df2}B{e*fowCzdXOyXF`5521%Wsd6=- zl7i+-&{gZPT{#W9-<_8koV=!H^YwJnPX=vdwmt- zSa00CiK*)lDf}MQO=aT)U0u60|I)hDzcnQ9W$g@Z+uS&}Lbjnx@v>fYtwtopoKMrF zz)dD^fy1m8FavnzlT%7v6Xz&V15yl{My9Ak>%JPZ;yb2(pVpTAvw?|hMZJG3r*ZLz zrEL%t;AV6GIT=iCe(>PIu<-FjVYpr&?ejmIVZUO27waQAdo#1^kSE~QQ0)R!vf50a zTom1Oms)Y3{@ZcyCS^NVUM*c9GbZlsf>+{w3@w$FvO^>6k8CP$`%)p^q}-Ut$^)Aq zLf&O75fV}S9#?(X5?6C}zrSNZ?zf=RiK!oxKbROgzm#6}n+U+!>meO;*NGSG?b3M zn|N!>>gyv@_saCd99XF-sgZZyIgg@p{PDkUwf>Nnp|kY2sfOpuFw+||&=S2Bb}Mi`Ad>ae!F`VaM7#Nthzo~W=C3W!`yPy!Vy8P(h;qtF^)R))TR`R0;(Vo>9?ogr_?7AA`h{d_RgD;q8xPW7}cy$gQ+^Ny;|gd zt0L5Nf~98j&6bo(vmnjRHx(YA^rM(puG;paTVC1+&7<`fGbe5-aXjlf&ilNoXUiq~x4*WIz+g53)098Dt1C zz+m9?{aQb3wElSCMY8a=6cb(BuP<0}xk=UFeA7ykWzfQaPgVa?%Jff887H3EuFESh zhXiD=bE!frYDEERI~`kJP&kAx2{%S?T|LA^_Vc#IhBNm zUuV1jWj56p^@W8hC0|`;4%P9il#EQlV9mEu*`>KQYA*P3YKy!53%kZ})$zvxqf1hX z^{85TljK|U@+Ui+eysnoti!Q5GqQeUR#NJocL${FqHHF#PX2tkGTGz4x*~`UiGUU< z(gY3cX2ZI|X(KEd zEgl{43tdSdLXg5YxAYk`cu$3xJvM-q6iqS23*{A9CsJ|H*!5U$Pa^{#wr|=IllwLN zI|U%}-V&!j3hxPVFzlqaB1c8MfsV0%_d9Vdoj9T|vf$DCuj*)R7;+y!qy@NU%afaE zCsMoR6s`HuAT@4>3Y0Qj9eKItO0<#eM$E>t@Oc?|>3xBx;cgtZY7rKbPi_D54OOce3 z0N#Gmqk%FqWp_l_s*6Bh02N7mhpv=wCd5RK_pnpMR1Tf3s|R(`q>oSrTE=(Nzwe|`wIqKZc6LeElaszP(m;}oYL)1*n1ZH>ZYMk=rR@!K~mj|KeT zv>P`{sTc)~2SS6I!*fuT} z+V2`;Ia*yICY>P+NdqVSouC+UrEUw`8*swvyg4D#SaS3b(TE)Sl$oevwdT}m-+-2Y z%tH5Fvv;e65(=4GT)(9ffCr>9ZDI%ph0@Hz#W62qt~UP?Z_Je^cN_3-5%j5z6hngV zzOQiKdhM0$S>FH`yLsWK>Qs3#>AZrjyB%jrY)Fnm$3t_oT1DgSce}qjYqkjkQAvSO z85y$do;yn$#Yg{oJ3T!RK8cAc4-RHVU!U*)!}aLv6J!dLL)*ULcA$e5HGZ6W^aiCW z`&`Ju%Es9ijc0GZ{UmsYDES!lG>{$$%9#}k0_0X!QF-K;NWp@?1VDq_m1NMPN^M4t zLBMWwHo2Y=@WT>ZbcOY1V^7$>s|)s$iUWN@4d4y|v9PNsYNN4?teKXoYP?#a5=GUt z@?mt~`jLZ_3yPeJlN}jdeYqhNGamQB$nX3HfImq%))pXQ6!yV`hQo zeU>=g!J(f?cc@oMNy+uXCf77pTd2M9(kBzCLlA^_;7g z*bh`SgAIF0Rb~Laufq=TE!i?vu0o&<`Fni5t_Kd7tIuNfag@NJC1oobB`g{Qz1=EWI@R(W?Y!nvtw-m?(Rx`~Qn^La{q1d8rYc;m{Aq2Q zYJ|b|Uw#<@eL$T+fDjy1bQ(iO2KNN|*;7PdsRcihYbgKrjo5NEnEgI3MB(p|amfbv z9hF~o-`{e&Z8+8zhNutoT&6j)^aqa*f7X|__A=BCMz^C3C67GN6VipLof2KTpR$}S}VOPa=n zO#=%NT%j5`eH!VXkAAWmZG!^;l1x;}gRmZ|rH;o<|bo}ao{vLGoCn=$ifa9*3J*X!>dRsEcq z|A~o`Fg9cm=kCr0_zZWit@^*&_tbf&@e4*vVx?e_JRTi(BSVe{Y7@+3?08?t>4W*0ugcG zz*fc0{wrxOJ{7RHtE;Q#(n|flj(jY#uFS%kW)%3(sntI*D_=XKJ}p>;g_~OWwW3vbl+dlgkn(qTK3+9L2&w9fy?l8P8<#)@!9(RD*fffZ8BU0_ ze}`FR=QLg9<2*;N0iJkMx3l`_>!9Wm{+fnJTiXVogP!qAaRmvK5~hA=OA=qwIHjX9 z!VoR$O(-_5n2-PFW;W2C>q@v_R@WJ%1Q#JGSn$FpZC9FH9G-Y=I%IZbTZl zGFNn8$incaGC!r!iZk(Xa{1bzW&MLLuA80|D2Yl_jmC$N1xD6mn)iMk~)6aa(MA8e9kb<0I?Mag);v`N$i8{>jlD zSP-U%*s~qU%~m;m8k=%jx}Z1!JP=k8zx;|%NbojMnHq9}9v~>(9Tnv+A73z-mM4om zM>%eFB4ULE0g~~q>76dTG`#q+>Eg@Q46T+OnlOHV#)SdoCfE(Bk_Xc5uBLs-2ygJv|eTsU_*q9cP{4c{Ty96^XY zCKy%@pFLDynLcs&z#YD8L~@);2m4k^=xB4$1P;jMtU6jB>37_uy$lphV-UOp1A(k8 znwa0hlR#dw+JBp7o^MY`X(H*ep{YM>>Q>t^BLOb#dgX>I8}_-;_oC$ed1PL=?j&#Q zeKd1Eo`atL>yQzMCNNw#<&$nJYE;hBsSYY|dRIC>XOW7>digtB75dE|KVq{pG%~3B zL47rk9Ko<6S%W8VF*em{0L$vDwsKb@BlN*@2?zh!+c!m2TsG+Fb~o=O z@pc`De=diIf-&SMmF)~e>F%+-H)CGCPQbFp9vjyB%5~Ls{O#jaD}YEeMznF%)nxMn zY~#gUwT4I&Qxekq2!k0DGrDFX>GYBApUr9%xPN97lf4`r1wBk;)ZqRNG>Aa+l!x1- z-@SLw>+In^V8wfMINi8$o59EwYCAIiFcKNAsEw6QG6p+BlkYrV_1 zZGD6>@7^Fc5ge(rJm}2azGshccFfL%q&jz6+>J$(M<(itKU5$F{dFuA-??BQ5_l}q zn(hE*!cG)AY2bQ97X(6Pf5E!b}brF9*ljAUgV95n9o^aw6jeJ-8KQGE&`)8Q=%W0P{U22%c znQYKVZC&-ALBeg}s{hXEQZ^y53Cx40OlV;#8Hk}6-Fkn$(==K8*n_PoI~EdUeD)aT-Wuu9`~MZx&^I-GWsDdyB{C@FdBJsE-p6`*!8C& znZ_N)CwbGV~l>W(KJ+k*jch2JRuycqVF6# zBaLQfJux(X;6OV>%6o#ZM@8*kZq~h0X5^i!e_4dga)t{vDGVziA7)jSo^a zFR0IUS;Ne|(Xll}ZbRAz$#v^Lw5K9m@m=WJe!Hyqezy8-G(8uaOGm|VIgs+s4v@aSnqTe8m#0!4d+x$l#2qC0}>YF>0-nD^q;^<*h zEzGMK3_4vgdVTxd$ZG{%nV)}hr!rQD=({LA+7H7_(L1RrGwGE7QN{C?{3i1E%`OMJ z*!Gs&faQw)dBAcKsbFGCM_uf4{d13hJ0wO;tZ!=A+4M<0$+z;A;f#*f6V=UEuTI7V z*afh=$hh@~G2xVfCU4~AM3{jiZ1A33-4IRV4&2S8>;nirN-|OD#$Ue`nF0nOmIIa$ zNKBXOr^fGub}DqL8w{VjU|Zh_<(P9(7KsG`SUM#vojvpZJAf@}7J96_-Tw6jeJXaX zf4=6b`_U^mi6+879(0)b0mihwZx`5^R2(7F&erngvI*X1->)8QbVy}Oh3!3)mCsIV z^ddBK7vbrJzr<#}#(}UOuR}h|_bKTIvV~NErA3D{>9BCB!g_jqds)K|CS`Y|9L+P- z*vTY@q2Y1hMGnwu(SBj3c;^UfteHd-+aa17f?G9t*vjo?50seuNV~c*hh5!J7QDgU zzCTq5IYL!qj-Bgv*8PoX+z9baeyVoUYih^fV7lr;nZ~Ymcg^ltO*p6t45eM^B43y2 z^Z9ThnNKW2%+fO`{hT$e_u`j`Y)f8U`>Wo~rM~moN58e!>s7J+yZ-B~>>GFQ9zh;c zSBS`RWP3-MZmvxYCBL0PY9KHk^5Jcm#Da9l=&=05hoC6~deHfjn_P+-f5-RE)&T>c zl+I3Yc%^XtymC+`)9Wjub({8vZa4vONc9YXBGeE&lA8O&iQepY&Vhv+^Y%=j-VqEd zhp`@4b$A@>@g;{m?iEO zP>5yj9Oe9~$of33HonEajzbhfevhe^F4oYFo5J+Oo`BE+`v;VWS1(e_x`KcV3n91{ za4Am@{`jALHKE-Po$=Y76~+j~$|l#(O*U`68yi2xrl;=s^4$r5oX*Xr@z-|BKNw?N znZy_mFYo5gvikbi1vg;oRIM%|uZNQ3CdRU1g8^iTwFpdwjYdy?Y@g~sd9n;PnoywP zXr34eb_%_Z1bq0(jK}Le@Yon7AwROef%HO$Utb3%{{AvfFf;y7pIm59SqMC;{lH`4 zz~Y9fQ>OfU*tk7LaDNVLh(Y)8oehaJYmbLI+9Y`7F_Knu)|LS&lO*TC609XUfQ#2| zk6^9J#yY2g=16G+rwEE;_?in?&OI^uJNiQszlmaz`LdZUO_1o57WWHOKr*maZP3O$ z5di)S5V82*L&-KwwN19Sm8TwD7SQ$mQg< z_zu3wW>I-0%#Hih7aOwJDWhY*++F%TUT9A~ZMgg}dSSl*9!HuLqA8rUY1PaHYgsjd zl&192!XDv)xhn4$hpQ3=7D6Xi1I{^Z>9 z^I#o|PbrVrn(1SateCp$r0DM97rLCSX^8WE%a(S~WHe~?l=J5}E8JC!qh4u`& zl!pI4ao*zLlFffT0_&kfSY3MKmZHS>=3OJ+N_u!D(Z6x{Nq3NM_il`S`_xm(80i4_ z82db?1NA2kIHj)#KuVD#>fQJPgOR;~@Hs<~J-<~|h2>g40Qgxn2K(A4hO;^a2~d~HSKHa}BNz$SVc-x5crM|$40?7&8mbZ$^*ys#1 zAX6%LOoRMd0)WH)4=B#1)mgi9XFqNduarGFhmoDN4OPuRZ#N+qEfKw&r0a}Fjz@R? zPYb}f*RZ@52xS9SdV2Q)F64ufMwkFu5%w2_2`DUcO;^1~eVZNK4i$Xv);kv34wL+* zo?W2&VBY0rPa57*@GM}ed#v`Z0%MV&_>3nqF1)+iDbaYZkP7jma=;PCaVAJ!Jx(AT zv~50rE3=g8B+jb%*s5pI=Fv95AzS=Rhq$A_B_)edwT0tob49g7eiSz&{2rL~@@a-k zW&$!Qoj#G=&EQ~{^?miTFwhYeX}~pHCQQ~Ck!#w_HI#Wko4QBOS2|LAYK zHIB|$)=M_y$_%EkXyNc764n|x!2!Pugtpwy-c%0Ak1~&{4{ajN)6E&zE;%@YB!)_O zubS-q`^V+4sx+4LS#jMSF(%^Yta7xL)vcK7cyAAwSC%4m?Jv(}ub4Njy-ONP_BvPC z-M`+Dj&P~^(=S7_$l-9fuTge>-K$QHo8zJ4Da2g>4WZ(*d*X@k#ksPQp~LRTU(7GJB6}t#PNf?)UD--ZePwkb6M?SkyG! z0|!P}J6b0WF7T-;FF%0U%^2w~UtaBbm%Y^YwQ|s8Q62NVh0*uk1g42b_8UmXeU~Zw zUJe1JZu5`l8WvC zmY70kPf<%SkPK)7I&1sw73+RuUO=*xi~|?zKwW z91DHHNn5I|N7B=2%+a|Cd>^7hz$uamw8)zo-!@i;`^?<@x8bCtINscRra$Ur@~>Za zx9wVmqcxBoo`Kl((u1yBzrOfI|0aX|hCJ?coQW84NJ#ZNP(mS90!C@NUv%X z^h|P>YB&x)`*ah}i5~mSFVow$qA>sleP@YAni2>Inu3!r)iERK&$--YC+TL9CJ;f| znnrOh{{Fi=G6fiTvV6_nah&AWuaD~umpqZ!^80U21Lngk6Nk`%DQXQ`oM5yph7ACu zYTKRah%9{P5M;?*nIF!g!pOHofT4MP$dDne$yYXoN}l9S0kE+o#G)@Cnt;Ubtl=R# zuqhctA4mlRXSnt&8^=8jM;`?86x^hSJbALi4cdZS3gFzMXHRBqM3Sbu6QvdRh5Nje z9?fp~eU?Ad<32#x24#U$T*C)3Gog^BplonE3h@IlPZ`?uCb)SZenfO38&%UmKNA~1$(`+uB{x>aVqdPL*L z9S~X=X7>3c)Ci2RF_88lt|~F*!2=222_Nan{?wgUZcWby1i&WdzT{!LJ5+;sUESAB z;T?%`t(P=NJ=pjMe)fE=+BjXYQqE)!x|ysB~5X_7}z_jn5#pb*ww`- zdqay)MI-FzP!u3E2dPs@9H_A-UBVzBGxt+R8^d!?=~10KB~^jKkw2{FQQ>H!&< zJZjO`2FWlFlR7{r4~;!H43o%HUD+4dkFiZn^%7}sDdmVTU1L@&@dnWwi|$fice8;i z%Y4(QWr?RCCg`YXOOQJU=*6tLCUVq_ly$=I*FX|nrTc{{`xnQf~L8gLmzZ`2L0i{_C#N*+Py=vjb(deS;7vRqtkP2LIHQ8OCRG zqy+?!Fiz{(v|CpDwh#nus+wvd4zUUe>RgKwf!TZ;6QV6 z4D&Jt{%{7WbN`4+~pB|6J=b31Z` zjiTLr(%VDk(?3Q&I(ofF`cwEtBPYMik+(h={YwC6{Rlcmc~uA=Hz$EDj}fz z^{W_Z;{zX;rVZR42m=8j7zJl-Jw|j802t8t#*rRmMe-5XibF2s8fCR;^Lp?XL!H75 zuh2zQNeZLY;w#p^^4wJ|zYGhDno`lHA|Jv2XCp)%}9qK*RZDbPerVSv5jf zZ2L3U>$IqAp{Q(qr*C^p$Ub}NcB{+Rz`UJu<)?oQ`52JS@x)EMxkb>F-!W6ToihtWqGtt z`1JF+&A;+T>y8dQ(&i}1UZ+og+u7N5z*+v;1IcT6v^)@_1OoVdb89p2n!9*a0!J|+ zOk*D?79c05FyiAgZPe)lxb=sv&aa#b1%z`?^~a2Uq*I)`kh36t!1<;pTZm|E@CXo2 z0lXLraaPZ)y*#u_y>ND3JYPsO#6K4C$DQW52M`VU!pX12SVv*A+^G}#NYFWV@MBkx z=mBbw9F!H4c-9=5gOD4LrZiht@h~_6mXR;FYG`SpR}%+-pDf%dH?gVyeAk|kaAdLJ zUpA*Kx++j?LZm2uOyMl(Q(0f5+xT^I{}mEJqaSjoM2SHjVl5MJJ~aY4n?js8ha`mm z&Bx)3J785j%5`>v8G}bhE!Db#*r0tNk@Fil^i&a8Yv`s47xxVtN=$>q0_0yMu(_PT z^to%66sryB;iy+xRmD*i{sBm$(LNuU6}GP9bp76NKRDl2V>Znd<}-9TxF1;ukdTFH zS1<&O8h`%W1vs@(7m)}(a!?jMnMV0o#TmyRHh!Oc)MGScFjj4&@m^`1Gjk@#jTQ;s zGo@&NySlIH&*ddXB_48#ZJHRDhHm!=MqmL!MZZW2rURpc^lVA%<8z@TbJHy$@MmetBiETDUfl+@;hpp2 zqscpTs6Ef0Cak;MpbJaNwQnsoEKu3u-#RqyjtjDNMhOtuh?UsTTl%-&90o+dnGFAJ zVZ+_;<>e1bq-Ii~(mdGluKQDz*Hv6PR#f8PTt z9*;b_0=o6+X+tO*laLQ7xFoU!3iB*77kRydKBh{^xw(DFK->o7ZjI7I&Fz5m$-!5* zvV|SeWWRvI#*d$lPHx#9{!S7tP%FzH1nx2`%y;zp!VcPoA4@)NuVY=jC;tJwi-l=& zEQ`|6)&`>oQ0XA3cLb<0`8|0_h*#NbYlIC*zpZ!ney#ILFS%!}cX8}E`?x?SbAAJ( zF!X>h^VQ38=+zO|Fd0Lw2MolPn_`SXyW0Ry8@q*G?uYj1n&45~Kp4cF>oQz~vVa1ZtaiN4qZn zx!37xwtvM<&ve%{*EYGlw2HyEEet%W1Yi8MjrR(x;Nph%_q3U$k$M(Zlcz{48nP(% zSM2A=FV1Njjd#bHD)03%P^34AjIhI&EDLIt>frF4)9%e~S$==ykF) z?oOY^uCkLdW7`XT%~auwL%EMIEBXmhA7zq2h1OiV*~;<~64>i1ZB5MsKrfi4OdQ}C z_WSbgr|DMao^2zC@1zS{WUd~4EQ1S-PMQsxSX_}H{|P>n{C^S>|Asc6a-4^~`~}nZ zfWc5>KbOp`x^lco51;lUdYbCExgH!Uuv3&+Br57?3Hzbx2p80Fu4RcxQ zC9!Rq5y(9j7?*$a{Rb3AyELU}VykIPz9A{?o{7T1!F%54JWk;~Ho>k<28pMszW0G0 z9Nk56psl5L^DbLVB>%QF|-ZE)Fak43i<)pTF?wYv6=x-{$O_88p0gDOkEB> znAh^d>|@ijRpxD)IbGFfc;8~qg0}oYRoyMgO-yb<)e=^;m0xTVXbkm4JBP#q>RhTX z>29go+O2+k9A?SzWdXDABtb_0RJ+U3@fe+m?Zo2cK>YvBgOv`Q6_r;~E_LA&3t{;a z=vz=jo9#1wrp5G9y`M{iEZGY1a#!}Jdv!awpGrJkFeQ;41`g9j-IFK2}!YiG?j282SeGQ|94+e~^H{ElT<)O(m_wrq^c0q%EdJ=ii7EW`3blh_h}tGrgpE*k*}c`~w^j7a$Ssk}%vZ>7R6UE(iq!;fIa%yY)9HDsF1Y4^Jy04mEDnSV*i1C#f3FJke${d8nkBT;x5jtt^4>y*W$(-{YE-I|PFveosFGnqg$ zv8yU5Md@xIfU)T9V5gg0s(+e{`h~8_E2jy~9`TwGcHTr80MIrJ%$G$AYV;0O*kpTm z-$?N^E|$F-^Mf-KtAYr`Aig*UHvNi8+UPoOZyLPbq%KWhsJRqsd?w z+}0RAu%P3sx%^%Wx{01j;DF>8xh#7sdUsU0vPMXZY zng*yQ+bD-YItr}4RrVf_(=+nFMG%_^%~a&2P6xW_Z!sSb)PYv~^*_U=?Db4l{nhwM zb&A!wSO<8XHYqIpzu4x-R;fGPQu_5qQ-upqOIHe!fZBHmz;IJ`#&oTAk7jV4V}59! zr}t787xZ;!HPl+74YfEQYIvfQ%&e74GJ=)Fbw*Az7)?Y{eX5EWiG&#jjFNOnG}qfx z^D*sf-syQ8|CbR=l*=|bV*L7W(2LjD!g}DW>)K3EWzooJ|HU(vOFyaHnqM$gd2i6l zm(A2v3{c1D>RI@J1uL=yg7rqb&YeBGuC>f)SV#ajZbfUv{WAhf6=(9@I~-09Ef`hu zTU>$l(e*RJrs((V$Ce_uC{ z#jmev@2M9-+;E-ZSL#!R*6upHk6GUS$XM{z6&0(zE0!OBCjH$}2Vk!_Yx9?^A-$IM z#z?;BkKLCoCtjUMC^?9`BtR zaEUWbPS|adfnkCQ-rP&w!pO5`vnfFOd2=30@|N!3PR9vNgJS=x1v?iV3`Sj3Kleqy zaGHXT&%2q@YpF89d*$WlMLfUyZPtPN*wt?x0dGwfuv7Y<5y|zgOP==Zbck$5Kn$>? z{y->7AHWgR0g{TE$tpjFSUGS2pzEWphu0b|3f)Xy%`3SY&Ils;`ygd2Uz`XO1e`Zr zptC-enLcOMO_XJ0em-L+8Ky7Vr2o5TV}9M1c?KYIN4<>XC3BAjHE*Vk?r5XkZR z``z5)s@u7;Gl_~Td-b2|OzzofAyB(<|NfJPscYf@hH@?KwqHrvlXe44RhDFpYNtgqqgmv0u~4+yY{j(|L;zGAd){xnwVFL>H*Di}vBcomZC9 z{_Ff~=U}yRnq&R((=A;Yf*`rDSx=VVPqoLdL^de5_ZQ1bx8M7!!ENJ;p`OIqSh?Bp zNgRR%APU`J^qizS5x*F~SaYqDb?vW;Sz(i2mGzXF=aa9jAZs^d&G`OEFpGcuurr8t z4R^^|_O{4f{z2hkO)f7qRn&wk=b*gHjUi>(`4h(wI{qO z;9}Lft#$uT`W96@tN6LI~Tb<^LmZ%_$|BjeeCRUC)>?`pAJX;)g*}Y$YjUGC0_EG zMid1l%!f9yiiuYDjeA(|nrl=0ex}di4t#L$P}SG=+t}6fuK-8(nH6U9sCq>D*4JYh zBUBj^mE$lU&3;Ijo!OfceV1PNhf)*y0%ZzN<{N!BPy$=XA7hazl4JQxXM=;wd_N6o zzvesAELUWXINj-fxY}vgfbWr4Gd*AR8EdiR zZD~0UCAEU8ebxKD%p* zgl6YUWYmp2cRGUY@ulAz(wO0R21XB{HOKd4csC^jtNs_}=xA$aKG%`z)r&POh~S@` z+zTg0>QV2PJB{@FZ{_6p&WJN_v&DXAEqnXt-&>pj+d8P8Q=Y zQaWSUhnk0}Fz#Q1bT~W*aq<{>iY2we%TPn4=b?Uh0%TR#81T=8bW7mCJ1v z`j@Qk!G*GYyWKMQpB4Z+V)dbt*>w+h?&4GSOLN5PEVbjWKgO1X zmhE|eKR9NiLoppvQLS`0SFV`K@((ViS%oTxxeb_q=2w3D6r6Y9u+b%l;HF_))h3*t zhvaIrxm(a3n%v9FwtjTdHPCC`5EJz$iCcABKl9Mz&fUE%dv%bc8i1cQ`6$vdN?9Nl zNUyNdSkK@z#}tR$=w8Z)UthDrLO}lydj79O{BF;YlJ?8w^`jC#Hz9CWzRA!yik3qRS8PlKokeWR@v-`5JnE#* z&tce7|M($|Zk+4NPELb8iw!IWfS;0#&@StTed$U1Vo%6J3sq4+KQF&= z?u)g4=PC7z)(x)ARq>3$<{5l|mMvk??b(L529*D3@c_laBpyZ)37*;XXw`Naa_&>@ zC|CQc{DsZZnnU!$E1GUTT=+Ij`S_XL-aFrAcfP;5JaJ^432%W)>DSxVGDl~xtdlcBJ|x(j zsco3D<}aQzn{wV;)p?CdcrOhixM{MtW6k%L*-!i2PRvSQP0>54CctH7-P4UNRTZD6 zsqA~-P+N1?#6BQupJ`V2%-mhWiaL9^zgw1-@4llkeqfC0)n!UihCd7+zh&$Z_f8G^twKJTCm%8tq=E1Ki4+Yw`$AxqNbW(`Pe@# zV*UT&YF3H6hAP^2Ra1Jy6rfcMDIX^v8uSkzKX%}}Q5pfT{oEVqKk)H%-Ka;c!)m5> zQ&jWVD=;pX<|&Ay4&uJar$_aQ%@F%Hpo^)41^nxRke5wh);$Y9R)*|Pjg*fo%Ihgt z|Ir{oFW9f+iqE^HM{v{H0090dyH7T_xD4T;u*pJ3?m*Jm=|@u5aqKvAeDK}RI@BsN zZgO?ZZ{)5_6V`rCD|+;BBaH&Tfd^8gp48AY5!E#?0{@blfWZ@r%$>PLh+nX`U`V04 z&q((e&6bri2_~gK%U+t3{|+)Ei#Xja=6xXD3qo2 z4|p7COqPT{+m1E-@<0}Qd(Mr8h1OGKkO=jptbsW zaEmm|Gx&pF|4Y45oWbu=pW7jg^M3PJmd?8mBEiaOpP60jqw>yufkCU6s# z#QGWIkM|pRpjeTjNmMCdY(YUJkcSX2>UVrYKcN@<&Zdk$45mOgnLK(^ZU;7tl?jYf zl%u@*NGk{3>odBAQ!F7fab-6^dExWhFZk$Zo_>l}@PF*j(qJ37XrY*A3J*0vQY#ZG zxDExK1Jtr{a5xG#A023DG}bRQpZCPGrrTw&!nisc$^;@1Y@nT1V07y6xm`*_yXx;4 zx5S{Z*6H=-;6W)JgMQ5H>GNXF-o^UmtgV+Fvsj_X=(N4LdaKRg#yZ359ZE;{<}2nE zC74D$_Zo9n4-u(=`645!^Y~%?zO~=c=t%RY@-C(%)?9CDR#~2MW@OK#mgNn1)b9}9W-njOFQRFx#|c^_Z4yf&=llhPjNvkTI)mn!v$&=?f>-2d$h zYu~{7soDNE`KeR=E7yG43@nCP93=(*53!nO4$s5;k2_>MfJMQ42F5A(KiyQ)DcEOZ z)k0?%6hFKg`de}jsw`pNrKhL&%(^B#Aj!PTIi=k%(;jAK=^35yXABd))3&Mk^9$K| zD_BqB$KHe%uXA#RBZEDhCbZSHwo7}<4KmPOb!l=wSD6O9+%7b~lH1HY3Dg1D-%XE; zJK4?9#I_xkH3Dq}ixix{;XxZ6m zdYBbh7^A^kMQg;b>}i|u^=HaFTgrfC)3_1nQfA*TKakWh%5qy6e4160|DeMNnA`qo zI5R-OwS{wh_bD%wjNCy?u8hsUHE78t7TbWrSO6bd9szGwdC-8jz&mnZsq2cCK{=GQ5PRp(t~ zEPmCN8xTCS4RCT|h-b~3`@gEj)~sW6LV%&veehUd-q0fYy=Qq$pw0qr;eG<{qNx!( z!<;V7*{Ts_ciQ3kuC$&}w(@?d%lYrHEX>WR-PG&z8&B@PeEh;PC0e2zSH3SVNve9gu=&(5Z4+!|Xpys6*p4HU7&hmy^Y20y z-Izv~%oCXP^}@Q5@6?yia0+ZnkFBf9{Kd-_o+2j7yVvEe;rCKoho8&rcFXej6!(9Y zE6Jd4g*y{Z?865MM((j;NahO4ns_xfShplzY&?<3pxxkxn03+7Qp0vkRwkAl8Qt|& z#V+1uLB!?co1A%yz`P2ZlZl{HYpy9QmP{H8Kpc@=@in5XZr~a}Ho?fbQU1ufJTI93 zG@a~^YIIGNf23ofP2#uvv0rx4P&(>n{NqU*UB!+B#4t@wiCr6i#s;gG-1W>(%8ngc zw3rGZ+iB}#mV$_`9#(&^B(O)@yrJ+)3ffx}{qa3>CwzZeTlfoM8cB)=$LZ36anH2k zq@X18BYMH%M?vRw^NuP1@}ttTcmVXh~p8B|BPp)vHcz98Mtw-n?9YsD~-VWqEnI@5Ac4UH_(B zIrnJelWB)BADDFdMGoyk>_nUeA5wb|DwZiFObl(*IrWh^jZUX&A4(`;kcSK52E&JB zTuO_i+%aGMS&=HF+D|vhP0Gr!Sumon&ZGm*Lp)RQ2;m2uU677dr==Rv@7My#d+<{t z1+uuPwZCWTcj1CZa`>a%lRu->-;+m6dR4jVvvREN`(>24#BnHO_*bw&zNJ}Y$N~8~ z{CV#)%$4yp#F!h)+q!eckvX;3uF`&NKk(1^vwfVCMMCZ+#OPxFmHpekNy80&Ic@B1 z3hSz}#QuLlrO!aUk9v?6RIAFrudj%NFz6_8*Kn@;+}?-P6;2yHS#@#A7K;}eBX@GB zXq&-~+QbA$EzLb9@^?`KaHTL(WCoHaT+&%(rlPNPGTR%9M|^<{hwA4on~!|TTwEKFw$ITGiu`Hb_Cg(VYwgI8Yp7u1(15n7VQt5^r^-X7un1(^p>9bXiZ zoCBx>LNf&>6bb|}KbY;X*W{j{Y5VZU(5lg05qWyKdz7b5RpITtX9=@GrqRcWM^I9I zOxiSekC0D;CNmj%YF?>pBf2Cp!c4MF_y!4`vp1^O#KyhGWZBdic z{1%)G^s#+GfR$f~ya3*QCL`zJM~&T!ng#{d{V~o$U4TT5_)Vg~j)}Yt>BrZK$SKc` z8hLHKZ7dWG1{P7DbI)IHxflWd!6PKNS@XPnP6? z1_8502gPAVXK0s(@0vDbenG;TYsF}qgrEgHmIuZu)9)9TBw|BpYKw0CSxG94)L7cXocs21#>DeryHCnmI+X|AZ!3VD#ya8Nuxq51gs zjlDROPV69u#zBc~6MCYr@2StU0irAC*ub`PADL^qk(8v@ccNbQ#=%eRTKL&_uglvy zMWMPbWai)u^v58c+UbA%K5;D+p_W#=Uo%bvb!F`(nms+ z6EC-JH0*$E4P3$W+ei7OHqX9WX4F>D`aptX9>9#MU-u;h1|9@iGpWo-OchaGRMiPp zF<2no{hc+H7q1wW$#f{+^vI47j%f8sSlOj9~>&+%l@A3zb6L zV>1YmVrUmyRhnRm)T>jks4XJj^TN{{cOY^cndbe1#QHc%UdBR3Feo zXQL)^dSul7GsWpOwy&V61a7l#>7w1;`apXsaPF zTc9GXhn{yPO*jPyie!i1C~Y1x%fLMc^`obP?tR@dIIs4=sC0lTf^zkqf6Hox=$_Yx zc?AK0>Q-KA>Xk|Dckh!3FZF(On7C4ySIg~u~oU*vG#t;2p**y!yTGel20?zL5n`TXG;FAYimL{{Wi)wSAq zJ4#fQ0}9dQ3qe;vfc)i(hMd}ODJOeVJI4mOeK`ZYgwaA;_!&?}JTj0S6IEiB!})(` z>PnU`j5sMLVlQJrDnEkVD&O+)iJ!Dagzocj&`ej88i`7OZ~Q!H(acr&MnpV60P2S5 zD=Kea$?vfsv;WloP!g40z}B&%yZo=YbeQ8iW{{Ve)}D0^CsJO#c(E@qXS(-DnXG<) zXtg#6C42kW;T)#o*?PbGjH~D2fF&-x#3x|KQB!9zY|05FWPyF~7(d$H8#iP&7|oED z@16ObC=5~V5Rd>@SuAv|?33yA1K*Y@b&SdPqJI-uq^BW{*HRp93QaV-Vq^?`05&5vCyup#;n>L-#67t$Zadtv5nk!5l{koc`ryUoLKFr}CE{p_jQW zvC~CukSpx|%cC@JqMFFBlvD5PBYB!j1#lC+>R4bZv%v@$))LE4`h1K*O-{zZTTjxDzp!Yl(26>p5!;oh2hWNf0k!^pP65=r`AHEz%4#)qbRIFBCN^qPA}y4bnt4)+Y|ewD z)qu0-&qnqZ@b@VBpow&vn!~WZWW*t-s*aE^=7`bQxg;ZCB5QD>3%#_Z8hKBqs(lZO#NC5nZk&>Afstc`% zb9@liK19bZ`#57v!AeFyX>&O~f{G*ca>WT0WDCNU$;941>F6fJ!UKu}uI{2SkFI>( z(RT+zDI(bhA33w18VC?LQ5beHtieP}Zn>SX)6Oq!=?^h3@-@9kTOy@tosPO1ar4@V z`-gTBye?(cE;B9PnEle<$sg5WLz=q~iac z62X}fpv#HUG0)o`;c*Q82mYxK?`oBr@ZH59MuCr=%G29NyAd#)STu+YC=2^2W2L7OGr7sylvE+OfEaqZyuZxN@9CWTGtGu((Y2 z_V%EN^q#Lfy=;RYiYxHYPOEW5MuN_-)Uh}J?R%-8c`;lxjTi+IADT}rXjC}nD39c- zK@>yKh+HHFe+HlsMr)J3k8x78Y`ksRDNA;;z2Fe-8E_^Fggx`Ltamkjxc^nUkMFw2 z*~S;n}))@YZaofM`>U{HX3riiG* zeMe(KprY7;re>^f^rPcF=jW9wQgK)nCgM5%S3xw)1`k97aL_zDx<|1enbo@8tG5KGd0iOqmwEM**bGUg$+aZan_W?x z-c1659C#tqBQz+3TkN!+o*YKH9c`&=H%7mImku{D`5-6rMhGN)K?o56xxbuuVj2x5 zst67nyp%v6oty&m`gTk~3MY-wLqA11z-{TEFDB@An~yvF#J57s z#YW|!!iQHE9u@NP^3#?5gmFeMw?l(#{JV|q#?fL0oNl(?u8ZTOyHSjC#L;L2s{>0^ zH;dI8W28lmQE5+ZUA_9ps2LR#NOX9v5R!(wD)X|4?Z_lX+?WxzDek?paY)0dzQfzl zS$?UKQ?g?U{M~dt6N^#EAqo7<^G%+-vvlr$-XPpho18|Pif()UipY-q3(yZpm3Q}c zc4i#w>T3gcx!gqQVV}=2Lc|Y%fsmqnuUo&*!T=u;fCvIHbm*?RY6&^8-vfFG@Yi?F z2i$k?#XFQz#OC0#PltKVsLkg4znt6k-;!$4(!m#S!YQ|hcYMPP4ZSnnTqZ}j+t zE#k~*1xjtgI>IZq%pz>RXy2}YfJaeO?a^>KrLjk< z85a^DjTA=(!Cgkt$2pKdx=JUw0rETFsrK@We&d+OL8hKo+@0_XyUo+4U?XPjO$Zuo z4W<37Gbe~y6W8pE6`S5=%l%d^x#<}tci;KKAj)9U@#{{bT+VFWi(B|(X;-OfRk|ko zm?soPgS~y)qz^Xvl@{rXo#w~A|CBKxWogGti;sWn$`B0gwf);Y+TMrRxGK0B{ubs& z2u=*i(>itoU0pf=t+RPB&m5kTAe}h6@b-Vuf?7vQhj~ZZI^@TV)3;gnZul^)R1cL+ zwZqUZ59cIvZ9W}Vn4%MJJp5RX8vZy$JfZB{fGRt!ZD0K|H$qYp>3ID_n}s)Kw_&E# ze*{2_oxB7}cr`KT3p~#ofDpSk6D{2`wv+EE@Sh4gJ z0{%a8_F>>>W>$Cx95^9>jLGK>Gi{iR_wD!HE_J&h8j7MlwF13om$7VsEXtXI^B1rA z;u1a;pJU-Dl95OtLS6qyHcsgQ=+K!xIpPfg7PYp{2%#CmT`gN_qq^oWX01WKImW`u zhM$kI0MoZQ(+`M7O8AKJSwz#OZ4JLp%mizq^Xl*E+Fmm%Z0)b>V%aa4K-~NKhHRH! zu=66PL1uzU2u)SlwBYR8*Qo$q&Ph_t$+=F;)ATTDOJ)4>as83g&mpECCXMc>rW6jv zymE=_i8iSg>6@R7%?AWJlMCC4y<}bFy&p#FHFV|dQiB>aE}>wIp4C!8St|UPX|07y ziTRI`p(!2hVS@jFH{5MLU@ZIW$u&YXE{2Eessy$55P2fF3>71hhb%3Y4|Uu_Y=u3M=WF&g`ED{R^}O~trs5thBqj)Mv&*NZsO<0sl@{t~-Ss;ehak%D#-Ora|@XH74oCAJCqPeef(T3$XjvH90uM41&<6Zxhk=-7cxFjXcYd_r~1>siYBIB*E~9d84DLTcDC^ zw<$Ttr{QTN@I~5s!A?RvyxeLNG9tC&^4v|Bi|YN(^%1=uGiNM6yw-5prpzg;aR|J( zU^@k$4Gkeu&F_CQgChd81AEDC)s*j*m65Qp=n_;B_lpDapPx6yA2SsU=g|_PNV8e| zDK*NqOU_#05uR&SS9$h-W{lyL*8r? z#tOncco_H)i8Vn@%_OfjZR;KHRORqdPb->QxX)AoL?^Cvh3WpX>I?MD1##KxL5`no z7AvjL)6rqL41x7pnPyaoQF+?0N3I-h zv|V`c?s*DF{#aMRxs~Vo{Y4SM*+D1kX)iT*Xl|jp@7*D>g^dqh6%hq8Fyux?258^k%wJD2=?qrtbtf2DUg;lZGI>I(NOSM@AZnNV4m{f4z-z7T9aqN0` zIFQ$WY%ZsUih~xl*QVTkzURV*cHxYFeH`EYh@4>8+5@3T&;|4GQO*TwVJSXxSgylGgE7qs^k+$h#2~ro z-8lP1)=g;BBNV)M@Y!lI>ZD`vyf}%?8 zL|d$VvyD0w=zt;zsSV(ugpi|AUj<1x236c#YB+1)ibb3xA$&T!AbvDrmxACefxgzh zc@<)4RuH;OP48^~abii9i^=Q)w;R)>;Uhv= zQlWFNTE6mGKmYt)VR~vuyFj*bdN5UMrLfwtCu>XWwo7o17s$@Qep9Lg}V*# zsR*YSIu*_<{9zpGeCra%+CPb{^;aw@+7*=!ro(~!N`WE5le+XO4H-MS`TD)h`(mK zplsv2y1_Qiff;Qmx_2OeFR;#^iD2_9x_aMYKgYdoMLG3{Sj+YrM>3&p;Z}4ywC<|C z_Sl~lKlg%t1%3Qvr`2GxxY>iomYE%~XFapjA<)Dw>A2H^DvZqT-KF2Ews=|e_w-3q zn7PKA(ATNd=)ausKh@OnfcF+#cKG_707^;y*D7Y;wylzP)@(PpbL_rthCX&VLU`Ay zgaVGvm1=kQMak^JY)$C}5S!dhteG9&Rc1+dwrc^mMX80B-?Ds$phT!ws@I;%p&aM% z@<(YO+vM2Y&{)qNb7JSh13FmPh51~zc1x2x`9*r{c;q9ThnCALB00tKb`crvF{|x9 zY=7C2II3L$demc8jd#mRLo3eByI6k<-SQCs?6M7;&z$PZ8cqm!!idE8I}t-v(=PWz ziG%_V91%HHjb#FIBzFVVnmP(A^73dWK)V(1P&1ODbgb?}O)=c`yqwd3&fi+{m4#|` zW4TzIyLF*OP92>df(+5}t%w}{_n{ZvX~EgOqOt#nR(xeM2+^Mn9A=jCp`res-Vv~r zCd#$vB6iJHvbde~*gb5Lo6vPSd=A3Gg8TRNFv1sfe!kPCWIS?yJG|dz_oj~BI68xT z4|+*Z#L;^_$(T>yjXdam^{Mfrf4VV#b2AGBR`z}mb1@iAx1k4#A-C#(rN8c|>zR5N z>PEI6?>j_oQ{&AGulLY;LYsX5nP*;7*iu=zs`*t(wQ&3smLk=W=FQ!JHy_pwZ~ZJP z@fNmB$}o`IL4woO^l`LaGCMmX5B4-3s5YR-*L`F@OSN;pb%?qBQ}eHvpa!wQIiMRK zC8^M*H2kDhj3;#ng(qX~9%jo<8M{0Go^*Brr-GoPGwA|_CYmBdntqu!OAipgSZTu2 zB;rMAPqJFzLiN5oG#R`h6&fp3#mfhuKY{?5ijPMPoHol0In(N*Z7^@~3`6@ITf=@f z(k+bZ*RY=H9f|Nv&9T2{T=OTCGQVbWCL(`>){-)K+ZP3D=xP9&oAc z*%HZ=NdrAPQ^)95sWt0=EGgPv*M5!L1trag8Z+zudAfyvt`0ycb50YEy zrH;!YhXZbdexOk8+eolqAJDVm+0(Sblh;asVbRgIO;|i|m0>oeBt$VPdsk{76_2G- zGpo1u^%`ha2MJr!B)$9ZnEd$7a3F9z1)f{B6CnA{&YgPY&%8Zkw^m?ldO_fn7+=%f z60N({@1`a8k&w0SWB7EG*QT_Sudi$Ty6~h_)^FV9_zN`yYrJYY)|}8O9VQbOpy6J* z`r^R3!?!-_Q`b{2y1iGYy&3IyeN!8=q*$`|2nXlo<1>f%N%&BZA5)c3_U?f9>?M+? z_9iH&)pmD17%O!u5M$n%%a^bEXJp#N11Z&y7mOO7cH2icUHPqX!q`qf6ZK2as-{0T z#^oY>%fuct$Dg?P%PlRTpZnp@I|e&_liJX-VL+{JR-M#VH#Au1o^C3f?4erqZ0Fdv zt!sSaNwdG-Z%{{mmwy+4F`BJ!Yp#vz|7h+BY(|-XPi!eXA0+id9&C|H6H*tVS7K;E z?iibMx4c5ux&z297&ZCE`6K#CDogc{RzMmpwdfW8$Sl`#SqDr5pGGXP-_xz;N?wKH zW4HeMpKEU)-(NfWeUei1DgEpnH)qODNRMbXJ2EAB!P&3=Ps1%YzpcD$*?yO)+xGn8 z_<)^3Sv6BW@7?*fafZ7tdp6AV4yC0l`W`(~75=^*CAwk$?%RdoW1cK}wKT_M-<>rw z?Tj8|G-K#AO7%kAd#mUQ3F}x(v%Fnkd!mhQ{T^#` zmY*7N_{-1Ld3XgTO*y)E-^t&3@iS9is$JcqmNp>HL*x7bQ!VG5Ut!)`WAbG@dsv2I zUEt~WJuGQvzbR^u^$&U7&MCcmQd;eEt0;=l!5Kjuwm<3*psRR3>> zww%k^W$M_!qgsx^tt(g37fw(37H?>b{5AbrM-5D`_lZJ?oT)q)Taz-+2304)B2#U z&_!zQ32PMF!iPIqwBJpiXY2JnTjH%X_IwfhG1ElYEH2Y))f8@54m!&Hby<2M#zai6 zFHLf@X|=t5eXC}8T@&dgWun1UwY{UKU3SzgObKY$lL4&-Rkt2Z>_HH$N{<*=bw0z| zC;hp4!K7`=g6&La^L(p{rE|WA?=`5Z4qO><_nVSdi81yQ(Xis{AWk_uJ7LXhQn&RZ^Xo;^}8GU#1*B{rv)K(f<7VU|9 z-+yCDsqN37Zf5FP+Q+Kr39+HSZQtbST`D`YGx2DDH=5|&BPJ_czTLl_a=Z80FNY^*s`4#zetrrMabEOiX_tn4|G4RrR9?w&VQ2x4 z3cEf4oc$x_3a?icmxlR{H5sGp%Z*(;woTm^UefEnj#T5Gow?4{yKG*j8h;!v*`1$9SN?+x(yw~>pu!skXWs?Wjx{i=e2#IfeH(kUeCVe{AE`s(;Yk3w4O8V zS@J4){juo0qJe5@vyR<$xR|{C?bKs;&9t|SoA%^4kgnkmwOs|-wS7OCyk2{5l*Q`) zf2kK7PVPG>Cp=lPtPWd^)D@q0%sAQq)TDpU-g$OU=F{pd&rbaU{;GI#)3LI%?81`I z%^jRyzpB?ty1ICE=z)G$(jQ0_s`PXyIQ%<(z=mU!?3c6m7B;j5!5F%iVkghLQ44Q2WqDsyUFrMr&oIv)XHa-$Zj57 zb$;%v%<1XX4m!|y#imWSe?D$dFkYQ^Nx`hFZq?L5S(9eI9qiho{W7EWm{j1d_${!_3fq=>^mTO81NVCqk)FAX08R6^gcRem-^0SbP8hnhuB=M$g& zcaEY&4ODRS3h}wzT&A7!k{*K877|vLNal$7LTd{oTHwHhM5sEG%<$I52Pc9 zG2gwup89Uh@%Jo~>X8kDrCb;6F#-p6tB^u(j0dlg=JzE(d-iNmV`(y0>97ZLs~3fZ0hUbVe(b!PdEw4T|2nOh#nGg8R37#)_XQa0sk^_*_s<1S@> ziMW{;XQ>%CJ|&>;o=4S?Lwj|5Y)$+ee*X8wOIkL|e~&#D_~^~7fIRV-!?p|p=@%o6 zG-%Ne&PayPq5&C>&*Z5a((wb36u7iH#s7Tu7wC_|oTRUzM|jlA&8PKaJ(~NjKQ~Iv zh;R*if(pZpb`Q%*Pnu$-6MOg;y}P^p^<6KR2;2+>yyH?W>v8}w zes7M4KAOLwW9+wBhKSjeZ0BE-K@+8Dz4-pZqwxPR_vYbLwr$(^GABbsGDebAk|ZfY zh0;JOl`@1@QAk3_SRt88GG&$_6&XTi2^ASqAu|b?5|a71pL*W!{k`Az{qgPeL5<8_%8o&h7RYL#2qVnNjoizS7(7Y#MOi*GJ|`7U2?T2`21PA#ER5s2rh?xtSsAtKn5)q_%u}+6u}W+ zHJ{vv@5Km7PKpii7wAd9x7}z;z_n0%s8E+$#&|NgE(9Yf6YkGWY1 zHj4rP6G`ikl4G-O|GNu>2dHAwhLEuv>hUxb(fxW$;c$XKUoE{(`q;K)mrdlQn{0Pr zmVF0~=)Z%-3s;~^#80}4fIO=ys{if{ysXI4CYdqGE-*fa4tF z_UKr*w0^Ar$NIT*owo0Zvf+=uf47_w+r`p@axhSg$5?@G4^?-~SlBkZ9U6gT5X?fhZ#uhWHZ;a9HyXl?dex{Q>r zmlyu~PW&JKm;P>cQ*zJ#`*r`f|MkD!%>TnL{=f1PjmcVWZF-4j`x>&eqZv(h-a}t= z^u%1)(#54~cQ0%dVAAvFGr7}3C#RXKn0c+FH&p+Uo=dZQr*(A&bI7HNRN>SdbHQV~ zPH|H|C*kSezo9wXB49?W=_AlYXr(cNi4ExY3cU)c2c*yyqU0|8d-VrB|M#!{2TyQB z1aklQCpTiT*8O5BRQ#D z{fk46-F+MaOf}Zkye2*Vbo@>4-es^Nd^Ngu=g$*HL+W^DGbAAsrcfG{msW|~@m6JO z&%Qu0({M(&s%<7a`IH-*5 z0}qV4dZ3uZJz6zwb#eu~o<=HxVwBE*rB#Kx|7?=Epv1j5y`kF}-J-9U7g#x5>~Whl z!)>t(^`o=&YL@S^3ripLir#N7D37=H^;5tyU$;(UrI-eu&v>O(6_%_iHzqEnx-PAK z^PpVU59EGU@OJ%i^W&1LH5bIKY2*trlw;2o%p~_6FE_a5e_em{XB<-do3i)(Jn`@G zInueX5zpVXH?=#KzLbh4(`yR2Ri{~Us{TD+Y{H}DB-we_qUEK<o3%Wh^-s4^JEk7H z6l=*0jk@F7BXI4$>Gj=qq1!4_c`QSgUX@osid=7H{gAI8`+vR;yGK@wL4LP=f_v78 z$*zqzt1{i|&d8UyrL?5$*2G?V*>^9yXgeR7C%@!y^A?e$H+r-ENy123QR=gijM3Sn z#T^@7|MwgA;QP$4$(_p}&k(s?zWT~~SMk>A&-}>BI7Y8vGW7e}{J+iWG&9z)?GMS~ zzAcq<6t0yv2Thx0QjXQVTqPSIshY#YT_;k1%TiFPpMBpxCUI5@&HG!c<+z)2O>0W4 z3h@3iBMKd(JM*9auKjm!GY8E%=26%CwHVn&;wD#c;uh-*&j}j$xJ4`Yra!(z_Q@g7 zPIjrK50HNd-)_@d0WX&79q%N*Nu4xju$8DY=VKHL{kt}PLFDqsu1f{gpRdyCOPKCe z7h29Q&3W6K`MnC|Yt{DrHI24k*k+5y=anLjTvp8Kne^PL+KvUqlT(Hj04^QhG5H>U z;lazd!}f0$j*}U*LWU%s20L7EYny=~}Ad=`;dUotqQv5*r8iHprryq7b_`nnwucTZW6$ zdmFMX&IEe^#>0f8k$^Y}96;R%w1YUK2fnD@j)GVLpc(#20ce0XQe0D;Ub~~u8Cnj2 zYSKr!vMu%pis9f-PFb!SJR9}!q}IUbK>q8I$%$lxv<)BPsk+@tH_1$78hgbk&giZ$ z6~k);?%?e(bMV`apA8_$;TF5nVtC0n-DtZ=EFGWDxl;GxeGJRLW&5^!OdXKt+j2R_ zKx*jg+Sn_H`!!Zdp1UO=Cqkoi#>-X7OIxo|XJ_?n&D89KRQ%lb3f`OBKgeh(XGr>t zrCbxpG4AoMxDpxB8=C&izoqNhm6O9&A^{4%ztsjen(m?TQywp1$X_0jPflQsK&cn) z?7wp-cF8Ud&Fxwdo|(AILO0k!sC1QF|L9@{{%nRKsN@Pokz3g-rir1hg_S10Y-`da zjMm-wYHEjD59Z5U4tN%PmSAxaXzgX=Sw;;P$Dg3M28he8wihN7vUxw%PXQE)Mg%q< z%#l{X|?@ctLxS`(xA%lu5omXDwC>OAg<4?~cUY%4|#7Tn5 z)Uf|8_pmgd&XOcS8R&BmMGXX-LebG7;~Vjx11b?WrS6n?nyXL+CLN+1JN}gNcdfL< zngArR6#UkUcZ)-o80>lo);ijN@(3RljWeV#PD;`CnqM#P7_$~)_aYsOy&}{p3U+jA zkSW6^fmTbj^H4AqyccAnFsKF0aHq*yPBn?3-Xc;tJt;a0=sF0A0o4OR$LXT(ghtMW>`3(^-WSkPG9=dQo9q9~VE-x?llNyB1;s4o`mNlec+r1zFc?xp=MR+T)5YDF~qrbZzI$455RAs3&R11Y8s8nnt;AY&WdmW-+%GNSZ+eA;e?{>0Y&D$ocJ;t-7nx=7Uhl#=Rd-({)~sMFtWv_3U}il%EY ztyo6GS^H;?LR2=5#92dB#CRJDJKZ@F2Cb~T0w}%-Er0TsPqJ)F z-4cak7`pJmD*@&$o<_Xr=Bk8Y~SMX{ee#U&ZevzhZf7%z5Px6C;z3cqn31ex}p)gFhgSMr5L+ltAsv ztyT&tJ61?seZPcM-?vV?0JeSLw_u6E_0n`oi-LUZ?Gks^&IVm7IhBs)fETu%DE}csIyS}Y{O(-;MsnS z!q_#H7Wten>8x4=f9+X@wG0OXwmGyY7!-Eo^7ab<0}8ZE8}ZbI~2Y)s|jqiQ2EW-c}lU`C;{zV-~XfM0$zm zTKu7VoP_kzhg@gVAH;anf_N1IxrSuh+e&nUS8gFTZJbMwx#t5b^l9@gt}B4MUtWrYJjYy&q;Pto&cp6iRaJ#X z5zH4D;M|}W`KkAy@dw3?HXKCVy9~JXth?XrytE66;Y81UL6=D6lotM)fJ*L2cDvsY zQ!}S_tn)p%NuuyU?@mV{b8DcpfqTxsyc1ErfS5_P>B3x}vI(GU3UbMrbc3-xuwSKG zh9vO9wIh__J8lisoMZ-U>@j>Dv{t+-WNavj zi6~iduK&2oDU8;G#>5jL)N~a1UXfmn<1rm!*fzV_I=%jk0AKstHtMnz$vzz ztCW<~J{=wCPsq4xSPF47KmbzQ2y@cPyKx|zZBE|1x9n6**X0YJcdp%=NtKO>5#MOlEO#rAd4=X_ z!?d5a`nG+AqfW(4gQC5QqP4GrKfm$3LZ|cEF||YaMe?fhf^7lG&fD7?ys;2Sf{QEclS;eB`Q}GIQQsp`tvgOlQ8Y*SvI# zObv!fvOu(w8W4t*6wq_4^A<8Y?ZSkBB};^m>_LoBKwcb>Y@ZB_q6bTXZHgoi11MLl zmFw^MnMZP`;6a3J32{+z4Fe^zM-K)J?!~wF!L6mF-ejo^Wq?Tf-V5%%~SxdiTOc7XD9?yq2qybjBv?~gWf|Q z8#$@FuOd#9#|M*n>ul#M3OO2Rzd~gIScvCm)Sc|0Rv3$BX7ZZafv2B`$Q~e+OL6pe z8CG5k=Fr+cGU5_W9*S4kG<=t=WgIQ{4K8J!M@|?~mu)pD0BBAGrbQO{t2xJ}1w_6#7FKZw-U|Z-(S_61RXrbGB{_t z)c34L`JBaiZXcJ-PkFsNE;=$lPb4}H_r7*8D!p{>*TE!KQ{K|;=&a6`1C3wQsumq4pR7r-UZs{G|GB3wz3Du!r#on7)F|hrCB!@U zOw!_nG%?*8mKT_uwp4IztjbIs#E~uf7}U`*Zg69+l7am%2Yo88Bkq&j0Ni}&!ynvb zkRM9IzP_z!bCnEh88J|`BJJwX`HEEW|KHYKwfds~!ulZIzNXjC;N^E5n>|Y;RH0B? zyj6TIS%uOU#TXLzWRGsk8MJ;al_lbItTJI&FQ{;w&JPOGz%R@TgxBKKGq!~Gr0CiK z%b7XoBH_WtlLM&Ks<2ShbAH|s&xZaA$SJ|vVIWx(m7cPpk?Nbiw%d(S?s%W~1?7Pb z!`||+tJV`rb&JIs4;C&JBHZ!zKS-SC43;uyKaqXo87ZK4$O~MY&Kv3dW7{`caJ6xM z_ttLN-oC_E;mZNISp@S zWp)dsxM#Qp11iT5dS_y7eSGL1rp+@&*P_=j;$=M2pep{5hC&QOYXW?>@TL8n+ONQH z?EX9G=iSO8oj^&#?2IzhaF;6{QQ(^p2|t*`IO{H8o7hTE|8Y!PPO8O-Y{6}IE2TCJ zX;uMJCm_NhC5_SSIe%z%0$}JgJs!qsJ9p{ul03bZNPJKOE%|ib`T` z$u<~?65n5>5}caZKK0DNKlRyouxnCg7E{RdTuGcpqz(pxp^#*z3uenx7=0sP7KD21T z3cwx?Yc0w8m#o6XmkMkmuw#m8tkgJ*a-Uei;qU=)48f7!E38)sCaRBSZR_?k{ zd>#u=H5j}H88(dgG14_)D}?YIQZT@#SjCV(Vk0ZbkVHM>^sAH!w?1u;*32B9DDdT- zR;NedHiZw?Y!bjnCALXd-AW<5JwrCT!N8-&CnFb<8jk7c_|E#r32v`w%I$enH1sWA zbAy`MrxjMYp%X_RJetT)G`lA}b3`L>sWA<)-{{(KM|NIH?Z$EOp&q3{#s1v`+o`&q z*41|w>Sv8wvU7!P*ej{#G1rp@V``ILbq{O=)IMSY~Q>Q zOoN_<6yi{Jo_ba)dlVOioT@Wj21whyQV!PKRWbd%_a;c0`L zg;B{q!Bp=IQH{XlT0q69(LaZ=B)4kw*?nCLfOpIucH-K?9fDtO%;?2Q+%t8JnEV)~ zQQRq2-7rg;W_uyK{4tCRXJuhaWF4A zCs52glB$`KbDN(v=BDb@qoVDbn6fQ=u3tBZ2h?2K$Hix?*=_zTvg3AUp-p>QdV zdKf+ouNUeIEIMdM-(KrCs2hds4&Jf9PDLigH}ix&jqct&G*#x%G2k^)S>G3x$*xef zY>yWHrodoy^RRm+8$1xCD?+Wbu%JW{SsGG8!Cax%6MYU(Q|>`?`atKM>`{q(x0;)q z8V){IzoU(MR7*meb~$?oxJiuIUq^XkT5g zv9b@nj;c6k-*`~e=7~1b@UJ1Z-)(ybk5=Ajf0(IQTYLNGib)~=*;K8T)absZKC>zR zxN#(>R4N_zk6P5(yrQRK+Ol6#>*P+V?#O173BANokD?Vb>;t3zH?PJOrDPg9e403vjo{wIoMkSL-YH$DSk9wCk^`m9YOv)qrf5f*L zUUY91DN|HC<@~HsEv;>z)|IllZ_<8cDyO|&rh0EkzLybGb04zaKW5+c+cEa<$#B& z^RWA1ufrw-VeWv{zXjS&Y8@AAX7_j>=Kns=w{CS|TF;LsWBY!(){QSK*{s05yJcbL zo((jDv#r)jqY<^1RT=)nuDdX@#Gq0w>dJv>Cnc;TSY9EChKRDL{7gLmC0p@r@~OpT zO%iqNN&mW4ltQFah>l+EBR)8}8;w4kL~!_`DFo&KzZ>X2b!X-w#(38BtSk0#fD!>F zX8k}#j%9i<5TV4hAivOGV2h%o;IC0&V&KoE+)L}qhkt#MP5DEu!&WqHEhUlgI!i_5 z*9TjLzGum4!-Au`&0O5lGh<{li55JHaaGs?|b+9*}qNjm*av5Y-V`Q z;{8nqIlpw^1cb8eLs+I#UYjLOVladdI=}51Gcn}x&#-{A1$HPk$vcU|1Kety7BG?T zljc=yOkI<;WzClzd9}D=j9)c>{4?Ti(iR@neCS>vabhiVldQsJi{=~3ggLGM)Y{BL z?C*K(6kIG6`Eg1RJCj5+gEVk(cwb^_MFqgUyzpAg9iE1U0O*RY?JPVJmDnD&x^3M| zQL;YR70j5!xsG@#9PJ>Gft%>);Fv-03Daq@ofTdSf^%Rma4_MYpeGt+f%lAdJ}kZ1hL-g>hxQ*182MCj_38=3W>rDB)!RpmlnUZhdg@?(8|K5P&WCL8tgO zl@8q_8ca~MZ;*wIk^_|)28*1L(3(_hkfo@|@~>)M=6^F-d$z+|4#&L6Ptmp&DVxz{ zJUIB|YOLsHptW|Z)u;G=HxZ*nNcbBf0d-c?gW zQ%W_2TAd7inLAM$@v;_WM?iZrHWbtIY5OJ^ZP7{PIq9jCNe`}T>i`UZ-s|$KO+W95 zX>_jvF^ne|*}g0mIyYC&__LLjVBT(%vlu!7w2Nc*b4|>5d<0Pk1G7XzosZHgigx?J z&1>zKHXX=XWtY>!K;ew}J~eANkaNuH1}mz<`&%7vB8SB ziQA_fRRu;+=aOV|Q11voM7~We`RmY>6hHs{aRq(MeouDhqye3&Z(BZRH)?k@tD0ODaExc{l#ba; zFO#cx43ab_7yJF3$zx)3@40CQHrnqqQc;?p&!+U>#~)8uxH>Nk;vvhX#$(JiVnUN)x9n3-X{c&TvoQej8h+(OJqZRjqo>0;k&A=Z?Am58&mj5;uG)DQfWFA&IUOifKO>wcceLRjX%u?yMos+h zOjBv}nk!~=1I5^0tvMd{WP1P627}qT*j*R+&&javS3E%H?si7lI_lLyBB8N5{62Nh zH_H#hcEuSS-aG~9S7A3qb%+xD5B0X?tFxr>_i2jIEi|RMEW)7z#h=d)39b|_>cs?a zsJ_4Ei1_e?G9px4loMM%>hu$aNvVVAJ8|YfP>&}`kBhn~i9ubW@5qUXY8<9&^? z{?<)@Qklwx!5#n`!xq^R7_{Ds@om|sk<@^r+?ccG+EfBm`%9UrcRkc>w7@LjNZ|LY z5V&zUhPBbgFVPH9SNA;nCw7$^YfB#aVG3B>W;cMs%JIJ10DTG{dgYH7%e7?rhpXIh zIwKS077!)io7>*m99P*d6UNP1_w?kUBT5Pz@-|*~0U(urW~eIIRZnFuB zOLxYqRU_U3z>Uh&lPez%V&6gufpEl>lzXmP7OG0o2c$yEP(MKW<mJkIh0~Vz7)qD! z=}ctVtlnyXk%Z^yP3q>`p(TxzAZxINSqlS6VA!FE@RT&XIdryUfYmo94>CN+0Rd-E zJ1x9*^Mr!ENP)jK#y!*M%YjSx?_AGWepb>M7Z1k{zEI3)^f)7Fmr64o4=G&Amz1Wp zknkc7uV`f*)(RjGuM08`=E?(EkVr%E-Mq{Cb!$2e8@Dvk1_9Q1Q0^_$3>iNduWlfC zLtkT4HsOwv^B$c$3S7&ameVfC!$Z%=5{FXbN2&LenS|)UH@8fV<;_eos5Wryi#Wj5>XP$)LY5Ms{F4wr;Rmr2LFx zG|ZOBm4eZ3bM1ITT%Rn%Mg-uI$*5-}*9g>+q#qyyi0H)ENRDpmr##dA11xWoBNN7x ziYn^e7*pU)ei+v=B$z8`>%hBuB1(m>n!EbYsH{@7R*=x8Xw*Sd3{s!Q^&4$b$358b z(^TXjlEo&0Qo8xTTvdPb+s@-)3la1|8<7WUry(fB{&{CNatAcmxH>~+Bm4-C-|X}mV^mx8W=MumQ@c44eVc*B4!ngMP#_tZgW_i`NZpmsH$ ztBTe)7U!9bTF6o8RB-v%`Pgd1K{J;6{HiaIS2G^&U&) z2b!$gqFL6=ZR$b`CmD-OX)TL|1LITz+wao|uJjG*vk%@lyWKD(b?-C5nAgR(Kg>*- z{;-Q}ydtfzm5$BukMOY3nbp z`PjOKBA|X?zl|kLk*T2N4$6S-aGdksQH3uu{IB&-?kqa6Iftpx;C8)PS)#rsC1yjZ z8{_^f@hu4QFfTFPX=G=cJMWU)!BJ=Rb=Ku8^6*qfYI%2GofbY_gVt>`G}#b)1wK6% zy$vu{-?;btzOyf2s`Y_}+W3b~-#DtcyHk4{EzQD7P9K`^7x zvo%-bzjYW5$?W3d#zBKuB~ucPZ{*}qMZX7w-@ouM97nuq@tx;tlB?C-EB;7yvHBgx zq`{&-){h%cZCZu%2?aCx*|Z8flJDP5O-D(CCx-^_7-{h0%o}Cf;i`H&_cZhBh~R1) zak03!=29tc_lRVEyn z269ooST~z?0`xU@HBcBu2*@9E_;~SzSpBuQ*XqkvVd)Uv7~L^Cdbmb4zr1cH1+V$z zC(YfqZ~J5K^C~A@VBTpV=uqc5Nub${U7ue5T)V#g2_{U092PUMHMkCoL$rp5wAq4} z-KCfv*Me-!?=#avKP5=?Q%}cLGlb_O0alY>0OVR@2LzP((m+$JVP8JdzpAopBNj{$@^>wwB{oIx{tzgJ%puH+1v}Gud;+`&(0^yD@o`Yc%NdYo(FK_= zAgcazF`9fWP&J_4KsokzuIB?2)pzToLoo3D8xu(KQmA~vRAlq1&|kO7Z;QQ?O1SXyC~ZbuxtkyAcljf|C}Omc{_SNrF0TrfP{a zeP6F<%`V?^vH*9~9y9;;7o8yM(;gYob`L~J`=c>gdljAmzNjq^+Vk_TD-W1VDoDf?Mb*w`u-KpqgfsaP|Tm>Dq38a z{rg_a1!x`NYJu;PKn-$1!~|4WSXjjH7e!0);`}HX0l7C&3?~63yM|e9BJY7iDkx`C#{3fWQ*VqI}7pxl4+ zFWino87&z1KNGRJDub67LC-`-Kr%T<2sKJ*l(EhP}K5$gn~bo2SVw?)dPXihu?DUIfp-Xt7>&V`RHii+4hmq2OIDv8z z)_JV-qVt(>_FfaJoP4PH45`6b#ic~V0R9d56sOhCR^wnhpV-k}vOsT+7H-H+lM1Q& zJ08903nL3o!=`NLM15{_IIkuNBV{-~oN^DJ zlbCSqG16$;jvecZLLT}H`>BH;r)UIXI>$!FiHO>9d&TxC#iY)ro)5M4GU~2c?lPch zNtT>M#Zr7`5UXS|?!Zkb6k#lX3!cAnMxssq2ic^*5Q0W+6)*DSu>?6^dfX z&?k1#lle?3FTa01uH+49R8CWbkVE-2lj_yp104x8wp-hcB1Rv>cnqc>;~Ye3sKF@xELQP1e*F{n zpOGY|t9ty}sC8fvl(w!HCaF&cgq_7Q91Ks_SlG!d%jeu>|^4Vm|0+n6-`!D?AIetS~)pQd}h?d89#}g`a zZXvph76LD|UO%6G-k8vMMPiGxI@N;Vu5LVNrDSeNXy6RE=hx zcoy?(xH0*8T1;36ow{#Torki!Lxm@l{Mq# zxk;a+HI5y%j>Q3azvlT)^le#mj=mQBr*q|Y=cqo1!#y5&m_?gOUN^FAz=YvY0JW{a zSz)}5;x(U&{b>nAj^*0kQJ;GpoHlV|O;1l3`2rbQ1^Nt8genr;;3P%Ig$ipME~HoX zkdvEO@0O%*+tQl`p7e!_X`tFydvS`(L|Zmh5p^s%NMY3&u*n-OfODSgEhsHG&4-6h zvtEyQ)NT^lQ!6WXsAEiHK61R%H*2UsA|~?ZbF*+!cO=0%Cg|1gT^fbJ_v^x+8MO68(PF)He|GIH453< z$KDb|@A=~`p;B0qo<;*CF87a-WlS2(lV3pfu7t=|lT7nyo{+xBJ|C!XRU#sVMNB!h z6*f`a6^J6!O&1F$?e145)HcYnrKA*%U6-iev|ji{;~zV=p02k zpN#)S*x#cjpyqa<;p;X_$VQyz#wyWWI=i2~`ldFvhi!1S_<7HvK-tfZ2M5oi z%mCo~rt9TjXEp7&qdWN73UF)^@eJ-Lv=)QortGhgBnLeZR12iGupRsw*0=L@t4xJu z;Io`t?&&AuYQ(7==){cAS$`YRYmfzxi$V~#8aSA;1(P%xE$WZ*tq1PXRPG)(ZFX&y zP)MBkfJeLRQIvfD0u1#fFDIBl?^$m)I_U_R7@%dq?3IkN^4ectpj?Hd1KxcfX5<&F zH>j}HKLGs~a*)X0%A=1>;6f#G=0saE&BD8jYinxmF~5P0>rjGdhDt-aDW&YS^XWo^ z)jnZnm3N`Y81bR%qV6KC?9ZPmXMzs4yu?jU*J(5}Tq9xiHjL(R#(i2ZE)R6?9Jt+Y8F%<(|>ky(_i?>NUh^EGmvC#lttAASPkcj@g=U^l`x^`29IIS z7YsY{<5h3H?9m6*Uu1A0PPFoUX8>;E^oHx^-cZ%a8bIEpzusCrx01|Q$K(ZGF`KVj zK8R$XqCo^c9H_y+4vL9>%Q^m3jOPcPS6BoBm;vv;lR3&F8>xE`bPT@xcS>4xUMJHU zzkVg>2r(ujT~TyjJLox!$pcd(S-sz%%N`|g1p+Z%qT4?8Y&}oLiTelB&u}-pK$1q) z1^p{|$OX=pwZ&6CNIIpMeZ-pyG}k#QsNgTK48w^<^R zCDmP+@y^C~r9ESJhd{-WJSAwWkZ#6kmFspkm`XC1U_n(|P*|PblD#EkiVI7?EymA zyoD5w%myPPr?R%nnjQ!fNEUuFxZ6g5^{~l{Q-hDKa+md;stMA$<*(6uJeUf$;(?AL z21fC+Mh!HbTmx+h3FqPzL+tO2-Od>EBSFJ>681K*XYSmvk~w{q9Ch^StFx$)4ip-A zRrQj5{(m$9MpY2fpA@&_waS&KHr~oLPRxG(QBOPP6MX&hRjrgHP$*fiU+ZaJIJW~p zwayJYySKC#H3CUBMBpZ&brHSKn$7R|j%M8*vaJkIk0$~V=cs2X_aITcWo#H?HYIVn z48X{;uhSC^T8L_F579nNA_6>?XaNzs9cHi2LeYgK4riOf_0F1@Z-ww~V*huVpKKIO zexWB-mLa*#?QqzDnRX#=dS$Hn>xp)mpTDiz>SqzdK$543DpDtg+E2A#8waXq+placgd5_w?BmpQp=aIKy+k#cFzfS#9-N#qCqS(~g`` zU!k7wr}={Rh2FmW(Ax&j;vRK)QF(vS-?}fxS6VHrxOeq0&%Vj9+A-E*lNKKH2Fy2z z%YJ=t<_|1ica*oezpGZ<*Kqxp=elDBUR)-pP9MnmqMCG8{aNV#v@dQJboW~52JX1= zwq(~2nwqY%?mt;&<)HIdbX))NRoP}Ln^)EO_jUPHxR;C#$C*vkU3pbkJsg+uM{u%j z%fSrR9Z{4~fxAe+qFYL1=&=Bv2q;tGi;@fm4#BjhtNq79ZS1(3YVP{<^CzT$w<^|6kM^6kL zd6JS6dfv6|`bTNje}?>`qCJH-wY0Q~GkWNTvvfe>e9yh^RkGJVbN8$Qw2a;QcPpYJ z=Y<@_qdyZ6qn=l^@btIrww)FB+`~?XCvVSGXR%c4uvTB}35XKLmoG#KIQ&G7<&qhb z)>$W8n}~<$Rd4?XPNC}cIV92H_&1ML@=hBP#=b%BwQbE|zC5Zf&$uxh{J1`k=f06l zUxb%;$I2|`S!<12ck|F%!Mxe77`8j-dstX)^hegZmsDN2 zJw-XZ>uxUKSs?k*X^n(g6qHzhC9Y({rQ=5oTAitcw`doE3$F@$ z1Bs>;v4!?rOSgK85S)K(-?)>IBaNZ1E~n3Vs7;6glc>ZVzP~%3pyr0}g%kjcw~40| zG1zZK*EGn2b5XrW`#}n7Geh0G>XDnjX(PkykmT}UF%3z^HVA#w&)h+AINXv0X#B15 zCH@Pf3kB$np-wL$>xEbbjO2qI&mq6WsX+qib6T7&E^3BLJcmu;u$2f0M@t@1TEs1m zjdqXy_1J#j&&Q~!ss4>6%6#aP)6X4zt;ciD?bG1_G+b}pxE?9|bLIj+<`F?u>9(#T zrMscD67{i8Kl7Hf6%cu$S3oZXmLvVw-{OVAViI#{3|$#oXHO#$L3R@~F(^~7)>}bk z)7~D%>vXf>meX0&BFLLbf(L2L0Em>QE=SxK$^Y~x=45ZxeMh>}oZ>bc|+4&0mnHsJ@{5Zk@ z<)!4|3yJK2h%g=s=rTo6{XTgQ_y#JV*G|vQ#;ED_;=LH~fny~$CFNZT^!L}ykCjQ+ zm6#$F#3v8-;(f9A&)ByNPQIOQUy{7;Mn{T-TPV?Sfn=a!lJYz=2`_zhme85Qt&l09 zAUX5-083j=PL5izRfFspwAy=hbcmJ!MU$jVg4;Hdq(>&*i9SG1gya)pb_`1b?8>_Bzr8A`e1bKdqEzP3OWBSQ@EtgAgwun8A0T z3B|AjGN%(SS<+>9A3O@qIpcGk81rH-2UmyIp1()~L^Xz<6b1;wBx3cQ!KbpEL|;=Cm1zT}j{> z5mQ-7uVOX5=UvB+gC|yQJdvI6cjC*c19A50PB&&pcJ&>8lOZX7{K=hZED@4&h7MID z5bF6GW(qrp0|2AvrS@Pz2_{f3E=9b#e@# zyfJPLqSJMB;^<8v%YSvY9MA(3003kakZ!n#1v&sm>_wrSS0Fq_4TR2~2-kx{LO9vI zV$Jvs3LQOgm|{+XOwy^n96&l)b%S*u1sl%)Jbh$h--RwGrGR2gJwS2^!GKVW5=52^ z{sa4pm_8tCa4ap^OfY5uFDdcibNAe)`}ciXuQ)3SuM^c~;MCDcM=q`P>*|%)vpmTK#vCG3#!r!WuIq1H+)k@nxvC%`U zvQz7$NJiE@f1?DPwmuVv5b<^~RCT7{C`&qoJ8eCR=LBxalYt7iCZJwLU3S$Y{tBUS*bDun7 zo&GqyrXQ_ zR(x+0ZFNzmPrLg3gV^XTtuB?wufpF$c`@K1J-w7Y6blm# zEF1hR2Ase+&@(qD7$}xrtH+q)`w~+T=vjiwM92NGVIP0`_K(0r{-lB{QIC9gfc`Zo zurK|p02#8muvY2Tk3{HHi+yATt5`%Y7*SCTA> zwcCxpQ9I@Ry}wHsTI_to$5bOfbpEVWMOT1;u(4Cv#rzy0!z?L)EY((?%M$Evmpc~L z{JsL48aA-%yIw~m-!IF=QO(P$SuJE$L1m)Pzn|1HEc0-S%m;qY!bJqt(Hvh8>$7jL zY*b>u-~2s!FxKU0+y=EDLdf)!s7wh=btd~N6BHaPvhg)?fUnBcmBo=7ZnN& z3Pm9+j!k~!uBR!O6va?Gj$LO+FP+7*yOe`ILC@qMa(Mh8 zl#Hd2A7BrgE)ZvBvTFgy=kSs$oQ|XYvJ;XaMbN#-ueZYp-%=sUw z^Be(!x{9s~f@q!UY$Js-V0!9an4k51e(;tjBJ_x-2LrZ+!~-1F?G6;6y~FNKejjrM zDQ@sOk+c)w(YZILq@d&stNVdbQ+TZ=lmfUfK$<=c9McKR{4XtlET9rXH2}(uV&=Dx zs>Eajta%XW$U_F)fZ74;1?7&qc+Ejx889*^rl2|najf25+c#cqF@woXpv>u{7F8t{ z#wvwjDd2Ml)k*9-K%=2Py~O@YHRdEY^@860PngKZDlcc?2~+J`@pwHKd?0y$G=P;T zLcqj8vl&)4irpJi8^dnL#YsHpJ`c@damS_Ji&Ilmx~stSldcu_(Tj5%J}Az<7&9_k z3Q`d=j0aI}qP(o&eI9W_v;({D#a_+Q|Kfz@?NSk#C!r(K zeXn|a$S3YDUYrn4s8C=H(iwYM-0grjY_3#@>bvR-GQP(I{nSa=I~M#Y zGcS5JF?l54W9+a=kMr(GWMczHDE?FA&a!X3DrD;zfB?XVKU$+Pqm6u7_y8y-Q0C*G zz{pB)v_h2;I@^n1hDVQ=&mnw19l}(o`}Cx?uQX3q?cw;h_qJYo;Kif z1TurD7+cVfncaT!!Uk8_ZqNLI`1l!*YFw+iT$Snx8~N|!)4WFcje~HKbx)7?Nn#)l zkAQF`pFl-q?JBj>XQ$JegwJRayRW)Y2PLRez`UL^Tdl2Lr&)5;h5Glyq_L0o0HR6=y@Pb$%Nr$l<#?04xfT z*r?68jyXRc`&INSb?`(=3IaSs|Q0LfTb zcOL>p5 z&u*UGp6mue=aY9xR5H~aV)$jb;(LPaL2K3m3HMnQ_Q!~PE!1R+%w|5^ST}0Z$?Vzb zv%br(Vy#E-=Eb5*x-Tbu#*T1wNL_6owL3qsCDe9R;@SOaCLk)_a4oyV)mnFjnkBI5 zY%lF@N0vg%)v1mCqjokEJp6qdOuuW^ZDTuV#Md=VeYaVFqwF_>!{YLT)`C%uqX&Qa zZ&fL>I5IeG)517$h-UaAU7_acf)+L{3D(tmraK2;p5@2Kt{FBO{ldjsT0Yhv&iL)) z?upBMjxT8UKaIP>Z#70suVLKyY1FPXe-Fogqq|1a-LJ>n8;2j8?i7$7T+W(s&o18I zKQ_Lxuu0)T2|jG;=1LJyQLEY8os}G69sRG{kGJ=l&<%58lL4<>mBL=z*`KtJ^`ZT4ORveTH$H7^7S&Wo4k z|Ii&?)ni(iiN?&&&+qAr8|qyrye~23u&nf2Az-4XZj;5O-9P8{=hx1))$&%-Y?!Ql zYRiKj`BN{(Wvm1%H$6W=ZGQYVZ9BU{C}YI6zdaRRf`*CpBNpYW{alwK(_S8G42n+> zR2bWpd@mdQ9wu9fKa{-6Sa9uAVv6a>N8RGJANfLrntHN3MpsM(JnN29?YFzFHZ$*K z+PL6++K=w-DK3&d zhnY|n{0-__#xLk(b;L2o_=e_&?i#4j(2;}*gjV$$@AHBn;*4a4Tsj^|ytOlvP|4)1 z^Do8PJM<^{9~@%;R&|rh=#gMPD*5Z(Y+hy6LRG08;*~Vk!I+eTEk>HveA4?29oovo^Qxt+)Te2O4ed>TWb|x0Bwp(;Kfxnk&Kr-iSh7;hZ;*W5K;#v*^f3{SWmz4D~ymRa&+^9oQ~@};+(`fBd{^-4Ekh5 z6m|@&(e^z~3)6jnp;HL6cMED&6--<(+Ir$LQ+e0)^sdeN;kA7YMd}={2jjP z>H{w;D%XskI|9YJuybPv7UEkCW(;Jc{q*nkMC-6G)`}|l0(v|$2@!dLWr9w_F-k?~ zYe^#O8=Mi4*y9u9o{WKv?|GJ1WK3^gmBBdD0O=p>-&d~&a1Ky_GW3udNn-3#2 zNUAWZS8j%s6qzJ)oTq2e$LlZ&G>QPE*U$J9)Q-<`aT8mAZUKEp&)lb#dQTe zE#N*xYdUQcIUhtkALqZ@UtHvy&u=zoe@pZ62D!fhi+@*;5Vt@vSkeh8hoChKw>cw8 zn;RPK{jM-C??ZcU*37y+IY@)n6CdiZ*pTr_p;MlH31NA;oUVGzuKHU0nwxgG73778 z`h)}k;PeDyfok$cmW_?g9rPn9T3`nuMK~xXlPw@o-UGxPXv6j6s1GXfTtQoQZUa$k zpqr``cD>-LrP+@v^a$_zDj7UDzkCemXhHXrse*Wv(XhjhijFv^h04VHN1i|m>%Jb} zIB52E7xNw|49R$t<~n~x5z`{k1wrlB*Vp$KwpPA@)wvvskp6U+-e&q+zSX5P#iPJ! zI`GB&Pim#h>t+fahFy0spOZzw1O5(t| zmaTjaY6d*0fI?N)zeT}JU;RBrwWqX_KjfkD+-yHxj{U-qZii)otBX{Zr+>WZ=CgmG zd8+Oi)igozi79LQ8Dqu0`-=mVEW3)#nT|N?on-9~aNxyXC&| z_~z5OPWqirW86+-VtNIbVdQlxVOinXedn&kTnSqjxv1dje{q4^F=1KhpqKIv7FQXz zZIZWd7`jr*Z>rkIU;8uaE&cU+v993J!Ra$+$KxE;7FdNED2r!v>nw7#mG|7yeinB{ z_r9pi_b&=}CDYap88B9|)E#n2Zl*hF7qxfukGfY?+F_(0I(U&dT`W)B>ql1GG zW+*I_g-a}WEl%_ZHd+P-Fi8DQ#u=A>h81u#8NG*;FboN(ye7F~pXSYnH3}0~XI<^; zl*r&aJGg-{RG(6^VduFrW>iF&tmU8=DwVZ3cO_+pf z|Hz9Qf1!>Ye(8EKBBid!z>ALQ;Tt|LWlN{zF>3g5ySh_-?bEZEsYS-KW4owoqg~yX zt!kApC=y#Ct^MeaOqOeI_4CW0`uaN?=q71IbxsJ!%{Nl7U0vKWKQGoN&|n!dqg&h| zED~(U)A;#@hObTY-~Dq>;cUm}Jh^Fym6YXZvDn>}^zO_-{WHm1fUJYfMqP3@?k~@x z*>U;#KeRcM3mup8bLv;T7m3H<#AfqbJ@PmrP_5e;(6LseIV(L8K}s%XAj%7=yzc!}!$ z7?FNAm)f!H;qQkL7j<-iM(=}J0Zq`VWy|a;Y4T@6BMJr_>ZWvqJwPhL-gfGmyI%iO z8MFPD^SSa39>zIER_Sn40$C6WeX}`w@_UvZYN_`;+8~Q_eftI@$E`5cKy6|Cb{Z44@RdMCzIij-{meJu#fdiO zl+14#3MZhnDbI^@Hj z<2v4_o|Vst>(IyDOqvdon+10UryT|ud;ebcJ!04P?PaZO6@`X3McwJ$R8_?rS6C+) z>g!WgCsjph)U=yk-Io>*soLiLo|u!or84ne63f+EQ<$8&ygxzSRu4xRA8rm^l>x8m42UGd?W5L_lzir7e-Yn z9cWB2Jw|LQ$!R-hih4|Z51UUPE{NWA(&xSw*##l>aluWJa}TX^gxtFG#t z`2UFd4sa~{_kF8K$|@@>36+FoWv>#FGEyNjqB0|UmKmaivMXd3$_fcl$_S}!D$36G zg#UT>{=UcmJsj`x9=+jt?&rR*&vl*GIR-sawzwac=0xe>uysEf&mR^h2AWr-mcE}b zo5J;tvxCk~j$1hEm1xq-)k}7{>fcDMwX?;!#`a=={YADJQ;~v-$?*{?@>j=h@_Mw} zOJ2;TeQ?XgJ#hEDEY}P-y>#t1;Ks8Q7v$)>$9;!}r5C65#u5b1J)yurNjA8JidVyO znQ7naA1ak!$reptU+p}Uv^KuK{uy1A^B#%#UBujZ|MSg5GL#N-TXaReuUT!TXq+2V z$aqh8^MR4_FsQ&htS=kv0twwC*?j~dr{}aiB=x@gR;rK2WnK< zQXH~e{jM`y9iV0uvQbjZO8R9mw{`Mx$mkJ3QcEOiXDa?TyWFC+yr+$3L@bJ&uLH3kp%gC%~N<(E{e6rI@ z$6G5Ohu#FwcwevclTk;vU+8j}92EuV3KFW5)9gWI%7B)N$++jyQBfGdm_<(2?A>$g z;e>0l$C#(~$Xd;`n?tT5Ky8dFsC{>8nSZ2Bj8@(o-K{{K>=&LvV_@)gbYJk~T&vFD zhok@I&q2(eeVhQEHV$56MgD*b8PvNVh(^O889#%(l}yn_ghoBei~ba(0(;(6pWY%0|P6yQ%r7< z-$0>UGq9n#m7{-@BEJ8*P&kPBKp_kaG^ifa3PDgK*9xPg!G;LdJXtbEE{uvNId+_R zac(l7l%XJh0dau!!!Uj*ea}wez_~v5;E2vH_AF>6(T-C!I$<@2NhA4Wh`~Q%k{q`U zzXJv1w;k%{BD;gl2gk-}2=4f{REImgpHTWl;@S7T>>d~UG-p7i#-~E>@blf4JCY~o zKTKF`=CkYBbm2eg?>o!bm<|CQJEoNcDsm(9FEUP@YhSw-<~Ks79m;$vT2E%x(FA^H z*%!{oT(h#a93P7~qU86gam-f%DwPP$WL_utJRLzL?l|me}(UDYVOL>oJ%5 zB$TO;RVe$+_t{qU#@*T-%Nl;Fv&Z9XF>`{m@OAel`9cg(Io4zjpH=5gx9ER`xEzNm z$}M)ODth&I)Q@@6LSHcwn{`BbT0v?5i&hTB0VEiJep|x_c%#S&~b7nc>;!lSZef zJUi>8b`QuniXN06nQ4rm7pvB;?CS|VBuoEPe?Kvnyb6m+4Wm(^qnHY5Pg4aXqmk+OMi@2a???<9!CK*cMFO+NuG*r=813 zhl=&Mp|KSk`W@iy02~0R5GH%*DSYm)Kbi)bb%+1g+nbKmK?#c_zk$hqL&Xv!w%rA{ z<*8M9xpB(^Sq2`{0I{u;yL*#`6dRrRb_xTJgqP&ED1qJmiTXjxvJ)L?rSA7Q9&6(Akf~{RzLaCjiEd? z^!nnaZ&uVdzE^NV{|K^%60hi=2ZdUuU$!(XZsJ|D@e`3U&}If6fP7ofhHM`|nS^RJ z;nAUD(Czv&LF@YjkS{c9hp#)j+`gN~-cJ>p(Nr(o89%|lh7OjX2Y{ek4>m+d{2?>M zefy6{3k0XHONVzJ5ft&M_X(CCj(wDoEDFD(eh!}F3=nK>e(UFaq9o!_ zf!2Jr?(O2)tQ$IAq5e|!Mod4q-4)sQvPs<^Z8bsA;PG81_k*g6uzrMy7*Yq!5{2Dc zVH2#(z#GozlFL(_+2h8Nc7&|&=z=7 zfADMVoE)I>FB^ihT{F^(CwNtAN|(&^b`Z(O$N4p4qab3@AMs9F*tzzw_hnqL2jXIKA1$Q@^N$TEZyZP3NB(J9~aumhznC$8W*F%qnuYPih{y95DODz#~JXT^R zWsDSZ>AgY}v^WQtXQuaX`~H45ef0PjxofdMbW*-%ueD`gDdOQh^S49V)3{P<@1g$1 zN}lQhr^d=2Q%dQwY=*(uSB_q5to#n~TkrP=J~`4}sgmXaVViWBJ1d&Qp<7nhdjuZb zI*i`TZjm7~M~UtjmK;N~)Psxo+QjSbgZgG)NN?{)_G|b_JSRpC;Da$HqVU&3+xXeBmpxEaRtI~zk2A8@e)XWTCVqfp;4UmWS|&AWdekew`Wt>wK9KV05?g3N zpYqj9yJF&8kH>u3#F*Km-d)kO0X!t##+a=CwiuQ)*K~r6jV=lqILe`lM$NpTedSE| zW;}yeD#jjc*|hA&D4(?1??je1`A{H@7(v(Nq5OW5|wNB%9Qq@HH3 z3*_=LwbM_1HfsDUFKEBnQOk4GJhVGPKvB39GXAHwZZx_yS#`^@WEQ4eF`o*1&Y!6o3^qj??-GA!H` z|ISbn{HI~Gh9HB(RR;zJShW+eN_!GOA^1B)@Ata4wzg?;GK!+ErueRigJ;4uL08(q z*PQo(U7J>WV_of^&neGw?|)o?{DZ&|e*V;c<`8;352l`k8uA~1%eAr(=h%Py?utd( zV+$kWl8bIkeO4i(Be%CMrgj{`?i@I)1V`k7!iSRHZsfJDOIzsPb{f3N(@U+i@2wV2 zl#qLA67sOVkAJSIY;v1o#E@695iMKk8s6(amgVYrA(#W{LbR)E3jYD;SdY^i-^Y5D z*oC;ehXR^5T)4cAY_Z&_;6|rUkLJGahJa*!Ir~`aPwU6GEMpb_FEeAU$wl%($dpgRTB>tMJ>8S~`5Hvx! zF($qq7f0$M#QKEZ7HP*9x%u?i4L(2Hx@~N!qTq02)>&amUT4Zop3y}c+1r|p5zU{? zx_#^)%&k1uTpDT(sWT_Wdq{;Z$xMJ01Mk=XwI4b?Dc5^3*A;OB>n-vzE%g>fu1dQx_w%oDXkT?)-+dGd?F>3tlcQ@kCqV;Z1^%>w&ZRN zrSRaeG`71J+$ikPzx?7e=DLnTpt`R-u`{^^FZ zB4tC1D?^cc&3|#wJ)s3ftg*MKujy7A`Z%(+?zrS$dz3iGs#Mhurxem-Cy5uMhpAd7 z1K7e}@6zI1n=xZBy>N@y18ZBd$q)@H3v zAbPn+)0|`&^t`fORTiCMMZU(p>a)Gdp1NxHFkRzT);Fwrf%v%%P7L+$auRizHXPO_ zkI5GFS#q=AkoPb+FO{tLcCGT^htZC6QWsuE`Nof0n|bvda5gFRO`abe8NL*0L(L;4 zM1R71AOB4CZ8@2%{K^{q`UgAzVY53L19_hv&d#Ny6pE_s6XDn75an=Zm7X0R6!?#~ zW#^q}SXsK$?E9U3AzD1$z-Nz+v$;lME&?3{+*E=dn{Np7T=1GckUaM75c5$@OL-)hd2FfLj6GBMZo*meJK-gcX^^U$%$;&=5Cw^Js% z#d;lp3^Mb@?EaGN;wR2=mwyZD7S^WVWJGENE%e~w!N`qu7y#_S=0i0%dnSFgdL_0b z`FaKX|N02c$|+=NUzLP<^!5JRw`G@xq_+d!Bjmuq#1DUi4K@ZvESxcwptyj{@t-ms zJgCaW-ktz7h6+~yXkJ(5Ne(BR&);ug5fF)wh}$pB6453RS-z_X%N9U55Ry#?(W7gKZaITlt4Nfuoz1m4yq1{BFh_zerF2khRVcj$k2m$l5XQ zVlANH3f1w<@8+s*cWrlhE~xU>w(%t0xLal3C%D<>C4GN(poA;a_RYww|0o`713s2%L7M=oqrIQDM z-F>}U^gNPj`$(CymmIfbO6KXOrM>S%c55>6#WWos{q?Z_A=lz^8i}`b$}ktEM_JAN^5E@$=Qe4r)^{gQaH+@c%}Ugqvbj)HS6NRkqkgxp+R9iv4PqN?oZIY6sV_KX zo8i+s{w>Q&VE*JKtjzkA~iJNtLJQiD`vU%e7v@{v<3Lj|9lQ*!6*^ z;$@`v$%t6Q!jxnW&{ih~GN3oOcEo=97i9pUltxpkC9R{9BSuGwd%DXmu7R@L{7Y)I zIh)bT>A96J=iYNi&zoRt2JJG=A5PwxFR4bAeVbe7@Yy-YRAG&Pt~e*@DPQZg`W5K}>#KcK-5*CEz0ZrH=v%Uylh|7TsU4Af^&sol^ivD! z{5ac_*CZceb&M;JM}H=c_e7}frCxZwGEfcWhi~N8+C zx^zMN#OEZ#N=1)(5?r-9R$wW%7_G_eSF^Et4iZDErM<^ZsXi66*58QVYVXI8f12Cc zM7{J{T95rB8)NWdR=4f6HVJsT|FYD>u_Z6Gp%|GRNg_W{E^Du5i`p3^Y};v)yREdF zx^`5x)$URLkwXgI<8Majl^Ckp;%MhjY`Ongt=leW=9$H+${FAOuVt-1Z${)yq zs@`$-VXxYb8L^Ry@e<|+WF*%_guV#~>LF_=`zVxXz4B1K;L9C$@#`BU zt3hkR`iOxc`M?_jhDWwX5X|0?Q_^Ac>s3mNe)A7;JX=8TY&76h?~+OE+y)0JTIi++ zZMKp&KRkZ@dzqFc>*=M_`?9gPzz?`)KzVdx>$PuZJJLVtyuS*A8i?xfpt4{u4Yvrz z_2oTqiGWB32769eBe_K`Mn)idCe!xzskjqQ-kxh3*xVjA>T&;7+O2FWW6F1Tos@RM z%xg1hMWYiTb`-cv#2a+Vz>v}cR9TQ{A@%?(HrQNya&SH?bjibAbFDysKS5Ez8sZ~1 z0N}WRc?6y5>8J1s1FE*Tgy%crQ1$D0GheW0YI|S8l&vZDCYAF%cmG*zrO^02?yyBC zO5SjI?=zxv#qJEmNx-l%H-9-we^6t=h8NTWH27eC05-)dAm-`UPMkQVu}ay+Tv33t zQsYr=lJ?kxV_WXP5gMF6F#BGdtNO(C!$gVJ8JL-*NlE$v#2SyzffEdO$sG-)ocbb^ zjHY3O-dtOfn=6f|x^y+MepO z4Lv+jA;7c+uY~YIA(+G97h$|2!vb?#&Dx{`SoUJ=K6e;k`Y?j_2K_?5VMLXb-j1(x zPL+KCa*STqR9CmWh1B~)zgAX4BxwUc*?p6KlGRz}gmLxC@pp46Vy0jn!fx|=%ArhfXXP1?R+(9hV`+-^zy%ccQ0 z^To$tXW}8x5K`%0D7|?Qkx5%SB+|!a0ck;B3xz*lJUC<|cty`uDZpKV+8^=vI{8PNZd zaB;03bdF0-RHqNmym#ugrb>90|D&Y=QQiJ^L{1lLemeelN@@lM7y&tX zcnB|fG@$sQ7~VRbj1S&!-cHl$m(A1WVNqpjvr~H_t*qOXD?s!nn#c;G_kNIR(&O-JDUcU_fOOtG$#>EIfgXjy7q#hmPE&`p3e|$8_`92bF{ERM zM3}3F79v-V4hw47G5FMXpys-BG+cF-f@Y zx;o%}Cu&=O*A$ZsBRtUWq0}eyFW(2z!p|0Us)KOd%bUR zN(VSdV?rK;LSCxvVC$k_!ij;8DNMATO2&b-<_jY;G?^?x{G6VP6YM<+uriL(+@~3f zUhf%Lx`fd%?(*!kz|zv8D=T~Z~*15D&%#K1K%nV2BV*j zU+A*w6OVhVjt~G0GdMTZ(D8@vQs9Ld0I>rA>_kSKJ0sGt1{65W5t4R7Z60?s9+qND zO{k6`>0~QU32lMIfbF;lF71cbR>)=7lbac%DYl5f`{JaoE;$(}#4*QjZefF*0_Q3c z)UplLrtill&G$mwH#70|@GH*+TD14CZ!r*jN=WRCqV~;mg4(&8h63tWhy|cqAT)$3 z>91*~^j8!2bJ7GlUL!$1f`h}M;S!k1IIcmwm3(MLL3z%oodZk2{~{AK)Uqg9bUFyu z2I0I#Sk}PW6Nyep%f-fXxEZ?rJ(@hK{~3A zplr5n3|RF>sPSpv0cH-t3}h+HA{Y~gi0y5-`cwTk9a*}FZ9%r)yHy)ewtB!!K-@t? zhDFwk)~PcrKTN5=-N2dja+-*-xut>@0LS4~diYe(-g?vILmlwbHDxq$5B(N-+enSB z8jM-L7gmp_Q*1H1d{C?F4KC%w5rkP|EA5h46rFWXs5Ih@U~S11rF*G(Z;SGDV}NfV zP@eQlzsFwckonM<4qV||Id+G``&tq2aZ|MLO2ZZfFTOpSXUV(2c&;t&^(ni{KU>qT z3$xj6Tj;nnlMy<$%CcG|aY1Bin|V#bvF=WCR_dyA6oNF4PI7xb-k{?OK7H$nEf^2N z`CF>a1*hv1Y<>PiU}UsRZWb&v+tAs{WuoeE)8z|JR!@ajUT1fZ&fO8!_>pN^8sS?* z9mOhAPuCsFt5Lgm#*y@Qrs2#TBT3*~V=rhcGRYqq-s<|qn@xNB+@^eIa*!UmI{Xi% z1hUZuEZugn9=_<@CTCfAPisZK(u+GWjc%)|gUh;m_cV8lS)b(PYuYO|P-LU80Ahk= zQcm;NxjgiYS0|@FG;CLp0If65cpv>4XgUhJshzLhV@@9k%V=`WEsddf9lUq}yawC^ zD4ZawFFK(t6bTI z0R>BF2PPn-!lD)YT3*UaGoc;zmaDl4ybY$X`ZyK-I#Fz)V8}LJYAHPCT%IudC_@Oh z-G9#WEebbIBzz_bf_#A==~k^Z8c=`j(cCNB7g}A6nO!_%a<7Q_rdo;DabI*)nYLwt zRUw=CC|AV=5qPj$Yma?U{6`IxR|q_#MV%^cpJAER+A;8H7(N8gK3)YTi$fchs8GVg z3=$<0n7|ndAQqBM=)#<>zH78iFA>3wLOX~xB%VZ^VNFGHjtAA!&`1!g&woKo+aW># zW($(p(5!pH@Ld)?I4SF&<306)sBs}s^o}mG%hZ4?(rw_?*Xci(~pyWYv zeB#NQCeiO#IccacHn=>1KmtfekWe_}z-r=&RIPLI)zO;GWp_ovi3}L^z`+l687%@N z=wh)+IXU>-b~ zV0Vn$7l;j982+=X!q5c35AKxif4*Pr4fk;kqZR^lf?W3m04cVmhya@BO^CzNL=fSr8UId5_q6ouSSd1 zZPFv%Eq~Gr4MVxBQLX{(t^ymZQ6E#+=LLyI&DItntvEVh8rOPaBp9kmSaO0UXZ2Wf zFmlgZN6z>Yd;iWx`_)4NmT*b{9s>U<*SZx_ol*y8M&J6_Lt~lhv&ep+R4YycFM5VF zfTID?X8&pHa2h`LiyDCuGa;%VyZD1kUC&Z;1iCY(2B_rjTfymSgb*%3{q?B5Y@Q=b zLUH0Y-Z7z#N7Dz#Uh#lo>=@R6n5|n)kTRE!TXOL2=fV43s@aeyTqoS+kN`_$qkXi7 zaFyKO!0ao`uHaqpmL*gB6OX302R0fTE2$gE64ijhkuT=r4`Z1qh)Zfgvj<;3_oUK0 z?bSRi+#PjyCS|Ko7$lWVebYB+z$ro)b;43}Mvh)x56(`+-Xv$g*2#jx!o#(@xvX#Z zRLgTuPXEAYRzp}uBKRKh84nn}ZA}bvy8s0vo0J# zMasz8W}nIvD5OjW*_|MGgfoIRyrE3@9?46z=w1{$ayFmE^Zq96in~SAcDh{N=`vQL z15V?;8!5^%3hq8+u1*0kQLAzzsXV?oHuiwkZ!}WipUCYm>zmDbfxM>Q89r!m!6u;9 zQ}N`v5xFJJ)TPdpXhla?)%d}TKMl9sGolq%*SH;ns;8aH$x=e3?kla%neo0V3pXsM zfzYlm4|rjF`;p5(C_3+|maUPOoY(|0Uuc0t?Jk|$eV_Sf6=!bTy-hvs^!K5)!dOx2 z-NQKp)6|VlSRY_h%AoF~)M<>^jE~ls!ttr`h7X^=lK8O1@9U^Qa<>Sn*I;D5q`=GF z1e$H!vM4n~5*O}0i|5kM3UKEMH^L*xc+-2$Z(O6EoYG}mnvUPPIf7%FkgY-$E6ur? z%=(o2<~^247h8YSG%x?Ce;)KOaK9)_xZoROSgy}NShOC##AskZ=%+x|1H=K}{Q$XS zsO*;S5EvnI2e;YSg*9l}F#rI}BtRA-3r$<#YhrUrPqLwR%kC)4Kcp#cQG zOceUoR-}{__xhrI)Z6CJ;iNfR?bo>!MJbq~Xu@FRf4dBw7a%$+WhH(lb4$D5(b#t2 zNPi}LC2g3dfj;>Wlrtmn91Y0H!MYZBHsMh-!mLwi^1Bqup*^oL1z}VL1`$ zI74s;0Q0d!#PW(22e$)6mbQ>8X{tJ-Gta^s0I;a^v)8y z4lIf>NZ@1#1_SlZ)}zk3f)p2FG!1O%NyO5)ukY9CU*#w<_`(znP#{x8P`o_TmYA-| zlhtVFc@B!gs?x_zWD7#_%bYt1eDrclebXeU050i^U`R$OXN`PYS`Lfg15OwT7s`B!MfV-&yaqn zX_Wsmuf@TbgvkLJ&H>KG=Q%^3eQ+%`EiC@nfhE=2pW zO4>Fzyc&3kJ~<7?ckMxq>HFUnJ*!~4>OQ_|KVC(s`u!p6)^Nu|h5G@13NWQ9zYrx8 z*V8x-7Rhmb3bMR!ZE%^zQ-Uwt6%VPO%Elt|Jk$1S9*=nDG2Y+?0i{B)Vj46~z>`0Jqjklt15+bMH@9Fk!1krh?-ku-x&8s@!LY1F zYhnRw0|V!TpI(oqiNRM&Bsvv$a&7wz@#hh8(m+k^km1fssTIP3^Ii9 zJVNgN1~(%tn2+A%Ri>07`b0SH1~K;F9wSN~F|ERL21qj?GwAq3aIq1%Ry;Ns#- zIYGw9rNCnU(Yv0eEq4(t&!}83xgH#1uH_AAVMshs3<#V0m zNMcF)Lza7`o#m#LTMk;Pbh%1Igk(=o^D2~nF_BT}NHdoFphipB_1Lk0tfRbcXW*W^ zsGGa>)wrAwPj$~)i(Kmf&%M`6jP9lz1+9Dc<-5tRhc~=Fh&yYLD9pYf;d;JJeFxcbZnqRu4dQynjkcUy3AJ07^aaX*3 zc@J1JncQ?Qv=V0rjfRZKk3+%y{=QjBO4I?KomdxvhJY|Qh-rXesjjRG{%-JemsqIu zW%PN9(uEaMNQRJeUiA&R!U59cu@$DZc8g8l5ymO#;d!2mgQ_nsuEAaNUzl1w+SrWt zY|hplBhN9`z-1bangtW@7sXBP;b#cDxVaf_Hk+QBdOw?aO6a=9n`~Yhgjpam)>7q8 zTct`6Tk5P24}!7OE~wQk9ThDb&;Q<_<_-|BjbT>F3F>3ypSmfjW zH8ytqtw@;hT8q`AQ5P z$%wwLb2VWL-rX2UDj)EbqpzN>o0DUo?m#9TjMtgD`!)FaxVX4b3(TrV>%Ujorv(;*6B#IeQ<$W9s=UH6KJ}toai28zPlJlSg2s8^6@d*JQMN#CNIq( zNrA5JPOba2-RrFxjFPanYjm3V-E{ym+|w;nzbZujQ``fn<&Ae{RsMvE_~*h7^_WJq zc>khT?{DO%@=R0}e;L3ahu41NhEa><0R{1Tv9tF#bx8bpPMZ;cw$aKz_qert_iVU- zImUII7cjdE8W9u_z+RQ)Rsa+Cpa?Y?*4#%pn^po2%03~=XzJR8&GJkgy%n_qEIKe? zfu^8KMH%jI$mp2#(V7U+J}WF>y9cdA)oBnj{yqZQZL zu#h3RhX_r=b`ijm)nN1de8~li`B&&F(L|G9Kf3g&T?iwvK#&eN!(kWgpVNMN^M-=R zXBfJxt3b3tKVskWDb<)E(iaSPIqvvmZsNsXjIfeWKNfa;zcF;rXnu$|WSB-Zsb%M$ zHZXA89%hO4`@x>`n9vE$95gPsYu0*d8sM$;{k9fs`+07dGXS+lH$b=|^36`n%$#~@ z!`T=!xlK$65=7#_L(nulfGNy;7IvZr)%)g^gB{_{hLGf(va{SiaE@p{zR7wX-viSB1a;s=(woU|=?J>VdOS)ahRgm%Pg z33{bax6WZlLWoHrWkx}G=TOr8yW6AfBMX&+ccb!Lw*Q;xf%N&`n*w?L3=%ld7-QqC>^fnPc(~<8RW@?8$47 z^rZdcTHf4l81UNW)2&?jZ7o>--vK!{C^7C+qrm#)x2(#L(DjbMZoBj|`LC}X`9ZsD zSSpBjOu&4(e^zm=wE8eaTGkJqongP3;iM*zH6+8eToAOnE<)Ryn!f7EJc)DojlwI1 z(xFWPu4|8_^n84MO$(yRii;_mi!5u>&Q_h!c2>4w4mf~K2anG@Bkmbocq`|O1aDRE zU1%kTLwJ0Z7^obs&Ap;;KD=;Vn)5BtegCPPNYp~Y2Gw_Yh}u{%be*eCl%O!fqzhf? z`i0ysrfsp=A@K9$R`jLoc{z)!D#I8iRF^Ue%WVXW5_u<3%Yq3o%1uAf;F+)*xiE8? zLt{-mbj|dIcXGi4<0_e%Y27D*y-2!F_X25jk^A6gH&pSy@75 zC$c;siDNF_6#Dul>P1kL+MUPKBymvztT$m;yPkQiM?Cb5#G3}*zTV!Xq;1H4#0Hjq zUXX%fWVVbmo0+ZA(MO>(Ni&BE}ky54tMIvXEHvcB!)9TG&cy$$c= zwkX)Oii>LTdxWlq1t*QA;iIyV?DUM*q6$uNHK|v6Fvh@8JPucKr>SAj2wo|%;u7gX zbRH+3Sd#USWFcBerZJt^MmKrPfArd3Yftz@iXbC&A_>8e2&{v66G#4nr-{Ph4CZlE zsJC??^k_e<_Ly`?pcLU@9qQ0kRL4`CD-=3gjP^LEUNiRVR;>bUUM)K#1yRvtiEqW3Qy6ARm9vKBT_3SUtk5M$wI9 z3X*TjXWw8?cG7z}%)c3KJxwIolSiF?If@5>OF-ab;ZwX~JzpxH1xvAbBntfD2o3-yDHP5Z|Leo(oU^sJp4rl7 zg_e&Whj6X_l*4BTW-#~O-9*=nv)+;OSW7+fTW&#j4vX-sl0fSPneP&m9;~8Gx~)b7 z$C6gjzF?>%MwAOXcs4A$wmNYtGbH7{3SD{+kQtf+bWz}?1CK>YY?85=TnO$^G`Iw> z7=G_g)BRP<$!u%B++H?!mzPJPe+<}4J}io)RQha|;o^uA0_eNp&z~aBQw${L!*3#Z zP08GGwtV_kLU+}t#{=edh~cp*Vs%ZDULq2j@wmYs_$^LqymVI!-Vr{M6dfg)?1I*g ziHV6sCPf^#XbLz#z~kk)a1B)JWEET& z;h6l=7V**C3f?qiKKn*TeNHwM$G@|j84BTf|1x!KSyo;Zs_?gX#{dP9K0}x3H$T>1 zF3Z-iIBa_4;Ya3c7iSglM~sBla_st1-KtEXaBXQ8&6}8>F+@nq$$4M0+N8%5;e}mH z1a8KZFQvzuTuh7vR+9{L7jyfIg|{NXQ{eTjI&)+OTVTXidlM~`g@yv(UR3oAy&^`7 z)HuC^^it~6OJCl+34B#xv2u}2{IVhlY<>344zb16`|8)w%VF?D*p}L#q~l2}&3#9s zf`AhuHEDRQkCb2h>XmTnhQ)IsqXKf@bH?`A=HsJ6Vwl2gRR?LUvZRFHXw)m!H@VSc z$#0d^>S~f$=KFam-pUw%=l*nJ$F!Dg2(SONhvRu2>BtAmMoMi!KW~_n)V3nH}~|h|8=Fz$s}(K zN(dOO9WQB@-JfPGceCfRQfZy&i00+YTd(KZMB>}z_P@6MoPJ9sv{i&}Zjch^^X*cq ziEetgAy?TMPU;j|v%R3e;53f1oDIK}4mSuhM?cGz6 z4Ej1j8QC-pypAUBC#mgoyG**$-M`3Z-8!nOb$KRf`?)6;1BI>Xw!vEt#0A8Sy%arR!h z?GU2z<~gO`=~mh_VF>q0Bv<id@^92+=z7@M{mwt!9$p>W(b(1MYuN6) zb3h7OY1Ck3)S0%TR^jQ#c>z@H5zaoz87qpu&{fS6#fFJuOix{_^*0iht6AQi`p;%^ zqBSd>dBIVFBaV45kRfmp07tL*2v*B`HSR^9+${kBB!GV5Pmz_aWVz?&lz>$8T)hK)uMha9%fEs|Ci#50E z>L!`fAGS*9VA@g6tFUJazAfc!vQ$s?2q!2MLZEDVTq?@!Sj(~48B@AfVg5>uMQk^Q z4fuMpo9$U`X_(tn-G?&()$#}3-0;zA5Me-O!o@7e@J?~(7Gn`Mv;(Kd(ZuLvkb0pErGj9ebu@Jo*TN%YdokFMSd#t>GSt*`A#q8v<98EQszGldspzX)%a#x;*{!7 z9K+#;8ad-*7PTwFX`h&0_Sr-)d;08hvJ{?*s?!;9e8$O(xF(eoT>MBl6n*f#rM=`{KW8%?^4jeabaBTd)}fHPh}znozjhp`MpNZg22iG3577lDcB@VW#KTb<0!d za6g{@OwtjM4I;bnd*w=L&mRwjjW}C7>vViCO2(|@<`!ldfZG}$8~%Nq-`T>^$om2? zU*ao5-FUP5F=JR%L*$SlB)GVX_SVV2)$WE~ifLL;m{44S$%DiTMkg8#Iu%&#;nc)f z0?c0gR5@%zPd{xkVD(RB!en_67F!tNi(^a1Dg_V$LQz3nIcpNvc9(hGAo{I)+S zI7gXF$(rb2xy$)lS2^jS(P^pwbUr@(^(n>=`+pCB4ut)8ZU&D*;{;ljcoyvBBLv%vQ^}^BDbL>Tj;$D#!J2!X-4g{nEZen zA9V^yH56lL9$xh9ap8z)0Ll`n#^+=atf8kH5oIx(7pgd%yr1c6{VZI5A@xYSENC5! zw*}Rp^Oko=@v$jjX66_z(NMRuVnGwa% z`h-S%=Oov2#o414etf&ts3s#M+kee5EL0|NX_nLm@=81Tr~!=CV`{1Nd;am48x|sQ zt!Es(H2CAf{O9|3G9k(uoCtXLB6){X7#$c=CB!}SJKtF>{=to%Du%^0RJ|XqK^tB@ z*U~^sGwpST}bn9IZI2ZPBY=&*#`b>x-;ebm< zL=pa-Q`|U=PK&>})IAjm4CSj`znl;1?~ilox-u+(km*Ie*STJY_7v72ovNjCz0>{M zk3AR14a;ox{o2BJgK_zo*TPXX=(Dm5>nqjnZJXsj@#M5~S7!g>ACK`#<+u>@N;{H1 z!@~3B0;YjrGRw;SFdITiAdD*_adV((!3y}802jGnjZ3``W6-|g^ZRiV)_>)t`Cj_| zmnB)+`F^!s`XwuBOjZsQxtDE1Js&U0_mS2PSljrTomQ=B8F@uwZGCZ;TMquF14CIs zLJdj*XAkOlkM?yw4sO0b63Fz%n{?zUbshVKMsNu^C$99-{M+8l;QyfmIPxf1`i7DTpdSI*U)6A=1 zd0NHl3Rj4tcTlq88u>Y)o3}O=bek0ZG8l?)6Qce0W6usV-{_Br))hOKOi=|GKmLfR zo?~cFlsOx=;;ymX}OxhH(Ty$baP2GoHnySXvBY;-%sB%GJ+c9olURh(Y1 z=jIx|{6mITadBx?B7)j|?Z+rul)xT)KFPz;D`p$p!&ZJA_G`UT+Fo#9&wZWl%ErR< zWtK(8hbgKO@y9IfHfu;bzsgZ(UsyB23wiXic+H z`;Y{O=+#~Ai5_2`(p_&3&d5effp*A!tx++pXo1!@D`X6mlDLt)D**q!Z(`JUBKMpI;kr&x7s`Mn6*39Hh*8jdW{4lN5hdoRUc8nl-pvRuj zV$^&*T&XobU0!g2WO%K|ccg0fH^kI3qd*A+na)=Ugif=^h#p!!;1#X)s@1b+*KpZt zU+ex=MK!acA5mPVjMPvq5Eo)PERtb1DZ!^)BI8v>%&weMF*<-iM7vHjl zPDeKLuNBFiDeV6vn3CE*yG?8;a(Qa+8ygl(EfFvC)nqVh+q{)$V2orIkZ5~RX{StyzeG_@#>A=6tt%)enBs_gyXO2X26_Wh?3Nony**i(;JGB%9fSNugWks z`!BBhI;aE*MRdzIYfmf98*kZWacBMd&sA5(0BXL_bNmSx7JP4WIHmAid?l5x?9FTT zxOyUg+LM_Bkt}(B#}cL2foR(_uv!&K3C-qhr)1RLZ{5cj&{Rdv#kyCPuYS%;wrQnk z`d9w|eIOamu+3}XH#a-A)M?*jUdW-TkV$Uijom?QQc%CPbBWylYWMu+FY^zx_cW}e zp1jO3>SwaBl=gnb)$6=UT~?}ZxZhg0U&A546)l;d7OVbI)-Dofc)e=7o4k+Dp^o+? zh4}m>&r()RVT;Yr`fmm|yOj*Etl6iA|Ka~lpLRY<&Ey5rTm8UcKbC$<`f&Qt{B)?Y ziJpksnoB!AO!`?bK4o3HpWAb)x~J#TicMtwSEL!L9BXd|*NqMk#X#Srv3?|)F|ot5e5VT%zOE{;f89kU4?Wv!@P8ufOtilVG29;F*onD&tQxcNZ? zC1<`|V@v9~|4%(~H`$)GyYG2-fDZKL)RAq~qjlFlx9{gkcPb0{)*`82r)2j_;q;BE zJum)}nO5FPE?lRNGW1g`cJ#k268&Ug&EfI==^LqqrE-_-bHfauhSu?GvrKOGP-kF+%lzwx@=JFVBaLjy|8cvEeuc8jb1Foaywpmd{EBx>=^>r&Ls>X5T7%`0RY> zx9q3ae^!0avrTxbn>9u7PL07E%}!Z{s<{``<{Wu6qys1F=wm#V$;A-_o3l&EAunUm zlNo1?k{(0ygMc_~;%)11GR+FYp%P`7QfJ$VS?l+jti~Z>@p> zt>5RuY47`&ICjh?yM8XSs9DuL{~$i=e_VjGzP5LW0yLg&wz+B`;(j4UU99=@3JNUX zl#b#1pC$-+VEXi}ekVFH1w?FW|CK+$w#auj<(G+?znjA=>=KG5wmEa=^N@CqaAuKW z)K%Jk%^sSSBf>6;ykqR(edity5c*tJ4kP(KM1YqGD3p%kZd1%5RW;24lZTRLrcrU7 z5&b9#N0_jdzu(n`CPBoT+rl*%H?eR2gLgMK^Hu0bJL_CZzwv`yEaKVnjZxd)@S0uQ zLxxBS{!@8|Ml0EqIu{vQyyVvSTAE)@GLLpu>aVym)`n!vZW*jk+qJFV^?PC16M1!p zLG=_>vZ7ZHGPktOS(caE$uRbW4-^S&wG8$iCG+m9XNn!JSgH2QSg){?ziwb*d(Nq< z-YKfbrN=8JW%S+GUiD`K>f_DDzmEbtbcm|xADx;%x?r;4HeGqP+tt{*-pxoRrN2n< zcag6=kA;`m8EXj+`RDZQU*(0X7INHKyqd>c<8u2~FH97c>WB#q`&h6AU(E`$ac2AX zq-QfO_?xrEd#0LM`;l2Ho#$urc%-Ay@A(eiZ&|~p z3j*g&ev(G7ll1Pvt)Q~1*)n4F(bCdpgZM>~++{T&{jKVi-Z!giytjNTN*k2sH8DuH zuFFQB;t#i5p}|pK(!4j}In^8H@{z>V0q#6{ zvK+?1MD4nhHW$?_fP6@7chl3dVo3Q=ds{H=x9pol&F=~;yKV|*I?n&0?p!o|R8$r9 zXO(t`&iJ}sr@eF7D0w)=M^T3Gy$q=)YR=~D8sGbhJGN;vU?V{7>?3kQZ%1|o`O%{R zPqP$5l3Oxf&0+^XyWF4KcAMdYJH@*vm5#f%aW4C9^5zLIP}MskbSlk`gD$r+@L2Bk zn;Pcy4e^RfdIs6&O4qnKO9gl6s9IXk{vzoT?Vph6bK~Ew5p$cXk}um}A1b)*V$EQ~ zk%u~nWrvRB{bOUX%G6O0X<1o9YmL$*Ol_dL(zJkH~I<&{Vauz#4xW!Z^u zvBGx#VxiI3&Q3KbNGkBxzkVHJ96g`0?D@mP)kLY4&F%9Qwl9r;6)PNBD;(+D_gw!H zvwUy7%Fj#*E(UcVya4z;trz#iq~0>0Df-F1@n!)x<2F0jwcT})JSE6^;qHfr#v?wc zBqCo(IO4TmOmRTIt~DaOj$<8BOm-w#b>+l8FCZ1r6@*fw>hJ9dh{w$m>wb-ufNBy01H@kJ7r1MM!Zz?3FI#j>xfyon0m5B!KDqM zsN^dSto~Nq3nhHi_rg$ql6n+M(ZyJMZ999D4g%pYTM4$V%CEnNm!%(#W$6I^p9?ee2w)KWZwZKjfc+LAt`$NqV+>yWg5^_8FONIP`J^-=$NhM zqq67W*{YveLM$DD)d|J!r>>;S>KKma8}iskqj%L}_4GN;siWcAKhszoWGWjg+pkqC ze~)HUI4m;$#)7)N$F#5PkrMb#DC43?m^NB!?5L%u_cA96Su`*z)YlP@fgy*&z}F(* z$Cs3b{nSgl40gB3al5SUAGP2S254nx(C@tbjjYj_FkHXxNte#CV-4#==qkuj<`vpx zsi%`q5$0v|cdD>g&-#K#hJFAr?}Rxcj(*;w=D>+okiE*KthO&uR9xMAuw)%joNyI;5`dvRF=D-o0|5lR=uf-EjQ1;D9SRp{{4X zvF**f6pjd?5*N0c4HvY1CK`^bbOt5n|MrNZ)sw}<5681~vAV}@k~lEjJyo9EzdLpG z3P?4(tY7IIS1!IR{4=4?)o9|IokxmHxendEHqGAo`X_uL<{`C`uTF6H(IXEk<+~}y z{B?w3VD)GFg#6gJXQxF-#?V>6vnRN|-eDNRuZ~P}=5!D@HBH%Dl&RZ7%g)YIdhtNcG&&?A+DtSV8CMODPEO zRd@qgaKd+Z3)4BF>F%D>u2kuVnSWiQ5W5)!9njhD<13%W3%{s|9O-7HbkRTC^4{L_ z%s!U896^W^WOu2HI6LsTz8u?7?#5)AwRqVU-Bpd_@d<~Su}457dbE-Ga*6J|QpL!8 zQNh8@&SU!rZb#*MIdW*4SI^j0&e}iH$s&+!FyR1U0La+iX+~zW09N-edMI`0Hq|fv z3bMGYG^@e&5fQ8T2aeTF+UN7_HUs@9csziT-dN_1wXWK4n(p7qgZ>h*Y|;@Cs7AiA z+`4LfZDF!?nw?=km_MkLgbaBC^VS4XWx*}JU0q%6bqFob-CoAvKy{aWJ}cqj;r6@R zqzSeRlA4Uq7s1S@BB3f2qb(9lMDFUt-%RuuKRp)*!HB4rq8K#k;qz4AIDOPHZh(7m zuGzEb<2Cjd);wpVeEY1*#T|VTy2ee09*E-N2)-C*@%O1Q`y4;0Uqddv`@7=UP%6$2 zOY4+5-E~8Yxx(eZH3PpD&O@CxUpcr7Zo2bHa%lwHhn0!#;_Y$o8Nt<3CU%6H0vDdW z5rKZ2RDFu%-dHg{RMH(>u4v)>=T+5457XIFPkyYTbOYY(ZzddD=_x(NHUJ#}mXGhv z9;Mglm$aH3cl|6qY$SW)^${DL_#~ap%Iz2_7XazBxH}bTXK)J0h%Nl zZm#!N7ZFC}4suH?ED^NoDkPek$yJ_RUFPGxCl9&BO>v5C5xq;h%bHTfAz3?3we`i` z*iE6TC(=l|EL5S{ZlltUPqS$UJ-+;9S{0Y@>pE1zTM^y%H1l~0Ee^b#BFFN(S2MCF zOg28tqaIJZQyAux~F=SjWM#anu6wq`J8aJ9Ufnu^d&IBL=(I zrQE;pEh6spX}!UhoL$_J8E~2g6x{eMXS9pKL-Y5tnz~D;WO^W0Z+Q6dhCRpDp~MYi zR}U+}b6j*$;R_AV#UbWBF8-_DCdKAnsD=jVAbpw`g)yn-!BCI@GY!Sxeyh{F*+2OX zPRW}x^hxdgWmZPPJF1bp_$VQ>*zxQx+%zqsztujo*lt-@YNK+Zlz{~-hQh90U=tdQ zF#jlE!Ue|N_qLHCGK#&b}%jvOLS{k`fWP1v(04r2TxGp?z1b$`wxWDYA}~ak#3FJcIGLx z$(iqem~?G0p^l<)PxQL@^BGBD&AaeawpOZC#G7DozUep{5gvL-jl`t*v^tn-{doB2 z1|#-rMg@b3bQAN?g%e7-C)`^{{Z8c;U*witrv(&=i=Iw(dTG7<@M`gpVRgc>{weL&L*JK^u)fLtWA}8-$4wGO&#swT3 ze{V1xgeE9Ym_0@ttYrVblJ^c7TAf=mlmwh@n;0Ku?$yMr!7CahX?pgJiuAmUBj_$)sDJ{dp#!( zZX~7I^5Edg$Zl6$T+(Y)`p?G_|2E-8%=ajnLuyfN%rR<@7za+f7t}IQTxjlXpNrYb9 zDr=wy(kj`zpW2f<> ziS;y!0VKwSeJ(j^2ewu)Dzi`yPgu|>W~jKITkz<8YgPKx0$;Rw0AHk7H7fc)AKFjb zHZ9#!zTY1ODawu4D~O8a70PQlVtg=T-q;2ryH zzrwcg(CEPg`<-8ErE?7KMcC6;hS~DwedlboBUb*uzcRM8Q3_bhN%pEu`6fovotzpa;8m`~qUb{1LOW;7 zxNdLf{Wp%K??yj5uf;1c|>P0qqoq|_7068@kJ;L4V8W;dZyzyH3S z46n@F>=zLVz*FxQZ^i|kqS+rXE!k!LH0%OzzNtv*{yF{($Ld*DjxLd3d29jU;W>1h zU;ft+3E?D~qG;=sUmFYioyMh@WXP zbY{7nnvOpkZzaTOAkczOe3%qDCe5=^yvpSi;u^;{c*?yK2i5xn2IIr+33#}LT7}f^ z8s^KbUK}}E`aHk!pN|TS9y>+6pt^9LVjA&s_#27#Qt@WQ52ba8<<75IEY0fGxQCQ|>6r6S($f20D$Au{~4&-rM7 zz>_HUR=af8Mbg^5{_lSb&-5{}5TCgAYwIhzUTVhgBnOI@6uoMJ{!2ZJ?B8s_&$MN*Q&AhMY~YFm9a-S<6;8CU|7mHe~|__EUk0Jc6{$vx0M%Hoh$%`y(CLZF{`u7*hiGXqDg=hVOf43f2nU~}CHD_R% z+mTnvPi;#ZkV$!hChrreF0MJdk)q|#`cFYMxb(gIOBX4A83mkashB;nK95Ve>PKAf zv|sTkoLc|eufJ<|%dgq@%*3cHPF=yC`*(Ey9V#~O#i^CDC*xuG&!gX0GO9RuS1-^q zx|z^~-3z*vo3=^RfwDT7a`)T_*+ehEUb&2_IxMJ&?voFV%{FC2mK&{<16%^7-&j^E z-*)`HSEjIA;Oq2hE?OVc%<(?Nk-H9Fjd`bx%ksGmLj_Is&&7vJ{hshh<%}Ys1aA~n)zwGY^1szG`PX>2 zZm?+-PZK-W-psx?M&ia-ZXI{Ab+z%~RymY8RnqC39`odlDs?eJK zb|b&$6h$&RU$K-nFw-2bsm%J(a!OZ;RaJnC<5M@w?=vlo0r44WeAE*hg7-;`lwXgK z`fq*EbTrjCUQ@F#1q-FeRhCPDB;nfSSpr$(c^_kR(0jrsfUQ6rhWc`HN!1!xzZsGy~e6{q*F=*x`%?`Iou2`tM(9%lxl- zY-13g$Yr)i5g|MdPq^t;*JKC-RcZc`pH(5a)yQsX3AUT)T; zK-H>o?o!o^SdiA6fwLr?+gXi7r*R#ep|Ex@cWTPQQRzSiya$<;(O(rH)h`)TK@ zisomB71{kIyjjus2j#hr%dcEbAh#+P#IY}Y(Z6ePY}Q${`#fd>O)pp}pn4IaI@=MO zy!Fs{-ukS!RD!rx-QDvG3u!uw&`x#5MO6)Aj3BPSbq$#~3`R3rhojjTtLQnxjdI)3 zcLf{>)a#ya6kB*+;G@A!6ad>PNkq#w#7B9eRoh-{ zR$3S8AyHghywfv{OLB0c&DPxYBB0B)2fJQSE|M&{zj1sNdX8z0j;1ED^S*I!@A_5w^&yNa` zUuBRT1eiv&p@Kp$vOXdt6gUYsKd5jZi1x(d9Y{nADlph(i_`0Pyv|#ZO=^&m6I`l} z)#L8$V3rXE-*~4r*4;*!=HQt?P=9>-B6rr_BCKdamjdNDh^E7z#E?!nRtvgU?)JNb zK0!Yi#}Dr(_6S*W=RJG6%9se-@AIT&(xdt6^ekpyO<#eoO{14|**yXnY4#KzI9riU z8d`q0xe!;Ze#Lls_i}6QpX0?x!S|)mi??wM+K}PSzSl?wVdwHevX$Wmgd4B=CJg=_ zX!Dp{qbp)wz`iTZf!92QU)dI0!n?Je3U=8_qcIBvXf)6bzWWUf3Pad-0gt&?|MtTb z&VEfCuHYD6-(Qjw1@6vQ9T3cC?+b{v&v;i^+WY~xt1-c2XmjY6&iCLtw%Y#vzFcbg za14Moy56w0AlyJ(no~QwEM`KL(a0It1WZa%$#R?L9*#sCL#a?a%j^Tiv01<4#ciY0 zrO9$SZPJMkJu~?8#79;4w#lMcbLC2=v4mPXfqfgMwoI@rtgv}i1^cnSd3_1mD6r;? z+j9Y5d}Ii@%a!`w$4ectK_F*_g3jaz*&aAQ5}uDG#`a&s+;(7Fpfdv!D}?_gE)r1c z_w3*Ax$CHVG$GSNxKOJ~~Srksq z$v}XX5al3bV0`8W6QuGch_)c~QsZDyO>c=Nf8}Z(gd)nC)(N!4=t$@dfFLq>Y~a9e zPCD$WS4jqE)dJ5%v@=!C|J+QfhL0zMsv$J+6)&F{K)e4WY>desEEc}zgyrSq1ojc`hJ1;ht&4nWFc)3s|j~z7c)Ss2O*h0@54{9 zC_|vVB|IvUe*`|>bMwvq*U^{=fjUR}^Lr~nt(+h_SIqq8x8bA|Z$y6-Gup|aR}wWc zGU9G;Ccc3%!2N;Ylv2^r(<9mi<`VqpH=@&c;BHfh=BA!{#LUcmSu<+V;qnmGnV3 zEO_!x8pGYnGoaur_$_f0lyRtHuXYARFz^xQ7JP6UKVO8pLqMvC&pUKE{KJLE(V45z z)YiOAs}g|i0Cg0Is91pi{NSXP&N+yO{nPmYJ}(*t4g&afjvkDP_(Hr*d)3N%T%6cY zIG^b${YY&N;gj9y#vob}(R^EvOWo5C7zfeDf4Ix(;Z)Bqj|#6{J#%5u5_(11(-qJNy3Y$;YR4 zj8nlCM)x}Sj%QtcvHXgPiil^2fxHImTSdGzF7q0UP(;k7J*NT;Y#yF`Od?4iQ_F0< zQNTuTNt1NG`CbEQ0uPPOQfny&kc{aH$cuj}M9;{vSDAsKJmRA1SHyGk3Toh3nru849V}g zEzexT;#hNr&^PnFivNu>2gxTi*x@Q1qb4uLv(HChUF%C{Dw_3XDh>KA-07R9-)K)q zr+HtFr~JxjzH{h6^@)Q(jXFk~M!U@PcdaITR-xPXsikKAoOsn!cGYm9NcNXO%1Yq6 z!^j3Rfqn|Qo`~UNn6tQIf~ba{c_^%OuRn$$FM7B+qZ0x8{wUw9wi-lv7{5HQjtFT4 zEJ&iaCCoQb{R+Zp3@$3zrd58?cx?3Pyk)rU(~~f52T2qdYV#?WTbAR6(X`BmdlE;r z21rYjv&QXl1^Q328QWd+%gdQ-B_;N$SfaQ4j8qRqZPCt3T+LWPa@V)Ua%e1fTr5P- zw6QNl8_J6h3V!RteB&%mSOo1%+wFcjp&kJ*5C{sG;zdAOL*NHM9i;4+Hd$@Z1VdX0 z7~r86$t9f2u4MgdJ2pmXz#y5>-$EZCZf_}rvrBw?>cnr|0f)bt_sQ6{qFo7%Qid0u zoNhIbg>(O1uF6?vUD(eRu@Ps9lLmC730zTH8JK0pX07+b!nce9><@(>c4;_Q&;q3q z7IkTdA8Ip@7$hVK!EvS|A&SI3>c$)uV>Ed)>98;%akc3;sEtJ9A-Z?gHC~Qyv{CS$ zVfJ4hH6uiFbh3~iAtarEQ+cC}E1H&388PoHS`u_O(Qmr#4;?U^%q7D8k2o8o@FH{Y z0iA^BH>oSm2U<>HSFc{(OO;sLCy6@_?RW^1QB#2y3!1TC{_baw*i=Ov*JH&!mY5!d z=Y%u{agE`+OFQ!f^Di3tib6_As8o;F<@}f2h0IMM66aQ8M3s#WtWsQG0G=CV3e>RX z<>mRRr#u%uILehdhLZA^o_{>tf6s~S!hO2Z)J~y)nk5lHBQnC`*;>XU)S5@5BDnYj zg>EEscqee4drC}Xk~DV4S@y1rrp^~q`$tP{W@d&^2Zf*;cEtUYkMk2JBI;tfvk1mU zyP5ymz4bW-0|QYhLSsO`P@D%7F~~gD!aukLBk>_?JvfP&5{U%vQsN`f zkqr57oYSb9K3=l+t@d@c|1D_cJf1-<0`WV{d9z1w3 zD=T#m2{5>&<>8!dIZv)iP7Fm*O1hThi2WL7;dL+)PN-o>g* zG7PQeUEs&0>ju~u;+V&?JD6-FwlIE(31=VO$1!{K{r&F#qZQwrZ0Pa*^2_ z*_z(9Y^>Ku&x-hleApY{=z#sRR}C% zh5zF$K5&ug;RQ=eOG=d8Uqb+B@FELJ_S&K?wyOc5%B43*5Y z3QB6-w~2AgK3$~F-nIO99r@spF=>08IWU3kbQkzC$u4rdh=wRgB5yZGYogenT(ZIT ziBXI5DYLA7kqd2)URTfV;eVV#w}0b>A<40C_W1_1Kl9xw#%$h396h@MZqcSkJd}e% z!CNF4hX_N^r;l_U_c^IDNk8}Od*%MfpZmN7zLnT~^3qtg_OQ#Cy)4O1J7Xo?d}%iL zZ}K<4qs`j=hrC(F_;2N8n&fI$^iTafmL0ERM+&Ha@!?!>tL?yIR#C>uslSFiY(ry8 z$IBG=r|WjM8Ef=Bd1KRV^{OnVfXwIx(?)mmRc))(Io(yo=lYqJwsd0*bj8XEVO-`}x8(^e4>% zUun~XkIJm}rM5D~`U~xJ6Ls@RKX;B*YL!*B%=GsStvt@iqRe0n_vQ1N>nl`alMnY* z{`6$n#In=hruo7CvlJv3k1jJmbGv;GsXGJOt;TjO&e(J=hjqvJJ@x2ZSuGk)3ufH; zcFSyq@#u58U8axPMrgk?A_#a*Sljm$L1jyD;# zzZm_j(f>{+i3JM_3dhidy28HFt)DzyzTCuZRleJHG3SkC4Z67Nopge0`vU6w_scfP z)^YdHy$yc=xCAnQB}HeRI4=2!+j94ZF>0JZM22gM5G?|%Q_yh@K@*}cSd&Ak%0N6m z(0BctKlLy9A)1LUJM=+ed1fGF#M%Z)6~P~r<)K=EgF%ufn#gh11d!p?ZZEwXjc z>5lUSiG)#>J^3O^zGo33G8n zB!%n>DLQ&yfqls&*;$ugc*%Cj`Bkmwz@9p8t8067>J3^X>%1Yv_Z24+)gI*KNNpg3 zy&;N5B7q4;SOX6!t4W5(ximzk*=yciEX3f|*caDY<|mhJ$qY#pjl>YKYj7<4320_= zxRvp-hEeHcISX>gL>j2BKHOGE(Gz!BIOC^eUpVkj1a|n17lQ_F$K{{Yq{8}%DP3<{ zt#j&6pS6$T_=G1e@@$=~b@&kLYV*@&wh>h@$<1xA#aH)FI^?-?XK~mcZXs3&l$WD5 zar%Lphm!*r6Arez&8jS#G#`5&`Zft|bSop&po!(dNb~kNb8N$F^-HPEvQE6yUI*!e z0?ri>vQ+REr&ixp(}WR)Ej)R4D?5F)eR1j{^KD^xSs;#6kT{USiup{|+3uq#fLZ-+ zKU>n1wOmqk8!XMMXiHY7(c$b=Ol4yYv+7mO+LfQ!V2!xwP5@r1&p{3XoE=?P3@+$Y z?T!>e{BCVXOS(7p{@h*w!x#-Z^C&dgfgs_qN*nbwipPnLEpMqh}|-Gp4fOE3g>6^p~ToocvYZvA7~ML zqhV6eU>(#rNLY{xvIjH|9dNz(?DIv8_EH)3SYYzo0?eCRS;|!dV%tztl61OLJI6F7 zxd&ztu!`S8_aClD$vfKrx;G^zR)6}$g%^ODC!RRhgo~R~g9GKUd3MID9v`^vH{M+Z zcm}WpFP5A2g3xJ)UL$@h_Fif~$msx4&P& zsE!7|KTE2kJPbS-bSC?2np^3Ln(r3l<1o>5!cd08*v0f2h|{Jv)0K6tXx0P* zsRcqq|I8Bw5tq`~Tkr5%kXwV4dhUDCB!tXZUg$B6p3QtCIr#{VvnL-D zNDR>>9~J)S%DoxBc8Z-=MzFIN-vAH`%L}uQ*}>GH|1?+s+e*h}nJtk*vHgMM?!D2M z7RrlwFJ`v(`l+ZpVQ!#7oY@+VWDzT?|IjUAQ!2>yN0}K*Y{Um3b|P;J>zVSLItO_} zh?j%myXGRMK*rI=FD5O36a|cXSXT(4ZpNi8dnP|$_{?|t8(M3zc#I>DCVaR&ZL(~Dy?SUJF2JmsG>bc zdYDwfI!7Vmr!!tar?<`G2LFOP`>1hIm;dncaD8O)$SV0oj&C!0{Q2_K3y<^~b90W| z8S(q=qjdDMT>ks7r2L*;6kqz3bM{3}t7pn<=NE@KG&mwN^1k-;Ib6GzaIY{2lHcBP z@^lAMbg)8mGUUG7pDWlDs2tF>YHRd-=%8y4eMd5|*BJ(>h%X}T12`v9$BY7{OUhu}G84NWPQ^Uj?4NOhzfHERs z<)EjQ8y|I$gXkhY&Dr3^eBi97iD5iZCjo-pK8jvbLVg8^&-IRvv(W8@_c-)pHaJIS zyiMIq^NhV=#0AK9&4^2-g{bBdfu6vUM}f6hxU$zlZtguuQ^tHbi_2!SuG3!2h1Y8> zy5Z7BRd>5~IsA|Gixpk|)CI}GoApAiYgg6Oo(pyJN5mrd*3W7aD-tOBPciso21-@+~ zN?|fd3BSY=0tM;6c6HfE6=sNP|BQKKiPRX`eb_RrwUn2PgNRYC&Ka0Ti&RXZ1{fUfYO6&wfQkLL6K^<>wIr+)$uQ zV`3kij7ZTvPW9o#w|%J#DK+K+qLIJ9`tqeI+dldHBU=DmZAh+a{V+PYhhr4tc|=S^ zPi>CcAwMT9lpX$HHutmefYrdSr1q+24sf za`jDs@XXdN931?Q6%KMt1jbUYVjWl;62T3e73%vvw@UVmxj_LAES?ryX$%LN|Cd9z z@^7<5T$Gyi=pzlot`|WjLVG?=+or&~9h*avn;GjS&p=$<{7Rt_Nm7vaw_BP1AKq`H z!1WZvD0`Px3p%XTC~)W^?7$r#TtkA1je&L$EyCrjwuX!h_f7svZjVT%*{qBbbvEPf z2ZkdlOK;I@aJHV(nURxs8r*J0UBNFOOyQNuKwChg8AU_;gvG0WFgaR}^lsF zOM**Fz3hZqZWpbKJhz?OS04&)yCJ5*eXT++8)lJA)NV<_ z@As0GmB)kMFa~!}F7J=1Y%E&rd@);1HnUk=THj~4xN0UQYI@k4v7qC<Qft3@Z*={CVXAQ?ZZ{53TNnxFsc{M|nSM0&!wm%6)T{&V+M;251adr=8o3fPKz|~C*X_oyCaudc-8Ws9eIFYxuyzIcpR)@J)N$Fnjrw{;OT@lw;{?gIV$Z#v2J(p z+$OF}0h{nc^QMt0r>+^7$+q*H)7!aT##rH+?T&4eqIX$t-I2$yWL+vpD|t z&BMqRSd$sX+zue5!-H(DL=Flu+*v;pd02PoZ5VAM+xEe+0lN%Y25v@#bQ^|aZ#GGW zUja6G;@O$~+xNUX9OhJJt^N2J=dmRbsU=3*Ujx(-?#0FnsFduGx-M+dOk{I)+?oI0k|?eJeT_j2S9w0X1?onG#Y97c7X0=C7eV|$ z5=`LWxUif`z6TU2I6NPLV>j+S!hjk|cph608R+S?0^>x{0;nZM0SP*xJ7-^% z#u9`KdS5fX0oGTlS*^B&?=mnc+;%L*ogb2NUUk&Ts5e6DC!h~S)3F}Z{Uw_2Nf z>?uBm1fnjfgvv|e-MgjiCGqrQgd*4K3VGEM^(;i?|5!fZ!{AQtbLDp0D$x9iHyh2& z2?2QB%!=(hrw<@Vgz4cD=IAx#u>o?>@wB(MPYSF~3INQYb#Tf{f=E8zHC@;IPvV(R z*2SHK`e_dgu~87=OKg5I^@FJU5M@*V@g0}rgZ;kEDpS{ws=|i^vT}qJ9Dx3}&)-ut zLskZ-Hl_f8c+OKRN$!{a!~_z45{RNGe6~Nw?M~FAQ4JxK)^NVFUcU7x(l`vq0G2-T`7ZZA3I#aQolYw~2e=11W z9?)}@AT(x)Miv0c#_#mSPG*wRj;v>5VuDJjUm$ei2>Cf&Q25V;&g0q+a(0f@>i_$G zZDGmC(9*|3J8noYtf&eZDljENUJY9hq>^aA6=FExG=h<|o~0FPU&oLd^d0?|grY6s zzk=$K!>!)Zx=cZO#2v4Td;ADpGhnU>DcfHNQI3`6cU*qyzpA(bJ(Ls%iUgKnq;o_y z69j;IVzN=v0NBypraN(C=3s4{R;bDtPSMFmvz1#Z8&S|ewUr33yed}C)O1D#7@I(* zpCHKG$%P9js6rd(}cr{eKC>y zA^c4BTi8Ji9Q=APM?ea|NLi{?TA~74f<-yQOxxH!)%3H;ykuL|MTu0qu*>< zPv6fENmQ&kSs#)VQ_0buvBs9iMAm@|1W`y-u&5BrqR0*DWGjFF)Y(&jsH#Q@$y_WW z{Ty1gh=(WIi*aMW9C0fHOb5@fO!5AqL%NLi{lRowmXnhUlk9ZIZCT)Q!_P zAj@I6n{4E1g(ct4~tGx zR~+RG)2&XSUP3oKp_6TzTaN?OLh247&5UNt@C><@tY>G$gtba<4YioA;-N7S*o?F! z9IFM~p-tgHLLhv{`xPf5iX{CXe@Iug8Y$wOflCPenJ3fR4V?^WTIgEzcWbJXBPoB? zFWPE;l)C#LF2H@(*7v-bT~DdxIHdD#)Yyj9Tn=r$81`l`@Pk0eLGIA4oM9bc*(v-)~Y22WhG=x$DLs_{S_&%U|WWFb&!9MKG*wusFfnxCi2f429Vh`#*r?e>b(zwj5s3OeK zW^777w0LaQ|5=*HmXwVuSpCk5$VP6??&hzArC&7W}WsX4qZZ%>^Zo3x)TukO>87kf}2R8e2NYxBk{vIH@b1RGGqJv zwvl8pTfsxk;LG5y5ls4kPZYU-i|NvuPYYpalE6e$-DC_De9SGW>mD$_?Ic|2`&ACV z*51ja)h;wivH$$VE~^NG^#FuZqQ4b^z2WaKfCBjsH+aYWR_!9smPFEQV`_Htdia`e zzxlfT3E>aaPm65)^V9i!3Yji(bhi`#|L}c{fDN&FxYhZTZs{!Q#BD6OAd0tpWHM14< zpujHsp^ZFjh~fEe~uE46UdVz-XpK34Gs*>6*g)Znj&b; zi_5>|w3@CDXliA!QS!aLaDD4Z0gh^7&4GgjH%k(Bj@R&694qD>e(+03THjrskM%VE z+CP%=sA>H1LMa#NfJMH6xzwFGm^!=!Y)m)ar>mpQqWh<>3Z-aGjcSQa4m^(~i z+}ewbp8dYpG3G|M5GCFSP#$l=P90cvDBcFa4%nwZ`f9`G7DjW4)eI0?#rw(UNK7s{ z7c$c+I#b?o{L?;J@@ou9;W) z3FU3Tkw)6G6mo5(?#SjGrKHOaaWU1{Tw&UrQ3RGN$((aviHu3Ru@F5a!V1qEKSWzd zGk{e4DE25Denc*&bqjVnCnORq3XKeJA8PU*kkMHWOBVo}$^gTq?hGg%%g8{;BZS>` zf(w`|4#N!}TM3d1NF`r2QY(Ys9!52YO9Nrfy?$zy-kogfCuy7R$zCuO=VDa2!WnTe z=|cu=JK=@~Z@M5sbHu*jZrl>iua4N6fHr5`*Alr$YHIpM!OX8z{NQL5#<^fx*45dcDcouc8UxJ!@I(&#GGG=={c5&LpmI06Rwd|5h^Bxr`Q*CH zw2jw43Zg399AQVVK9PHRr!j+C89&F1&(*|0N_<%{W~a{Sb%x27VSGo#7i+0WewYSL zmC^<`d&JP7!Y}bM>u}RA|6kCUG@|)n%u^U_M_u^bN+30` zl?(e??e?XI@4_b~{&q`j;*d5jTJHWiWn`?ixD@xp`YGS<84v6rWQjRta;=L5T0Yp%JE)zG~?^1W7?H}dxj2yH7;Uc z%+7YTB1*S+Z}~WU{8;ajZ{Q#8Tf&a_-dRgk+-(1HH~H(B8}270aB=Ut+g*3GtCz54 z>mDt6ZJ}#?ZKg&zRM0BfqUBD|%*WSEEu3fc^fbUc!aL$@mFqsb{tIeaQ2}8LeOA{d zWzr_q91Z2&?-dE8`2qdf&$b}ZKa-tvbjZ{&e!rR9w^_uv*Sl`z2ME${WoaJv#u1&= zdW)&bE%_2lXR=R4|tirX3cE)%FnTix6)lkQU_TTN?k|$;r7bD$KzY7w24J zj6+&mS$Jp;{;%rKQ{{8^emzWQ8}%j#dFz37#_W%B_a~2C_kt%FO1#?4M<1M=Bd@MpAQ+3R=5BVB6Ejg_<`o2_74&Tkt|iMOy^F5ibhRI!Ow((#az|uq7uPo z#@pehmA%ppGu78bVWH&8l`H+DxmB9^1_Pc^#w(~!xn5+D7b3ya3~&*KR>c0~jxVTb zgQJTBw}8o0=qA{+JVY}E(YE0MgKw-{#JL9f{4lTDx9^ShHy_0qq`a)urn2FA-!98o zNb~gJwW0mxR`yTn_Kvj5KnP|Jw0Mv^h=dN87?IFH=6(W>BiMUxbjO7`F#~x<&%V#7 zyC^9r06_}EVgcoVu-z!`BoI7|H2&;{_}0FzK_{Xk15hY}P$;uQ{ute1Td4|a!Fe$- zGE#-NDLRj$c9I;-FeGZQrFNf+I=#F__i84twjV_HiHLjMDTG2;!N4y)pJH(#X)HJ# zb9kPs#Z5j1wl5)g0y*BFPS?4#o2;b}zo9-y7+r(lg-o9Yd>Git5EMZ2tQA;ZsH{LT z4Zfcy?vE-ibkBNK;Gx9%OGP89;Aw(JCxSaqL~Ixg`XEwRZ6L1f?&H8HOjjWjjH)_} zyMhoSTxuK`7cPGt!r|w zd;1oYAz4?JhLqN38J=yO<6yM7A;NyRs@wFUc5sncb=o$;dEnq zak+eznVM3ZKYji;?s~-JmaQ6C^^iTWDCmHja*0sAlG;YpF&U@^1T7&& z$9Y6f0vQ6uo&<+&7CGXLF!Tn*GmtHr6~ShkgFIZjk}a%Op%R5zI*AL%I=+7Gy?Acp zSgn2qK#3j$gkOZEkQwB5ktre^{0dvlD2NAx<@(B6iU=fhMZxe)ZTNNjFR^pmQddHV zs2s%~1fUo!kbA%ZFV9LVe0qB4D%&h+wPk+l{j@$Q@G+L}1yz33sLHCgE~u3)Bb^m=QC;v~cGif; z<=%2ZMv&|IBfj5hKc;K;?hBiEx6^o1i~E_(@HKK)#yd#_1V=`9>vnlxJ)_7%a!~(~ zHyfC78~STS?8To=$~3&`^yPb}@?KVKx65wN%oW`} z+wn+6d}E9KC&Bh7y5FRd^QkYdw8RznmG!1{o@cH*9ystB6{u%tN`4u`<{)~r?AP?` zJ6%5VxPKOG4eGM5M_mkD5^Noi(9fSZJv-i; zUwcBg>6`9i?8LM*`|hCk+d`v@D#@cK%Wod8`)hJK#f6)TZF@R%7oDE20PUSO9J8q5 z6`Y_SRDC;ajjI&Nf1g9*l(uPZ!m)F2Pv1YiGylTpf2anwR~0HuucYY?l+P%C&U5B-qn<;93F(_-z>%CQbyK3?(xn*84@ zeZ6~mwMFC++i!>(JXsR9r>V}hVwF5vYHjYu*MH-^5Vj?1eK=u%OwLu8qACnhHVVW9 zYaWsB;*1>@dA{S_FV<1ayTim3h~fz%A(6U*0e}Yv;v0~m5iZ*WTNxne$_@bj!2O?7 zo^a{T$s;ApEj2eaQIZfJKJnH1to6DJC?!$!6oh>H(_uvLxQu}ofy{>M_N8M#ZGu7j zh>Q03EUMyGTZO&z8Z{44KLwsz(C;d~0h!NhlxLaNhktc~Q8c_lK($HTy3yy`fTse# z+nIxi<54t0!S@({AbmC%c9R~wDCWQ$zR`U>BTe@({vmSzX5&GQN~B8bT4Ec7iS#6h zR$jEQ(+;TtLB8)CyZ$}zBA@!(n<AzYl)T#6h%M^39bqFhqy@yh@xUxO_Ndwwfcaz5rusoX)M(fi3B ztyo!%JmC(hhlPO+(ZPxxl?}9?5rhF0K}=@Y*w_F@z19h?jz}XqNC@m4PmHxv+=Utt zeC?`jcq5bz#Wx(^_8zGKs&AA!14b(a(Z5(^~st9INI)VxGk4HXIeY zw_8F2)K7E7tf+m9#fE3RZ62BQaKI`Bmi`Y93`7P&&In^s-Cu5H`DmL#UN5m>O^gbN zBH|m2FNEh`fpfzjQ#<0qTHaQG(l_Cxt>C-B1r!C1Y^ZT-A2u{L9`IA|cRSaAy5s+_k6|5Ok&u(s)SwtwuwDo)9|2a^8@B zKjW1H{92+IYXYeN1Pdg~F% z0yrdk(mo`~axkW_PvC9=w16YG`BiT?;WU;tS*x`qLSTM?(4#QKE^jdmX^_m|3L(+FKvaO#&N1MttC?CK8!u%$Ep1Cnu*g%7#Zp?M+Gl&@LUNaySyOh_*4&~t=y_x!h{hdx;2Qu z3A5vjRkUj|MLkMs3KosjFKw!{Q}V|Bq3?|H5Gsl<(S(Wo1q)7J4vj;`2SvG=DM@5A z(@LL+u`#=xfq1kD0oeBW)3BY&QjGEAmy7#Qy~GKJK-_0(vL4YV?~Y{ep5?w$<=5{p zBE}LpGSO3YElKu@(}<~jjLE=vN4>O=OCqs>qLC9`6-4(cl<1h;AHQQ7+hc|=f~BKn z{@aWX7B}{uKzxQ0CJs@3nym@HM!B40A34D?ln9)uDZwfQwFv)2nB=o^U0nzvBcgG< z2Y6ND-{=B9d^o0fsr}+Uem1LQ3O$-niY;WSY{pE8JX-RMzy3QaFx{eGs;kc7RKfW8 zWRSu8bdy|_x93~+xf1zwhF!emGS^8Sh_3e5e*5I2z{T7!x6IImJ_(+By5U*xJUZ10 z;d8xr6GPvO1=cuy%vwy5kymAMdPbR^sHShLsz(vrz}&UtH52OFaogyx@(vh*w83&F zhRx1|<>K+c(>s;_4p~euP}C3Kw>>_RAx4jq#G`S$^qY!7I=Nw}i+DS2$oVRKd4+UM z2DTvw7mjQ+NvC;i6x1;y&#~`QHp$OpAnb{)0BnY5ymUXu9!%HC@;0Fu2r&+-j9eL| z&T2|idMupL=G#7ddi}+!7{B8xxov^pvYL6!CGVA=6}mV2_y5pz9pGH{ZQCY0JIM-3 zl1fIl5G5-_AyP&uviBZI2#+K)DxwgTRfMFHP)W$1kr1-^&b!|4JsiEy@eKa%@w=|; zT-V`3^#k8MA1W{SD$cWyoKJgEXr3*@t@rCkNRsv&Sw83)f#{=|j#3X9dNLm=cFy9^ zH4>=bh;0~n((&3Ca%-raFuUcQ>1J|Bmc>a=PtV4~;}(OXkoMpqA19}{%t$d_bCXtQ z_N!kFX6;y}tBVLlzd4n<7M?eA2VLf{;M-)z+TzJg&uvTbY$r0CnXo63mFm=v_%KFJCJrJY))ums7r@OZ7i>I|D3pa1DIsJ6P~ zag!ybewtjs_l8FmM=b_6ziWFrqM;fX?Av80p8p5G0Aytla98z=CWM|&su6FxDYONO zyeKiWSl6@R0IiiJoLvDW(>_3Dh`X_s|hnolD^UPs@!$@QX%{0ih~|PcbTzYYf*UI_`83OB)ucvf;s8O_U*4mQ{c0Lzj94H*Pq-v(|=XQ}_%su9oX zKNg?Y=;euiYHi|FP2EvG|JM^fI8UBGr!rvk4%O9tHf*i~RU)wmmD+#}IEmR~o!5pF zlR$4leFw&c=t=2D`Bj&e)+Y&oPlo835Li$%@%Gy(uWqZ7N=&GNxgIS5JPSBwbE&%W z_#S+AASP#(D#B?NKwdW8u|G1C_1>!dA-fBGi6s| z9!XhAa^ducmMMskju5*X<;kW2k^%c52ENxqa#OB``T=nBVK~JeEAH`{nzfOA#A?{$ zZ`j6-+zY|vrv2|?Gj?&u0C2{hNJjjG>)HH&?IldI`YV1)M9Cs{S|&Syd|De*fXaeM zlIWPUhK(3I=CVagvIKbJhKSXzgrQRi^}qVRtl*UkIc-*UAt-u(e?9Dq>->?BMuS)I zABjUt54Zvde;hxJUkhaTJ+E0X3No!JO{HSz+`G>%Zm{H?aQ-*dq0GW!N)N#zRvRq0)4uf0{TqjgzW zT=oMh@WQE}V-n@g?s<(z?;7MXG8r;x*#9SwAUp^!L?YD$)@n8vHwk+m$8iWe2<{49 zVL`b5YQn1-Ddt%p+*vrdH?LvVmQ;OYDn3X6Yfyw$m{U0m>Y$H)GdsHqw5yv;xDlKe=UGE)($nGCr8r^-SynuV^ zBWYQ+{pG9&u@)kxz<)leSNjmTwn~*HbOb?lO^4nth+RkYN!I}#X?Y#Su`LZ7io&Gr zmP`;we$G20&YtCV-Ya*sKmAOYvN!o3eG00aEtMTtX`c0kwsQvUWC``O<-IO`BwmAS zuk>x$P1-xuy){?_*j&e--f?bVZT%t0OlvmptEkm}AWV(p*SnOH({%UaVk}YTd4PmPV0J&!@`*IoeSAi%LQyy{F`*Tu>8A^)_NOd+ZFe5diO?`ToC($-+C0(^HM ztE1NbN$udDIxFcT{))=uF`DBiM7VS~esPj73LJ+V746OXFYR#A(062s^`tq?8=?6+ zzilXuo$0AULHd)Tmp3!b?U?lThjO0h$ctmGN|#36-CwHkznWw;zJ}>cyiVbgKgMbE z-P3?Ib`HIV0tli|o6}BJmKGP%79yYaZL_)26f}4vpoR}EIy!>J9VxsHEvMio!x3RV z>kWq=Q4YQ2>E-)R>UNH}2(UyI*RnM?K%j?12n)FJuHXw|+_Yrg>Ai&<=f{t>Gkm;c zbA2_Dt6$J~4CL4AlUWLHB$wUTo{mvX`P0*JYvo}%1V)~}Qr{!e{<;Qi8@w6@RKMbu zXxwfISk^jJpcN>m$*Upl+wx6MKG1;g-u`=2i^+;IXGh%oYPvl)X}6q zGcYaEtC9QJI-%mv#<&lLZp8Wms{-8c{rUkLbNY^jjhdE8%SPFI?}}KKfbQ6 zrDlsF@`}8W|9^%t##iqiBwbU4Bs<;n2fvik_q6_e3!}Gk>G|R}haCT@0Fe5G7@(AX zvxi2#qRPi8^aSlOad=F?JBLQZiFxEH+0KrPV4-31BPyHP8gQ2VnhK%7`(b` zm%Si|*!2S?{-VL=AY7h2sg3PLUkh^r>KgzuBAdlgdi$b`b^-yOKE4&=GW;T!-GRib ze&F^frJ!5}6^ppRGg2R2I8;DkJSoT$Zz7-4q?T4?mQ|H#IzYXM?gxsq+t<2~5RJLp z%gbvtxB!~`0~>8QN0ADlyiodTi@{nwlQIYm9_^*vY8D!ba+U}Td!L-4wFS|?G{=^g zJNBQ@mK2?cExA&gzy%P=gK&^=)`ujZEy_3HiwVhByZg#Dwq@)H^r)14*x)(QO`noJZE6~%>uDGQ!Vk8%SJR92K;$Gf<#l!IyJL?|Bpa=V zgm|&N*q3w+TosA4TEs53yq+sZ(N}HO{N&LyzH6V6{VEoQmZ|QtSuA@M2$=u4-BL3+!L#S0A9X; zC4uN^=qfPSdt2o26HqGJ5@Lxg+O`3PgbI)KowIDl=dl6ToT}Pff=RaNQ<{gRcE{(@ zW&ms)m#~v#c1$~n08Ahg!hriL(`Y&LEB!>(m_;U!7a?3VF(FBN#01xC2tOL?tIJlW zWg;>crCMb-tG{+y(=Q&(7d?_)3W3=7N_RS$E8n@6Uc$-*Crp5*%fNl_gs%T6Y`}C8 zGTls1uo>|D!naH)%LH{RlP^A7dDFYYYjb@-;l)7L7X?Zll z=wr_3i`hlj`!j{Jrh-DNeoEM2fl2kon!=Xe-am(Pdv>LV zgHDxKxER=R?#WEbwLi&S3m3jra?n%tnReiHg(DMh>~^Qt&V>V$?Z$7+p3T~U_E?QB z>zp>{IJM&_&rt4`iLoc9H}?3(Z#!AjXSXv3@CsyYK$b+3@#Zi2RhLafPhZHCexpa# z*Rdv>5!vX)sh^hkyX$EtZMYhs8NhGDrj!k{n5O|-q4xrCgH{^&I9zoSJLq^@>N)GZ zSNrDz7X8njy7Ycty6=$qd;Sfolo*4g&q(Iibc|UNL@mY>4~br5VpL@4#>l}X z>gLubO};a|^_uQH7yen_F;f}KNZm%`YAE`83V0*C6z<~*mSt?XL~~x$x~yBSD6!eL z;uF@d4R2hMPK6vO<s!^y zG?yULDy#j}^+NxZaF&a%3{D#*es{PW|* z|*?O5o$~3*&r!ST+6FJi82HEJwyp;xuZo0r91}*VZel!RKj4l+0wIsL+XAf93eFXT#M4uWSCS%%_ojV^38#P!|P62h#f| zk4^_nhC0kZn7`{98LK4Rqif!s%+SwRHf>h3C@r*2@Cbd46P^<-p}Kosr^$b;KARMj zvqG_hSI0OUJHm-*CP1r@kAso+sE@zk9hCK$a-qKIX|o1-;Jle&$?=5O#Hq*w zn)_L<AUJt>L7C%jv4vJ2FvOhs0lJ2NbYh&^O-J)#woJtZu37RLz}1{m>tk zSU&OQZBu3n=>B}eK3AVwb?SKbF(dNxN~iZKBg;*L)p)NvZ}?Q@l^q)Sw!Z(-w?&9V zgmiKB7ZYc#@lQQ=jR8fG$8fL?UufBp@o3U#**-rww)JE}x~CdE@uTrZGHo=uYM-JIPn}&uV$E^A|rQ z8=n`g_G1Lps{f&Og6S0pjfGBY)@O?~k9~9IT^k{Z*GVTR-_^n7_Mn3-*t#sBKWtpW z_!x5&C4}0ru>G;3_+!O&ICN`%SEQ+gNfnt<~X7HYdq5R&&(LoBP6}pRS(ylY>f8*n0S(W&H6Y4o#9TI3(u5(ipDZEMT9n zv@m_Cw1gc$=_l%J?u7SRh3S8|8}Kyp_g8=i;yyizt}klON9YzQ|6t0eonI~X@t@IY zsh3X-t37u?Ks&cx{em6&vw91u&qv2;>$Dr_Uou{-i3I9e(;eP95^Q&ou{TA?uar*m zHo4Q&cV?IU;%x(R-KXw^)7N_?FPlu}tvsI`)0iH6>OS#kdY84*l)>Aig#(M)SxdQp zR&}e2q7_;kI;ynNj1%q!`%^|~e(c_J)~)8?jmg-Zv9q7a(``^bAR`#Fa;uycv+=C5 zfSUjTHX$+OV84)+R>%IJuieP{dtBp^k~90dqXJXh)%GkUP%a=~2!Q^tGqIT%BB&8F zlKXiJmvQ$5ow-9>-6qh3qK&bIXS>r~UWa50nc=Ao3)0#1q2UM!kVOPbiVnmnuF~pHtWu zr^*%E!no`Y*2E&o4CoGzi4a_FjWNI}3RKw1J`tE-OYlpxG+2Biy4K% ziiBa)4n96`{54+{nVguLJ96rxCZ9LWj2%w@7?k~-p9VPP+La%3(3wXCK}&`fI_ct% zioGus>8W3e-c%Q|pYswhbVn8qG&zL)dF_W@;NM&_u_T;Eob@-)y(u0^U2aunwb-?si{7OLL1w#WFZW;OJ>R@;{8=iXZ~leU zXhig(yqEUotU#IwE&5-*IiZt*_D+-yvN$LMpy+9-F*28Z6co@ILRA?zsU%Y&-d4FK z=p_$=J?I=01Oa>auTm;sZctzA5bdpzm*6Bdh(2ic+M>qYh_ zW=C~e4;`4YdHzPKWdP2k(9*NK&kbcZ<=N-wSJ#6NsWMYY#0r9)#48Z6Nl_X~`!N1< z)dNq5(oSfm@Zco88&(+*U1&f;3z3}~xHiN`B$N^;HM$e9@9cKXu52&)=DIw$O5Blg zFwZ!8>wz7A$)T9QxilXOc8wV=P8-FT2wB5G4Nid_c^XwWf*e$vC2F(X-hk; zz)|l&=}2;qWSYos^2eP)rxV*g@YRnnW%W(f{c@=tb*>wAer4;aQrs<|uNv3rs*97V7)e*|wc3uu2bmo`D*Jo)vvhtgMZ)WQQgP>0o!Ypu;?U z-YgAi`n}SN;(el7rgbWJ6%RaJe#boc?6T$lfLWi z>vFwAnILgp#{X2ueN%z;z?JXb5{08)+qNEa2)xuwmv}}C)NfldOhDJ!PF#Ja!XE@p zB3DE(1dwk_ZXn7CxuW>GHo{so`MFH6Grgsx1Pt#3SXy|cVHn!Q}` z#_OoRh3lhCis+AbsK*^I@^+7|^3&7lwS9|G7qoM6d;259#bcP+NFv#cya6h<1`LKp6UW=cZ#(X!YLoKe7vXm($14jU}?D1L?1P=WAHHeT)(A^=0lh6+m znGC=|3YJd_&%*Pw`+^sl|1OKM6A5R~^y2W7(Z~Oz<3}&*n5(37Q)<%_oHH;a7>r}| zMcokaAI!$^ai_TjC#UgiOKP@q$a7GPTHXKVA9r?c^V)#)F){;48w4Xzi$L0j$gN{{ zJL8b15eE>Fr%XsGPd}ytJ~lgh6;c#ypcPLGfV%>3km1T5!PI2&X?MY0>b+?{*)o1BV4=*3Q92T za0??0Lke;MVETXlNJAzGt7Gu7`@f&_&g$Im+ca~1t)c#Y;Lzd$ii~LIlv!E$*lJd* zY6wv0*hnFoT1IV+~;@MO7sgZ$SPWT30^IK5m4?&N+T_gb$~qC)RkQ!hHqT6#t}S(jy!RJ&>C>8(lc}nY-eSIE7PN~e)T;Du-{jqHhn#@o z(4VI^L$4eSw>sDmNSv#hI9I!Rr7JIPpB<5>(b28nL0>OXNm{Q`mZMhC(Q4ccjRbz1 z;?Bo6%fp+7{J+iJZn?y_BG()e-cdW^!XW#}U_e?<^-Qrq2l6YgWq)76vf z)!|HZLR6aTw^j~SR5HbB&n?|m3QXsj%PTrQ1o7R0#?ePtFL7jNyT1}Q)!+|)ZupR* ztQ)HeKh$~JHJ=pX5z%G=905~V^SDCy;snU_?T4;vYWKX4%5&T4FotR+Zxahe0*A>t4renV!< zbv8)1z(_tj&5$jcLHkvo|LuUYiUC_MMSSI~F_R$JSV+_1fP%d1awq0cq7XRCe$#C> zmpd`>12pd(!h~K2y`M45iuszC=xVD$RT5o&qZyk4&5;iEPd$&!4Cv`iPppb#gWxn$ z8sQ7D_VMvi*IOJItU6Yg@Tn$S6vsL~2*UXB0oXK5(f}dC4@ZOrKge-xZX`xke{#f9Bk`KJki8L1mr^_ zIs#iYRhi4J&>UO(65TqgC`<|&GFYJlYYU>03D-T6kOOL3e9J*z_Y*P$DDa@@fGh&m z>qyK^UnUpgg_w#ecX5qusD73*d4rU|7oHCmj}&qyH{^A6%1qhad2kmYnAqN6+@l&`6lzfh8WsU$1R$HTjYv5g`3 zXvpWu&>rE8c~9=?2-5oTBTi+lVxw&jJNkc9^EV_odkKBhkfEsB@ym)!B9O%5ghrKl zkm1|9R4vul+E-KCrgQq*+dfOpSG#k$`UuGi_eGw##%HjaHrqf#|1C|T{D}2ZqsYMa z)G&Fjh~|N?o*!Y&Kf>B3RWC_rq2SOqj^~RBVlA{pA-_}=iKi&7Ds$^J`d__|J4QHL zT07iHibm%H%IHoQv_5RoAE8Cttq{)@a3vUsI5+>_Kb?URng6RUwp znoGRTC25%l;#|1@LrB-i2iXR*I+f|f;)X?rdou`GGCOb3;cjx1mxC|6JiBm`M~~;@ z$=eMtf=a^!HQg;-H?zsOCeHR89Ato43-Nsgg`a8ShhBF&!6s=0|nSMtNrEQOqG(KM%n zp#gf7m17Ol&30p@>fxIH7Vn*)z$LS^dyFL1xJK!brk3}K&$}-Jje)TQni&puc|D_~ z->R>f&_H1rPy77Hq=4Y8*=UK*63&$LbPZ_1n3+qR6>qGpGc^i)Sc@vVYB9IT*Wi`) zVtdq=Z4trXXuT8yY-dShP5`XYPFN}(HWX|kY7eRfbTud#IJuz!gPy8zt(pKE1|q|l zWLIr#>l(6R5*|3{3`RE&{Zv;Gx{uBk>?btO($XHkB8C-i%XAq%)E4TT{N@<0CO~Mf zA+x#jfDn?#@ha9qA3}&uaKsTBl82o-<}MZHXxem~h0cAOoRMbiQ@b3 zBH$`+?+88)4l#ri5?!^axJRLeJC-aV7YMEefhHn;wQK4R)8VTt7D_T(bD+5eZ-?py zsld0>z~TY%sC2laUBeeZ-i=i_ND#`;MeI2MdWE(Z>!5!9`V}5|Qn0lV#P8Mb$P_{+ z3N?XZY}iz_!TJh-RU!LPUcDmmOQkcnf|Af^qED%^pk!RUPeIv_bk zTT7ve%rxK?`1%*7aD&7B>AQ5*2Co7 zJ>}vubRylUkd{_d5j%`(keUOOkCFt%n$UAWsEvJE@1y8^^wjTVtL?3qEcd!1o23~Q zgBg}9Vt+r$k=H{QzK9GX;tnFJ=9Px;7bk6FA-MLiz2p#96|*1(Ssk)zHF?1o+arQ8 z217nKKok;a?j)6z7=d_d<{*Q0{iQ(~XkiSIbKMA2ePZ%CWR7g4DB5)j#aJ z%$vqW_!mP@j)edO0JIkJQx`3I+J!Y_O?yA5v&O5Up)zJCILY`^*7A=^~9?EhNy ztM6f^3UdkcMjpS%%;?BDS+nywe~B!w7gA*YCo5z-w5w(E3TxNLsBJ2$G=>7 z`aaw#n)he191nns;^-^ag85&}vaQ*iq-p+8&l*)b>ZPF~c|&QXlJVvJEDMS3ZbRnV zZ(pTq)emf#Age#@s z>G#_%I~wU+x^w&#dQ!n(dg8Wz_)t6W=hZJaQuVASomuaVMZ?AKRM^bB?KsCD*7{RW z?ckiV_j-}mN#Zxz3XCYVo^;?##?-JJJ`0yWR|Erowe`DX_NhHr4VMHf1*p`R9i?So z&qYZ(h45W`@k6||;^QMS$5;o`%kGDm2XtUss}WF3NeQ~(Vd?kzoaW>XGp|#UtOnPN zv_p9Mk`F$iq)QGy#B_#>UG$1th2#63NXb8FaUx1ZEB5JsW6MenB7M7HCB;|>^#w;T zR(PN>XrK>16~ZN?^5uscun_nTsDD4GFD!rc&NAVgx>VVXiOT=Z2o)lO!b-5@&tu7; zZU1>b?3mOV0bI$CMsT zBE;?gQ=p&)GJGiYD!K&I8zo(?HM^AQQokxS37PQ0Qc2v~ubbBmYKP+XC zG5<5<66;4CrRh|ef!cxv#;rhcD`<$Q2YbI4ml>zqf!){B=)uE|*vkyB@cXZ*xNL@$-j=fvAz`cQL(D`)9r@`toYoNoWL#6;@q!GrY@Tio*Y*LWVDfw-Ab><~8tnsV&V zO84cH3V?FjYt4ZF7mUM?R?Ht7VUj9m;^pHTf;Jazwz;Ce$)}E?Su6A)(1#G&*eE*$ zLUinY-NhfUd83yu_ns5M--Y)aB?do}*zAC7hkgS0Dc}JT*ihKCgbt`g7_(7=$nA#D zytMm69^}+u1Cd$=P?@kmiP9GQP{yty{4z?JaoUxzFWI@wd-rJve_E`S)=Ll~&=p(} z2>1Jr{7Yf`7xlRX;Tu=Vh}1VYW6?jWe3pV7=0;qc4S44Mo5#Z$>q*y9L5Ni(2tntf zcd0|dVTNFPKyuc?N4Sh(PJ%KI=jmW}7>-I}%@YYM<`r4j(p=E9AjSg$szNLS{HfkN zOmPGFb)dk(gK?#0P9^H13pqb-GA^nOJXZ){gh>hE)a5VVyFyToc^G?)B3WgP_*K*a zTZTdvftm!+@wXy$7_e^J`1q`RKQw+qC447p{m=8Na}2vr;E_XV!}oNp$r)4^qqrb^h7ZvAWWliwfc^G#td^LV%0^ zwzwcrAGIG^beOs^zu~%Lz9Zu6gtY50FyRqG<=Q92onjFSv;y0A@I<3O+r^^H)>;Z- z10gMd#3RDmdEf^u%uu0|1~rZW=0jj_(wh#9Q3dm*NaplXP-<;?%d(D8x}}MNB1qd8 zOW~J8EE=3b5S9=Ia{v__(bh6YBt{xkUMXv7H{+hm)@ces1$&h*kO+8wUW?2CD0+}S9?(}H9!3NjuBh|FCq&uKjl7<;GHS6N@TIDq=bJWQh_0Us-gn4D z&5x5p_GQ4+>vEXH=B3ybPaHYh(KeYU$$iLe;=tiU+fJ8msUOeDb+~zMi|l>Y+~(@S zoX>fn_FEmkfC!LPWZ_8&t1eVMshbAj7O%M_A0WBQ6*oL8Xe=DEb1xNl?&w=#AmLKl zPAj>c($AU8c$MZz7*lF9Bgha>N&2%Fd8)rSxUe}JUPEzooA~{1_0RG^)SJ!}q#)O6 z?~m>wRV{w$(l2>d&#ivo7HEl9V`I@}ah~k$=|1cdMSRk^bN86vxEpc}%zIh;hMwC` zmrVWm2%D3UF*^c3la5`mI*Y0WAOMBtgPIVBZs=eOq|hxy0HJuLB##{;N4AUdJhvL~ z4??f1DZqZ=U~M>!Ir6KAj;fy)pwx@jx-?*6^UeM3&lRmrO-8?0d+z%lZ|knBqF7MX zPJkZVRCoz~cc36+rhQf8(V`-gtOdFi{?E_SDz<=NI;KNVDY82W{j^Cl1D^=NE(|a7m0D}`dR!l9v&WI zXDOa7j_4;thZ>4XJH_!GSzk7*yS&fU7r-2aHy1B5Cx?`N8f>y^2|{QBkz1w0PtxD+ zr#VJ8=wvqbz0JyKIupP5?ZkG3tz8!c;(WVX(|5qY;uLcjimuq(-B?0Y)strDQPpGj zYiM_$x}3&fG(50)5xy%R*%}#WkbjO*7i^veWLnoyd_a~%$zFc|kr8SLJpB}zB03Kw zMfX+=PG!7s+8^%?Yrp@FD71HtK!y=(gj)r#34yF#eAc!5lh9W43ud-1)1PzofTBSK zhCymM!=BY0!9E}|+S}Vtrg{nPtTdPC9ae-j0X~a&&xU;nB|(@jz<7e}ynoa9R^cS( zSTq%Id;By7v5L-{#DI-3Nh3WmK4A^EUz8u5G%yO`m;tuKST6}Q^7g#tLn9(b5%L{8 zHM*9}TKg3;TXH%k1^%uyO*br7oL)iE#kND}hEbQ%zrgKe((vaRe#{I!84=p4>SPMQ zhH!%MiDUE9F9fn;C`G~uY$-?~$S`Ey%@Iw6D2FRL7Pc816<7FG8TLJbZjM0E!Xxsbju_Dn`%69X2y!Yg}n3F}?)EYR31)4#>@Ln3-`Fy2LY5PT>N`Z-G|t`%7O=q%NQVa2iou zSq~r-W~t2wDoQ{9AU1(+{@%A4vWd`FTS3gp=NChmC{ZSeReeTc1SYuHj3Re-ec}{+ z{cmulLr;a|5Y!6fo0_I5bI~)-pCuDM6}fs)k5!7UJ{<7gn+MY}wlcAkq0sR+cg+%z(?G;34M$IXYNcbnA_+tMgp zGK7HS<@*TKW`rDs!R>Dn=%b<{a@pPHgr!87_KqtPt^{&pTZnv41`_O%s?`hl%_?#J z9tkshLED^G!fW*&h!z@ribC(J&6FPXw0>4rDrKm5D$RC)b}9?af9gOF3wIokj$_rEoF zXe#^NR}OG39Nr$KF45B>qo`7Uo2iKR0ju~ifo&Nrr_16TRL+? z-~P8-U+swHpE#}R?80qftWnkz(tAoWp-VeHc-ViW_$p;~ zW{+?kYfNpjPCaYv-GK7w!7iJ(^4kFeU)$g}-{Aj#mj+m>LYwS8FIwHI4nej6>MZwb zgSf4PDU;`@dsUCPa|u@DVtB;dv&YhtQOgS9vFJ!KxDlQ8vR}X}?IOz}1n&>v&_{M* zOBkdCOhg+9Wg&!ETK@@YgJB0T;CdKC0I^2~#(aX*q0ObFn`SW**Q5Lm;C~0%!*9Vn zOC-*PabcexDP9Zrq*P_bAWS4E#lknpiQx$ID&Y@a1v!uYldzd1LFR8R#ab4Rsp5cfnJq9OeKUmu?#&`@kO zxIF+WZq@kx4s`506W6YH#6 zMhqvoG!ZCY)g| z-E*a^J#a69@Uj%!YSZyaUAy512SI+}y%}SaSfnE$3q?Bt@NNX~P<1Z@HhA)gxC2o( z{h|(HtGcmqinz}E@_UC>zTU9CDL4*U!M}he^l6}I;J(A)US1sZg&9=ao>v*htt$7_ zp~=M*0BY0Zg)$};#D)SbMZ64h4De^m7_l`180WvetJ%n?41;L}s7N_70V4l;jF#hT zwqH!f(}9H}izk99(d*nhAtovpon==TcBr7p@~QW|$;Na08H?dFHsOQf)Fj01b=HQ7 zIEPv79qDPK{W}oj4X+PTPBBEG3xP6&i;wRp*505(+&3IJu?{6JIyUfUNSk0K*;iua zdN>{A6zW>J=QJQQPM=~Jfhgk1ZeMV(q^@dW-~1dhCZ{m!=Z-9=H5 zW;m=LxjAxCoqqh4TEj0VbQsxFSNf_MjExCJ8dM_CHR7ZL&Vii}6r2ACPwBOu=c%vnGn-aBM|6IoUeyAVt!ZuY^0bBt#ODdwxSx1DCDz*vWI z72|n7K>WX2lP3w@fNO(DNj#QQfUg3o1mtc;>Er90+7pj190^qyVxwu;jamt20Ld=- z8#oZyu}VroJ`1n|XaURRiYTjN!xznNrSS7aH$YK`E)_A8auuv`jYJgG_ZQxY?8AK# zdxc_(fLRlRC6Xv`@rflXG=|u3vj=Lnb|LTro%bV*4#Sopbpk*YreYG){UM`lE|O*l z=>R=Ty~wUn3xNq5cOXA`&ulPbpDGN0z$4yOl)9q3eV9J-2UCb{o3z9**#HZFh~#-xh4&EM4t=6637 zO5J0rz|GiKv&KTCv$GCBRRH;rR{)7Ftz911t(7jBK0k64D=0}!CQvFN6F+C1{|^ba zEW(g2&vUcxOC*IMM?NBS&zz~pPpZmmyYmY>-LQC0H<##;(e}Ay=^Ola+ybk<2)l@5 z`k?|b%jcPo3kzMYGA@*&;hD_!Rz9M^f;QnNFUQ~Tx^Uu=?73otTNQl?MI2RlL$v9@ zo6@JJFl9=oyvwX*&o^9zu|KWfAL<{%jHDG#&eDDml*;}7($rPzOuxX2MV z21o%WY?$1lt;cV8#h0D}rrB5mlu2Sjo|gwCq18I_~p%F6_|p;ODmR3$De&0C+K>fhip&}-m@;2T5f zE~0}?HI1^?I`ZUiwd=xLGc3X&Zn4;#(_wJeWKZiETAo7)(Yj%>n`w83I@*Bi!U8Mg z#Z>{OlOvuCpao!k`xr2+bbKh4;8Vk3@14a(OY>JP|v{E~T$iVp5>WrU4Bo@pLlNn8r47^wQ~9 z$NVO_=c2bi7Tqc^l-LyDSJ)Z^sH#csd!0PI>A3zsZxeE|(o*shpQ&hxqiuRW2p2fh zsVJ<)Z*V+yiftT%)rW!v@F1c8-eE^c#8nb+4@PcSKhX1~JayubzWd!5fq)nX@wN-b zs~kw-VHV+DaNqq4fmE_0x=;G`MJZr%)D&(y0BJd7V@SY~wLHFj!R6xb)pcU%^LaUz zpu9T*nhzqP8;9H#*3l3u900$tIC8&|(iL`{)8*2l`vBPz|7{?u(lwASJ}*D2PoM_C zy95m)Dx1w)o>Xc2&>cG0oM0y>P9nd zX@FHW$}DHJbnN*nO7?rSXGM5@W@#Iuf!F-lsA^x+4s38eHs<+Wz}{6Lx12 z`#&u@22e(@9|bYkVDaqlyb*hj#75{9(ZLa83nc|alhT&zj`yVThB`I;{`%-#4VIWA zm)PX=_U+rr*~`!Kje1l;JWADdA7Kx87Llz7Z{?fc?u*A?Kd_ggt?6Sz55R*?L-S7> zhpvi-tdY(aY%g@+rmqib8mWV9j)*U#5?)NCL80A+s;i%GV}OrXhD9*J1jdO1 z0sj&aCDL?&@8|Af^hk)^kd&7GbNbR784cVXFo|aCW(I5ow1c{SnU_-eVbBCVo6fvn zpDINV2;%>xebi$P+@vorM|Uc635G3rpb9jHDh^~T8A4Pgq^ zSJ(#gMq3`~CKwTM6-h-6@AYr}rfEl3;T0=8I)0`bMbTB{}gK^opFQ5=cu8 zgFyY_}pFwQy{CpRKcPO&N9bR3WMw|Pmj(^iE%d-N!Epf~7yTtJ++kR3qJC4GMBde5-v_j2- zqRW>^bICiI9lXLGOs4Xb6H{ZW`+Ap+2ul=kjZo>=3bA40x4^%<$@s6~bzJ7yeyl8Y{%35+Nh#Vxn*-0;-YL2BR0nAZT9Pa@5FIwxxu0 z4~EvIrJ*>$2*k_fr^KXTP)?GRum{XHw&W1v4Zp|Fy*QB}gtc!!kx?jfyn$Y7-)8&S zb#|D>-mWi-X$KgR1e}_5gfSSoxt(3oEK$zZSRfSqzyW~M_eQj{SV|Ru6#UMVDdItf zuY=epLu8@>;Q`l<$Jx37?RQfHJycUfKlwLF6e%8R;v7}K*8|71=1ifpMK&CGb7CDa zUSd3WGt0mUVSj-Gz^DK^B*qZ5%WG?ELG`_S8#B}yfff>+*zQ+9paQEmw&a==N4tqB!=R_8HKf=dIc&4rmtjQ^&>GZRC9U;bBZKLr-_1;&tfdchNl(cWnZh%=)Utc{Kj87 zz^wN*Qa6XF?q_QsGdm zj}1xl&4a%_I&$*P_;YsA`wADSVx%pL3K9=JUzrl?zh`@`krlCf~(!*^5hYXReDx^(Df@iP) zvz~GT@HR*3RO42wf%K5yr1!X9KzjyY2|TOwl}?_bn8qQ3uS2tlMq4)7(fVnYza@57 zVb=!;7zCdXadJI9J;A1U)CRc55ld3#3tfTC#H1mk1~bLS1tmORdKn4>FKY@X{d&+~ zV$L8KG^NbA)gA0oK)dh?W*WBgMl{>n>`DVVf)f(Ol%(IkSgr;OB8Zg3&Q3F=CKa4L znbB9s;{Up7mKWs9O- zF}*I*p1&Lr5b&zD_mLwXbfm}bOEi=L?A4Jyd*;k`Sw7|>Fy2p!`|&Nnb?VAxo+;+QE?4!$0!W+8NU=Ld zkjWgn^z`GB zB2yX>qSt?GWR2ZuIQOCg#)+}hhNsJU^1Q|nje=hZ04m9=1_Wys=WK~NLYtuUFCM54 zk844lf8ev;9ERNB@qUQPmEB9_lT7VwbBe*rQg`5O$1Zp%=sNZRmL0@?@ zYQ+f09qRWU;Wh;HFadU0qm+@tQ?_I3HDp_yaxVbv6XD z_VA)^sp!WsYX4pD%>psmG_g#N`yJi zuA~dEz~t090Yz^25JQy6ewlgMbz`Pi70XeG+ZbiFAf@btJVsAA~yj0Z{Gz>6) zlA;Y-@PSQHq5r1J6j}}E>0U)8bH+X<_I%+)Z2|zmr@=Gu(CDH{e2*mk2EkF_-T7YO zN=>}wgw>%WuW5vcBgGp`lz_RSzdi;iU6B^%=f?vQ;3p3&Z8%jLrM*%*lGHZn(`0E| zZ|BriB0Rmpv0m0GO=90ZS3aTKt%h#|%J`0PHI;mrQoY3WIGD}q_U+sK8|5GYf+2Qp z7z(PEN!KkE49*t)`ILkNlRNg-*@WC*R`j25JNl?MquJIwtyMM?>##Ai*UUA|bDrL( zJ&W`X5a@`7;saWcu4`u%kmVi-dq}3=?=6^;y{9N7f6tCP%bH$YN6aZU8$SB3Hyygd zCcKQ;R(QH+5Q=qGEx1to{TWaefVXra&YGERv-iYmY2Y=0Qwbd5uZ=C4PLp)T%goX; z;`0w6e-uD=pd1&yPRz2InSA`*`kM$1D0iRyn$%u1Ha12blKFHKT|ZIf;*x;h;Ew%D zrnr;qu*r#IK}*PVuLUCdDmrjz+$;i`zvO>15QE6D|4E#F06K`^HGmd)^hx>`$6!0v z%>@WWN&^1y-3k^=Ws45x#3@1k0qkh#59R^ZV;Fkz>OT_S*K~lsUIon?7K}Y4D`<)h zMY0S+$cY^<7zIdxb`XO&Hu(exIRnW=en^TApbE)Dhr;o%nY13JlS=-yHoD-`x#f){ z{d0yIMA&T%?%N=PfF6gF-&9j{F!i+K>O~zn!mMh%BRo-W*wojk`LjPX_PZJ$*A< zMA#rwbu#aP!=u3?;hL}Z9F=r=EHJ_+c0;)J#WCMK$KKpLxFfwq-C)=|5Ym1q-^s_) z2j+@k#-*oAp5->E8PaWHW-r&{ne3WZg z4`Wp~`F^g5(*^JVwo0(yJ+yt?A^p@ZGEPN1WKJ%g|7kHg=R~Jj!<5sp-D1_86{vcZ zXx*q=HU!F(dE=h$Tz!SEwz*UXXNTR|>b29}?e-p+(Q=LyoKYVd|B|miBKF0H{q)NS zlihKeRg`EfrmUzv+88#~scvy}wL5ou2?P(O4AbwSAtN$eADb-e?7S{mwJo7^dlW7G zp6X9>VrVJ}PsJgXe&b+2EYPj$;dLI*?mK(I;{F^P`zuD)UE|ds#(J$sY7JhS?Jk^H zT>ZCOk^u%S6jqZ0%6du2VB|coMG{yca3{Iyi|mI?y*jcE-ub&K3*90B7M^HU3Q|1B z=sa+e5c%>zA;mdL2bVc|x9cm^4mdkM`tMw%T`=vBy}3Me)8zJ%!o2m9A9vu`29Jp7_d5@sL^H-) z2PgW=HKgp`7@b>w6mWSUMumrv$YR(6{J53{nHo}IfN|kvM?2y1Vt=D$@F_~H&?Y5XDf|Tc-c>`wNMi%sn_?i|E*jAffwtJ6%f%OjqZH0YQ`(V5S}y zv*t(}icrhpMRU~#%!~x-GwgwD0s|-dHYG(j2tidv4GvuI@iO-wnH@~S6lx9hueZ`i z)^l=v)K^mGjwzvId7SwrQrmtH{(99)EUDx-KjEjI;ih?GY^DLog^*}LhKobvK+21f z%2>03WX9TiB5fFH^Tl8(fC?OM`QQBi5M*)+Qjm~ucQm_&p%H2%)<<{;?{Qqdp;aq` zoyB8w45H=+s>G@**c?hb)d7v36T18X4SM4cc6<|PH`Y&)g`;N_vKM^PIvUHfEAQ~r zj>%e_=J2OO&RcRr>4@?@dK-~KT!jq*c6+f{Al2U)(W@jn)Kim}E^ush+!;?p?1rg= z4IJWX$f*zrGSLxX4MYj-2vDFa)Lm#yE&rbpdu?25G;_7{ge9^P!Qh~qtYM-cQ~q}1 zTxsXf&>4cDy2Ry)T*W0WDtQqA$Y@nW*wIm- zKf#y8xBX|6$@FoPK^QlR3D=$Zt0u>FiPP3FFr7_h+zLkr9;0}zuuvN*j<@clUdH9B zEvl~@u$d6-I{H(IZ+BI!4yW^Kc}-SgK3%HT@=hP!M>`Rf{dPzm7&{j|5$&IKv$sKG z@#vJ!P>|LH+co-dwY1y09y!3J#j)=N@_a-Q%yDIt^Q<#G)F-tH5oxY+;{HGO-aMY_ z_3aH1-hMZuy>uxkIT20q47s570sCa^^omh^dhRCH~ zq_-tqXD&jk_6t9XU9ORLg9W|P(?*W9)d^?9-7MpIEOi@edhP}8HumMZuxg)8%R*?- zVEv=j`&M5nZt=KBS_uyq2~-_NrZKu-bsrkP$bS&oehW~*oyiApa71R=1HOeR`e+|7Z(|qO$p}nwLo+_KHd981aJN>ryJg@kn3EwFDVz zhhq00`-4o}bwbotORUZaq6%ZvY7h`$J&|XGTQ+*{i`SVIp;uNIa9ps?X^AyvL;Ay`aKL;S5kL9&ul8YH^niri^Mr@3-H0?mW>qx zZ}w9Ovk?s^(T&RAj)$f9nD=P4n}-t)Wo9|GEF8QucM&^aTwz4|prwN zXZyNl7h_4r$6- zh>^8U@$FBBb@}gPuw?N_y59c|D#_xjW8BIbGoFlpHC2Thd31e6IBoyIZ_3|wVioeh z#_Dxr+|_~$<%^;?frE`XEnA`9hyd-3xbj4%t{=uPg>n! zCgYQi1Dq5kMURi=cMh<>?(Jsme?`0aby%D?yz%9N9^;Op&Uw4RZFd8Xj!9p)a*97% zo{4Jj@85TX&GHqQuDkQdM4ln#`SAV3nl;7NZnygcJRjD+VMEC`P=e(0xr6)G)oof> z@~6)B>g_ICt0Ldj?C2MYKA@e~DbU%4+VS*f^Sy1qbr|ig_MAIxN^MD*3t%V zFG^Rdk1)tl8-M5H*x~o&SXRi~yuJUn=7IyC&T7f%4O01c3b1?4emduR1K7Fu(6T#B zhYFV-Fp~DJ4r@FHc2=KelSau1?L^w)k;=ENw?C$*W~ACRB)z)it=?=8HJE8Mki9(J zD9$&#nPS{{ga1AE-lYg+kS8K2Rd2`g5Qr4$;$A(-QLlVHWwMQrDf7*;sz_96Qf%H{ zZjTU+*&R|hdH$wV>h7zoTs-HG@abA{@Oa2d_oi+O(OkfZ@!N?e5dv|nD<19i-}Laf z+W9jp$cKG%=EnQ=2bNs=7G{%BA-nk7RD;nt$LiN~9VYEBW(_-04_=WvV+>AW*5u=h zisKr#jC~&H_)GMBM$^XE{Wd)W*)igWRa4zYI@##U(s|4F8_{4jrIse#|Ki{P0{fX0 znOedolSb?Ip6*{MS&U}~Z@4tA*1%-CLhW8=HJ4fh&W3EvuYJ9Xw%pyM-%4OMG0h8pxA zclcO$PD_;tJe!uCUkz7FgYV7!?H<~v=Na@wB)vIo=6$QK&vcY8Tt%x}ov%V?h7X)v zc2cx3Z<+obm0C(mk>f7MTSL_-2aZ&{%D)lze@6GU%@5>Yb+byP(tG z5@*^<17@djC-6g(N%r{>$1ZoKOqO;5T8Z>6NhSA{P6;!o<#LfPlU0tFck`Ef3Y7fA zhYw~{io_G;um?@I19j_fG&1koY#Iip$7SL5B7(jRY!?TtaRmOzi?*a%!?jsD4mT_v zbf49iTw`SqjJvr5nIS#lRT(@oqDPL5x0_vVsxmlbds>)nmyGn4+wbLb;+HjOE?H?C zUb;begRbZ{FG>|q1spg5K#fyT+)Z1OKU*9MdGB7bL6`vu_F}zW{8RZ`TUaU%G>dH& z-gG=*X$s|Jre@6>cqDg-L&D3IFSD##`bK)uW7oTd86l@mv0W?cTj0PV_>eb_zkY(( z_?bRUbC`GQzkDA*J~CL`G4iV4=H;RHs>|dj&_#qqef06D@}Zb#M?33O?_Tz5A-(a%&&jJe(NcN>Xj7qrw6+jC}W^7V@K9DbB{()VXwP*V1e6XPi1cdU*Hr&-+X%ynaTBJ%0D&vas%VE7s?5KYetzLYCnT z=n6z~Ba<@D^Fr>uj4a2@MvJQ^WmL}s|K(J59lq+2KNXuxjT$pXGrk=aNF3;&r(C(x znxyEc;3zJ7;?dT&qD%X@x=t~1-lpAM8D{fMI_scZaCtOu>x7tg1z@=~cN?>#a$-NA zpGem3@aFqoqVAFlVLoM+{M`bQZZl(|deIqgJv9d@)^Sbmsh$j1lwf(*lGcVCI$k;g zQ$$wRs=Bc?Iy)rN$Rzf3(a`gW#@(A*%I%5={r+VsKAMxI64&&0E=qEfj-s)~V$X=h zbd#usq;{@d8JseE9Dt`k%@KQJm&P4Xlew}!soS=B>qzs%=D~Xt6MHAlb*~CJv=_Mf z!PR%aRl2!sq~$q3Yn*FC8hr8DHh+AVRWC`<#|_C`Wp_c zy|B!=bo6=xBii%H;GevfU6;$WLI%Iw)Cy*Ptr9C;*G>JnZX*T?!AJpz^9`ytT7Y-h3$RL72kH z^?KiU**7vY>-&P7BwId3^R8W1b(ymz>*jWE8}D;F)XsS^b6NB4NPP29pntNcd5@Yz zba_ns>yA>gy^D|bgmceuGgSw@Y3c0pTiAjFx|(<2>>h}1oDMGUE~_z+Qg7k((i+=M&T zHZ7-YOFqQ1*3zwO*YZ0>of)9HE2LcPx0zL^u4$$;4%)sKela=OO=H4 zLAj>a=I)-i+p;wm6-F#<+l1@WnrF{>i0Za_T&WwEKxw|Fgwh=Qad(e}ImcSz+b0#K zgcYu&t6jtawlh9H{GL)WePOyv#xuDE+-Yx$qZnkJsluj|DY_*2EZc6>|3335&os>q z<0_s1NU0i*j;Y#(V%i5Ih!B2xtFAgq*Jm(7 zu6uXpb=||!bvdRVR7Wj4n|EJt9@cFxv@IK@2ap?W=Pb#SvvymX#}xvsf+^#wySnW? z8xpI-)ly{{hux(X(ih4+b|q33>Y|V4_qddJ(_1ErWIfpz(y3CG9hNQ@TERi}ZT-|o z)aFd#H5SgU>h9(FLwywjr8Ef-^0?AwF4;7mv8~K=-+ldZ-Wq5uH@mQ?(sMK3gsKs@ zaMJmJ&V#r+-*dg%qZvMIev;~CMXR~Xt#&i_^>&tiUe{-wG5ev#*i-31_q@X8P0wv8 zQF7rOym{ia>ZU&N>splAFNC>nQD}5wo1c%HQ!4kD5>t)oQZ}GO+4Ot&&T0qropnC& zP_9>|&{U_&@anD7gLTfX`vM{bp69$wFkxsIHAWwPli?YwZx$nN=vkFeDpdkTD{HGMS>Nuae*fruw=((Lb=ekO_0DpJ=Xc+Ys_iBF@kJ8R2U*t( z4|15=nPGZ|t^2F06Hk49htrJB36wqN@!(TwU#!r$<+_K{Jxusq@ zRzZhUJ+gUg1hlU&mJieK>^-Y>Wmo_3jAXROwuJ7jdJNxpl%^`Ds3atVoTEZii4)yI z3Rh^e+NHWmsInyE#4=AK?E|Xy2JNzLlj?kFs!|vJs5YB*ZTfEOn>r)eAo1qm@3TIs zdGqq$Q=+yD`gJQ5z4-8*N)MSWoh+Izc4L&!m(VR}b<^&<*xA2MOsM2)DWImTEu|}@ zjXF%%jUuNOll6-CzPl2GY0H}Xa=fug-rj6>4*KU6Zty%I07XuDcO)BC=#xK z!w46ps3Oe!>Gd~StrfKMx7)VSJg-crA_MWWG+#N@;wo>bc0*SZyEGV(;dAeWCfw z11pISZM5CO;qu#5mjtJ>m|hUf#KEG;*BbvGtxmLO50M$_2?V-`rP++ z5~<@o^lq8n;xmEFCglv}ARV6C9KNc{X|s0wJgs=CUu2G2X2}T8e2FogmI}I(!n4** z!FS`f)=d!+YjeG9OqBn~WKRvMtAmLXIgw>^{M_kh*kvsN8SMG-+6hH65e2qNkCmhX zuF14ym%X__7h=9rIVL}=rQ>^*G$`g%yQAe1EN02#%U4a8Xcg*ZY9YD{Jg&R;b(7|h%=XEOP*HuIHfs%KBOsU8hlO0D{C#+ zqKRI>=$)IaYrU3j9a3m}5X;+c9E`_T~0hH|)EnyR>4mFoVJ}V?mw_ui%|_ZKE^n zx(DStk=?1;rd9OK!&1(Cxuhg{>+YPC1>Gszz3IP^8Y#Ki*>UINVo)pz(c!ab5P!`Wfr09P{H`&iSMJt@oj^ge-yCZS$>ztcfAA1W((y|devisVc)o*3T)ixY3m0XOh?Kn{zRLizr z@z@{-FKfg>z2xLS+ekWAp4pop41NjB6zvzeY9qI`ej&M}@9g89hqsF6VM%Vb=Xs~E zw^-<5r2N_TVKCeBm}EP7tUqIAvDDiaX;sls9~Wa~gqSsbsSBasF{_t+hkwVYF^%DNFu(2NXG-*!jdPg|(3dp__0p3U$6gLZCH2i%Dzn$U zu-K8vb=FJ1j&wysWo)cbv47I{gXdH_b4#GfVGid5nVV&p>y(qTlLw3#7js>s;rEnv zh%F`aF`tc$iW)*0NVC4+C04ICzkZWk*=lQOqDhq0oOGK(Es0NxJf^&$NiKl|MzLoo z$_}bC06TGzT!_-MRWBrG-E`pr`1ToNwW_XPo^)9#Dqf*2qZNZ%q9Hw;bJ=>%ABPvv z>yAG%a2b=v;mvG6mXM0-=wYBhY<|^6uG1N>1XT*PKT%ayQ{+gE z;tle~>76(T%Kk$JJ$?Fo3tZcsN`vYIcvqNrUtzCW>t#gAHwY8ryK2aPAn7x&UUH3v zJ+UQVa@OW1%2)eaKT_=orWXwL3GrQYs8o$_uUS}960m+Etzq6H>s3eHvYs=4-X?I;E0v9!%C@^MN5$Nd*LUB!>`PT2y}r$7s+@8<7FDQx zF4uLV!FKIXMa8ah|A8w?vZS{S6jkFK)lyfz%Fpt!n5F|t2FH(w`ew^~dbz8gR~18* z+j$j6Haj{0>51$yRf@v#Q7MW%Y-x~4-c6mchc%9&3@UF@gx2}t+UXV{t=hpUwdKVl z?A6GWT*+<=#%z_F$l;5@ z0as$W0)P5Wp`uL^V_R30#adZgXcR_#yIcHJAi?{4P*|FquGehGR=F;t#d06-%S~HO zHo5mL28|v_ir`)OC~(>tX7~|P~gd#z9oBZdFPyYEi`Z-!{=a)2ULw9DGu~eR6dd-9Avjb==XDtFlJ$CTp;He!=scu@qrviFL=OlYG24 z%kG2Tr>ql-jF9grQ#r>x_`T1gX}qj<{NR8ne`zVF_qM)m=WM*Wu%SW+V)vaVae6P+ z>jCf~d`Tj_CdZ<`R<-|7YgK_(u(_E(kFs%2Lu0t$lbq+KBsKd2na!`W93vT2Q=P4B zW7iOfwRd%?eIHsl;2jJ1$z*K%v8<hix-9)#bY{Vn{JaQxl9TYs1KXFgZ18hfq&ox1y>lC!zDr;lgk zY`%LCVg)P1eL@C&>`c_7;8Wh4l`eAKZp&Eb>qQ|NOnNz(AFm#}Uh%=Myi) zHij_nEa*_AUThe2y@=>+ap$2mONpW4pQH+>@Q){IcbRyZ3dF7|*93YevPk>1PFilh z>QffYARcLqmE#hrSLQR4*hp#^s07%eXrc=4Zg{VVCl=++_8+@1-yN zerD{!t8tkJh8J6lr~5@ZY`hah{ieMetBM;u^>t))e|#N&eBz$xxMxVZ*fO0|u)fQl<3ys$v4lrlkFU#$nj5%xJW2T&>*wn{9-kZTp)J~p5uDvcb=p=d zHEcFUQv%v>?i$Z);q9%WBxx^`+8hh$)1i$VsSeaDTiMdjP5K;jQ!5#sr`WE~UqjoG zzl6rRTWCPw*0O;ksB=C#*3(l|Wl>SGV(iG?m?b}lr*V{`T9FeXlXN+D&kb)fJ&198x{OBL=Ffj27tSpE`Rs+ELNYx^QKy=R z_6EI^w!dhqRda${_)|$=(O{STC#etP4P>_VnGmUz@v)=z%k;+06CneEZ(s1i+&J(H z6lY4gFdsI5~iL4y|! zWAk#qUTM{y7|-e`l&!XJDWw*8KJT-jtjiM3OW?dr;*^V(u@(;D+!4%$o%nR1T`ncl zbn&m7U}F0G{Vy|tg_Nxf%!`yhewM^6OU{0^Xd^g%KruGB^lL|P(Lr1}-Sm^xpW`CT zJ4iP*v*eG`MR&YuC+&!DZS?&2c(vKrtpE9+ieoR=CtsJ#N)YsyKP&FD+4*HmVUC6Y zqg9Tc>a8G}qv78ryp5jEwY6S)QvFa%{tsvo;I140Du`TZr>>A}Dm%ONymcn6;8v0S?LL)*tyL5@trMZ|ohH zFx?L7`MC}8>db*rKiul=)PL`UzkO8W#IHsA+aD|1{onuf-;({WVI^=)LjE#)(DprJ zJM}fw?cqNtn~1G_+wYNk=Po8row<9WvYXpmqNYO387{^4)aa?4q?02iO0LA;a`bL7 z^5`w9)Q5O_flhWeULam9FPh3M9$kDNHAQV%w8$vd%Sl?G&%B>gqWfi^oZg;Ot}voE z9W#kbmH#$8x*b=e*U{h4DbW3jPcpgjJ%4}O>;~)2O-soShBsn(qH74f_OzNwxn%ce zV*W++|6~3wUb(H5l+ey+Vsc|8J~+)U`gmA(_71MCFw*UKmD~vxkCFa0=b)zj~F}?1r`75Ze3l2N!t(25^+t4E-L*bYG7n9^lh^rQtY^<1XX7SF73w@Ih z5C6QBA|bK*9Lr?JJ#Kg0pZ&e#w)5{BchI&I%9v2fGh@T+x2+a{QjvSy}%!fq%VMtTBtPpXSW_ zn5m%k*LL}J8REB@k*?#azyIOK^NQ++zZCl4UV8yF=WqZ0`Sf<~zxVauUQ65cf4O1* z_D&|V|2G@-Zx{IJ`#(ed+jRW(^nYgc|C6%XPzLjs8jLiXSi>Uo>n9c+LJHp<|G9jh z6Zga{$ya2rUVHb{HCA5cvzoyPatX28{EnSE|MrM}eW>KjnPs;kdAwPc-{B5;B+(dP z(UE0w)$7{ZEWRCHdtVD?6o-@fz4tx1_I%bY18=K-$yt+UQJ8V4+L}MNlv6g4^U(mU z(5;*7CKQARZrW*fv=pmp)0%x8i5%0Y$Q&9iw^Ht835S`LGf)_X~UyK$4_rE7GyOR z3guxw3pH+Xxu+tFZ>!HabhItZD9&22FV+D7{=A!NCLSqA0w$20=cDPzUU@p z=irbCwut185jibR863;R0qKjVp8BuagarM1cw6<$SAv05I+i4gQX|N$2|rcvmUfU= zrT#O}YRLh+mkQDUEat0CZp9(2N%Z0{H8L{dqK7Lcxy*h~D5H;_D0Q1ve%*74G=YK( z0`5W>SoUG6d0g`N@g{XW_XIGF%aQ<&eQ}QBt5eQyQ%O+Pl>HBf?XSsk`xE0y4Pr3UIkOj9}-m8pOLF)VsNj=>NyD9F#9~Km)28kWq}WTch@^`;K0zN z3)_Z=IP6?Yp^@7ye_ig^Z>QbFg^xrx9%Fk9xdgC)D4+SPEJ@oe!K=+Y5p%7ZnFNxd z$s@=-WbWMXaLA#!{nIBE$!JFXeJ{J6ICy^X$!~oci6*sq7AsCa6Y~}5>=yIQj(^f- z9&B;+$2Q>c_Gp@(_E+PNhJzZ3 zbZhntvZR|g1fv^+;KOu(pLG+d=C?7w#%d}V9o-Ra5{9>@cU)|IJ=~|S8EoFMSmmb6 zaaOi%N&%2-SX*A7m`5LLQgz;Zd!3A*y-2tLOJTg}y?!-}6wwQD;DYWv?Fl zkCk!SvYBnHN#-6ssU;aZFH4`oD%R3jik>;TsJ7NhM=@9Hgkr{&3_R7Q<-LPP-Mu2y-9`V zrsY7R;mI{$&nUZP>9Y=I9Wz7X{%(_>uP><1v*Lg2ub|z^Kh#K56Ojz4kw#4`vC39rffQ#;d1K@s zd4uGD>VgH`R3seA+eT+Pa`VpSq{Wbj2f-AmDWrr&LZ{J1ZSq2uwG#K1dJ4_9qtv`t zN!)SOJwol+ySb$W1Z;r^50eFN*jxn4r$MzxX3Ivc^kB)^F9Cd)D`^s?PUC0rZKpTM zXmMUe1ypCbr4D0Jl3m$qn9tjC2#p4DQlj1NG^eg5PP8%wTmzBn+;Muc1K2M5= z#Ns}4wbE@LZLgzP3x%R`ttN{vSL^Uemx-_2`*wN7oR4cNOyOVfU}!hLu{i@e6Sbrk zw91tVVf}@M{iWipwjdbtQ!Fy2+iYFZA`FePZHz8?h_3e$%e=T)hVu#i^fL1fHSD06 zOYjdcQs`&R&$dY|{uF0Th8}g;T%@I~Efi-MC#4uPsrJs!dbI&1^)g3xlt94NnTki& zPA}+<+#qq{?g$Go34+Oy(a{u`La3jP9KMSvN;uS!DOcW2MkSunN%uLGD2hR~zuKQq zwow{|6|m^IY0OEr-|k^Q&-plHU1*A}Y}iI+3sYB#_hb6tnuOFPYx6^kz)lZ;{(S0Q z83}N!zbLhf5fuR#*MyEf+UZ$w~!Q#a<(fu>qFv@pTm#ro&AW}? zC!hEZzj@;&N&j~Aq{qAlxB)nSAuGS^=qDb+jw0mVCLgJ0D#*Nlotol3S~u6~MWN=c zJ?JiiFU;Obu`P@Ut51aTuqljq)AthU5kIlUa|>lF?-AalHaQ3dIMxWdp{Y@^;7+R- zox=^pgb3*Iu3oysE9Okh6ID+b+%jtPy89jhIk8oBAp(<~j@^UA)Iy+f74weZJ~0^l zHj$RkL#k`;p6a}%4hAL7@6uYb`_oT7QN#J;Q@(xAJ4SfD_gPq2K%~lBY9~%_`Z_bC zL8vQnaT1l@Ry&OefH#nol+>0ocbVm_+`YrUY5@+)ZU#jO{23V%4yb3kl=73&NL9_j z15AP$mgNg-dhU}!*jtUNo?zi=D?EPkq$;rp@q7N)z6ZUm^cTCpEUU48`T{p41|i-jhJo=pAYg~aaQRDP7tBcQj< zyh!QQ7km$)!kTl#qm5}9G95h zm?yf?XzOm`nvsg#VIMv_>u&qJNT@9;)hP9Ch#VIPlTqzx9*)%)7ZZD{*667>XgZUw zcIcuaSRXa-zPc%NIlrS72mFFrNK0z2UgWrW4Du_3AQ z$!0zR+jM5;;EhLRCEd?$n5(I*Flm;Ah;hHUr=DL70$!72u zpopFrXcpO0d~q8otwRY7T>r7u#Ip)jq$qZ$=O~|LulA1Rx!Nh|dqXv~)trl#DrYRC zZV3LakH*5vtX?F-_#;`1emhD??&ev*9tf{1nUYXKU!L|q|Gbo|RloE(g;t1r0HbtH zaWN51AQV6t3!ed>B=lCKB9T&rZIdJ6uoNHNbxR|Scq>}6pGNr+oG;WxAx(^pMs8^E z1!-lL>1K%d2Yo>e%`<0{RhkiI!DYnUsNisy_o~Y*CSvS{m2JS}q9_Nw_hD@b@w2cn z0mlZH`w`}|8N;U7qevq|G_J9xbc`6*EWuk>FNIT?7Iq$~?mn@$S?9(1Lc&xbe1P8x zvHoFN=4W1_S`n2Auq5N-FQF)LchHhB!%>w9q6Lz+$SY+~g#I zP|!42%Vq5I9imVJ4h{qsLMrA9W`=BMCwOw;0>IWZY-Rt?Qoxk4bItmhuL+OVn<(F* zIJdX8`5S0rB@#_^YdjEDY}l~D&dO>}_jI;rBZY?8>jOAg0_*w-MZkkRofm?2Djn59 zV9swo5bZ|Fy?BTpA_#X-{`4ju?|mp2A(kIK?h4Chde?!pEi^Gy^)l<@^H}mQ#U#sb zMnyvNP;r1VOu}mWZQ9%0o68qE$_L>Az*U8~V1<82Xce0&i>8fFN(;gPH>-{a*dFqpZwXwL74!r$;+m2x&`J!RPzMe1|5B#3L1_w0`JNI~$vQhzZ1e z+eh-yprmm2NO*nLeJ{9-D^5S*(q4XgPm6H29Onv9>>pa}YG1E{pTdYP+uKsJZJlTVKr;e#uk1;iv6} zY!HDHe3j`maL(X1zgF+|eAm9}^fP$Ju!nHX(@Xv%@%3}9sdw+M{2Vq@=E)yZNgx>s zgcaZa!s04!HDee~9okydOfRIJIji-!LRbON5!`+l8JNQ#(fn``Q367M39i$l$!+Fx zBA%{r!!*Pb*1x$mzdE7TU%UZ4)4NZYL zz{41KicsD96bC`OZT7gaS8MjZ=3V+_J%N%T7A0hvs5*hu(PRt_w5O3a0sB^F%@|~i zPSZ8FKFm=v={=iCZh`b$2y_Lv)ULVA5}Dd?BvhWoD+U`75oxI|5t>RylfmIEx)HvK zMB^1W!3c&>+!9g;OQ^YZT_erT#U*T#2w+mTT@N!$6Z`qB6n`jXg8Chem6PoC3pnAFMW-EnjVOmv|B>lr1hj)?OO+CbXw%?}k+Z>t^ zwM|Af8bmv6T%p zd{8dHM}~{wD`QIl3lj-9k&Ldh*Fi}Z^qG0WBv2eoYMVJXgdC6xhaW=Ih802-n?lF6 zGD?TkDV*oIl5pNd=M|f0N`D3eW9|@5D7sNyoYE@>NP}Kw z|CIap_^oH)?Z(sAY=S32$=l*}&7t!j)BkBX;XMbbNn5dB_~|rs-ZDyk(~;-Sb!X0m zRDt@qOmdytA4&iMqTnt9dF3=XU6(9;X&y=rfg_>@1c&Rwl{Hu40QuTq?{b6$_}DxNH5^C?Wf zQc)XK+G5^Jb`;;Wb?pON)r0)|BKQvu_tG=sIjSf+rI=YMAKLS7vh(%-z`k|(F zfSe9E3~*uC?~rz>|2%;|9kfW{_u>e9eF6#jOA&z6NkHkt$3NmfPOLcc-lVWldG5hg&PAO0iH|RcBrQto*+oYYVXou zmb|NC!L#OBGVRgbYFQtelXVVQo1+KB8D)=)?&fA*&p7=~8*OdUhT9l7h9YQB`B~>KTFx_4GE++X!}QN-DYWDgUc*PQ=kT4 zXTOPR(5&0y$6>=llDyU$PVd-n-Kni9L?D@-p03Av^st{F%)>Sxj5fiW2Lh0UhrgmgAF`QvJk!fmIbR$eJTn7nsAd%ne0D5Og~H!DY> zCkc%72Sf={>{m;^Y)2ViI_ndFBy)%WCLsR02wmV~%aY($em%d+H0jU~+4R_P-+%>I z7Y_F7rm3kskjxf3n2_EAQ2`EutW-n2%>yrlCjGQo^KU>35J2h~2#N3kx9Qd$ zGF${(07VC1ta;xOLy_=>>%}1mxkYv#uB*K}eX_BorsfegG-MkQXE2Yg|4r7{f?3OV zwg|(RKp0_s3AD1rpZmM~4}dQ>G9<^+grR(mbqFdR+8GtJ7rg)tKyhsq>T)OMHxf~={8$ zd}4*wFrt-7SsAo%Vn0vr-hO(#OYtI2; ztx2Rr*&Zj;!3TQ7u?cJrfEBUk&=WBEKw-2rHJgTpQch+RDsrE$O#3VaTg&$N+t8xF zjP$sZc9XG+Zl?O3Q>O;)w31cikaKX@&b;Lm4uc2Q+zeDqlHzHrmJo%j1{TpNLT@fk zdot8BbxW1^`9C2Ks{#l|8W^{{@?PcDWuv9KnXygJ@la^+CvXJDTqdL0vD^JXd3Pp! zk{=*EP762~OmhXv9o+ngXY(Uat+(AL!4iPp5taZEecK;vY$MP{ zg^}4w`VTo&@|+7mx1$t0af^U$@1`@!!?@SA4_|9ty&BL>C8O^;mA>Xbn^vnGKh}R~ z%$7&@J45%fZ_t}-V15Hf51FDLi7eIZww&Ufpr$#@&u?mAaIUVdE@{zSkQHzM&P80o z+kp9Jl8GlTH9ma$6agv7YD)vH8iF(AXNhv1)K`rRKpdh~{)y8k*L1-X%hCCnH&i~Q$@ov59lE7136Lpc zVP3y#0(HZgh`43BnB9H;zroZJ(H1rCyA-TjSpWtziU?NuRnLp{bGw;WS}LR+!PnsEP7qRjxI5Ea^H|JN zaK}h+H#J|1hV z@=v$&6%zmK>iP%_rnFi&ldKy6!9b*=&7YKzkl z-2aFJAQG%GnsR=iP9q=;l;IQ>6-~lI0cI-hWjw^#w6wJ`2`v~FQhp1c072pIu+q}| zV!+@DwXjprV@s(1q@8$rX8EF4sQh`*KsEs_AiO(s5sNIDQr(cc{^pxPO?Ws-sPV8U za|c`&3D1>Xqc|{&_K`nLIf)ZHPM$kXp8M#eBlk6Qz*Ib`%bj0>@vI-CrLa&*NK*t>u6+M7?Kq%qJod|xz^t3coTnq!aM@kPK zF(wBX<*odylF>>vH!#_dzHwti!}It)WHA8jy@y%EX5lLBN;VK? zJIKDl%)>+X&Z&w~PJC^{{HWo9H6e9e`5oyPH&PHoV`J~5`hZF1Y6W$S5Q%GJVG^Fa zd-r?qc){jJ_Z4dYAq@p8hnlM`{ z5G|I+=ebHZ#Oq$Y+Vbkv-Mf{aLp{wl?}nh@^fdN0XWOm=^B(0Tx|t9e(AaUx_Kj=c z{t#xNzyr4n-PvZY24#)AA8~NyjUfovy`}d}+1BS?)zJv&y zErfRkL4P%1_o%BOP!bg2_y{D6VTDJn8`^hXC{hgBOh$z}l@a0xMmz{##U&Iqny!(ai% zVx+)~CCl-xm^h3cMv;I39=;Zd0YS9cwQHwgi6o<`uc$tC{Ulfw2@Wz=>DzIj_Z>IT zNsMdr8fa!-R^}9xH0Wk_G@`+{RB+)5JI89lWO82U6??uz63{OP!uf6C0IE) z7=V{D$pI5C{gcMRDOO@>mw4<9UyQ@~$nSswQNhdk9YGV~8seC(pQf#E9QvIIi*j=)VIodRZ-; z$_UqY4q(?;du5rc1qbrViTx2%rS(IL0~`pR;fCV7=$a5K{*a5<%+RQ7moLRt@Dtw! zo((CwexG6OIQRi$!TsIX(Cv8SaO~y$1 zwR0||eFEM35RuC!64m&o;{BB&dLGF{BPcRp*OAGckIzpnJ~fK#7y;~Ehkc9PfcaxV zDk}7On|V8>H*I<(mRhGT_*70surZ@8h>Ly0A%7Wo4nSN@P$h;3KG|=hz{PPK1q=W) zU5TTLBMu#-`;M?ZHdjXb(tHZ*hKJB;2-m+%(=$uv|50lvwjTJ!HxTf_r0#U0PP#z( z8QpsfoBQe3bP%<{s#X58BYXe=-$)VO`4ihwm-^7+{q@a9e`ZuAIQ(Fp!NSi!S%d;D z3<3{&Va%nk%~I1@p{HDIbs|p>Vsfz$Fux8L{FNqM!=5d`yelSwkv##E2&PJB367Jl zQ}PUuW5p2A5i%(r<%|#aS01qKlqb~z{v;3QHuhC(r}`8L^o=_WxrRqdziX`K-X@lr z+LlnsDg3n41pKsXejF&+QJ>cqMna~3xI9pU3Hx>nNAYXc9&-8 zpp)al|geI;d{JRD$J8w;2h>SZP zOe>0#xIp{IDp+kn?M$QZns4@n+GsH#!qiQn=}af<9N+S3A*T39#6Z!~_MGTf4pGjT98R!{*ON*^EYz#*Kwi0y;flJ-<8lF`tQoM|Chft zr0D}Z`aMjt(x+Aamk;KD2JpYv3{Xl(w^WR@{3u|Acb`F5`P(k~`IJl?JSjvPR*Es( zh@5KS=#_r}RZL9UTc+)!|H~(t3f#2=YHr$e9=Owiz8tB*eOm^#r<4E78#C7YH%Bf$ z*B1W&bHo4dC1L*$bS@!`kd51jF%hH@c@sQ1{J3NRAHmIr!GeR&A2J-)$Wq|((}*D= zLzIFC=?iU^g?VNFq^_c+}&WK=~U^`FI={N6o zUpvBU+o-U^tX8#F-HjJRicBToaFlE6hFlxa5w=yGaiX@AS2Z55>!3|Ey`yeixr}hO z+*~E1l8XisbuHUzqAf{_+L2!48I9zJQ&)_4mvmCz`z^V(c^`4|;nr>7iwL_Bo&i7g zC>M~0I}J1*-jJQsZAJ?)VZe*$g0;_-B^IlN~^j}K7?v?e@I8V4Jzv=1V1<;nyE_mE!?j{P(EG?BV zjOJnKO!%7`e2F)OocpS)6zK1lyDD&Rf%my{zPhwJNne-^s^~eAeLqS-FOm=nBQW}j z?#+8JjzterdP){LLPJB%BwNC(0}BoLTi->OrKdrxkT?PifiMGADx`RQ!RI%HKpc^X zfa%6E8#~_uH{d&>LipxN|#PNjJkmM0RSRa5}DAE+ZO=ac$#savaWa&0WAYIH&Rh!03vDNS7a1q zqT&;;0K6bTYM-jrZx>~Za$D+8X#pof9o4<^#9i08y|1PR1A{=M9#39o@=`+{O~I%B zSYJWE7yeHE!&6B;pq+?m@c=ZQ8WkFp8)1R|2gaUmlHe!X>b5i^byo|OD45A9FPA#D z_xKM;ZLCHKlAoYB*2r|N=qt$U@0L#@#X#tjlVsRvXk>uqWtd|CCq%Ovu zx8;T5Ir(hUB2ANb)aj1pYbXW2nxWrHs;ldbxb5M%BS^;$r>~1>ysxQK|6#?z!WsaV z5fdW5OM2&@ud)gNGK?S=y&;o3Z`6O4?Di7FUV8g?K$K<1$_%x?5`@hLhaxm;0*v}pj?p8 zd=dTe*vNcO*YGWD^34ykOODy6X&!VF3YAa4Yf-DSI)Me7tG_^#AGZu*GVF7_*6ywMxwOF3iMSV6}F2*5kn|!;zr7Rq> zu_PWq9QuBSg@<3kZdyN%J2kFV|svRbc_t4_FXAjvK1&S5^BJ)G=N_zqPan4<4-BcvJ9D0gF#na?n!f%i#ZmtM`t_vhV-L z8%7cpA`~i}qLP&zErkn35-Q0iTZoLLC96m_QIW_V*<@v&W$ziXl4P&n^SG}2^Zny@ zKdwKnM|T(Jc^vQeYdpv6_4>6rL$7l53-ET@w{Epz)jBQ}bRa+GoacTIoP%ZI7FJvY z|H(y2fkf4LelisA9@0a+Kth9s2M8$npQY}u!6b(~Et&;@hRfLA6A!LTWlB?@xm$Tb|6dD6jtiJv^Fse{QtAEnSpk+h%+2u$>lal7oqd|%a&OMw`_>F0r5sK=fRrLl!0lhx%p)?t@ z9Gqg|tijlzlM-4}oC82<>_-ejbj6Kt9gHSV z09(aL!he+`vn@>{aSSAAGv3JILe`WBB@*(#*smpFS}`bbP4D0D>RqwSxx;t>1pr+# z9T*gD22c!u@9+=qL2>4JQJ1JXl}p%F4qHHV=xhs*r}f+KkdM6r+mZG8^M~^XZN|3< z7_DPp*Tp`!l}}I;aVr3I1#AussM_F9WAUhkP4D`Y#c>`b*-tQ9K&lL77BN5ubp|j+ z#NQcVqia_gOTzvm6ClYmb#Ne*q;Qr~8Ck$K+M8gYTO_;m<8&2R6&rv~Y1Cxw9 zf~Rj@f~9-SfBvN7o?oJKKq?pQvW7Sp{CwlY^F<7a0v&~N4yl(oHdEGa9|-oN>~{D) z%nlXU)4x%Knu{0)cAD!ANc-BMnF#0oDnf~3A;;!XOW@6h_rTz1acD$8f zCTu2mDB(iT27rW@0Ze-2MvW-DYtkels5s9g%SX)>nJ&zu)8)+ls6*2nTyuye*ZVQ10;pFmk`t|DCZ9bKAIkAI|AM@dFmFb$^5~hS?6$vudS`G;n1)cOi>U~iEol+ z_rDiCG&F(=3pgg2h}%&MB}7^fch5{<=o&f$Gvm}Qju+2!-g1;E z8&%HcWM_jF9(Kq_l>;N(wsLC$DY;g&lOR`rwj^F7Vl8y#D6=fmk&wd7Wbc7?It}hw zG|O|*;?fJzDZ!?qJ6YO92pOF<=GS)Ek9w$Z0pUfIwXyqthTTsCp|pjzDTkwbcXU3& zmId9eojE5jvvL{;XX4JCT9OE+`X&e(W*qfXFnLxr1g+NqQP41YeqTE*&5h|39;ESxGh>GQ39Vl-Z)Gb0H3&`*K}BDf{Q9n^i1eH-f0iA@5% zb7Of)ZZ#{}J9~3(Ikm2sIvhh=0V%*ZxhTbH1BVLQhTDwRDwP*?vM;^o-0H$dkmUM& zUm!w3+`D!|n6=yZWKrGoE+eSR)*C3!iEpet6}sM?-(z{dz+Zu2BCcG&n0q7nN=lhR z-0mU3yYoR15D+ygLK2a@Kw&Pw-CD0q>5k3@Umgur#;?O26Ik9wQ2KECh8oFj%{aMQ zeOxTEk34Kkv>YG~Zt!MHqJy16|=<2z; z-UOe72LSfJ39M~QM@Pr?+$PF~s=M#dEDA3k)^g z#l_3q^FQrNXHQmy!q*Y2OW94VHzPOL&tY+qm%7W}Lz|A^d(}6rTCo zEd@g9jIfw8e80cxtklRj8ejxI&|9639g`8kD)I5z?9&S?Xl-By3h$Kc@p&5AD(a5g zVkD&R4<&~CN>|@sSk|VdrU-jp(^$JrU!c4G@>Qb%&48YnhKV_y<%SoFnW{VcNy1WJ zlT%XQ!1+2f12~!&N14l_K%u>_z5Uy*jpb{E=u!IMj4b=%m=>*S>*C+n!_DJEowjqs?-tb4uD7=uIZgw_EFSfIMST zQM+M+#p^u&AQQ!^wYbZjt-t&1InTy?Qx~=RkHpNE zgiuXDT{WGN8|_xyXPI*^YGqJb*3NuXu>Kc=Oth65CF7{0BdK3pw#%i6_qqz{YN);x zb)oXoZ8vhRH|nM|I_FuHaDDL{zmvnEPV4oqf=90*B-bE}}ag1pD z;%$GUHQkPZ2Ce@}d!Xx?nq~F90J#sICC=+)q2qdC zQ`PI7kwV+oTSrzx$->cNE-dx#Sij_uPEN8MoVf>pN6cRNQg&a1b58w9w5Qpp>|WY0 zR6Cb1xz?Q&m1SA$hz`i;9DBn02tyQ6g=1%NQ(avhIVyfOdpa_DEJ?P0_Gi-QrBcm8Wj3aQ3|Jjd--VUM^U}m`7;pQmCAQp%t$s=x~9`E`<(EhPP1W5U!;<0ZX@ih}$G zp^DWI+qo-usv?3V;sePm)#;N3Yt*Cm_VZ_y!sl<&l#Fx$r-2A2(}UK?zL97z8ifNL zfTa{Qkzz`H{t40D)*SkI3`1~-JWqTdY}Y1ucFf&JtV5u)(P-ikYimi8haCjtD=Z$CRePz8Zyxv9`H4EB zuT4V7K0R7DIVOYSv){r><{g_qy%7$ldNraVTQ>x!`>Xe7DzPuPR;pg&=NoWwQ=z(3 zl<1C5EN+WHM3qRDl!V{Cu-atKi0QYiwKnr|_3bXOorfwX&3(XB7CrePDR7@~-9jR(P z#N#??H=jHdYWwZgKbAQw#iNJ+o0ED~2Qdkb2*7j73`*78!X} zgzePHm{KQ|2GLH_HH}#s7=7Ih~jmN*K?;u6+fFTQQM zu`7eJsj)F&9cJ{F^C`2=GxUj>93rJvaZS}A%Z+LxY zOTW^4Z8&EO3qL`{8cxAgNH4!yal$R8qM{<9#41n%cHcdSB76ZnSY1jOO^J?7d>>R! zI7Dsm#}OwQCcf((uN4myHSOYJyCv;h%$`p)+#P*=(b~S(oSQTS=%Vp=pdB&8!tUVVH0{ablGTmjRO`pA_{hr61ynqvBLT%8XgRrAci489SBL= z{?m|hU}(bkP#QWw(Z9H%$(`gg1{r`O^&evW-Q;hxS3;oDp_Zfqvxj@p-dGr5QJa*t zo&%pC`A$w(7Gr0?U+SZ<5|!zaZSv6t|TP?Hjad9R9e~ zIFsg~w&Ieh;EX!e#1J>;vwMxukvo^>oaPBZhLkuIT_xA^d!pD%PENn=c$m{S-BrF>_{iknwBb*zno!E*@{jBn z9{zzv$(_lKT7F@=I~m^)Et7v${Fc)ZfueCcpmVu$rpPjqlJ%SOV zx-wmpBD=gRCLQ&I5}QKzoPx9k=!x-!=T5`UkU|}DYxIZ{Fq3&F;kge9j&*jovv*@i z!j-@;q|)|WMc)MDx)pQ3X_sDQ3A2ZMXa-&9-?vTMq1vuGbt!eNs=%11Z^i%9f^2h@ z+b_eQt#OgTgd!#_GO}|I-4X zx%;ifwd*pT6-2f~w*cg+O9*~Gh%tzH)o4|Lr&_tH02BXD>NP$GBj}e`ENIRF;AOX6 z%KPP3V(*R~)lDhaqTo}{onBYrU$+~+|7DM;TM)D$<~5*k`)*=T^$gqxo&va@W{Up! zW4m zo2Bf&Y9Lkb!~rIzLht)LjWTDoPgdd7qVdh68HXMz@QF^*Ku@>P>oX9^JSoiP8|P3a z!srlpW9FVTz!bu!5TuyBlZF0iUt-@zuJDjMflnf80H1Vi#fL$IG3R}2^(;Cg2f<(v zc#gA#tQPXu(CK}1o90TYnzxQD3DTB?%hR>9NJdM$cerbdfy{V>2~2bivJt#01FYOZ~&%UUtcIr z#8?alulCb&SLh9dSm6nfn#Z{bQX&D?P>Bo|D=jwiD^2=kUc(GMFNieMzAYmXiF6h< zN=;NkbGfDq@JlC9vR(S;YvH6*#)7GsoB76kwxDY=j3k7%Sx_oNpu48#dC~ItQ7B2< zH(40xI$}Q$ko_+DwOuTNR;$_i?((apD-rpoU7->;$CMBLu3q%)YCyewb(ENtfU^dX z9nq9Kuk`nq@cVR{ixNIVAZ@w1xeJOWpM4fz2FhgLGZhe-2Hp~K8-h3P&U&Qjv!qX-h@^YmEHaU{ z&|+d7 t9j#z4dh6~vTu;WYla?$h3L)d!PwfSsD?Cgrr{%o{rgg=ObqjS<(IOI0l z*vJCfEutUwuOhF7#(6UPUa;R-cf}9-rc(@D4VMH>spD1w8g`?JvkKVdwxv`tyfF2Ydq;2Jp$V2ecDg05l@v6E+c;-SXEufFaWLV&;+P&o1tr zA@okLAYdR;g8H)7gHJT$z{wq?q%)k$s!PR2e_*{!R!3RGX38;TYT=jiNANZE?@Mz? z$!8b;I|yaW{)w$jbk3QgxHH{jcj4WC1Mt%*!<4#9IMfF=0bQxbq#3lS9l8cMosI?2 zKoTrKf_gfm%}6m01h97CUmaezGPV=q{n}wzE+8jwK^=i;0f;k>YKt-}VO<#ig?bX> zRaBTT+lKuOyMGl(k|}?hUm|pWkWl}%oF2MArEINr@FfOt<#as?JM-(mNf@AXYKn3i z!#z?^4dve8ho9&l=fc@iLUW#s{StD((B}(f@95Sj`Nga80?|ZXT}gIF8umN`32eUo z`=#r~@!zVK^3e*ehiu+F%B_CzW$I|S;(PNi;Th)Oi-Gv%*gvQ7L30eCE6t<4#mwHU z0QqN=v`*o#5JAQ3`!Asm+@f{L)v0!1*)fBPbIF0FS?<>Xk2|-u!<6(KUy7p8ku?oB8fI;G$$_MOU6(&cR zu1HQ?G|n&Cw@|=H$GH+E5xAc}wpTXA+tTXBT?v7G^cI&xXIGIr6TG$>#fsu#MauCH z-PdV$>S2!CCoolvV1gXI!okBpbL!sj(*U1Q1p+8vRVQrqX-JI#ClOI`x8iCJ&i01O zyft+UckVaPRTK)%#CIV5NXh54?um9+9nO~Mt#(Ms-AQ_;FU`@lpA{@F{(FsU*QQQk z5CCAmY-NyGQ`|Aad+U@Qy;I{w`HKhz#dUj0eR!vz)2+Bw$b#qB!yNoGjtMg+;~haF zp6z=t{Z=D98kp|H7c^;SxJ9Op`wrM=@;9xBDQd=Mz*5}sUJ)QE-V!bEg?-5KyxmD*=(&x%oDNOWA^Of&8Umf{R za;)343Oxe7z(-BA#C}|<4Q>VS1VGXWNINlU9_i6r4pRrZ=Z#Sg6SR&JX}00KFWHM` zUIHc^f*Y8h2a0cQtp(2!q5?sUvL8XzoK*Zwy8M;m<%07mI3^#L4J0_u#ZJ8Rc56^6 z+Q6g0ESW1qiw%xV6b+#^O@UJh3&-R6^JHCh;(Ag+?xQOY|&u{LXFj#pl&faehAu>z3 z!I7~3jKB`h7PA)7Z7Nj!VJbOx-`dRm4;TK52(9T_P3dT@zT3EC^ub%xq|jMA%PD*2 zQG?H{??jGT;MsR@NjHg$dA{MyxEd`yl%aX7bMq^V)PSfS8NRFi*T4FhYe;LbYxPZJ z)SVJa)DLa>f_D!`PHS7+RC%~oOl#6d-E{8yIpMZHndHS67IBu0qpr`+A9M(4Dxik- z22sS}@J`0eLX4P!E%QY2sv4|emhZ(v3wpyQ2`-snu+Z|`=1OPzM*TI)N=C(iivq#I zSg9v9$ZXYvFGD0@Y>WSIKF_}knb3H<&R^=<@{KzWh0K^^@k%60X-tP9%%{a0Yty3E zr@&pe6|ZN}EuN%ngk}v)C*e0F!@zRR25O<6rX5-R$t+|4f3g zCni`hTuFEZFVp0^)TIJEl>5HQ%=ZBVfasrlD!W?huqE>7$_}cnC}ar`p5V{W(bFx8 z=LThT49*j(gYm?ayEsl=yJg4=z(VI&LMCl84n6?AchemA_b)!-RrDcQkz8ZSPm8rb zL3L2(7LAYBC1C>K0sRN(#^Oa}9uJjiaS5SE5_pJBN%Evbm zbRYo|tuEO~olZ?p*Q-5l+yMRhqb z(lp34qS}Qfh<{A^;TMEsSRFr&)<5`0Oe4gT%}+1I@fn8msouy!U=9-p@o_Au1>cJe ze*@tK#QA1InUY(G$0Kh(tEXi#DD(+!MsV?}91xg*UW#Ln&c-Cr?82!;$i~M7z@SVD zO!%WOuor-(nT3TiLNlZ#jT1pR_0z;mM~%e7xgOCoMMsYw{a5q>agN=m4|V~pA>Clv z2#YXMFFW@@_v`jdvJ$&3nCFeZmp0zDw6t6=ob?KvWmB+-AkS7Yu$N15n0 z*s-r;BInoSeI}ijab#c*_Mn^4#jC%X|_V_{biIMs_r7i2q1Du9k%aaJO1V4(?C&LRc=D@>vz205d=0#cSbw6|gE zW8}0pA_vaHnrTj#Sl<5GA|K#y3U736NOt`#n(wK#a12#w zF~iu(=)Z#%w8Y#)bw1@!=SOD>dbe~_Tqv%sUYXjVE;e-Ye`_H>`1<-rE6Zc@@7i37 zdkYR1e3GX4@04*nKqE~{7im2!OB&BY-H(xntuj(Zw|xU$#RIurFb!^P{918iO-+q@ zZr|8ll_^=wUO0aIIKW6#$UTD;D_SWVehfiKF%iA76m8)?n@tfb8S3^6Ne=RxI0qjb z!)5&1PncyH44^M3pD)ZIfhGcs?7TA>m`clz1nLje{IMJYL;b`DCQe>iSy_T}iG+-4 zO*_IIIIw>|1R)%flDeNjLud4O6az-PT59U;bQ@ZvCyWZDgKLR)PW$FfVu&_CTl7Sx zwXTdutkGrVKm45G1Ac3n7$Ha~7+?{F83)k<4*(bf#DwuOIY6Eo5VmOYUpx>{n`KAlUD>zpK3|lW`)RVq`M_wzHi3s)h2AFHRn-dUhL|ANcu%tyH-zMw89%5Lih8L6bgdHb0bgF_wM2YikHj*s)X zZLIX^ z@@w9_xhKndoj+zyB6dzv-}8K$W;yPywTp|7fe`OXw8M*ocev+f?~~RN;v3lOmLttg z#ARh!2GdmHJ=-QKhzH^VWEnq3j%(Kg{7y$Jq0k>aux%wJCqLJvydzt(wlEE>R zkURV>B3<(V`SKyBk8TdMKBGSrk~wxzHSS>8K_&P1Mg5&MH%|QM(a}*npx75KJx)29 znUDHbN5lfE-qquZh)YU-sjjAJAd?SAg(Y42&H@jbn(*Qf=f;tV!917ccg0n6Z=%do zGcr7jF<-X5yZceKLD>DC$z->;tus@0n=7cBw|ADDklwLQKXuC8C`~d%9p#co;LgPOuXMY%vq;>g7}T%;w4A`l@zbf#_YmWMw-Ct%+wgZ4@11$pX_*g zt~z6LNk<23ddIO%UqyDc)*8#@bVbs-8|b)oBrWlOTB6jh2qpXOm6;Yd)RvYW;}pK zSUCNVcIR!#StFAI7fLMaR_-bV^vex1W!diD6SUVy&Er6Lz2bm3-_eW!tG()t9)`O; zhZ3!h_W!w0+Hm~i*zC@{$P_s3VJ#Vn^9751K&oM|*?YA!F_(84?zFcV9~|5=-B(;y zTDn;+feB0?cXJ4Z;af}+4nBLxTW3fA2i>xUe#~qfLL&TXl+ifX&=)VnvS(?JoJYM; z8RVnK6@TuN3z6sEFxcuCc!Y|UeXfBrC38!(_J-d15I=g44G=9e1GfyhC zNVgT_2kZu-xU|Sq4rL}H&!>h~5HZDNWT-Z&roxtO;V|C_GsSarBnGou%x;UYdFgGtY6i+7l6IE+LZSNxN6M|@%8H7|AN)VA@pe*n27fns z1SkVS0g2Hl;m=~4?G!Qr*V?&89P(c(kqpl^SiC6)Xl3o@<_9Ulz}!Te{ChQhz6`Sp zBNe%f4s(=pu>~mqh8_8Ww)3Aj@t(KDL?rg|5iZ<0R-SxKr%y=? zB@8`?Uv?99GBeC^%2rt}q<^{NuwDs#R-Bu=<+f-z&QpbK4EfTQLY4S)m`5=CaJ+POlb5557^jTOX_;sk8W-ZxK8~irC@P2o=jnDFOGdZ`#%y)P{ zWDz2qAKKown8_i%!42;U>1bT#HFGAh@cIC;$%U1Gz(b}Gt3NxX%J`UL!UbdH7*a5k zQK5*$w7qnCq0B5x@f;Odyr1PE6VHm^HRTrllQydo4<`7+>-~n^Fhe-@`w?hTQ?s+5 z$aDXxE;ig6tbHJ-Ybq9_EpJ-s%sgtH@R9o{uNk+ zMX8=uLua@dLPKwStkfSp`cn+D%d{%>y(Hedc6N*aFNHkJJM)eARgavUV_IJM)rVOM zY5LkJ>?2_v=9POjU3CPD6OItKW!S|+G#m_e09mcLxLWd2*A%lV7vJE=c!b772^KH_ zgl5h}@c7=Owx7D%!;NOcAOQI|fJTlxOikVzoR)eh4>=zhJ}78U+U|(v;CdsQd-tB< zvmbgXkNp|7&ey7}#@Td$Y**IW)ZJd1;z~5@B)4SZ^O9##r(6wHvNZgJGL4pUF``OU%!!qMB1$(9Q({f zGO}`D-~bq6p86Gs{F|h<9%IGt!8@{adGCa5`oa40=i24ehUAm~ztl-L>v!b{g|xRj z=-QLsM@<)KA8!GNFB@u9eiFgsWcG=;+e8Xo+-EWrzm&s77vo;*^`3Ua!DG}25mr@O(;J877-i2%Fi0 z01Fa~Db(3Hc9#v!f8}@A#owDDi(-JxS^iZfgBOL{;sCYF;_owgg*e1_qX5eTX*ytl z*^D$_IWiuUabL^mU`l7(WAM-WfV+zyFD7M_Sc*f1_5-Qy5F;Ay($!>8PW{p z8?=$~q(HTZVR=dBU{<9oG|@v5T9-AiUuVCwEEHZJ%Um;)zAvNhd#pO|ke!sQ>{Fy^ z^r}ji@BOszrXmTT?3x*GISOGz44%;Q8v=XFYihQ`AMmk-52V|ques<^N}mDiXN84@ z35VpCBJz9qBUIu=s%NC%)#Ka9b2Fe`Tf`cI-QE=(2Ei;LJ3p^KEaiglfqT)o-zrVh zV8;msL!A^BjQ)_P(C+Lq?LVT(d=ZTBu9WGmB<+xb*0H;1FX(OwpZ_sFuJ}CS@!8D) z^P=>StPWM`|f$&c-)+Nn$9%<(5OyWa8dJ z-L$Pj%tOW^<}xw0-CxYAmn*pTw!q$P9!|KWmO1P_mMnOgNCZI{n5{X8)V7JstI1m2 z=kk$9iHS#NHfx$Ay2PeJ+byH&V)XE(>@}XhP;Dw5f!6?+cH7gkFdvU7`{??W?r5q{ z#n+BX>E?6|M*G{(iz|0&$&8FbEqC`9L$TNg`mpE;TmI~_)S6^ z{%XqV;AaePRIR_Qe+#LNKN!mjRv+Y8^&yRJ^LjKMjL(XL_g;+#-34+Dx{J?wc@K3` zf*($1Rgqz=B^ zR%awUI3vDE{#@Vp@4KO?`#^pvvY-AblEQb(9E^r~sS?krwEV^$+Ofmr@>urYFF)kv z^Yk9@h@% zedp}gs;Ew+e>&8;2H~%@g9GWK^Xx%s_hemR_$4pmp08C$yy}|{b}3gLpI1+NJ9t3$ z{x#l8|Nqkh*w7zq*nGTU^GLjfC_C!pBmC(FdzOpklfHbsg4f`l3rXnFv*W=h+r zj#$2T-y+k@RPdI!-{H-pLqEgqF1pKl`)yL7N7c|vFCCmAw_;8NTH7%smciVW_N=F^ zzFR#WBqdh$b6S6?M~{t8wK9wqAXQp%u|G@$pO(61pwdcL>;mMuf9M)oSG;_;gLzuGD2$E{ z5nDcJ$H-U#(tync)*TvrdzifC)cN*6>>B!z(*EV>pFe6_`YycnK#{Dfi?UZ}OY7(? zjaGl|haj25?Q0FHcflE|SBAd2O6|AfE;8f$u6A94lF`}Q_wTa*&x zxAg0O!T)vJ$%z$UW~@MY+FS1JP*H$pdp4!&zf9sikJY~BqE)1)cyheDeWQ|=<!!O|8M86D?E!z&P;5U~Jl!`73WWtBOL=n2ROk10SN z=}GOCp&qiS*B8N{E6j^7xZ^&^FgyqN!&qB3{LZ@66`+OvmUEP9B{-U~X>S4;i4o@`JxQ2_nseTdEQr%UBsndumkL|Bbud+nb*I}csE&>uKaP?I)0zDut4l6YHb4%2?8FIg zGd+R5kI19prY@)2Sf*rVFPYgk^!9SR4h}})V=mhDX*y(yBDTlUJN?h#KFx@&J5N#X zt*X)!R1G#9+Q$APyq;8Nl<{IFygu&8iKk_^XY5CI-x89A4oHLXR@;Z;s zhm>0fIxKfYBk^z`yh&V4a%b{;IK1;mTxDK}Ks<4HC%sGEKNzA0*<-aT7WJH0W5~v3 z0Ni)}-NQh28J2@>`W{8CKe!z$+qkDc>aRE#%Cy7*xq=Asz)QR3D2>r)c{Uen?JwNF z2ZY8&#$O{5(X;xCoU@x+zPQS8f(=_t1%`-FhnEd*YcTo-KaZ~oDR8VqnJxc> zNj!W{N`kljK$p0lZ7-zz!6+~aB6;)a`SarYDgoTLi+&8q;N@ZPDlyA)8=MC8ANpW$ z&Uv@*h4+^6q$k)CcjM+wWYlVPnqMAHt@TE8?BVqzf#o^3w*R~ZmH?j&L3}sIn_C#l z@Hp|TpoFN%`GdPLFmq<8{gJ@Y%#ZB&hMJ#R)8-_0u5TvuiYBFnV&JK~1y#6!?nuS=>t*;NQm?gy^Z`JUw*LMq9%gY#M?A-YRA`5bM&#F3ku4?NVSWT zfr%0QWP3*kuEW5;@1*{VMJ@gw z@O&Y*XEP44?r~7lfLgNe}~VzBQ;raTYAapZ3xv}?V9zm z2|&QVF}-;E=J4|(eoA-qd%lg`MNtHtF@S9NCcCJ}kJ#3;g&P1I<)&XwAD0ESO4F>S z9N2-WKr_8XV9x$>%gZxuuF0M>eO)DKPJwS{3v^Dn!JajUkf;}UzmNk5lY8Y}Bse{$ z-SLs8+@Gf>C;Q>q4cy|)A54JsK*VHMi$Xh^MV!=VE($aRvFQ+`S$)E) zi#Usb{Kb72Lk31UaA zU!!^is1(EH<_(2rgpW3_Ab367S$bYP=p=I5F7XeOsICU?O0wLSr{sGUT5p8nPBCL| zP%_?Bl1Ac-sJJtc+lKSeNc>qs;Q;_&uS}i_;YFw(o0gu3g@98SOv~H#!O{c=TDE7?I!+Sl&L)f7nDmaWbB_#LnHW;brw^5XsQ!HUl&Q}I zLY0DAWW}LnE$tqjwWm*YST5d?Wf3U-#BqPes$Tqbm*xejbCj2h7=vpcqsl9ASz;j4 ze z1S^NQKak?Sb`Y75YkG?Eb^B87yyZ&~yKKoNw;B=biE8&R#^&Sj`cT|xQ ztj(>vm!{5lAe!tQLZ$Esj*Ueub4p83-vR(e@%|Xin z7e9^>a&zaDsEo_v11#^DVJG)gS0;~cVPGchnRT+%KKANWCO-?(J(er}V!8&c-yO0mv^wDg{{iq&ojzxe4g$J6*Be*x=lnBH@(9`>M|`ZXi0)? zGVd}QQ_{{rirXwAzFaq8T{3oeds@xJ+?R>D?K9qf5_c@zUsh&^() zasLh~eZK>HWItS%4?0FF!WlrDh#|F7um5tX25;AN$X`>@*Wz4`2$Ak;Z!bZKcULm< zZZ>w&5jE6wlT9!F6oB?V`lsA{h?bKlNCFDK4<3j^!FbppL|mOo?$y|)XIm&4{atQF zJcGwP`Ko(}gDk$_tKYrF?k9><__+~?aTQQgsao@Vh?qdd;dOqO?}e%&M-ev6$tp1r zLM!w2RGM(40lUM?OALL@Wben1R&G=!q4w9Gcuk`RkPDzOfI6vg*3&$0Cnu|eyEj_a#|P@Bxt6HWo&Q}!q^A)8@%u7oj-C?%9-fEN|a z8c8M=R`DL?ohWy4QV;Fj;kRVS>^-a^$H(`6^NT@>HHqcBkt);>&-!d5Q|09|+iqjF zP$C5?E$T7w2iKMw2*qe5dOc)kL=8`ehuFcpua_!{j>DgN=H9+6Z8{)ULNYD5U}1$` zJKl1{B$G^MdQd#!O?I#k+p@xu)Vhh5+pPE{W1;-I?w8aX{I#R19!jcAOJc}^uUMRn zAU8RX*A(2pbRo#_I2)T~mvQFktjq$B;0gD0ktJ@=_V+velkSN{dzF_y{+Ydj%$=T- zAX&CCBH1_&TraLXb!Tq!=-z3if2{kMy}K#0BB9&Rla-ghaqCtU)GaU0Jt;eYf?o9q z6DGa9zehf)me`Vf+~<_WJ^?!axi#%B3l)hFinU(k4`dvW<%%z#GFqUD-z)ff=1_bW z5uU89-1ROgK*nQsmMe76rD5~6g9?dMaq7%d)FJUk zHu+6s+e$;`A1Nl?u*zgbA&y}VY}p-*j;_&KNtm~k@Dl3x9Jz-@n}0vT(LnY zK)@ikx#8Dvg#Z0Avh`ELOWWKX7@*Q2d6a4U!EOwb0Q2&Fdg1V;5A$8{SmgEaW5bq_ zTbF(dZ`?rM-hk0yHty#DU&kxS?DS6;XQMZcNjxb zK!)MsvO{= zG)EQMm2H2H3((t7Spqeaia)L&7+9g_(KPnTiw(HyU{?HtJhv4lw+<2MSGRuO@fMcl zsS{QSJsDiO7-GDZWOLXqIPlGJBmK4?_TP#z>{;mhgk6cqM(ePxg7DkCH2M148;d!# z$5Ks9G<7!vZ7an{mqT(v3Ovc69De@XhRO${m-Wwl52oPT34JmzBtWNlAAH+<^XAQp zY4?r$G^xMXRt07>6VxB|%Ng!Phc~XYuhb+>vyzD@SwQpw|A%(bW?~_MM+%LV*Yk75 zeW0!Pq{oyEyzmoQ&<{R<;EgGFl29NG*^+tB)HwG*o&zLTpJ4m)abVi~4o_zWP4;9=fv=+#l=jooz zdpy?VTUCzQytD90{^?JjYj(S;=jZxut9K=U>$UL5?4>c3j2XRp z5zXVXIQu3dY=io$Uax2*_^x6?f{OsPN2%jKWiM}Iq`qknd(l8L8f z!fOK1hgb1k7O8c0x4I-Q)#&M`qGCacF)e1U4EomFce|d;imu)`%=@4zc+sVWGW8bw z=V+cOnT6u6DUMky1qNf`FF!`VMMt%+Dh~SP>3)df15`7-8e&-Np1^g+(6&z{ykGbz z^JvN7zHfi_^*bxRSG%{dehPpaG+O0RA1t62VbWs}oR+4Wjp5$Jl}nK^H~0HqXxyjR__Kf%C94ss z$1uK-Ty!EJ{%16qHfQRVidb0A#!UO=wv)|mOPb`he>^4o&fK~q!sZ&=Eqyd(gPzJW zU^($gflP~X6U0V*gL81=@IB6fb7s2umA+d06g zgz}8?`nGZ z?_F-C!3@SSq9gm`rS}}kF38J!jy{6Ivox(Z{#kiT%N}C&^1G%np1j5mf#bc)S;g^` z%-`6&pFE*tUSt6p3uHGnCx<$kyx{e=Of^#d;hNf+i%%Pl@4vyfS0Yjc)fMtFMs_KD z8AwLige7ZG3#r|s=BmIxq~}m#V2K+*&#_n@&Gyma_`Fh!YG#r^^BGRs!n;pMdpMcB zR;#u$ny81WA59YY>?j!tbxo{8>ATIp|2cElwmC~%!NXnC#o$4epOyW9cF%Eh=o6?; zXZRe}=zH55qJDKiGFmD1(t1(egG=ufiniLv8fu@3mgTu^WSgSo_xHNC_QSG)#q()~ zuUB&UW1M@kda1enpdGN_cA$u9Ip`pYy39rY8L;^y$>B|5+mmPb{pIvcI)?2b zsaF%{X2{3RQx3*aEa=O6Q;zIB8FT)h`%-jf2RCnYQx}XVzJCZC3YxZl)aW1qP`>@k z{+(kZAK*|7^SkCVrOJTblN2oXxjeDUikC!=Ozt zda0MTZIc+u`7=FFpD=C5U#AvP|M@F)_6L0LyncS@*yJ_7JwI$Ez>e|qp)U+=9GYD^ z@9G`s_`siU9}pjx>+wyz=j~C4DGV-UyIz>qzDD?<@@CMRCDM3GO9{&~StGct0KS1W!N9pxDlnbTY1Ameg#npAFxZaMYAlTTn z`((7p8lUr89&M5_W|;NcYFVaVaX`Cc%5F9L1D=+1UO|JI<~3&h6t0t0y6( zU@g_7wv=0h7(*=qT^`yTkY!(IMMIUMZ*U5x%s!~;OA19}aNw7!Md%MsK5oFhD2_qE z?A<|iz(l!B|3`@CRw&T)>vg;Pa)t%t&h%UsH6^zwi~8Hk2o(Kj&Xy}0aQn>b2X!MUXJlhaDX#hozBZrd4_MLC7=rq{-l zcAWqU0H&X9Hxkw@)Zx5?g=?Px-{XV)`E`uBwwdaXXQ{x|pFZv>BRthmof!uvjC z=ibtDS<&pzC$Ro|(v77N4elr=R0OVn6!p;rNGGpbLO>h1ziDl8^qa#KtraA>qt z_6_Hk4N^$XkD`9J8@l_<_?f~}n`Om9l^Sn+&d>J-I*6W7Ii9uW|7-8P|0DN z@4o<~M?Bd`#&VF_Mqhy{xU^XQ3+tj4qk~>)fWvS^Q z+Yq2)et$s~vkDYAcd6!{1oxQoqp{Y#c-D=opocQOg93E1`xo|r&XC(xbX(@B?uE;j z+nxi`j=h{H`Fmvp$W4`^A&dAROT6X$3TxX?>$Wq)&{UO$7whc9c4y13(L|(Abh|!} z1@=ca**vri?3g@yG#b5H+W}?uSmoq?1Oi0xau|DFyp0I~rZgDWmVqpV_N~)|Dg4$T zvHXUQp>`d|?UJq{LHI;**YRh0c(*eiF^tSe-)FJ z5J`zGt=r{0w==q?asHmJ%+Lqa3va{OAO{qNuwNEsfBLZM>v0HBDy$C*>p)6=>3 zkWocF)4+giWQ66UFBvY&P|r?&R;T#X%fR-ajeTC;FaQ2Y+O8(ib#G9s`yU^VzP$(B znLCdhE|KS3>ye>Z>%cyb8e>h02 zEiZj|MTm|P6QzbJ5{hUXGos1^V-fJEIj9H;WR+LWUV~Me(YYjyBY_SABsH{-gkGY( z_G;S8ma7pjdqR{yjaE=sp3XhGF5^IYN{S;Q+_?LRJ`6+{EcA^*#~5B|sx$37*0I@T zyQ+t9Q~#po>9YzyiBm4%&B#+{vEYh-JcaJ%JjnLq85XPY*}93b z$v#7#DB0+lHFY5Kx1vmt^)b%uu)4`>vnF@s1naEp&KvNls%i2ho$)ZLT?Wd9!A0xe zm>uC~9p2|o97N)m!S@8yvwt+wx%f5P2P={2kBnv6_B|g8s+fL@NEKh1=5OXYb@?;6 zMTfAF#jS4m3f_Oo_D0E|zcb+s$*B;z0U}wz9E6X7UFw6}1I-&3#l?1OsC0CGud3&D zK6krgxQ)*%kaDMMX}HCe`eYnsMAiF7MtgC{fE^?DppO1Q2Lds+wqihshB2CL-$~!f z;L;&0z6s92I5E%s6PNF;eXPv&-@&ewHRoPNshHzeHTYvsdA1eUs@SCsUZ>Xci}5!d z3JxqJ(8}M7RCf_!#pG@SW`EU=h|L&vqWQ~vN9wd=Z;Pu03FZ%g!xbogKR5kVBXR1~ zO+NJjvB}HRp`vfKtySvFh;gWOo!)WgsgJ<$xzZw>Ti~u0euu$GmfSg8evc4F=#uKS zb|rQC`cDYjBF+ABL3pSwYT$d2lgov<+~Q)&pgPjzHufYAMK8T){imCvF$;&SDu00x zcN9ZGe5K){x#Y=$YT)=Uh48^(v8JVVn=Ho
FX_9vU5=Ewd$)mJow#MLALF;dtaJmHWRtq zxg@v8Z!+>}Tb#*sD0r=80^Kj4&T^I^xMAfWLpQj>Q-ertmrLp@ROWCZZo7&vljFydQ zM9z9EP0mIJ^%Va#b};enud##XW>_6{$6YD?=(-`mYz%Yb_6Et1S9Oz4timQ>ynszhS6x3uG8K7Cls@mF~UDMKtM_TFEKqBD`Dp=U;IaYrjM)@YD zre$C)4f1)v2s@5eThBsMEb^GP33R*j_X20Hh1>kz8eC|d-%lK}WvgTI(HIKrex?3- zz69@3z@{ei@Nf(yfX4-L^ae*G`K7co&l?B3H?yBzA%%0lu`awTysjt;4-10$j_lqI(dx1h?lm=%xvNr%pA?qTo5~|mWaia^= zYx~v@+={0Xi#!PU6%3}GPr(OcdhOCOUI7#jM;El`inz{cYdS_o3=kwz@Gy75pt}m^bWT=YGkpoP`mW1p;antoecjtoAoss-B2Mz!f zD?Khy_R!w`9#%cNJfKM0KhYp6Fo4arVdE>dN0LbX+9UvVZ0$l*uaQK`U z7`h;oM*~`{u8@3dmwrJ_h5fB$IE>n`=Ak;FuqNYALg-0&MWgE6uH#56P`7Bkoi}+A(DqScmO(!wcO$5|H1T4 zFrT#@!_y>7?aA3;;g(v;PviNWCNXWs;Ho^p;{d#!3E4o-)HIGN7^TRf8GVX&`e(A0 zS7h4?JT+8`w6-OCMs9f2=sxS1l!A~W&I8>qfn=!^8E0#szwNL41Z9> zplf@e-8vhDXE@-W7-<>b0y&mM^zGw!$3K*eS{FoGIS6qTII1Mnc;1S?+fFf904k5W zHD<^5K^s}h$?!BT)8#o$^Rj2%5p#&{qZ2FTt05VFohBYoEnGNG_|PoLbPcZ@do;;3 z!MF@toppEnObLi1>t`Hro!oN!;Owd=&|(rfmY3!=8x zhDIIy^4zqkT6dSLa(G~vi?osP4g6&B%|$BpRGQ+=<$D;D)H26{d>Sa#1VnXi{-E>f zE|6;JD64?G?P*$~iwcL5YrvT&%}Z%5xK7-^_J>R>(eE!wcZ> zyWyUG->sOA$)dXqZj4PkqA+fqPUFydaX!<^ z2i$;1?(Fzzh9S*2C#;kKu7sg{;!%wt|Ippjpg74uNBP|lklPR4aPuptY8;L2!JdO` z%G}9`vjY0`mmte*{EmK_SgBQj0nMifP?95x=3?frsz0O}*$cPKB3 z+5l+7_b|@ehBoNqHG!{{u{vJt8>!ymn_d^$?dg2sK-h7HJ-g8!qD3&oM07yi7elyrKhOgJ3T0uEFI1yN!7PtbO2;!Y!I zY<3x~rIXS;O?z70`G=H7^|v}=5Al#}IuU29mzwv{Envy%$E#uc2vZ3MJiia~yLs-{iO(4N^uR`08VR7L*FPS?&;lyJgk^D? zKcesvC)Q|J_9#t50Y;?2UjGORCRRy8{Ca4gJ5h|<(9n?afHROBRG{}d3A(oEV7y;w zrF$wQJ_oZG!NFVz+9T*7J^}fz@j}7{F!CO_VZqT2W^j?-9AJqTv2>$+`1u)ynEi&@}z$1i*`j<+8@l_zC(==h*18B~V$na{W(HUy$K~#{CRcWV?@@8S1G_p$)z7 z6*E;i&>TRB5ffJ~%@6@qiAvmy+Zi`fTW?66$^OCM%?Y=bla^|QFyb5_2TXwjl2O8O zB6_5j z*=pRZ-W^|~8?ZV79_&YXrm)>jSR>;PRVv;tMwl^ptcQtSOv~a;Fat$&K0~F7l#Fb;D?9+&RGJz(i|^URgBBRN{dYA3Z##LkL^(^Exy8)@=gk;9mp!vpe|md-of+CF7@) zR)Tbi%!ZtHH#i}`(nvW^gA)5_Kr#neZomwh+bFE4v=(n641xMgbq^;WLt3^!O#!@v zYa??{L9jy=_2HBz+9g|C@2o7|ke#qT#^97=<=S>LWI#VodQ(w|>{ra|{`vD&+yg#c zL|){=n(x2@Fc(Fqe7Y7`3jPt4Q9FPJ!TG|Xn5%Keqdyu=JFbYH_AuDdw1-{6PZ~NW zQdm=6GxLeJc!W|M+71tfU)PYF0&C!taUsW#bx`X_1&jB6W_D;y@W>Hr(FEXPMky!v z2bkEFDO5V*RZ`NW;u zX1q6J2IPMhyK^s?o?%76Ws~8;JsMC6t}_~Mq$np13!y`b@rsaRk?L7V`<+emQc6QZ zV~;s6;5wL-?7=p9Et7NK_D0mbsZo=7CKmq@Cr*bKkM|NJ=JP+8id@gQt4f&V3tB{ z9sS!0hbt51`~P>`e^F5RcBrME)_=S&GeY2{^CXb7oX;3P|pGtR9i0}m8?w7g1 z(2c$T{z}L!U$?fzu*v&piB5*ckOa%|pArbvlCF9Tl4vIJ!QkExH_dndYrGkT38eh^8{& ze)#)VS_y?CBNCrPs&Zi30(xoZt%XQLO5DiHc!-FX&=@Bw?j*T033EvlJTB$^F3@mY zS%$j+>>t@qLEJ{{EZ~g$o*f&m*c`!lLd^mF1JW8A3|Yc_5m*PH+97G+Bgoccvs?yh z#++L+tMHY_w*`a_LhY+NP5`!v!+_m8b*L^TY?X;aZbw}`a`+WW^wZAJis5Erx3TRE z$g9Be{uRSvV*L+kLh6KP`DylqFs20v(E!O2t8oaUa8}=kd@lA=qxxW7Ko8tM(+`>v za6@Z{d5GFac&~woim4KQJJdawze8x|m<4MUtE7gV@&~gseGt1a`#tJnSLBDBh*XDH zK2(c-jycTee!ssv2eG!pE)wea#K+mNg&-1cE$xNPYUDNMmS?KoGpJRn@aTl%?*b3w>l$4Mn?E#z z2M$5j3&V@3b8-v~oA=H?^=0ES^|ZGgBOeAz!u>x_fc#>qr6{>- zv%Xf1E{|;Q#lRYwUVxr4eY15!@TvB{E(eejm!fohJW);^8a$>KrG3$l54b-9P=@OD z5G(7^j=du~RNl`I(aF$ZtAdU%@y6AFBrE zF?6>hlRi#S|FCRl;4J+c>{Q~f0W2#(3pGHImh+zl=LsRwrX9bWpmVP*XIUfay&cF= z?>Sd#ylxLxp}T$$WgPmfZzCfXf&`eWl*)^5GB-rjpXgKM4?&xaZ5FKdUd)*oCL@&L ze=#SJUPdNVAChM+T>+Qo4;%pV*= zrGr`K55p^O9RdvUw1Zg@E3e%CD4~(6OuMimWB9RXeq$3A395LE&d?ftdvF*hn#g)Z z+C}#IChstj-+8wR`iq}F>g;bp1tVHQoW7Jb_q@y-UI!6pCm$k)p@+a1<+s?k<(t;$ z&s<$2Z!vyYzwjfT!B?6mI%uv1jSUA41I1=&&Fy-aAXi5yv`Gi_&Id}BiW@V;S0gW5#)6ii(OL`10H=G2t! z-9?%GqXI;#jc>s+M-v1jJcFBLQ;p?Gbxp3+ugS9sF(YFod97`dcbad2I+^5R% z;c7(FD5yx>a1?^GP;+%gEC`n%QX{^BgpLb~an?jq?OFOdWHzQ3DDjX5@I1&o#gYM6l` zqCkA56#P^%*|&Z{$V!MFF#)j>iC~|5*>S6tkZ zQ^jX*-@d)3GPIrTY7JF76N5k8;j6cT`J9fe>S-px)KKwyKpC?bCaUKOUsqtDB_%3! z($~+CJk%ehG(dQakbjOT(olhg18PknlzZ&g-$R}jP|eYt_346aN*MpY`D5I`=n=gRGB{Lo;FsaTJZl=Y ze>W@XOH3VQIASmvdvTD2*$G9mB*5;0Rwx-Kza+0u-reBNxSKB{OsoTINJug}Q!0MG zh>229t+oG9%S)=%YOv!iO=1Mn0E6GC9p!~<*dGS#0Wi6UWG<(jhlceqw;K>=wh*ED z*^QNL_{Xr9k-Quz#&PsJSSg3daeRsM(wrPJlq0nR7$u->tfmxM0cJ{eFlo5=DD^Mc z+@Y7n6opBmOlVi=U(mu4zXlBR=fTzmj?<$&Tve09Hm|#n2sJB7la=?axJbR)QCfVGhbE zlxi6VQ}_+(YNPiRz3J}77aABpM#;Mp_Vz@W)EOH0Ea83ysD!8J{9R370e<9g8d!_ z_rQMRNe4Z(@;y;;^?jRe(ny|^?|6cgM_O``f+3NYNK`Z6^CNl9Oxet9fZPxegqViZ z2a?0TSa4=9A)04U>}7D<;b`HMD{P&9uNNy7vO(Kd8AU_IXZ?HRr6+@CMp~> zm?p-p=MI&_$dHTBMM`t~@BcIx>@utJFg-H9{DckwP8xlG{Z%wCHHM{#Y#6ZbbwRRB zqEmOHViJ&*5It#a(RzxOeoT(*{fW4%;c~noatfO5v#Tp{$)6MluFsoo2A6c+P#R52fr4wTR7*Q`hdfGDh%oTOnJ>zEoiWGOJ;-Hs zz{M~%V~mY<-J3%Ez#EQe|7A{p=)=AXlQIx9F*+EwsId~440$dqPSc5z3A0zQ>R_+I z5%u(rx{ER3?<1i?R;@945Tt6B3J47kCo!*Ukgp%Y7~grdoDmq)+)R%!v~S?WfII_& z%!?SRzBU9w?pQieC(ZJ`6b3-~#9oSzejr^cx$Dn^U9WEn<1wO@Lpe-L>`*Qe?U)KNm3`!{40W{xX&AKOnCN2u zOH>{K*hNxX<_=sZm<87C#;geF(Q}yZogO8b_JLrKXaESP(Lf)(LGwy&KR`;U@%=a|S1eE?R@@r)GTy!FAt8X-8~{80~3^H(#}eXqusfT{=> z3uDhAq2r9~FP;Gyk~sC)VBo^wRfN=U5Q&_7QN~=7@mmjSd(`(C8LnoWh;#@!I zIMz(YTn_RU0hW^xhJ1tF9J-U|B2p#cldldW!y*IoOk^t*I9O`mJB#@N8RiGSi@;iQ z*j(x$5o7-=PT}0)p1l10Ba8upGU(t<_%;l61px4aUxi}EIC~$6L3P7esa55mI?@6T z*1xn^H<)MAplq6#gWL!r1yq}Q56Xr@Mle*$&UVMGK<5P8d*Mrb7swAdFgq$Ny)nT4 zkBV)x*+%QG{4_EWF4=a8JF#bLPd-v(;Q4H%5H%+Ca%a&P)#A|&>gipC73q7YE(hM) z^L}rR&cn>a@sd65ji(=dJDG5`lOR^Q=g`lge!-0?+f-3O!4vpJ=CG%Osn>aXGjnY^ ze!XTb4|1@xZf1>Lx~099mC}VPz4mi8tdSKXBl_1!O06C;&Wow=@5PRuH+O?kd+Hb( zvH>3J6cF&5WE58ZdpEga?!N6@C4u*^T&@5ZLd~|H-_tp?NVZj8Ema6^t3G-Z*Ny7U|X~gthy@* zghH`7`PIbi4w%`Zvh!J5TEaxd8!IO$mI9@(UAw^(i`p@BP~3T|@XXqe{{FwA4dtn< ztgQ8ziQJqrIP1a^IoJ@&Kd4f&uEfE?QPH=(wLq+Sw?8?<7E7X5Hi|-^$7vxxHqMgMRu(|#5<44rh{%}vN!aES8fB59dV*TcjRyysd z^m5&d_|rg#6-zo`q;xPp_JuX`x0)JR5&LBh?8`2Bn<>uEZ}W&Rf<`!G5y z^Oc#N<9N4fzBsTtfB)Uzx_6>G3NG?TL8p*QK)LkhTSw&kCrSEF^)g*vU;cb|@9ZqH z9KD5j`R1kv1kq}1Td7(BrY+JA578ln}v)9x6 zI2c^yBPoUxwYj;8w#5ghT zl~~&>{*-+BbO=8U0QgBX{lkVsrft4N)+M)?7(;`8mhRysvN!t=9t@H#z-e6l?RIcr zdAiGdVWh#2hhId54rZ=r@5$ck9rcKth;yI9ZnwtD^>sVJXCkcFqHHh-v$4Z-<-aG< zI!_CHJMC{55f(Oe{FT1+rj#TgdS8Of;I-N> zif!O_X9FQ`0mP2AoxTT15{}PvOk3W~Qd^Hbg~4 z97g&AS8Pp9Ik8dWc7%%lP>s-+b$b*l6O?zHIE+25)LJ=-kZZVx@r(+nS;oQr*8^0r8rZNiTvHUtSWf>S{0rNNi~IC8ZmUJs;YW< zdEuU@YG}~?Jn%X`&txNV$&k|pyAj@5pO=3OrV2Md|FH&rUM%6o@i~p{qzs3}Kc}}j zUC$HYOf2uDOm;DFBO?i1ZKfRbO z*%tdXZGEjW7Hd{4j?~uI%jFe!EU_kUMj)^SWLxR$vw-QMWz?6sS@Z1`?(4iDe)+SO zqf*CmxHck|l6z*`R`^c(2K0Y8wj^OC@&_M_ho!!zed5w^Fe|GQK<(p*O5DQ2_i7)c zI~kbDjJH`Gc%SkzD`n&yJKoD2j0)b?j7;d~?KiE-N1=A!w69om!aX`VTEQBj2p&=b zaq;nkQ1+vZp7u)-6QsmO`jpJhO&3VWmr*yw= z-Q8zNAzi#JLx?ToDoe?XmhE$uYI=PZIE+4{mfyLvjw^2jsRa%9J%VW4^=n0ik`hIvk3Pjh(}iwyNc2BE=`j`VrnNo^1$*r z81=AEkEw}^j~?Y+lZucFu_RHmPFixbDwijV9Of08T|mEYKH3|avy9?mxGM_7IHXfIP^Ztq$(AuQq8HH}<5V18j!^w1 z?_<)^(qM&dXd{TGv~H_;LQY#kSl(|aklJlsxAnMDNls4gjZeYhJGJ%5kOac0xIf0= zyi0)##kX$WgdxzKHLAICVKH%WMwqH`>TJ2YyZhPBIOQJfNm}@T9+>;mB_$qX<-RQR z`@!Mi=a+h;YSvHHtt>7+k-K%PvaIZipk}hrK}Gr&eBZY%UoaWJP8+Zplzd!%F+Xg$ zPYu5*?OV$hHB>vZM#~3FHkZ5n);#GvZ_3F%c~iBpm~1DgZLX}gZD8RlqQ}C&N6d*J zfU@@vpX6{>(>iM)bA3}&)xf|5*{uNQQJ&dZr8l7&85sf3hid{^QC1BAN#&}U$mGaX zPbKCaSZxP~g>ldpV->>#1e!2O^Y>w9HB@fI>lOOG6l1qo zZ14^PWIo!P{t;R5R`Xj~Ss5H@2-Vw6wx<;7pdm&@So+WP+RqmK=&K_s-9Pg_BEk<> z=jrm4+O??r#Vu>SeFZk@pF28dmUVhkx|Dp)$fervpkUD}w`e!OKtJ85$>NO7yHGqL z9CBi03yz53T=|ni+!a~bwiZ}7C0@<58E4H@B@bbc5B76bPEDCyd-g8*7x|N(;eyDG z1DP5_Pnq08nkG|*Cz6)^7Ynr8CpSrpP8v`qCDX-+85GSG4%TWk3D0iW{t}A`(;16k z{gSlxXvO~3LLVN=+41qv^<1}~z2!z8x5&7y$abaL>8e<{S#grc-Mm@E_t!OjJFF5o zPrG-a54(<@?3d9paCR1F5O*TfdU%PDU1T+COu{&fbh4+XXI|KX`@FxU$fFAu-!~qs z1r_ejv5xnlt*?B}bO zyA8jTlxWv#QW7lZER2nl9IUP1@V0c|Kd(svs-TjYbt$yrMR0JOj zXJ%)E&ahc36y}${dKqCmjq;j)ID131rmv`=fEGKbZ{nxEYeHn;>b#jxPCjj2pc`Ag zt?%)skiV^-YmI|9P?Azc8%oW%zCemDOls+Al`JK`Lx&U|R3gq>uQC}rVKd~y<`xaF zwd*W{^-L{AQ6aS5FmkOWoAF{K)5S^*+@+&bR8)G=3xyp>@>&y%g{+6gDO=;>;)F6& z^+sI5fPcD0uodL9Ff&WnbxJb*QYTKV1fr5A8eM{#xFYXmYpB$z^u}l9h`WtsPCM(j z_I7r9v&u(QWEnJFCv%&a`ydz+a_xyq*HVWbeO*(%ct~5Ox^H^8zhP&bAZ-& zODp znsRz8Mdz%rm>B0}do}0L2e*p{)tn8aQ|Y#qt7h_KLTHtfnqq|_5|$6Hi&Bo^p6B<9 zJuF$d*xA%nuh8jVD~<`09`~p9VtwJrs)rpt{xy94DTBUsh2l7`!zc7a--H@fR#j<_ zZx2boN9CdrIUE_bP2i6>TCyh)6#%7u#muzS75hYSNV%xSVPmy1eeA3R_ww|xvG_;}Q zSZT|o*hrx?X*TU*{X277f`zYo%b}56S9t(5Bwk)#y+PTnO^)O>+mKCcoQsN|mdF;X zGYD{OCTOmSWcIyTa$4CqGB=oiIeQCxtGRi26mFdfMI1jtSuom>U)O+5@P~=h*gU{uSQ7p-%N?*77)Fz6i?3B#A!gi%ZS z)1&JJ8%=t4Y(^ZXmYy<+XQ>Qx$e;Sz)8k8wfWSiP#sl(l@Jk=TCI$a(WUskS zgUD~h#%gsW)QoAs(KhuQD%K`PwHjzObf;&AhCURUbsAJ#N9QLN zoNNxA)C{><{L8tz&oX2sw_}(^oNHqTxN?q0eg6Lbbw)~pEA|!^3OwD?lAV%4j&XEc z;&zK{TZ{4H-(s)%Gci4|w$2H2APz7q2?PhgP(jNV`vOthW9!hl&~h^KhD!sBTgtf_ zHVwY5wn8_SHM8W~ix`~S*DAcmR9UNSxHfjK5t1eo)Mj{szo`me8f(5^8b{rG zj%rO9t4eF1vP?!frGyqWD)}Ibh*gPjb8m#$zOc1AaBB2NmWE=LL7qMoyD7uM8|_6} z{KZpSZd2qXiQ&~$45%k-Wkm(K0vDug%YL6~``DQ_?Buq8cT>^16CW&kmx344$`|dn zWlA>mih8v{IQ!XRJ`y#tByg6gQK_*>IIVvDp|kV0#_Xukh8MBWoD)+T4N8k~?%HI~ zeI=u%Q_wRL%$2lkW?6Wtdh^~GOVkb*o9nljRs8oi$^J6VDOzySrzA_J+SkUhFFkbi z#+sR>!T{cZoO%g`T8hQ|NBb5HN)}7*IL7aP1lba`?s2k@j?B!+u50Y6@s6)oeceel z@x||D^?Zoiy2kTbsU8h%qOlVuUOSfgp=b@dk}OH zL>0$OuYdz4o>?EZVrjo7Y;UXS#==^bRXDWUc}XlHo>BV|MJB!fDtGL?98$M1(Fu{Xdqv?C zKYA#rI$86ltQJvy-szc@w;}{ub*in43l`VpbMSv2DNyOKY_N|+saVNr-+!OFhaEv6u%CPP8;2}f9DHe)*dSnB16zy z>(!|(w;oz=ATdc3s-E7}Ny@r#DvD8>OtsFAl9P6%LE{CC}I{O%U2c#b)zULYM3vHaMSZ z+Io2@T+u6d?YWH2p;>y}~$-S4p?61+`)Ty^*7bZAH#0iu;%r!dewx&aiS!av3 z&WGJ}HW1noRb?^96YG+E?cMO}z+-FmQ}qYYRqs%A8VMznwY`}atX33Z^Y;;^tP0e| zZ|(0k5Pmcn4%7?1qM+&NXm`|HOF<2OPB<@bgQujwu*3ZD8XdAgo%BnBUvh9otkxQb z<&k7{4^G~n4Lo;J`;t0C8x?YwGK-7YZBtL|K604es#VF!EQe~%Z^W^dPomO&E4i3F zXWCq)yD?;- z+*Kd;Nu?yL$0|A3vhY6Io8y^qF_Ay`3nC=rivfb?*7)Nx0c2mXYVvqBQBoQQor|^>R_quE$R! zCk%_lzRk>-60qgl7m~GY;+Q-C4DWJt^KHSSBUl7Jyf{$5Pn6hZptVPn!5{I$l3`OL zzS=%0p5ID*qcGm8n7@dFUo9uACv~etYdg+_Jip62Eq#XjzD?g$%y5ZNycYejh>@1@R+SsUpuEkjG;ZAxz93h_UB%eNOlrFX)lzlMhz~b?XDuTDBtb{E> zEZ&{avazulOinL;!BxB(kfDu5%Ljrbu=?HaSUz=Gh z*#4E;F&yV-`+FizsFzcvO(oC7_6z6mc)mteSDN<99Q$O+WN*{x#G>(bwbgXc(lg(_ zqL$gU;Yp#il!uXSP9$OS+Xm$%|NiCj@H{^=vAv_(i_YvYIus7tVV;GF>S~#bp$!ND z#4dRCPx(QehY$Zk-=*71O7ewylZT}D($16r`4%>1_{{;#RBmlKeO6BejP8X320bUS z+OTx)#rHz1T7gCn5DNt2`jV2E2z{HMw~&*U7j|`beLY)_W>dsr6(dS%co+Z*zYN4q zvcf-f61aj6bPs^)AS$WAQpCab)O)C~Oh&e757?U$gKVK%YSzSMU_5(0rqOf1s<&i~=3vhPN)yvxJ0%USK|q#&~Y1 zCJ;L)X#LM@8|0OZ~-!J~}10(tG zAtAZ_|Bs8&k{>xrLb8iTR`RkU3CVx2Cke@aujhZSC(_>k$MxKBe%vuDGU$=KMoez9 NQnw`2uH1k2e*l=@cFq6* literal 0 HcmV?d00001 From 3aaa9f8dc2c42ee67d66967f906e5f04a5803329 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 19 Oct 2023 16:57:51 +0200 Subject: [PATCH 222/225] wire-api-federation: Disconnect from federator after consuming the response (#3663) --- changelog.d/3-bug-fixes/federator-disconnect | 1 + libs/wire-api-federation/src/Wire/API/Federation/Client.hs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3-bug-fixes/federator-disconnect diff --git a/changelog.d/3-bug-fixes/federator-disconnect b/changelog.d/3-bug-fixes/federator-disconnect new file mode 100644 index 00000000000..1731c0dc807 --- /dev/null +++ b/changelog.d/3-bug-fixes/federator-disconnect @@ -0,0 +1 @@ +Fix memory and TCP connection leak in brig, galley, caroghold and background-worker. \ No newline at end of file diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs index 4104c41d92d..b27b833b2e7 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs @@ -126,7 +126,8 @@ withNewHttpRequest target req k = do sendReqMVar <- newEmptyMVar thread <- liftIO . async $ H2Manager.startPersistentHTTP2Connection ctx target cacheLimit sslRemoveTrailingDot tcpConnectionTimeout sendReqMVar let newConn = H2Manager.HTTP2Conn thread (putMVar sendReqMVar H2Manager.CloseConnection) sendReqMVar - H2Manager.sendRequestWithConnection newConn req k + H2Manager.sendRequestWithConnection newConn req $ \resp -> do + k resp <* newConn.disconnect performHTTP2Request :: Http2Manager -> From 80637b8eb0f43b1c1da06aae4aa5ee809d30817c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Ivankovi=C4=87?= <33936733+Veki301@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:33:46 +0200 Subject: [PATCH 223/225] update outlook addin version, remove unneccesary value (#3662) * update outlook addin version, remove unneccesary value * fix: spacing, yaml formatting --- .../outlook-addin/templates/deployment.yaml | 2 -- charts/outlook-addin/values.yaml | 29 ++++++++++++------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/charts/outlook-addin/templates/deployment.yaml b/charts/outlook-addin/templates/deployment.yaml index 4009fd0ffc5..a9679ab816b 100644 --- a/charts/outlook-addin/templates/deployment.yaml +++ b/charts/outlook-addin/templates/deployment.yaml @@ -30,8 +30,6 @@ spec: value: "{{ .Values.wireApiBaseUrl }}" - name: WIRE_AUTHORIZATION_ENDPOINT value: "{{ .Values.wireAuthorizationEndpoint }}" - - name: SUPPORT_URL - value: "{{ .Values.supportUrl }}" livenessProbe: httpGet: path: / diff --git a/charts/outlook-addin/values.yaml b/charts/outlook-addin/values.yaml index 66da50a43ce..6bc14091928 100644 --- a/charts/outlook-addin/values.yaml +++ b/charts/outlook-addin/values.yaml @@ -1,13 +1,22 @@ -containerImage: "quay.io/wire/outlook-addin:0.1.3" -allowOrigin: "https://webapp.example.com, https://nginz-https.example.com" +# Default values for outlook-addin +# +# +containerImage: "quay.io/wire/outlook-addin:0.1.9" + config: ingressClass: nginx -#host: "outlook.example.com" -#wireApiBaseUrl: "https://nginz-https.example.com" -#wireAuthorizationEndpoint: "https://webapp.example.com/auth" -#supportUrl: "" -#tls: -# issuerRef: -# name: letsencrypt-http01 -# clientId is obtained after registering outlook service with wire OAuth + +# host: "outlook.example.com" +# wireApiBaseUrl: "https://nginz-https.example.com" +# wireAuthorizationEndpoint: "https://webapp.example.com/auth" +# whitelisting for CORS +# allowOrigin: "https://webapp.example.com, https://nginz-https.example.com" +tls: {} +# {key,crt} and issuerRef are mutally exclusive + # key: + # crt: + # issuerRef: + # name: letsencrypt-http01 + +# clientId is obtained after registering outlook service with wire OAuth, more details in README # clientId: "" From 4b765e583ffeca73e8ca93f93fd9d46d891c0d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 20 Oct 2023 13:25:32 +0200 Subject: [PATCH 224/225] [WPB-4928] Stop using Servant client to enqueue federation notifications (#3647) * Move a Brig federation endpoint in the API * Move a Brig fed API notif endpoint to a module * Move Galley federation endpoints in the API * Move Galley notification endpoints * A type alias for notification endpoints * Add a changelog * Define Galley notification API via types * Convert a federation notification endpoint to a BackendNotification * Stop using Servant client for 'fedQueueClient' --- .../WPB-4928-notification-endpoints | 1 + .../src/Wire/API/Federation/API.hs | 37 +++- .../src/Wire/API/Federation/API/Brig.hs | 25 +-- .../API/Federation/API/Brig/Notifications.hs | 56 ++++++ .../src/Wire/API/Federation/API/Galley.hs | 110 +---------- .../Federation/API/Galley/Notifications.hs | 181 ++++++++++++++++++ .../API/Federation/BackendNotifications.hs | 48 +---- .../src/Wire/API/Federation/Endpoint.hs | 8 + .../API/Federation/HasNotificationEndpoint.hs | 67 +++++++ .../wire-api-federation.cabal | 3 + services/brig/src/Brig/API/Federation.hs | 2 +- services/brig/src/Brig/Federation/Client.hs | 2 +- services/galley/src/Galley/API/Action.hs | 2 +- services/galley/src/Galley/API/Clients.hs | 2 +- services/galley/src/Galley/API/Federation.hs | 10 +- services/galley/src/Galley/API/Internal.hs | 2 +- .../galley/src/Galley/API/MLS/Propagate.hs | 2 +- services/galley/src/Galley/API/Message.hs | 2 +- 18 files changed, 378 insertions(+), 182 deletions(-) create mode 100644 changelog.d/6-federation/WPB-4928-notification-endpoints create mode 100644 libs/wire-api-federation/src/Wire/API/Federation/API/Brig/Notifications.hs create mode 100644 libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs create mode 100644 libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs diff --git a/changelog.d/6-federation/WPB-4928-notification-endpoints b/changelog.d/6-federation/WPB-4928-notification-endpoints new file mode 100644 index 00000000000..b900bd95735 --- /dev/null +++ b/changelog.d/6-federation/WPB-4928-notification-endpoints @@ -0,0 +1 @@ +Reorganise the federation API such that queueing notification endpoints are separate from synchronous endpoints. Also simplify queueing federation notification endpoints. diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API.hs b/libs/wire-api-federation/src/Wire/API/Federation/API.hs index 5e6b294e122..b1859df2339 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -33,10 +33,13 @@ module Wire.API.Federation.API ) where +import Data.Aeson +import Data.Domain import Data.Kind import Data.Proxy import GHC.TypeLits import Imports +import Network.AMQP import Servant import Servant.Client import Servant.Client.Core @@ -46,6 +49,8 @@ import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Client +import Wire.API.Federation.Component +import Wire.API.Federation.HasNotificationEndpoint import Wire.API.MakesFederatedCall import Wire.API.Routes.Named @@ -94,14 +99,32 @@ fedClient :: fedClient = clientIn (Proxy @api) (Proxy @m) fedQueueClient :: - forall (comp :: Component) (name :: Symbol) m api. - ( HasEmptyResponse api, - HasFedEndpoint comp api name, - HasClient m api, - m ~ FedQueueClient comp + forall tag api. + ( HasNotificationEndpoint tag, + -- FUTUREWORK: Include this API constraint and get it working + -- api ~ NotificationAPI tag (NotificationComponent tag), + HasEmptyResponse api, + KnownSymbol (NotificationPath tag), + KnownComponent (NotificationComponent tag), + ToJSON (Payload tag), + HasFedEndpoint (NotificationComponent tag) api (NotificationPath tag) ) => - Client m api -fedQueueClient = clientIn (Proxy @api) (Proxy @m) + Payload tag -> + FedQueueClient (NotificationComponent tag) () +fedQueueClient payload = do + env <- ask + let notif = fedNotifToBackendNotif @tag env.originDomain payload + msg = + newMsg + { msgBody = encode notif, + msgDeliveryMode = Just (env.deliveryMode), + msgContentType = Just "application/json" + } + -- Empty string means default exchange + exchange = "" + liftIO $ do + ensureQueue env.channel env.targetDomain._domainText + void $ publishMsg env.channel exchange (routingKey env.targetDomain._domainText) msg fedClientIn :: forall (comp :: Component) (name :: Symbol) m api. diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs index c2a25af65bd..8703e3d8501 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs @@ -15,17 +15,20 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.Federation.API.Brig where +module Wire.API.Federation.API.Brig + ( module Notifications, + module Wire.API.Federation.API.Brig, + ) +where import Data.Aeson import Data.Domain (Domain) import Data.Handle (Handle) import Data.Id -import Data.Range import Imports import Servant.API import Test.QuickCheck (Arbitrary) -import Wire.API.Federation.API.Common +import Wire.API.Federation.API.Brig.Notifications as Notifications import Wire.API.Federation.Endpoint import Wire.API.Federation.Version import Wire.API.MLS.CipherSuite @@ -70,9 +73,11 @@ type BrigApi = :<|> FedEndpoint "get-user-clients" GetUserClients (UserMap (Set PubClient)) :<|> FedEndpoint "get-mls-clients" MLSClientsRequest (Set ClientInfo) :<|> FedEndpoint "send-connection-action" NewConnectionRequest NewConnectionResponse - :<|> FedEndpoint "on-user-deleted-connections" UserDeletedConnectionsNotification EmptyResponse :<|> FedEndpoint "claim-key-packages" ClaimKeyPackageRequest (Maybe KeyPackageBundle) :<|> FedEndpoint "get-not-fully-connected-backends" DomainSet NonConnectedBackends + -- All the notification endpoints that go through the queue-based + -- federation client ('fedQueueClient'). + :<|> BrigNotificationAPI newtype DomainSet = DomainSet { domains :: Set Domain @@ -143,18 +148,6 @@ data NewConnectionResponse deriving (Arbitrary) via (GenericUniform NewConnectionResponse) deriving (FromJSON, ToJSON) via (CustomEncoded NewConnectionResponse) -type UserDeletedNotificationMaxConnections = 1000 - -data UserDeletedConnectionsNotification = UserDeletedConnectionsNotification - { -- | This is qualified implicitly by the origin domain - user :: UserId, - -- | These are qualified implicitly by the target domain - connections :: Range 1 UserDeletedNotificationMaxConnections [UserId] - } - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform UserDeletedConnectionsNotification) - deriving (FromJSON, ToJSON) via (CustomEncoded UserDeletedConnectionsNotification) - data ClaimKeyPackageRequest = ClaimKeyPackageRequest { -- | The user making the request, implictly qualified by the origin domain. claimant :: UserId, diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig/Notifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig/Notifications.hs new file mode 100644 index 00000000000..efdc16722b9 --- /dev/null +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig/Notifications.hs @@ -0,0 +1,56 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 Wire.API.Federation.API.Brig.Notifications where + +import Data.Aeson +import Data.Id +import Data.Range +import Imports +import Wire.API.Federation.Component +import Wire.API.Federation.Endpoint +import Wire.API.Federation.HasNotificationEndpoint +import Wire.API.Util.Aeson +import Wire.Arbitrary + +type UserDeletedNotificationMaxConnections = 1000 + +data UserDeletedConnectionsNotification = UserDeletedConnectionsNotification + { -- | This is qualified implicitly by the origin domain + user :: UserId, + -- | These are qualified implicitly by the target domain + connections :: Range 1 UserDeletedNotificationMaxConnections [UserId] + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform UserDeletedConnectionsNotification) + deriving (FromJSON, ToJSON) via (CustomEncoded UserDeletedConnectionsNotification) + +data BrigNotificationTag = OnUserDeletedConnectionsTag + deriving (Show, Eq, Generic, Bounded, Enum) + +instance HasNotificationEndpoint 'OnUserDeletedConnectionsTag where + type Payload 'OnUserDeletedConnectionsTag = UserDeletedConnectionsNotification + type NotificationPath 'OnUserDeletedConnectionsTag = "on-user-deleted-connections" + type NotificationComponent 'OnUserDeletedConnectionsTag = 'Brig + type + NotificationAPI 'OnUserDeletedConnectionsTag 'Brig = + NotificationFedEndpoint 'OnUserDeletedConnectionsTag + +-- | All the notification endpoints return an 'EmptyResponse'. +type BrigNotificationAPI = + -- FUTUREWORK: Use NotificationAPI 'OnUserDeletedConnectionsTag 'Brig instead + NotificationFedEndpoint 'OnUserDeletedConnectionsTag 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 a635ee1cbfa..f40417e303b 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 @@ -15,16 +15,18 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.Federation.API.Galley where +module Wire.API.Federation.API.Galley + ( module Wire.API.Federation.API.Galley, + module Notifications, + ) +where import Data.Aeson (FromJSON, ToJSON) import Data.Domain import Data.Id import Data.Json.Util -import Data.List.NonEmpty (NonEmpty) import Data.Misc (Milliseconds) import Data.Qualified -import Data.Range import Data.Time.Clock (UTCTime) import Imports import Network.Wai.Utilities.JSONResponse @@ -36,12 +38,13 @@ import Wire.API.Conversation.Role (RoleName) import Wire.API.Conversation.Typing import Wire.API.Error.Galley import Wire.API.Federation.API.Common +import Wire.API.Federation.API.Galley.Notifications as Notifications import Wire.API.Federation.Endpoint import Wire.API.MLS.SubConversation import Wire.API.MakesFederatedCall import Wire.API.Message import Wire.API.Routes.Public.Galley.Messaging -import Wire.API.Util.Aeson (CustomEncoded (..), CustomEncodedLensable (..)) +import Wire.API.Util.Aeson (CustomEncoded (..)) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- FUTUREWORK: data types, json instances, more endpoints. See @@ -58,9 +61,6 @@ type GalleyApi = -- This endpoint is called the first time a user from this backend is -- added to a remote conversation. :<|> FedEndpoint "get-conversations" GetConversationsRequest GetConversationsResponse - -- used by the backend that owns a conversation to inform this backend of - -- changes to the conversation - :<|> FedEndpoint "on-conversation-updated" ConversationUpdate EmptyResponse :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", @@ -70,9 +70,6 @@ type GalleyApi = "leave-conversation" LeaveConversationRequest LeaveConversationResponse - -- used to notify this backend that a new message has been posted to a - -- remote conversation - :<|> FedEndpoint "on-message-sent" (RemoteMessage ConvId) EmptyResponse -- used by a remote backend to send a message to a conversation owned by -- this backend :<|> FedEndpointWithMods @@ -82,14 +79,6 @@ type GalleyApi = "send-message" ProteusMessageSendRequest MessageSendResponse - :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Brig "api-version" - ] - "on-user-deleted-conversations" - UserDeletedConversationsNotification - EmptyResponse :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", @@ -100,7 +89,6 @@ type GalleyApi = ConversationUpdateRequest ConversationUpdateResponse :<|> FedEndpoint "mls-welcome" MLSWelcomeRequest MLSWelcomeResponse - :<|> FedEndpoint "on-mls-message-sent" RemoteMLSMessage EmptyResponse :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", @@ -123,12 +111,6 @@ type GalleyApi = MLSMessageSendRequest MLSMessageResponse :<|> FedEndpoint "query-group-info" GetGroupInfoRequest GetGroupInfoResponse - :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-mls-message-sent" - ] - "on-client-removed" - ClientRemovedRequest - EmptyResponse :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-typing-indicator-updated" ] @@ -153,6 +135,9 @@ type GalleyApi = "get-one2one-conversation" GetOne2OneConversationRequest GetOne2OneConversationResponse + -- All the notification endpoints that go through the queue-based + -- federation client ('fedQueueClient'). + :<|> GalleyNotificationAPI data TypingDataUpdateRequest = TypingDataUpdateRequest { typingStatus :: TypingStatus, @@ -180,15 +165,6 @@ data TypingDataUpdated = TypingDataUpdated deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdated) -data ClientRemovedRequest = ClientRemovedRequest - { user :: UserId, - client :: ClientId, - convs :: [ConvId] - } - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform ClientRemovedRequest) - deriving (FromJSON, ToJSON) via (CustomEncoded ClientRemovedRequest) - data GetConversationsRequest = GetConversationsRequest { userId :: UserId, convIds :: [ConvId] @@ -281,28 +257,6 @@ data ConversationCreated conv = ConversationCreated ccRemoteOrigUserId :: ConversationCreated (Remote ConvId) -> Remote UserId ccRemoteOrigUserId cc = qualifyAs cc.cnvId cc.origUserId -data ConversationUpdate = ConversationUpdate - { cuTime :: UTCTime, - cuOrigUserId :: Qualified UserId, - -- | The unqualified ID of the conversation where the update is happening. - -- The ID is local to the sender to prevent putting arbitrary domain that - -- is different than that of the backend making a conversation membership - -- update request. - cuConvId :: ConvId, - -- | A list of users from the receiving backend that need to be sent - -- notifications about this change. This is required as we do not expect a - -- non-conversation owning backend to have an indexed mapping of - -- conversation to users. - cuAlreadyPresentUsers :: [UserId], - -- | Information on the specific action that caused the update. - cuAction :: SomeConversationAction - } - deriving (Eq, Show, Generic) - -instance ToJSON ConversationUpdate - -instance FromJSON ConversationUpdate - data LeaveConversationRequest = LeaveConversationRequest { -- | The conversation is assumed to be owned by the target domain, which -- allows us to protect against relay attacks @@ -324,38 +278,6 @@ data RemoveFromConversationError (ToJSON, FromJSON) via (CustomEncoded RemoveFromConversationError) --- Note: this is parametric in the conversation type to allow it to be used --- both for conversations with a fixed known domain (e.g. as the argument of the --- federation RPC), and for conversations with an arbitrary Qualified or Remote id --- (e.g. as the argument of the corresponding handler). -data RemoteMessage conv = RemoteMessage - { time :: UTCTime, - _data :: Maybe Text, - sender :: Qualified UserId, - senderClient :: ClientId, - conversation :: conv, - priority :: Maybe Priority, - push :: Bool, - transient :: Bool, - recipients :: UserClientMap Text - } - deriving stock (Eq, Show, Generic, Functor) - deriving (Arbitrary) via (GenericUniform (RemoteMessage conv)) - deriving (ToJSON, FromJSON) via (CustomEncodedLensable (RemoteMessage conv)) - -data RemoteMLSMessage = RemoteMLSMessage - { time :: UTCTime, - metadata :: MessageMetadata, - sender :: Qualified UserId, - conversation :: ConvId, - subConversation :: Maybe SubConvId, - recipients :: Map UserId (NonEmpty ClientId), - message :: Base64ByteString - } - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform RemoteMLSMessage) - deriving (ToJSON, FromJSON) via (CustomEncoded RemoteMLSMessage) - data RemoteMLSMessageResponse = RemoteMLSMessageOk | RemoteMLSMessageMLSNotEnabled @@ -406,18 +328,6 @@ newtype LeaveConversationResponse = LeaveConversationResponse (ToJSON, FromJSON) via (Either (CustomEncoded RemoveFromConversationError) ()) -type UserDeletedNotificationMaxConvs = 1000 - -data UserDeletedConversationsNotification = UserDeletedConversationsNotification - { -- | This is qualified implicitly by the origin domain - user :: UserId, - -- | These are qualified implicitly by the target domain - conversations :: Range 1 UserDeletedNotificationMaxConvs [ConvId] - } - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform UserDeletedConversationsNotification) - deriving (FromJSON, ToJSON) via (CustomEncoded UserDeletedConversationsNotification) - data ConversationUpdateRequest = ConversationUpdateRequest { -- | The user that is attempting to perform the action. This is qualified -- implicitly by the origin domain diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs new file mode 100644 index 00000000000..e5a401f3940 --- /dev/null +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs @@ -0,0 +1,181 @@ +{-# OPTIONS_GHC -Wno-incomplete-patterns #-} +{-# OPTIONS_GHC -Wno-unused-matches #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 Wire.API.Federation.API.Galley.Notifications where + +import Data.Aeson +import Data.Id +import Data.Json.Util +import Data.List.NonEmpty +import Data.Qualified +import Data.Range +import Data.Time.Clock +import Imports +import Servant.API +import Wire.API.Conversation.Action +import Wire.API.Federation.Component +import Wire.API.Federation.Endpoint +import Wire.API.Federation.HasNotificationEndpoint +import Wire.API.MLS.SubConversation +import Wire.API.MakesFederatedCall +import Wire.API.Message +import Wire.API.Util.Aeson +import Wire.Arbitrary + +data GalleyNotificationTag + = OnClientRemovedTag + | OnMessageSentTag + | OnMLSMessageSentTag + | OnConversationUpdatedTag + | OnUserDeletedConversationsTag + deriving (Show, Eq, Generic, Bounded, Enum) + +instance HasNotificationEndpoint 'OnClientRemovedTag where + type Payload 'OnClientRemovedTag = ClientRemovedRequest + type NotificationPath 'OnClientRemovedTag = "on-client-removed" + type NotificationComponent 'OnClientRemovedTag = 'Galley + type + NotificationAPI 'OnClientRemovedTag 'Galley = + NotificationFedEndpointWithMods + '[ MakesFederatedCall 'Galley "on-mls-message-sent" + ] + (NotificationPath 'OnClientRemovedTag) + (Payload 'OnClientRemovedTag) + +instance HasNotificationEndpoint 'OnMessageSentTag where + type Payload 'OnMessageSentTag = RemoteMessage ConvId + type NotificationPath 'OnMessageSentTag = "on-message-sent" + type NotificationComponent 'OnMessageSentTag = 'Galley + + -- used to notify this backend that a new message has been posted to a + -- remote conversation + type NotificationAPI 'OnMessageSentTag 'Galley = NotificationFedEndpoint 'OnMessageSentTag + +instance HasNotificationEndpoint 'OnMLSMessageSentTag where + type Payload 'OnMLSMessageSentTag = RemoteMLSMessage + type NotificationPath 'OnMLSMessageSentTag = "on-mls-message-sent" + type NotificationComponent 'OnMLSMessageSentTag = 'Galley + type NotificationAPI 'OnMLSMessageSentTag 'Galley = NotificationFedEndpoint 'OnMLSMessageSentTag + +instance HasNotificationEndpoint 'OnConversationUpdatedTag where + type Payload 'OnConversationUpdatedTag = ConversationUpdate + type NotificationPath 'OnConversationUpdatedTag = "on-conversation-updated" + type NotificationComponent 'OnConversationUpdatedTag = 'Galley + + -- used by the backend that owns a conversation to inform this backend of + -- changes to the conversation + type NotificationAPI 'OnConversationUpdatedTag 'Galley = NotificationFedEndpoint 'OnConversationUpdatedTag + +instance HasNotificationEndpoint 'OnUserDeletedConversationsTag where + type Payload 'OnUserDeletedConversationsTag = UserDeletedConversationsNotification + type NotificationPath 'OnUserDeletedConversationsTag = "on-user-deleted-conversations" + type NotificationComponent 'OnUserDeletedConversationsTag = 'Galley + type + NotificationAPI 'OnUserDeletedConversationsTag 'Galley = + NotificationFedEndpointWithMods + '[ MakesFederatedCall 'Galley "on-mls-message-sent", + MakesFederatedCall 'Galley "on-conversation-updated", + MakesFederatedCall 'Brig "api-version" + ] + (NotificationPath 'OnUserDeletedConversationsTag) + (Payload 'OnUserDeletedConversationsTag) + +-- | All the notification endpoints return an 'EmptyResponse'. +type GalleyNotificationAPI = + NotificationAPI 'OnClientRemovedTag 'Galley + :<|> NotificationAPI 'OnMessageSentTag 'Galley + :<|> NotificationAPI 'OnMLSMessageSentTag 'Galley + :<|> NotificationAPI 'OnConversationUpdatedTag 'Galley + :<|> NotificationAPI 'OnUserDeletedConversationsTag 'Galley + +data ClientRemovedRequest = ClientRemovedRequest + { user :: UserId, + client :: ClientId, + convs :: [ConvId] + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform ClientRemovedRequest) + deriving (FromJSON, ToJSON) via (CustomEncoded ClientRemovedRequest) + +-- Note: this is parametric in the conversation type to allow it to be used +-- both for conversations with a fixed known domain (e.g. as the argument of the +-- federation RPC), and for conversations with an arbitrary Qualified or Remote id +-- (e.g. as the argument of the corresponding handler). +data RemoteMessage conv = RemoteMessage + { time :: UTCTime, + _data :: Maybe Text, + sender :: Qualified UserId, + senderClient :: ClientId, + conversation :: conv, + priority :: Maybe Priority, + push :: Bool, + transient :: Bool, + recipients :: UserClientMap Text + } + deriving stock (Eq, Show, Generic, Functor) + deriving (Arbitrary) via (GenericUniform (RemoteMessage conv)) + deriving (ToJSON, FromJSON) via (CustomEncodedLensable (RemoteMessage conv)) + +data RemoteMLSMessage = RemoteMLSMessage + { time :: UTCTime, + metadata :: MessageMetadata, + sender :: Qualified UserId, + conversation :: ConvId, + subConversation :: Maybe SubConvId, + recipients :: Map UserId (NonEmpty ClientId), + message :: Base64ByteString + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform RemoteMLSMessage) + deriving (ToJSON, FromJSON) via (CustomEncoded RemoteMLSMessage) + +data ConversationUpdate = ConversationUpdate + { cuTime :: UTCTime, + cuOrigUserId :: Qualified UserId, + -- | The unqualified ID of the conversation where the update is happening. + -- The ID is local to the sender to prevent putting arbitrary domain that + -- is different than that of the backend making a conversation membership + -- update request. + cuConvId :: ConvId, + -- | A list of users from the receiving backend that need to be sent + -- notifications about this change. This is required as we do not expect a + -- non-conversation owning backend to have an indexed mapping of + -- conversation to users. + cuAlreadyPresentUsers :: [UserId], + -- | Information on the specific action that caused the update. + cuAction :: SomeConversationAction + } + deriving (Eq, Show, Generic) + +instance ToJSON ConversationUpdate + +instance FromJSON ConversationUpdate + +type UserDeletedNotificationMaxConvs = 1000 + +data UserDeletedConversationsNotification = UserDeletedConversationsNotification + { -- | This is qualified implicitly by the origin domain + user :: UserId, + -- | These are qualified implicitly by the target domain + conversations :: Range 1 UserDeletedNotificationMaxConvs [ConvId] + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform UserDeletedConversationsNotification) + deriving (FromJSON, ToJSON) via (CustomEncoded UserDeletedConversationsNotification) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs index 3fa1aba2871..6ad8ddde899 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs @@ -6,22 +6,15 @@ module Wire.API.Federation.BackendNotifications where import Control.Exception import Control.Monad.Except import Data.Aeson -import Data.ByteString.Builder qualified as Builder -import Data.ByteString.Lazy qualified as LBS import Data.Domain import Data.Map qualified as Map -import Data.Sequence qualified as Seq import Data.Text qualified as Text -import Data.Text.Encoding import Data.Text.Lazy.Encoding qualified as TL import Imports import Network.AMQP qualified as Q import Network.AMQP.Types qualified as Q -import Network.HTTP.Types import Servant -import Servant.Client import Servant.Client.Core -import Servant.Types.SourceT import Wire.API.Federation.API.Common import Wire.API.Federation.Client import Wire.API.Federation.Component @@ -125,7 +118,7 @@ ensureQueue chan queue = do -- queue. Perhaps none of this should be servant code anymore. But it is here to -- allow smooth transition to RabbitMQ based notification pushing. -- --- Use 'Wire.API.Federation.API.fedQueueClient' to create and action and pass it +-- Use 'Wire.API.Federation.API.fedQueueClient' to create an action and pass it -- to 'enqueue' newtype FedQueueClient c a = FedQueueClient (ReaderT FedQueueEnv IO a) deriving (Functor, Applicative, Monad, MonadIO, MonadReader FedQueueEnv) @@ -141,42 +134,3 @@ data EnqueueError = EnqueueError String deriving (Show) instance Exception EnqueueError - -instance (KnownComponent c) => RunClient (FedQueueClient c) where - runRequestAcceptStatus :: Maybe [Status] -> Request -> FedQueueClient c Response - runRequestAcceptStatus _ req = do - env <- ask - bodyLBS <- case requestBody req of - Just (RequestBodyLBS lbs, _) -> pure lbs - Just (RequestBodyBS bs, _) -> pure (LBS.fromStrict bs) - Just (RequestBodySource src, _) -> liftIO $ do - errOrRes <- runExceptT $ runSourceT src - either (throwIO . EnqueueError) (pure . mconcat) errOrRes - Nothing -> pure mempty - let notif = - BackendNotification - { ownDomain = env.originDomain, - targetComponent = componentVal @c, - path = decodeUtf8 $ LBS.toStrict $ Builder.toLazyByteString req.requestPath, - body = RawJson bodyLBS - } - let msg = - Q.newMsg - { Q.msgBody = encode notif, - Q.msgDeliveryMode = Just (env.deliveryMode), - Q.msgContentType = Just "application/json" - } - -- Empty string means default exchange - exchange = "" - liftIO $ do - ensureQueue env.channel env.targetDomain._domainText - void $ Q.publishMsg env.channel exchange (routingKey env.targetDomain._domainText) msg - pure $ - Response - { responseHttpVersion = http20, - responseStatusCode = status200, - responseHeaders = Seq.singleton (hContentType, "application/json"), - responseBody = "{}" - } - throwClientError :: ClientError -> FedQueueClient c a - throwClientError = liftIO . throwIO diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs index 509e73aa61b..664835848f0 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs @@ -24,7 +24,9 @@ where import Data.Kind import Servant.API import Wire.API.ApplyMods +import Wire.API.Federation.API.Common import Wire.API.Federation.Domain +import Wire.API.Federation.HasNotificationEndpoint import Wire.API.Routes.Named type FedEndpointWithMods (mods :: [Type]) name input output = @@ -35,8 +37,14 @@ type FedEndpointWithMods (mods :: [Type]) name input output = (name :> OriginDomainHeader :> ReqBody '[JSON] input :> Post '[JSON] output) ) +type NotificationFedEndpointWithMods (mods :: [Type]) name input = + FedEndpointWithMods mods name input EmptyResponse + type FedEndpoint name input output = FedEndpointWithMods '[] name input output +type NotificationFedEndpoint tag = + FedEndpoint (NotificationPath tag) (Payload tag) EmptyResponse + type StreamingFedEndpoint name input output = Named name diff --git a/libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs b/libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs new file mode 100644 index 00000000000..d9d147b6fc3 --- /dev/null +++ b/libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs @@ -0,0 +1,67 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 Wire.API.Federation.HasNotificationEndpoint where + +import Data.Aeson +import Data.Domain +import Data.Kind +import Data.Proxy +import Data.Text qualified as T +import GHC.TypeLits +import Imports +import Wire.API.Federation.BackendNotifications +import Wire.API.Federation.Component +import Wire.API.RawJson + +class HasNotificationEndpoint t where + -- | The type of the payload for this endpoint + type Payload t :: Type + + -- | The central path component of a notification endpoint, e.g., + -- "on-conversation-updated". + type NotificationPath t :: Symbol + + -- | The server component this endpoint is associated with + type NotificationComponent t :: Component + + -- | The Servant API endpoint type + type NotificationAPI t (c :: Component) :: Type + +-- | Convert a federation endpoint to a backend notification to be enqueued to a +-- RabbitMQ queue. +fedNotifToBackendNotif :: + forall tag. + KnownSymbol (NotificationPath tag) => + KnownComponent (NotificationComponent tag) => + ToJSON (Payload tag) => + Domain -> + Payload tag -> + BackendNotification +fedNotifToBackendNotif ownDomain payload = + let p = T.pack . symbolVal $ Proxy @(NotificationPath tag) + b = RawJson . encode $ payload + in toNotif p b + where + toNotif :: Text -> RawJson -> BackendNotification + toNotif path body = + BackendNotification + { ownDomain = ownDomain, + targetComponent = componentVal @(NotificationComponent tag), + path = path, + body = body + } diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index 6efd7ef2ebe..3d46abff3d6 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -18,15 +18,18 @@ library exposed-modules: Wire.API.Federation.API Wire.API.Federation.API.Brig + Wire.API.Federation.API.Brig.Notifications Wire.API.Federation.API.Cargohold Wire.API.Federation.API.Common Wire.API.Federation.API.Galley + Wire.API.Federation.API.Galley.Notifications Wire.API.Federation.BackendNotifications Wire.API.Federation.Client Wire.API.Federation.Component Wire.API.Federation.Domain Wire.API.Federation.Endpoint Wire.API.Federation.Error + Wire.API.Federation.HasNotificationEndpoint Wire.API.Federation.Version other-modules: Paths_wire_api_federation diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index f7bdf0f387c..90ddd22a281 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -90,9 +90,9 @@ federationSitemap = :<|> Named @"get-user-clients" getUserClients :<|> Named @"get-mls-clients" getMLSClients :<|> Named @"send-connection-action" sendConnectionAction - :<|> Named @"on-user-deleted-connections" onUserDeleted :<|> Named @"claim-key-packages" fedClaimKeyPackages :<|> Named @"get-not-fully-connected-backends" getFederationStatus + :<|> Named @"on-user-deleted-connections" onUserDeleted -- Allow remote domains to send their known remote federation instances, and respond -- with the subset of those we aren't connected to. diff --git a/services/brig/src/Brig/Federation/Client.hs b/services/brig/src/Brig/Federation/Client.hs index f0068f64320..87c44ec4465 100644 --- a/services/brig/src/Brig/Federation/Client.hs +++ b/services/brig/src/Brig/Federation/Client.hs @@ -152,7 +152,7 @@ notifyUserDeleted self remotes = do Just chanVar -> do enqueueNotification (tDomain self) remoteDomain Q.Persistent chanVar $ void $ - fedQueueClient @'Brig @"on-user-deleted-connections" notif + fedQueueClient @'OnUserDeletedConnectionsTag notif Nothing -> Log.err $ Log.msg ("Federation error while notifying remote backends of a user deletion." :: ByteString) diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index e6994075680..d8d5c530cdf 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -883,7 +883,7 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do -- because quid's backend will update local state and notify its users -- itself using the ConversationUpdate returned by this function if notifyOrigDomain || tDomain ruids /= qDomain quid - then fedQueueClient @'Galley @"on-conversation-updated" update $> Nothing + then fedQueueClient @'OnConversationUpdatedTag update $> Nothing else pure (Just update) -- notify local participants and bots diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index b264724a043..044447c488d 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -137,5 +137,5 @@ rmClientH (usr ::: cid) = do removeRemoteMLSClients :: Range 1 1000 [Remote ConvId] -> Sem r () removeRemoteMLSClients convIds = do for_ (bucketRemote (fromRange convIds)) $ \remoteConvs -> - let rpc = void $ fedQueueClient @'Galley @"on-client-removed" (ClientRemovedRequest usr cid (tUnqualified remoteConvs)) + let rpc = void $ fedQueueClient @'OnClientRemovedTag (ClientRemovedRequest usr cid (tUnqualified remoteConvs)) in enqueueNotification remoteConvs Q.Persistent rpc diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index d7f2e3539a1..3a22a033b23 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -103,24 +103,24 @@ federationSitemap :: federationSitemap = Named @"on-conversation-created" onConversationCreated :<|> Named @"get-conversations" getConversations - :<|> Named @"on-conversation-updated" onConversationUpdated :<|> Named @"leave-conversation" (callsFed (exposeAnnotations leaveConversation)) - :<|> Named @"on-message-sent" onMessageSent :<|> Named @"send-message" (callsFed (exposeAnnotations sendMessage)) - :<|> Named @"on-user-deleted-conversations" (callsFed (exposeAnnotations onUserDeleted)) :<|> Named @"update-conversation" (callsFed (exposeAnnotations updateConversation)) :<|> Named @"mls-welcome" mlsSendWelcome - :<|> Named @"on-mls-message-sent" onMLSMessageSent :<|> Named @"send-mls-message" (callsFed (exposeAnnotations sendMLSMessage)) :<|> Named @"send-mls-commit-bundle" (callsFed (exposeAnnotations sendMLSCommitBundle)) :<|> Named @"query-group-info" queryGroupInfo - :<|> Named @"on-client-removed" (callsFed (exposeAnnotations onClientRemoved)) :<|> Named @"update-typing-indicator" (callsFed (exposeAnnotations updateTypingIndicator)) :<|> Named @"on-typing-indicator-updated" onTypingIndicatorUpdated :<|> Named @"get-sub-conversation" getSubConversationForRemoteUser :<|> Named @"delete-sub-conversation" (callsFed deleteSubConversationForRemoteUser) :<|> Named @"leave-sub-conversation" (callsFed leaveSubConversation) :<|> Named @"get-one2one-conversation" getOne2OneConversation + :<|> Named @"on-client-removed" (callsFed (exposeAnnotations onClientRemoved)) + :<|> Named @"on-message-sent" onMessageSent + :<|> Named @"on-mls-message-sent" onMLSMessageSent + :<|> Named @"on-conversation-updated" onConversationUpdated + :<|> Named @"on-user-deleted-conversations" (callsFed (exposeAnnotations onUserDeleted)) onClientRemoved :: ( Member BackendNotificationQueueAccess r, diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 2830fc16d43..b33bab98ac5 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -420,7 +420,7 @@ rmUser lusr conn = do leaveRemoteConversations cids = for_ (bucketRemote (fromRange cids)) $ \remoteConvs -> do let userDelete = UserDeletedConversationsNotification (tUnqualified lusr) (unsafeRange (tUnqualified remoteConvs)) - let rpc = void $ fedQueueClient @'Galley @"on-user-deleted-conversations" userDelete + let rpc = void $ fedQueueClient @'OnUserDeletedConversationsTag userDelete enqueueNotification remoteConvs Q.Persistent rpc -- FUTUREWORK: Add a retry mechanism if there are federation errrors. diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index e9f6ac089d7..6b17a3a8a62 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -89,7 +89,7 @@ propagateMessage qusr mSenderClient lConvOrSub con msg cm = do -- send to remotes (either (logRemoteNotificationError @"on-mls-message-sent") (const (pure ())) <=< enqueueNotificationsConcurrently Q.Persistent (map remoteMemberQualify rmems)) $ \rs -> - fedQueueClient @'Galley @"on-mls-message-sent" $ + fedQueueClient @'OnMLSMessageSentTag $ RemoteMLSMessage { time = now, sender = qusr, diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index a78166d3e3a..66657736a6c 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -663,7 +663,7 @@ sendRemoteMessages domain now sender senderClient lcnv metadata messages = (hand transient = mmTransient metadata, recipients = UserClientMap rcpts } - let rpc = void $ fedQueueClient @'Galley @"on-message-sent" rm + let rpc = void $ fedQueueClient @'OnMessageSentTag rm enqueueNotification domain Q.Persistent rpc where handle :: Either FederationError a -> Sem r (Set (UserId, ClientId)) From e91963791de248b95782a1b2cec30ca03b491355 Mon Sep 17 00:00:00 2001 From: Zebot Date: Mon, 23 Oct 2023 07:30:48 +0000 Subject: [PATCH 225/225] Add changelog for Release 2023-10-23 --- CHANGELOG.md | 290 ++++++++++++++++++ .../0-release-notes/supported-protocols | 10 - changelog.d/1-api-changes/WPB-3798 | 1 - .../WPB-4668-disable-defederation | 1 - .../add-conv-id-to-welcome-event | 1 - changelog.d/1-api-changes/delete-keypackages | 1 - .../1-api-changes/delete-subconversation | 1 - changelog.d/1-api-changes/finalise-v4 | 1 - changelog.d/1-api-changes/get-mls-one2one | 1 - changelog.d/1-api-changes/get-subconversation | 1 - .../get-subconversation-groupinfo | 1 - changelog.d/1-api-changes/introduce-v5 | 1 - changelog.d/1-api-changes/mixed-to-mls | 1 - .../mls-conv-add-across-federation | 1 - .../mls-key-package-ciphersuites | 1 - .../1-api-changes/mls-migration-feature | 1 - changelog.d/1-api-changes/mls-upgrade | 7 - changelog.d/1-api-changes/mls-x509 | 1 - changelog.d/2-features/WPB-4547 | 1 - .../2-features/delete-remote-subconversation | 1 - changelog.d/2-features/delete-subconversation | 1 - changelog.d/2-features/mixed-protocol | 1 - changelog.d/2-features/mls-ciphersuites | 1 - changelog.d/2-features/mls-conv-limits | 1 - changelog.d/2-features/mls-one-to-one | 1 - changelog.d/2-features/mls-stale-app-messages | 1 - changelog.d/2-features/mls-x509-improvements | 1 - changelog.d/2-features/pr-2952 | 1 - .../2-features/rabbitmq-external_helm_chart | 1 - .../reflect-user-removal-from-parent-in-sub | 1 - changelog.d/2-features/subconv-commit-bundles | 1 - changelog.d/2-features/subconv-leave | 1 - changelog.d/3-bug-fixes/WBP-4959 | 1 - changelog.d/3-bug-fixes/WBP-4961 | 1 - .../WPB-1908-guest-creating-conversation | 1 - .../WPB-3842-federation-completeness-checks | 1 - .../WPB-4425-fix-es-migration-script | 1 - changelog.d/3-bug-fixes/WPB-4629 | 1 - changelog.d/3-bug-fixes/WPB-4787 | 1 - changelog.d/3-bug-fixes/WPB-4835 | 1 - .../duplicate-member-notifications | 1 - changelog.d/3-bug-fixes/federator-disconnect | 1 - changelog.d/3-bug-fixes/mls-notification-bug | 1 - .../3-bug-fixes/mls-self-conv-creator-ref | 1 - .../remote-member-removal-notification | 1 - changelog.d/3-bug-fixes/sender-welcome | 1 - changelog.d/4-docs/WPB-1103 | 1 - changelog.d/4-docs/WPB-4240 | 1 - .../4-docs/WPB-4556-internal-user-creation | 1 - changelog.d/4-docs/hotfix-pr-guidelines | 1 - changelog.d/5-internal/FS-1564 | 1 - changelog.d/5-internal/WBP-1224 | 1 - changelog.d/5-internal/WPB-1925 | 1 - ...-not-cache-federation-remote-domain-config | 1 - changelog.d/5-internal/WPB-3798 | 3 - changelog.d/5-internal/WPB-4240 | 4 - changelog.d/5-internal/WPB-4406 | 2 - changelog.d/5-internal/WPB-4748 | 1 - changelog.d/5-internal/WPB-485 | 1 - changelog.d/5-internal/WPB-4910 | 1 - changelog.d/5-internal/WPB-663 | 14 - changelog.d/5-internal/WPB-664 | 1 - .../5-internal/background-worker-nosync | 1 - changelog.d/5-internal/dont-return-to-sender | 1 - changelog.d/5-internal/empty-push | 1 - changelog.d/5-internal/galley-db-subconv | 1 - changelog.d/5-internal/group-id-subconv | 1 - changelog.d/5-internal/group-id-subconv-2 | 1 - changelog.d/5-internal/key-package-mapping | 1 - changelog.d/5-internal/mixed-protocol | 1 - changelog.d/5-internal/mls-mixed | 4 - .../5-internal/mls-save-migration-statistics | 3 - changelog.d/5-internal/mls-subconv-creation | 1 - changelog.d/5-internal/mls-subconv-messages | 1 - changelog.d/5-internal/mls-tests | 1 - changelog.d/5-internal/notification-500 | 1 - changelog.d/5-internal/optimize-stern | 1 - changelog.d/5-internal/pr-3538 | 1 - changelog.d/5-internal/pr-3540 | 1 - changelog.d/5-internal/pr-3572 | 1 - .../refactored-schema-version-tracking | 1 - changelog.d/5-internal/shutdown-cleanup | 1 - changelog.d/5-internal/subconv-store | 1 - changelog.d/5-internal/subconv-update-path | 1 - .../5-internal/test-joining-subconversation | 1 - changelog.d/5-internal/wpb-3888 | 1 - changelog.d/5-internal/wpb-3915 | 1 - changelog.d/5-internal/wpb-5033 | 4 - changelog.d/5-internal/xml-reports | 11 - changelog.d/6-federation/FS-1868 | 1 - changelog.d/6-federation/FS-1974 | 10 - .../WPB-4928-notification-endpoints | 1 - .../delete-remote-subconversation | 1 - .../on-new-remote-subconversation | 4 - changelog.d/6-federation/tcp-timeout | 3 - .../6-federation/wpb-3867-queue-endpoints | 1 - .../6-federation/wpb-3867-unreachable-users | 1 - changelog.d/6-federation/wpb-4984-queueing | 1 - 98 files changed, 290 insertions(+), 163 deletions(-) delete mode 100644 changelog.d/0-release-notes/supported-protocols delete mode 100644 changelog.d/1-api-changes/WPB-3798 delete mode 100644 changelog.d/1-api-changes/WPB-4668-disable-defederation delete mode 100644 changelog.d/1-api-changes/add-conv-id-to-welcome-event delete mode 100644 changelog.d/1-api-changes/delete-keypackages delete mode 100644 changelog.d/1-api-changes/delete-subconversation delete mode 100644 changelog.d/1-api-changes/finalise-v4 delete mode 100644 changelog.d/1-api-changes/get-mls-one2one delete mode 100644 changelog.d/1-api-changes/get-subconversation delete mode 100644 changelog.d/1-api-changes/get-subconversation-groupinfo delete mode 100644 changelog.d/1-api-changes/introduce-v5 delete mode 100644 changelog.d/1-api-changes/mixed-to-mls delete mode 100644 changelog.d/1-api-changes/mls-conv-add-across-federation delete mode 100644 changelog.d/1-api-changes/mls-key-package-ciphersuites delete mode 100644 changelog.d/1-api-changes/mls-migration-feature delete mode 100644 changelog.d/1-api-changes/mls-upgrade delete mode 100644 changelog.d/1-api-changes/mls-x509 delete mode 100644 changelog.d/2-features/WPB-4547 delete mode 100644 changelog.d/2-features/delete-remote-subconversation delete mode 100644 changelog.d/2-features/delete-subconversation delete mode 100644 changelog.d/2-features/mixed-protocol delete mode 100644 changelog.d/2-features/mls-ciphersuites delete mode 100644 changelog.d/2-features/mls-conv-limits delete mode 100644 changelog.d/2-features/mls-one-to-one delete mode 100644 changelog.d/2-features/mls-stale-app-messages delete mode 100644 changelog.d/2-features/mls-x509-improvements delete mode 100644 changelog.d/2-features/pr-2952 delete mode 100644 changelog.d/2-features/rabbitmq-external_helm_chart delete mode 100644 changelog.d/2-features/reflect-user-removal-from-parent-in-sub delete mode 100644 changelog.d/2-features/subconv-commit-bundles delete mode 100644 changelog.d/2-features/subconv-leave delete mode 100644 changelog.d/3-bug-fixes/WBP-4959 delete mode 100644 changelog.d/3-bug-fixes/WBP-4961 delete mode 100644 changelog.d/3-bug-fixes/WPB-1908-guest-creating-conversation delete mode 100644 changelog.d/3-bug-fixes/WPB-3842-federation-completeness-checks delete mode 100644 changelog.d/3-bug-fixes/WPB-4425-fix-es-migration-script delete mode 100644 changelog.d/3-bug-fixes/WPB-4629 delete mode 100644 changelog.d/3-bug-fixes/WPB-4787 delete mode 100644 changelog.d/3-bug-fixes/WPB-4835 delete mode 100644 changelog.d/3-bug-fixes/duplicate-member-notifications delete mode 100644 changelog.d/3-bug-fixes/federator-disconnect delete mode 100644 changelog.d/3-bug-fixes/mls-notification-bug delete mode 100644 changelog.d/3-bug-fixes/mls-self-conv-creator-ref delete mode 100644 changelog.d/3-bug-fixes/remote-member-removal-notification delete mode 100644 changelog.d/3-bug-fixes/sender-welcome delete mode 100644 changelog.d/4-docs/WPB-1103 delete mode 100644 changelog.d/4-docs/WPB-4240 delete mode 100644 changelog.d/4-docs/WPB-4556-internal-user-creation delete mode 100644 changelog.d/4-docs/hotfix-pr-guidelines delete mode 100644 changelog.d/5-internal/FS-1564 delete mode 100644 changelog.d/5-internal/WBP-1224 delete mode 100644 changelog.d/5-internal/WPB-1925 delete mode 100644 changelog.d/5-internal/WPB-3797-do-not-cache-federation-remote-domain-config delete mode 100644 changelog.d/5-internal/WPB-3798 delete mode 100644 changelog.d/5-internal/WPB-4240 delete mode 100644 changelog.d/5-internal/WPB-4406 delete mode 100644 changelog.d/5-internal/WPB-4748 delete mode 100644 changelog.d/5-internal/WPB-485 delete mode 100644 changelog.d/5-internal/WPB-4910 delete mode 100644 changelog.d/5-internal/WPB-663 delete mode 100644 changelog.d/5-internal/WPB-664 delete mode 100644 changelog.d/5-internal/background-worker-nosync delete mode 100644 changelog.d/5-internal/dont-return-to-sender delete mode 100644 changelog.d/5-internal/empty-push delete mode 100644 changelog.d/5-internal/galley-db-subconv delete mode 100644 changelog.d/5-internal/group-id-subconv delete mode 100644 changelog.d/5-internal/group-id-subconv-2 delete mode 100644 changelog.d/5-internal/key-package-mapping delete mode 100644 changelog.d/5-internal/mixed-protocol delete mode 100644 changelog.d/5-internal/mls-mixed delete mode 100644 changelog.d/5-internal/mls-save-migration-statistics delete mode 100644 changelog.d/5-internal/mls-subconv-creation delete mode 100644 changelog.d/5-internal/mls-subconv-messages delete mode 100644 changelog.d/5-internal/mls-tests delete mode 100644 changelog.d/5-internal/notification-500 delete mode 100644 changelog.d/5-internal/optimize-stern delete mode 100644 changelog.d/5-internal/pr-3538 delete mode 100644 changelog.d/5-internal/pr-3540 delete mode 100644 changelog.d/5-internal/pr-3572 delete mode 100644 changelog.d/5-internal/refactored-schema-version-tracking delete mode 100644 changelog.d/5-internal/shutdown-cleanup delete mode 100644 changelog.d/5-internal/subconv-store delete mode 100644 changelog.d/5-internal/subconv-update-path delete mode 100644 changelog.d/5-internal/test-joining-subconversation delete mode 100644 changelog.d/5-internal/wpb-3888 delete mode 100644 changelog.d/5-internal/wpb-3915 delete mode 100644 changelog.d/5-internal/wpb-5033 delete mode 100644 changelog.d/5-internal/xml-reports delete mode 100644 changelog.d/6-federation/FS-1868 delete mode 100644 changelog.d/6-federation/FS-1974 delete mode 100644 changelog.d/6-federation/WPB-4928-notification-endpoints delete mode 100644 changelog.d/6-federation/delete-remote-subconversation delete mode 100644 changelog.d/6-federation/on-new-remote-subconversation delete mode 100644 changelog.d/6-federation/tcp-timeout delete mode 100644 changelog.d/6-federation/wpb-3867-queue-endpoints delete mode 100644 changelog.d/6-federation/wpb-3867-unreachable-users delete mode 100644 changelog.d/6-federation/wpb-4984-queueing diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bedcc42ccf..62637fa9742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,293 @@ +# [2023-10-23] (Chart Release 4.39.0) + +## Release notes + + +* New field for Supported protocols in Galley's MLS feature config + + Galley will refuse to start if the list `supportedProtocols` does not contain + the value of the field `defaultProtocol`. Galley will also refuse to start if + MLS migration is enabled and MLS is not part of `supportedProtocols`. + + The default value for `supportedProtocols` is: + ``` + [proteus, mls] + ``` (#3374) + + +## API changes + + +* The JSON schema of `NonConnectedBackends` has changed to have its single field now called `non_connected_backends`. (#3518) + +* Remove de-federation (to avoid a scalability issue). (#3582) + +* Replace the placeholder self conversation id with the qualified conversation id for welcome events. (#3335) + +* Add new endpoint `DELETE /mls/key-packages/self/:client` (#3295) + +* Introduce an endpoint for deleting a subconversation (#2956, #3119, #3123) + +* Remove MLS endpoints from API v4 and finalise it (#3545) + +* Add new endpoint `GET /conversations/one2one/:domain/:uid` to fetch the MLS 1-1 conversation with another user (#3345) + +* Introduce a subconversation GET endpoint (#2869, #2995) + +* Add `GET /conversations/:domain/:cid/subconversations/:id/groupinfo` endpoint to fetch the group info object for a subconversation (#2932) + +* Introduce v5 development version (#3527) + +* It is now possible to use `PUT /conversation/:domain/:id/protocol` to transition from Mixed to MLS (#3334) + +* Report a failure to add remote users to an MLS conversation (#3304) + +* The key package API has gained a `ciphersuite` query parameter, which should be the hexadecimal value of an MLS ciphersuite, defaulting to `0x0001`. The `ciphersuite` parameter is used by the claim and count endpoints. For uploads, the API is unchanged, and the ciphersuite is taken directly from the uploaded key package. (#3454) + +* Add MLS migration feature config (#3299) + +* Switch to MLS draft 20. The following endpoints are affected by the change: + + - All endpoints with `message/mls` content type now expect and return draft-20 MLS structures. + - `POST /conversations` does not require `creator_client` anymore. + - `POST /mls/commit-bundles` now expects a "stream" of MLS messages, i.e. a sequence of TLS-serialised messages, one after the other, in any order. Its protobuf interface has been removed. + - `POST /mls/welcome` has been removed. Welcome messages can now only be sent as part of a commit bundle. + - `POST /mls/message` does not accept commit messages anymore. All commit messages must be sent as part of a commit bundle. (#3172) + +* Key packages and leaf nodes with x509 credentials are now supported (#3532) + + +## Features + + +* Add reason field to conversation.member-leave (#3640) + +* Support deleting a remote subconversation (#2964) + +* Introduce support for resetting a subconversation (#2956) + +* Introduce a "mixed" conversation protocol type. A conversation of "mixed" protocol functions as a Proteus converation as well as a MLS conversations. It's intended to be used for migrating conversations from Proteus to MLS. (#3258) + +* Added support for post-quantum ciphersuite 0xf031. Correspondingly, MLS groups with a non-default ciphersuite are now supported. The first commit in a group determines the group ciphersuite. (#3454) + +* Remove conversation size limit for MLS conversations (#3468) + +* Added support for MSL 1-1 conversations (#3360) + +* MLS application messages for older epochs are now rejected (#3438) + +* The public key in an x509 credential is now checked against that of the client (#3542) + +* Add federated endpoints to get subconversations (#2952) + +* Add Helm chart (`rabbitmq-external`) to interface RabbitMQ instances outside of the Kubernetes cluster. (#3626) + +* Removing or kicking a user from a conversation also removes the user's clients from any subconversation. (#2942) + +* Add support for subconversations in `POST /mls/commit-bundles` (#2932) + +* Implement endpoint for leaving a subconversation (#2969, #3080, #3085, #3107) + + +## Bug fixes and other updates + + +* Fix nix derivations for rust packages (#3628) + +* Ensure benchmarking dependencies are provided by nix development environment (#3628) + +* Disable a guest user from creating a group conversation (#3622) + +* Adding users to a conversation now enforces that all federation domains that will be in the conversation are federated with each other. (#3514) + +* Fix ES migration script. (#3558) + +* Fixed add user to conversation when one of the other participating backends is offline (#3585) + +* Create a new http2 connection in every federator client request instead of using a shared connection. (#3602) + +* list-clients returns with partial success even if one of the remote backends is unreachable (#3611) + +* Defederation notifications, federation.delete and federation.connectionRemoved, now deduplicate the user list so that we don't send them more notifications than required. (#3515) + +* Fix memory and TCP connection leak in brig, galley, caroghold and background-worker. (#3663) + +* Fix bug where notifications for MLS messages were not showing up in all notification streams of clients (#3610) + +* Map the MLS self-conversation creator's key package reference in Brig (#3055) + +* This fixes a bug where a remote member is removed from a conversation while their backend is unreachable, and the backend does not receive the removal notification once it is reachable again. (#3537) + +* Welcome messages are not sent anymore to the creator of an MLS group on the first commit (#3392) + + +## Documentation + + +* Fix: support api versions other than v0 in swagger docs. (#3619) + +* Updating the route documentation from Swagger 2 to OpenAPI 3. (#3570) + +* Elaborate on internal user creation in prod (#3596) + +* Adding a testing config entry to the PR guidelines. (#3624) + + +## Internal changes + + +* remove leaving clients immediately from subconversations (#3096) + +* Servantify internal end-points: brig/teams (#3634) + +* add conversation type to group ID serialisation (#3344) + +* Do not cache federation remote configs on non-brig services (#3612) + +* JSON derived schemas have been changed to no longer pre-process record fields to drop prefixes that were required to disambiguate fields. + Prefix processing still exists to drop leading underscores from field names, as we are using prefixed field names with `makeLenses`. + Code has been updated to use `OverloadedRecordDot` with the changed field names. (#3518) + +* Updating the route documentation library from swagger2 to openapi3. + + This also introduced a breaking change in how we track what federation calls each route makes. + The openapi3 library doesn't support extension fields, and as such tags are being used instead in a similar way. (#3570) + +* - Extending the information returned in errors for Federator. Paths and response bodies, if available, are included in error logs. + - Prometheus metrics for outgoing and incoming federation requests added. They can be enabled by setting `metrics.serviceMonitor.enabled`, like in other charts. (#3556) + +* CLI tool to consume messages from a RabbitMQ queue (#3589, #3655) + +* Removed user and client threshold fields from mls migration feature. (#3364) + +* Include timestamp in s3 upload path for test logs (#3621) + +* Migrating the following routes to the Servant API form. + + POST /provider/services + GET /provider/services + GET /provider/services/:sid + PUT /provider/services/:sid + PUT /provider/services/:sid/connection + DELETE /provider/services/:sid + GET /providers/:pid/services + GET /providers/:pid/services/:sid + GET /services + GET /services/tags + GET /teams/:tid/services/whitelisted + POST /teams/:tid/services/whitelist (#3554) + +* Provider API has been migrated to servant (#3547) + +* background-worker: Get list of domains from RabbitMQ instead of brig for pushing backend notifications (#3588) + +* Avoid including MLS application messages in the sender client's event stream. (#3379) + +* Avoid empty pushes when chunking pushes in galley (#PR_NOT_FOUND) + +* Introduce a Galley DB table for subconversations (#2869) + +* Support mapping MLS group IDs to subconversations (#2869) + +* change version and conversation type to 16 bit in group ID serialisation (#3353) + +* Brig does not perform key package ref mapping anymore. Claimed key packages are simply removed from the `mls_key_packages` table. The `mls_key_package_refs` table is now unused, and will be removed in the future. (#3172) + +* Add intermediate "mixed" protocol for migrating from Proteus to MLS (#3292) + +* - Do not perform client checks for add and remove proposals in mixed conversations + - Restrict protocol updates to team conversations + - Disallow MLS application messages in mixed conversations + - Send remove proposals when users leave mixed conversations (#3303) + +* New cron job to save data usable to watch the progress of the Proteus to MLS migration in S3 bucket. + + **IMPORTANT:** This cron job is _not_ meant for general use! It can leak data about one team to other teams. (#3579) + +* Subconversations are now created on their first commit (#3355) + +* Propagate messages in MLS subconversations (#2937) + +* Move some MLS tests to new integration suite (#3286) + +* Check validity of notification IDs in the notification API (#3550) + +* stern: Optimize RAM usage of /i/users/meta-info (#3522) + +* Additional integration test for federated connections (#3538) + +* The bot API is now migrated to servant (#3540) + +* `rusty-jwt-tools` is upgraded to version 0.5.0 (#3572) + +* Refactored schema version tracking from manually managed to automatic. (#3643) + +* Avoid unnecessary error logs on service shutdown (#3592) + +* Introduce an effect for subconversations (#2869) + +* Via the update path update the key package of the committer in epoch 0 of a subconversation (#2975) + +* Add more tests for joining a subconversation (#2974) + +* Added `/tools/db/repair-brig-clients-table` to clean up after the fix in #3504 (#3507) + +* Distinguish between update and upsert cassandra commands (follow-up to #3504) (#3513) + +* Truncate `galley.mls_group_member_client` table and drop `galley.member_client` table. + + The data in `mls_group_member_client` could contain nulls from client testing in prod. So, its OK to truncate it. + The `member_client` table is unused. (#3648) + +* All integration tests can generate XML reports. + + To generate the report in brig-integration, galley-integration, + cargohold-integration, gundeck-integration, stern-integration and the new + integration suite pass `--xml=` to generate the XML file. + + For spar-integration and federator-integration pass `-f junit` and set + `JUNIT_OUTPUT_DIRECTORY` and `JUNIT_SUITE_NAME` environment variables. The XML + report will be generated at `$JUNIT_OUTPUT_DIRECTORY/junit.xml`. + + (#3568, #3633) + + +## Federation changes + + +* Add subconversation ID to onMLSMessageSent request payload. (#3270) + +* Derive group ID from qualified conversation ID and, if applicable, + subconversation ID. + + Retire mapping from group IDs to conversation IDs. (group_id_conv_id) + + Remove federation endpoints + - on-new-remote-conversation, + - on-new-remote-subconversation, and + - on-delete-mls-conversation + which were used to synchronise the group to conversation mapping. (#3309) + +* Reorganise the federation API such that queueing notification endpoints are separate from synchronous endpoints. Also simplify queueing federation notification endpoints. (#3647) + +* Introduce an endpoint for resetting a remote subconversation (#2964) + +* Split federation endpoint into on-new-remote-conversation and on-new-remote-subconversation + Call on-new-remote-subconversation when a new subconversation is created + Call on-new-remote-subconversation for all existing subconversations when a new backend gets involved + Call on-new-remote-subconversation when a subconversation is reset (#2997) + +* federator: Allow setting TCP connection timeout for HTTP2 requests + + The helm chart defaults it to 5s which should be best for most installations. (#3595) + +* Constrain which federation endpoints can be used via the queueing federation client (#3629) + +* There is a breaking change in the "on-mls-message-sent" federation endpoint due to queueing. Now that there is retrying because of queueing, the endpoint can no longer respond with a list of unreachable users. (#3629) + +* Remote MLS messages get queued via RabbitMQ (#PR_NOT_FOUND) + + # [2023-08-16] (Chart Release 4.38.0) ## Bug fixes and other updates diff --git a/changelog.d/0-release-notes/supported-protocols b/changelog.d/0-release-notes/supported-protocols deleted file mode 100644 index 0de4e14e8af..00000000000 --- a/changelog.d/0-release-notes/supported-protocols +++ /dev/null @@ -1,10 +0,0 @@ -New field for Supported protocols in Galley's MLS feature config - -Galley will refuse to start if the list `supportedProtocols` does not contain -the value of the field `defaultProtocol`. Galley will also refuse to start if -MLS migration is enabled and MLS is not part of `supportedProtocols`. - -The default value for `supportedProtocols` is: -``` -[proteus, mls] -``` diff --git a/changelog.d/1-api-changes/WPB-3798 b/changelog.d/1-api-changes/WPB-3798 deleted file mode 100644 index 42435b4730f..00000000000 --- a/changelog.d/1-api-changes/WPB-3798 +++ /dev/null @@ -1 +0,0 @@ -The JSON schema of `NonConnectedBackends` has changed to have its single field now called `non_connected_backends`. \ No newline at end of file diff --git a/changelog.d/1-api-changes/WPB-4668-disable-defederation b/changelog.d/1-api-changes/WPB-4668-disable-defederation deleted file mode 100644 index baef31417a9..00000000000 --- a/changelog.d/1-api-changes/WPB-4668-disable-defederation +++ /dev/null @@ -1 +0,0 @@ -Remove de-federation (to avoid a scalability issue). \ No newline at end of file diff --git a/changelog.d/1-api-changes/add-conv-id-to-welcome-event b/changelog.d/1-api-changes/add-conv-id-to-welcome-event deleted file mode 100644 index ae5e423b297..00000000000 --- a/changelog.d/1-api-changes/add-conv-id-to-welcome-event +++ /dev/null @@ -1 +0,0 @@ -Replace the placeholder self conversation id with the qualified conversation id for welcome events. diff --git a/changelog.d/1-api-changes/delete-keypackages b/changelog.d/1-api-changes/delete-keypackages deleted file mode 100644 index c6ce843eb50..00000000000 --- a/changelog.d/1-api-changes/delete-keypackages +++ /dev/null @@ -1 +0,0 @@ -Add new endpoint `DELETE /mls/key-packages/self/:client` diff --git a/changelog.d/1-api-changes/delete-subconversation b/changelog.d/1-api-changes/delete-subconversation deleted file mode 100644 index c3a53610acd..00000000000 --- a/changelog.d/1-api-changes/delete-subconversation +++ /dev/null @@ -1 +0,0 @@ -Introduce an endpoint for deleting a subconversation (#2956, #3119, #3123) diff --git a/changelog.d/1-api-changes/finalise-v4 b/changelog.d/1-api-changes/finalise-v4 deleted file mode 100644 index 41a18ee0ca5..00000000000 --- a/changelog.d/1-api-changes/finalise-v4 +++ /dev/null @@ -1 +0,0 @@ -Remove MLS endpoints from API v4 and finalise it diff --git a/changelog.d/1-api-changes/get-mls-one2one b/changelog.d/1-api-changes/get-mls-one2one deleted file mode 100644 index b34d49e3c21..00000000000 --- a/changelog.d/1-api-changes/get-mls-one2one +++ /dev/null @@ -1 +0,0 @@ -Add new endpoint `GET /conversations/one2one/:domain/:uid` to fetch the MLS 1-1 conversation with another user diff --git a/changelog.d/1-api-changes/get-subconversation b/changelog.d/1-api-changes/get-subconversation deleted file mode 100644 index 6a632571f6b..00000000000 --- a/changelog.d/1-api-changes/get-subconversation +++ /dev/null @@ -1 +0,0 @@ -Introduce a subconversation GET endpoint (#2869, #2995) diff --git a/changelog.d/1-api-changes/get-subconversation-groupinfo b/changelog.d/1-api-changes/get-subconversation-groupinfo deleted file mode 100644 index 32845ff8279..00000000000 --- a/changelog.d/1-api-changes/get-subconversation-groupinfo +++ /dev/null @@ -1 +0,0 @@ -Add `GET /conversations/:domain/:cid/subconversations/:id/groupinfo` endpoint to fetch the group info object for a subconversation diff --git a/changelog.d/1-api-changes/introduce-v5 b/changelog.d/1-api-changes/introduce-v5 deleted file mode 100644 index 0d2498b3d31..00000000000 --- a/changelog.d/1-api-changes/introduce-v5 +++ /dev/null @@ -1 +0,0 @@ -Introduce v5 development version diff --git a/changelog.d/1-api-changes/mixed-to-mls b/changelog.d/1-api-changes/mixed-to-mls deleted file mode 100644 index 3eafd6734cc..00000000000 --- a/changelog.d/1-api-changes/mixed-to-mls +++ /dev/null @@ -1 +0,0 @@ -It is now possible to use `PUT /conversation/:domain/:id/protocol` to transition from Mixed to MLS diff --git a/changelog.d/1-api-changes/mls-conv-add-across-federation b/changelog.d/1-api-changes/mls-conv-add-across-federation deleted file mode 100644 index 6c86f1106bf..00000000000 --- a/changelog.d/1-api-changes/mls-conv-add-across-federation +++ /dev/null @@ -1 +0,0 @@ -Report a failure to add remote users to an MLS conversation diff --git a/changelog.d/1-api-changes/mls-key-package-ciphersuites b/changelog.d/1-api-changes/mls-key-package-ciphersuites deleted file mode 100644 index 9ed10cbd19f..00000000000 --- a/changelog.d/1-api-changes/mls-key-package-ciphersuites +++ /dev/null @@ -1 +0,0 @@ -The key package API has gained a `ciphersuite` query parameter, which should be the hexadecimal value of an MLS ciphersuite, defaulting to `0x0001`. The `ciphersuite` parameter is used by the claim and count endpoints. For uploads, the API is unchanged, and the ciphersuite is taken directly from the uploaded key package. diff --git a/changelog.d/1-api-changes/mls-migration-feature b/changelog.d/1-api-changes/mls-migration-feature deleted file mode 100644 index 67470870c70..00000000000 --- a/changelog.d/1-api-changes/mls-migration-feature +++ /dev/null @@ -1 +0,0 @@ -Add MLS migration feature config diff --git a/changelog.d/1-api-changes/mls-upgrade b/changelog.d/1-api-changes/mls-upgrade deleted file mode 100644 index de9bd3f4d81..00000000000 --- a/changelog.d/1-api-changes/mls-upgrade +++ /dev/null @@ -1,7 +0,0 @@ -Switch to MLS draft 20. The following endpoints are affected by the change: - - - All endpoints with `message/mls` content type now expect and return draft-20 MLS structures. - - `POST /conversations` does not require `creator_client` anymore. - - `POST /mls/commit-bundles` now expects a "stream" of MLS messages, i.e. a sequence of TLS-serialised messages, one after the other, in any order. Its protobuf interface has been removed. - - `POST /mls/welcome` has been removed. Welcome messages can now only be sent as part of a commit bundle. - - `POST /mls/message` does not accept commit messages anymore. All commit messages must be sent as part of a commit bundle. diff --git a/changelog.d/1-api-changes/mls-x509 b/changelog.d/1-api-changes/mls-x509 deleted file mode 100644 index 5f07ef57782..00000000000 --- a/changelog.d/1-api-changes/mls-x509 +++ /dev/null @@ -1 +0,0 @@ -Key packages and leaf nodes with x509 credentials are now supported diff --git a/changelog.d/2-features/WPB-4547 b/changelog.d/2-features/WPB-4547 deleted file mode 100644 index 54a98f7352a..00000000000 --- a/changelog.d/2-features/WPB-4547 +++ /dev/null @@ -1 +0,0 @@ -Add reason field to conversation.member-leave diff --git a/changelog.d/2-features/delete-remote-subconversation b/changelog.d/2-features/delete-remote-subconversation deleted file mode 100644 index 01c7da857e0..00000000000 --- a/changelog.d/2-features/delete-remote-subconversation +++ /dev/null @@ -1 +0,0 @@ -Support deleting a remote subconversation diff --git a/changelog.d/2-features/delete-subconversation b/changelog.d/2-features/delete-subconversation deleted file mode 100644 index 08ab83679d9..00000000000 --- a/changelog.d/2-features/delete-subconversation +++ /dev/null @@ -1 +0,0 @@ -Introduce support for resetting a subconversation diff --git a/changelog.d/2-features/mixed-protocol b/changelog.d/2-features/mixed-protocol deleted file mode 100644 index 507a8a7d586..00000000000 --- a/changelog.d/2-features/mixed-protocol +++ /dev/null @@ -1 +0,0 @@ -Introduce a "mixed" conversation protocol type. A conversation of "mixed" protocol functions as a Proteus converation as well as a MLS conversations. It's intended to be used for migrating conversations from Proteus to MLS. diff --git a/changelog.d/2-features/mls-ciphersuites b/changelog.d/2-features/mls-ciphersuites deleted file mode 100644 index 7886487e8bb..00000000000 --- a/changelog.d/2-features/mls-ciphersuites +++ /dev/null @@ -1 +0,0 @@ -Added support for post-quantum ciphersuite 0xf031. Correspondingly, MLS groups with a non-default ciphersuite are now supported. The first commit in a group determines the group ciphersuite. diff --git a/changelog.d/2-features/mls-conv-limits b/changelog.d/2-features/mls-conv-limits deleted file mode 100644 index 6a499746c4d..00000000000 --- a/changelog.d/2-features/mls-conv-limits +++ /dev/null @@ -1 +0,0 @@ -Remove conversation size limit for MLS conversations diff --git a/changelog.d/2-features/mls-one-to-one b/changelog.d/2-features/mls-one-to-one deleted file mode 100644 index 2f2603aa07b..00000000000 --- a/changelog.d/2-features/mls-one-to-one +++ /dev/null @@ -1 +0,0 @@ -Added support for MSL 1-1 conversations diff --git a/changelog.d/2-features/mls-stale-app-messages b/changelog.d/2-features/mls-stale-app-messages deleted file mode 100644 index 5005ccbac9d..00000000000 --- a/changelog.d/2-features/mls-stale-app-messages +++ /dev/null @@ -1 +0,0 @@ -MLS application messages for older epochs are now rejected diff --git a/changelog.d/2-features/mls-x509-improvements b/changelog.d/2-features/mls-x509-improvements deleted file mode 100644 index 36e3d457df8..00000000000 --- a/changelog.d/2-features/mls-x509-improvements +++ /dev/null @@ -1 +0,0 @@ -The public key in an x509 credential is now checked against that of the client diff --git a/changelog.d/2-features/pr-2952 b/changelog.d/2-features/pr-2952 deleted file mode 100644 index 0bfe30efe71..00000000000 --- a/changelog.d/2-features/pr-2952 +++ /dev/null @@ -1 +0,0 @@ -Add federated endpoints to get subconversations diff --git a/changelog.d/2-features/rabbitmq-external_helm_chart b/changelog.d/2-features/rabbitmq-external_helm_chart deleted file mode 100644 index 57a9f4e7ccc..00000000000 --- a/changelog.d/2-features/rabbitmq-external_helm_chart +++ /dev/null @@ -1 +0,0 @@ -Add Helm chart (`rabbitmq-external`) to interface RabbitMQ instances outside of the Kubernetes cluster. diff --git a/changelog.d/2-features/reflect-user-removal-from-parent-in-sub b/changelog.d/2-features/reflect-user-removal-from-parent-in-sub deleted file mode 100644 index 8f411ae91f2..00000000000 --- a/changelog.d/2-features/reflect-user-removal-from-parent-in-sub +++ /dev/null @@ -1 +0,0 @@ -Removing or kicking a user from a conversation also removes the user's clients from any subconversation. diff --git a/changelog.d/2-features/subconv-commit-bundles b/changelog.d/2-features/subconv-commit-bundles deleted file mode 100644 index a6db49b6183..00000000000 --- a/changelog.d/2-features/subconv-commit-bundles +++ /dev/null @@ -1 +0,0 @@ -Add support for subconversations in `POST /mls/commit-bundles` diff --git a/changelog.d/2-features/subconv-leave b/changelog.d/2-features/subconv-leave deleted file mode 100644 index 0fac932a479..00000000000 --- a/changelog.d/2-features/subconv-leave +++ /dev/null @@ -1 +0,0 @@ -Implement endpoint for leaving a subconversation (#2969, #3080, #3085, #3107) diff --git a/changelog.d/3-bug-fixes/WBP-4959 b/changelog.d/3-bug-fixes/WBP-4959 deleted file mode 100644 index c053654fa0b..00000000000 --- a/changelog.d/3-bug-fixes/WBP-4959 +++ /dev/null @@ -1 +0,0 @@ -Fix nix derivations for rust packages diff --git a/changelog.d/3-bug-fixes/WBP-4961 b/changelog.d/3-bug-fixes/WBP-4961 deleted file mode 100644 index ef17d4d7bab..00000000000 --- a/changelog.d/3-bug-fixes/WBP-4961 +++ /dev/null @@ -1 +0,0 @@ -Ensure benchmarking dependencies are provided by nix development environment diff --git a/changelog.d/3-bug-fixes/WPB-1908-guest-creating-conversation b/changelog.d/3-bug-fixes/WPB-1908-guest-creating-conversation deleted file mode 100644 index 2e06f0b2bb3..00000000000 --- a/changelog.d/3-bug-fixes/WPB-1908-guest-creating-conversation +++ /dev/null @@ -1 +0,0 @@ -Disable a guest user from creating a group conversation diff --git a/changelog.d/3-bug-fixes/WPB-3842-federation-completeness-checks b/changelog.d/3-bug-fixes/WPB-3842-federation-completeness-checks deleted file mode 100644 index 47959209790..00000000000 --- a/changelog.d/3-bug-fixes/WPB-3842-federation-completeness-checks +++ /dev/null @@ -1 +0,0 @@ -Adding users to a conversation now enforces that all federation domains that will be in the conversation are federated with each other. \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/WPB-4425-fix-es-migration-script b/changelog.d/3-bug-fixes/WPB-4425-fix-es-migration-script deleted file mode 100644 index 66cbb384787..00000000000 --- a/changelog.d/3-bug-fixes/WPB-4425-fix-es-migration-script +++ /dev/null @@ -1 +0,0 @@ -Fix ES migration script. diff --git a/changelog.d/3-bug-fixes/WPB-4629 b/changelog.d/3-bug-fixes/WPB-4629 deleted file mode 100644 index 5d1724fe66e..00000000000 --- a/changelog.d/3-bug-fixes/WPB-4629 +++ /dev/null @@ -1 +0,0 @@ -Fixed add user to conversation when one of the other participating backends is offline diff --git a/changelog.d/3-bug-fixes/WPB-4787 b/changelog.d/3-bug-fixes/WPB-4787 deleted file mode 100644 index 97cb562182e..00000000000 --- a/changelog.d/3-bug-fixes/WPB-4787 +++ /dev/null @@ -1 +0,0 @@ -Create a new http2 connection in every federator client request instead of using a shared connection. diff --git a/changelog.d/3-bug-fixes/WPB-4835 b/changelog.d/3-bug-fixes/WPB-4835 deleted file mode 100644 index 148ded40f27..00000000000 --- a/changelog.d/3-bug-fixes/WPB-4835 +++ /dev/null @@ -1 +0,0 @@ -list-clients returns with partial success even if one of the remote backends is unreachable diff --git a/changelog.d/3-bug-fixes/duplicate-member-notifications b/changelog.d/3-bug-fixes/duplicate-member-notifications deleted file mode 100644 index 120b5bc7ebf..00000000000 --- a/changelog.d/3-bug-fixes/duplicate-member-notifications +++ /dev/null @@ -1 +0,0 @@ -Defederation notifications, federation.delete and federation.connectionRemoved, now deduplicate the user list so that we don't send them more notifications than required. \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/federator-disconnect b/changelog.d/3-bug-fixes/federator-disconnect deleted file mode 100644 index 1731c0dc807..00000000000 --- a/changelog.d/3-bug-fixes/federator-disconnect +++ /dev/null @@ -1 +0,0 @@ -Fix memory and TCP connection leak in brig, galley, caroghold and background-worker. \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/mls-notification-bug b/changelog.d/3-bug-fixes/mls-notification-bug deleted file mode 100644 index cfe1d68289d..00000000000 --- a/changelog.d/3-bug-fixes/mls-notification-bug +++ /dev/null @@ -1 +0,0 @@ -Fix bug where notifications for MLS messages were not showing up in all notification streams of clients diff --git a/changelog.d/3-bug-fixes/mls-self-conv-creator-ref b/changelog.d/3-bug-fixes/mls-self-conv-creator-ref deleted file mode 100644 index 8ba14ebd2f9..00000000000 --- a/changelog.d/3-bug-fixes/mls-self-conv-creator-ref +++ /dev/null @@ -1 +0,0 @@ -Map the MLS self-conversation creator's key package reference in Brig diff --git a/changelog.d/3-bug-fixes/remote-member-removal-notification b/changelog.d/3-bug-fixes/remote-member-removal-notification deleted file mode 100644 index a94c916a689..00000000000 --- a/changelog.d/3-bug-fixes/remote-member-removal-notification +++ /dev/null @@ -1 +0,0 @@ -This fixes a bug where a remote member is removed from a conversation while their backend is unreachable, and the backend does not receive the removal notification once it is reachable again. diff --git a/changelog.d/3-bug-fixes/sender-welcome b/changelog.d/3-bug-fixes/sender-welcome deleted file mode 100644 index 22503e2aab9..00000000000 --- a/changelog.d/3-bug-fixes/sender-welcome +++ /dev/null @@ -1 +0,0 @@ -Welcome messages are not sent anymore to the creator of an MLS group on the first commit diff --git a/changelog.d/4-docs/WPB-1103 b/changelog.d/4-docs/WPB-1103 deleted file mode 100644 index ff6644084df..00000000000 --- a/changelog.d/4-docs/WPB-1103 +++ /dev/null @@ -1 +0,0 @@ -Fix: support api versions other than v0 in swagger docs. \ No newline at end of file diff --git a/changelog.d/4-docs/WPB-4240 b/changelog.d/4-docs/WPB-4240 deleted file mode 100644 index d7dd76196ec..00000000000 --- a/changelog.d/4-docs/WPB-4240 +++ /dev/null @@ -1 +0,0 @@ -Updating the route documentation from Swagger 2 to OpenAPI 3. \ No newline at end of file diff --git a/changelog.d/4-docs/WPB-4556-internal-user-creation b/changelog.d/4-docs/WPB-4556-internal-user-creation deleted file mode 100644 index 399ec6b8b86..00000000000 --- a/changelog.d/4-docs/WPB-4556-internal-user-creation +++ /dev/null @@ -1 +0,0 @@ -Elaborate on internal user creation in prod \ No newline at end of file diff --git a/changelog.d/4-docs/hotfix-pr-guidelines b/changelog.d/4-docs/hotfix-pr-guidelines deleted file mode 100644 index 940c66b8fff..00000000000 --- a/changelog.d/4-docs/hotfix-pr-guidelines +++ /dev/null @@ -1 +0,0 @@ -Adding a testing config entry to the PR guidelines. \ No newline at end of file diff --git a/changelog.d/5-internal/FS-1564 b/changelog.d/5-internal/FS-1564 deleted file mode 100644 index 9bb9235cc7a..00000000000 --- a/changelog.d/5-internal/FS-1564 +++ /dev/null @@ -1 +0,0 @@ -remove leaving clients immediately from subconversations diff --git a/changelog.d/5-internal/WBP-1224 b/changelog.d/5-internal/WBP-1224 deleted file mode 100644 index 12dd7e6cbab..00000000000 --- a/changelog.d/5-internal/WBP-1224 +++ /dev/null @@ -1 +0,0 @@ -Servantify internal end-points: brig/teams diff --git a/changelog.d/5-internal/WPB-1925 b/changelog.d/5-internal/WPB-1925 deleted file mode 100644 index cc2af9f8948..00000000000 --- a/changelog.d/5-internal/WPB-1925 +++ /dev/null @@ -1 +0,0 @@ -add conversation type to group ID serialisation diff --git a/changelog.d/5-internal/WPB-3797-do-not-cache-federation-remote-domain-config b/changelog.d/5-internal/WPB-3797-do-not-cache-federation-remote-domain-config deleted file mode 100644 index dfd7ed0f27f..00000000000 --- a/changelog.d/5-internal/WPB-3797-do-not-cache-federation-remote-domain-config +++ /dev/null @@ -1 +0,0 @@ -Do not cache federation remote configs on non-brig services diff --git a/changelog.d/5-internal/WPB-3798 b/changelog.d/5-internal/WPB-3798 deleted file mode 100644 index 625e4d9b15a..00000000000 --- a/changelog.d/5-internal/WPB-3798 +++ /dev/null @@ -1,3 +0,0 @@ -JSON derived schemas have been changed to no longer pre-process record fields to drop prefixes that were required to disambiguate fields. -Prefix processing still exists to drop leading underscores from field names, as we are using prefixed field names with `makeLenses`. -Code has been updated to use `OverloadedRecordDot` with the changed field names. \ No newline at end of file diff --git a/changelog.d/5-internal/WPB-4240 b/changelog.d/5-internal/WPB-4240 deleted file mode 100644 index bca7dcb1fc6..00000000000 --- a/changelog.d/5-internal/WPB-4240 +++ /dev/null @@ -1,4 +0,0 @@ -Updating the route documentation library from swagger2 to openapi3. - -This also introduced a breaking change in how we track what federation calls each route makes. -The openapi3 library doesn't support extension fields, and as such tags are being used instead in a similar way. \ No newline at end of file diff --git a/changelog.d/5-internal/WPB-4406 b/changelog.d/5-internal/WPB-4406 deleted file mode 100644 index 6c064b7594b..00000000000 --- a/changelog.d/5-internal/WPB-4406 +++ /dev/null @@ -1,2 +0,0 @@ -- Extending the information returned in errors for Federator. Paths and response bodies, if available, are included in error logs. -- Prometheus metrics for outgoing and incoming federation requests added. They can be enabled by setting `metrics.serviceMonitor.enabled`, like in other charts. diff --git a/changelog.d/5-internal/WPB-4748 b/changelog.d/5-internal/WPB-4748 deleted file mode 100644 index 823b14baece..00000000000 --- a/changelog.d/5-internal/WPB-4748 +++ /dev/null @@ -1 +0,0 @@ -CLI tool to consume messages from a RabbitMQ queue (#3589, #3655) diff --git a/changelog.d/5-internal/WPB-485 b/changelog.d/5-internal/WPB-485 deleted file mode 100644 index a35171937fd..00000000000 --- a/changelog.d/5-internal/WPB-485 +++ /dev/null @@ -1 +0,0 @@ -Removed user and client threshold fields from mls migration feature. diff --git a/changelog.d/5-internal/WPB-4910 b/changelog.d/5-internal/WPB-4910 deleted file mode 100644 index 4d2155b181c..00000000000 --- a/changelog.d/5-internal/WPB-4910 +++ /dev/null @@ -1 +0,0 @@ -Include timestamp in s3 upload path for test logs diff --git a/changelog.d/5-internal/WPB-663 b/changelog.d/5-internal/WPB-663 deleted file mode 100644 index 303cf529f7b..00000000000 --- a/changelog.d/5-internal/WPB-663 +++ /dev/null @@ -1,14 +0,0 @@ -Migrating the following routes to the Servant API form. - -POST /provider/services -GET /provider/services -GET /provider/services/:sid -PUT /provider/services/:sid -PUT /provider/services/:sid/connection -DELETE /provider/services/:sid -GET /providers/:pid/services -GET /providers/:pid/services/:sid -GET /services -GET /services/tags -GET /teams/:tid/services/whitelisted -POST /teams/:tid/services/whitelist \ No newline at end of file diff --git a/changelog.d/5-internal/WPB-664 b/changelog.d/5-internal/WPB-664 deleted file mode 100644 index 764b4019042..00000000000 --- a/changelog.d/5-internal/WPB-664 +++ /dev/null @@ -1 +0,0 @@ -Provider API has been migrated to servant diff --git a/changelog.d/5-internal/background-worker-nosync b/changelog.d/5-internal/background-worker-nosync deleted file mode 100644 index b9eda2712c5..00000000000 --- a/changelog.d/5-internal/background-worker-nosync +++ /dev/null @@ -1 +0,0 @@ -background-worker: Get list of domains from RabbitMQ instead of brig for pushing backend notifications \ No newline at end of file diff --git a/changelog.d/5-internal/dont-return-to-sender b/changelog.d/5-internal/dont-return-to-sender deleted file mode 100644 index 3e3df3c04db..00000000000 --- a/changelog.d/5-internal/dont-return-to-sender +++ /dev/null @@ -1 +0,0 @@ -Avoid including MLS application messages in the sender client's event stream. diff --git a/changelog.d/5-internal/empty-push b/changelog.d/5-internal/empty-push deleted file mode 100644 index f30fe164e8f..00000000000 --- a/changelog.d/5-internal/empty-push +++ /dev/null @@ -1 +0,0 @@ -Avoid empty pushes when chunking pushes in galley diff --git a/changelog.d/5-internal/galley-db-subconv b/changelog.d/5-internal/galley-db-subconv deleted file mode 100644 index d57f71df5a6..00000000000 --- a/changelog.d/5-internal/galley-db-subconv +++ /dev/null @@ -1 +0,0 @@ -Introduce a Galley DB table for subconversations diff --git a/changelog.d/5-internal/group-id-subconv b/changelog.d/5-internal/group-id-subconv deleted file mode 100644 index 2706db951bf..00000000000 --- a/changelog.d/5-internal/group-id-subconv +++ /dev/null @@ -1 +0,0 @@ -Support mapping MLS group IDs to subconversations diff --git a/changelog.d/5-internal/group-id-subconv-2 b/changelog.d/5-internal/group-id-subconv-2 deleted file mode 100644 index 75eb7947025..00000000000 --- a/changelog.d/5-internal/group-id-subconv-2 +++ /dev/null @@ -1 +0,0 @@ -change version and conversation type to 16 bit in group ID serialisation diff --git a/changelog.d/5-internal/key-package-mapping b/changelog.d/5-internal/key-package-mapping deleted file mode 100644 index e861208c19d..00000000000 --- a/changelog.d/5-internal/key-package-mapping +++ /dev/null @@ -1 +0,0 @@ -Brig does not perform key package ref mapping anymore. Claimed key packages are simply removed from the `mls_key_packages` table. The `mls_key_package_refs` table is now unused, and will be removed in the future. diff --git a/changelog.d/5-internal/mixed-protocol b/changelog.d/5-internal/mixed-protocol deleted file mode 100644 index 235a1f2ac41..00000000000 --- a/changelog.d/5-internal/mixed-protocol +++ /dev/null @@ -1 +0,0 @@ -Add intermediate "mixed" protocol for migrating from Proteus to MLS diff --git a/changelog.d/5-internal/mls-mixed b/changelog.d/5-internal/mls-mixed deleted file mode 100644 index 7e35c12b3b1..00000000000 --- a/changelog.d/5-internal/mls-mixed +++ /dev/null @@ -1,4 +0,0 @@ -- Do not perform client checks for add and remove proposals in mixed conversations -- Restrict protocol updates to team conversations -- Disallow MLS application messages in mixed conversations -- Send remove proposals when users leave mixed conversations diff --git a/changelog.d/5-internal/mls-save-migration-statistics b/changelog.d/5-internal/mls-save-migration-statistics deleted file mode 100644 index c418bae2040..00000000000 --- a/changelog.d/5-internal/mls-save-migration-statistics +++ /dev/null @@ -1,3 +0,0 @@ -New cron job to save data usable to watch the progress of the Proteus to MLS migration in S3 bucket. - -**IMPORTANT:** This cron job is _not_ meant for general use! It can leak data about one team to other teams. diff --git a/changelog.d/5-internal/mls-subconv-creation b/changelog.d/5-internal/mls-subconv-creation deleted file mode 100644 index f87217e5d42..00000000000 --- a/changelog.d/5-internal/mls-subconv-creation +++ /dev/null @@ -1 +0,0 @@ -Subconversations are now created on their first commit diff --git a/changelog.d/5-internal/mls-subconv-messages b/changelog.d/5-internal/mls-subconv-messages deleted file mode 100644 index ba9d2579d12..00000000000 --- a/changelog.d/5-internal/mls-subconv-messages +++ /dev/null @@ -1 +0,0 @@ -Propagate messages in MLS subconversations diff --git a/changelog.d/5-internal/mls-tests b/changelog.d/5-internal/mls-tests deleted file mode 100644 index 2320d5ebf67..00000000000 --- a/changelog.d/5-internal/mls-tests +++ /dev/null @@ -1 +0,0 @@ -Move some MLS tests to new integration suite diff --git a/changelog.d/5-internal/notification-500 b/changelog.d/5-internal/notification-500 deleted file mode 100644 index 7af7198c513..00000000000 --- a/changelog.d/5-internal/notification-500 +++ /dev/null @@ -1 +0,0 @@ -Check validity of notification IDs in the notification API diff --git a/changelog.d/5-internal/optimize-stern b/changelog.d/5-internal/optimize-stern deleted file mode 100644 index ad7c8c83a55..00000000000 --- a/changelog.d/5-internal/optimize-stern +++ /dev/null @@ -1 +0,0 @@ -stern: Optimize RAM usage of /i/users/meta-info \ No newline at end of file diff --git a/changelog.d/5-internal/pr-3538 b/changelog.d/5-internal/pr-3538 deleted file mode 100644 index 37868aea9ef..00000000000 --- a/changelog.d/5-internal/pr-3538 +++ /dev/null @@ -1 +0,0 @@ -Additional integration test for federated connections diff --git a/changelog.d/5-internal/pr-3540 b/changelog.d/5-internal/pr-3540 deleted file mode 100644 index f1c0ef4f559..00000000000 --- a/changelog.d/5-internal/pr-3540 +++ /dev/null @@ -1 +0,0 @@ -The bot API is now migrated to servant diff --git a/changelog.d/5-internal/pr-3572 b/changelog.d/5-internal/pr-3572 deleted file mode 100644 index 2b6825bd5e5..00000000000 --- a/changelog.d/5-internal/pr-3572 +++ /dev/null @@ -1 +0,0 @@ -`rusty-jwt-tools` is upgraded to version 0.5.0 diff --git a/changelog.d/5-internal/refactored-schema-version-tracking b/changelog.d/5-internal/refactored-schema-version-tracking deleted file mode 100644 index 16d655ef96a..00000000000 --- a/changelog.d/5-internal/refactored-schema-version-tracking +++ /dev/null @@ -1 +0,0 @@ -Refactored schema version tracking from manually managed to automatic. diff --git a/changelog.d/5-internal/shutdown-cleanup b/changelog.d/5-internal/shutdown-cleanup deleted file mode 100644 index 86579b0906b..00000000000 --- a/changelog.d/5-internal/shutdown-cleanup +++ /dev/null @@ -1 +0,0 @@ -Avoid unnecessary error logs on service shutdown diff --git a/changelog.d/5-internal/subconv-store b/changelog.d/5-internal/subconv-store deleted file mode 100644 index ef3798fdc6c..00000000000 --- a/changelog.d/5-internal/subconv-store +++ /dev/null @@ -1 +0,0 @@ -Introduce an effect for subconversations diff --git a/changelog.d/5-internal/subconv-update-path b/changelog.d/5-internal/subconv-update-path deleted file mode 100644 index 11737cb0bec..00000000000 --- a/changelog.d/5-internal/subconv-update-path +++ /dev/null @@ -1 +0,0 @@ -Via the update path update the key package of the committer in epoch 0 of a subconversation diff --git a/changelog.d/5-internal/test-joining-subconversation b/changelog.d/5-internal/test-joining-subconversation deleted file mode 100644 index 41a2a42111e..00000000000 --- a/changelog.d/5-internal/test-joining-subconversation +++ /dev/null @@ -1 +0,0 @@ -Add more tests for joining a subconversation diff --git a/changelog.d/5-internal/wpb-3888 b/changelog.d/5-internal/wpb-3888 deleted file mode 100644 index d18f4de6508..00000000000 --- a/changelog.d/5-internal/wpb-3888 +++ /dev/null @@ -1 +0,0 @@ -Added `/tools/db/repair-brig-clients-table` to clean up after the fix in #3504 \ No newline at end of file diff --git a/changelog.d/5-internal/wpb-3915 b/changelog.d/5-internal/wpb-3915 deleted file mode 100644 index fcaeeec676c..00000000000 --- a/changelog.d/5-internal/wpb-3915 +++ /dev/null @@ -1 +0,0 @@ -Distinguish between update and upsert cassandra commands (follow-up to #3504) (#3513) \ No newline at end of file diff --git a/changelog.d/5-internal/wpb-5033 b/changelog.d/5-internal/wpb-5033 deleted file mode 100644 index 4a16fe7e78f..00000000000 --- a/changelog.d/5-internal/wpb-5033 +++ /dev/null @@ -1,4 +0,0 @@ -Truncate `galley.mls_group_member_client` table and drop `galley.member_client` table. - -The data in `mls_group_member_client` could contain nulls from client testing in prod. So, its OK to truncate it. -The `member_client` table is unused. \ No newline at end of file diff --git a/changelog.d/5-internal/xml-reports b/changelog.d/5-internal/xml-reports deleted file mode 100644 index 6e1a44781e6..00000000000 --- a/changelog.d/5-internal/xml-reports +++ /dev/null @@ -1,11 +0,0 @@ -All integration tests can generate XML reports. - -To generate the report in brig-integration, galley-integration, -cargohold-integration, gundeck-integration, stern-integration and the new -integration suite pass `--xml=` to generate the XML file. - -For spar-integration and federator-integration pass `-f junit` and set -`JUNIT_OUTPUT_DIRECTORY` and `JUNIT_SUITE_NAME` environment variables. The XML -report will be generated at `$JUNIT_OUTPUT_DIRECTORY/junit.xml`. - -(#3568, #3633) diff --git a/changelog.d/6-federation/FS-1868 b/changelog.d/6-federation/FS-1868 deleted file mode 100644 index 208bebcf0c3..00000000000 --- a/changelog.d/6-federation/FS-1868 +++ /dev/null @@ -1 +0,0 @@ -Add subconversation ID to onMLSMessageSent request payload. diff --git a/changelog.d/6-federation/FS-1974 b/changelog.d/6-federation/FS-1974 deleted file mode 100644 index f3dd1419254..00000000000 --- a/changelog.d/6-federation/FS-1974 +++ /dev/null @@ -1,10 +0,0 @@ -Derive group ID from qualified conversation ID and, if applicable, -subconversation ID. - -Retire mapping from group IDs to conversation IDs. (group_id_conv_id) - -Remove federation endpoints -- on-new-remote-conversation, -- on-new-remote-subconversation, and -- on-delete-mls-conversation -which were used to synchronise the group to conversation mapping. diff --git a/changelog.d/6-federation/WPB-4928-notification-endpoints b/changelog.d/6-federation/WPB-4928-notification-endpoints deleted file mode 100644 index b900bd95735..00000000000 --- a/changelog.d/6-federation/WPB-4928-notification-endpoints +++ /dev/null @@ -1 +0,0 @@ -Reorganise the federation API such that queueing notification endpoints are separate from synchronous endpoints. Also simplify queueing federation notification endpoints. diff --git a/changelog.d/6-federation/delete-remote-subconversation b/changelog.d/6-federation/delete-remote-subconversation deleted file mode 100644 index 816d9435eb1..00000000000 --- a/changelog.d/6-federation/delete-remote-subconversation +++ /dev/null @@ -1 +0,0 @@ -Introduce an endpoint for resetting a remote subconversation diff --git a/changelog.d/6-federation/on-new-remote-subconversation b/changelog.d/6-federation/on-new-remote-subconversation deleted file mode 100644 index edb9c811d3d..00000000000 --- a/changelog.d/6-federation/on-new-remote-subconversation +++ /dev/null @@ -1,4 +0,0 @@ -Split federation endpoint into on-new-remote-conversation and on-new-remote-subconversation -Call on-new-remote-subconversation when a new subconversation is created -Call on-new-remote-subconversation for all existing subconversations when a new backend gets involved -Call on-new-remote-subconversation when a subconversation is reset diff --git a/changelog.d/6-federation/tcp-timeout b/changelog.d/6-federation/tcp-timeout deleted file mode 100644 index db622b73022..00000000000 --- a/changelog.d/6-federation/tcp-timeout +++ /dev/null @@ -1,3 +0,0 @@ -federator: Allow setting TCP connection timeout for HTTP2 requests - -The helm chart defaults it to 5s which should be best for most installations. \ No newline at end of file diff --git a/changelog.d/6-federation/wpb-3867-queue-endpoints b/changelog.d/6-federation/wpb-3867-queue-endpoints deleted file mode 100644 index b3f9efb6b7d..00000000000 --- a/changelog.d/6-federation/wpb-3867-queue-endpoints +++ /dev/null @@ -1 +0,0 @@ -Constrain which federation endpoints can be used via the queueing federation client diff --git a/changelog.d/6-federation/wpb-3867-unreachable-users b/changelog.d/6-federation/wpb-3867-unreachable-users deleted file mode 100644 index 19dde73310e..00000000000 --- a/changelog.d/6-federation/wpb-3867-unreachable-users +++ /dev/null @@ -1 +0,0 @@ -There is a breaking change in the "on-mls-message-sent" federation endpoint due to queueing. Now that there is retrying because of queueing, the endpoint can no longer respond with a list of unreachable users. diff --git a/changelog.d/6-federation/wpb-4984-queueing b/changelog.d/6-federation/wpb-4984-queueing deleted file mode 100644 index c258c8eabda..00000000000 --- a/changelog.d/6-federation/wpb-4984-queueing +++ /dev/null @@ -1 +0,0 @@ -Remote MLS messages get queued via RabbitMQ