Skip to content

Commit

Permalink
Add GET endpoint for subconversations (#2869)
Browse files Browse the repository at this point in the history
* Add tge GET subconverstion endpoint
* Add golden tests for subconversations
* Test injectivity of `initialGroupId`

Co-authored-by: Marko Dimjašević <marko.dimjasevic@wire.com>
Co-authored-by: Stefan Matting <smatting@users.noreply.github.com>
  • Loading branch information
smatting and mdimjasevic authored Dec 14, 2022
1 parent 64b8d60 commit 72baa93
Show file tree
Hide file tree
Showing 40 changed files with 1,050 additions and 308 deletions.
27 changes: 26 additions & 1 deletion cassandra-schema.cql
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions changelog.d/1-api-changes/get-subconversation
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Introduce a subconversation GET endpoint
1 change: 1 addition & 0 deletions changelog.d/5-internal/galley-db-subconv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Introduce a Galley DB table for subconversations
1 change: 1 addition & 0 deletions changelog.d/5-internal/group-id-subconv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support mapping MLS group IDs to subconversations
1 change: 1 addition & 0 deletions changelog.d/5-internal/subconv-store
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Introduce an effect for subconversations
4 changes: 4 additions & 0 deletions libs/wire-api/src/Wire/API/Error/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ data GalleyError
| MLSWelcomeMismatch
| MLSMissingGroupInfo
| MLSMissingSenderClient
| MLSUnexpectedSenderClient
| MLSSubConvUnsupportedConvType
| --
NoBindingTeamMembers
| NoBindingTeam
Expand Down Expand Up @@ -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"
Expand Down
45 changes: 44 additions & 1 deletion libs/wire-api/src/Wire/API/MLS/SubConversation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
21 changes: 21 additions & 0 deletions libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
]
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2022 Wire Swiss GmbH <opensource@wire.com>
--
-- 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 <https://www.gnu.org/licenses/>.

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"
11 changes: 11 additions & 0 deletions libs/wire-api/test/golden/testObject_PublicSubConversation_1.json
Original file line number Diff line number Diff line change
@@ -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"
}
17 changes: 17 additions & 0 deletions libs/wire-api/test/golden/testObject_PublicSubConversation_2.json
Original file line number Diff line number Diff line change
@@ -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"
}
4 changes: 3 additions & 1 deletion libs/wire-api/test/unit/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -59,5 +60,6 @@ main =
Roundtrip.CSV.tests,
Routes.tests,
Conversation.tests,
MLS.tests
MLS.tests,
SubConversation.tests
]
46 changes: 46 additions & 0 deletions libs/wire-api/test/unit/Test/Wire/API/MLS/SubConversation.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2022 Wire Swiss GmbH <opensource@wire.com>
--
-- 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 <https://www.gnu.org/licenses/>.

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
2 changes: 2 additions & 0 deletions libs/wire-api/wire-api.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions services/galley/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
, HsOpenSSL
, HsOpenSSL-x509-system
, hspec
, http-api-data
, http-client
, http-client-openssl
, http-client-tls
Expand Down Expand Up @@ -276,6 +277,7 @@ mkDerivation {
HsOpenSSL
HsOpenSSL-x509-system
hspec
http-api-data
http-client
http-client-openssl
http-client-tls
Expand Down
Loading

0 comments on commit 72baa93

Please sign in to comment.