diff --git a/changelog.d/1-api-changes/mls-ciphersuite b/changelog.d/1-api-changes/mls-ciphersuite new file mode 100644 index 00000000000..4d24c2f0060 --- /dev/null +++ b/changelog.d/1-api-changes/mls-ciphersuite @@ -0,0 +1 @@ +The `cipher_suite` field is not present anymore in objects corresponding to newly created conversations diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index eeb69bae965..2a59f980579 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -202,11 +202,9 @@ createNewGroup 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 - convId <- conv %. "qualified_id" createGroup cid conv - pure (groupId, convId) + pure (groupId, conv) createGroup :: (MakesValue conv) => ClientIdentity -> conv -> App () createGroup cid conv = do diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index db831848077..aba592e4ecf 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -12,6 +12,7 @@ import qualified Data.Text.Encoding as T import MLS.Util import Notifications import SetupHelpers +import Test.Version import Testlib.Prelude testSendMessageNoReturnToSender :: HasCallStack => App () @@ -331,7 +332,14 @@ testAddUserSimple suite ctype = do [alice1, bob2] <- traverse (createMLSClient def {credType = ctype}) [alice, bob] traverse_ uploadNewKeyPackage [bob2] - (_, qcnv) <- createNewGroup alice1 + qcnv <- withWebSocket alice $ \ws -> do + (_, qcnv) <- createNewGroup alice1 + -- check that the conversation inside the ConvCreated event contains + -- epoch and ciphersuite, regardless of the API version + n <- awaitMatch isConvCreateNotif ws + n %. "payload.0.data.epoch" `shouldMatchInt` 0 + n %. "payload.0.data.cipher_suite" `shouldMatchInt` 1 + pure qcnv resp <- createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle events <- resp %. "events" & asList @@ -412,12 +420,17 @@ testCreateSubConvProteus = do bindResponse (getSubConversation alice conv "conference") $ \resp -> resp.status `shouldMatchInt` 404 -testSelfConversation :: App () -testSelfConversation = do +testSelfConversation :: Version5 -> App () +testSelfConversation v = withVersion5 v $ do alice <- randomUser OwnDomain def creator : others <- traverse (createMLSClient def) (replicate 3 alice) traverse_ uploadNewKeyPackage others - void $ createSelfGroup creator + (_, conv) <- createSelfGroup creator + conv %. "epoch" `shouldMatchInt` 0 + case v of + Version5 -> conv %. "cipher_suite" `shouldMatchInt` 1 + NoVersion5 -> assertFieldMissing conv "cipher_suite" + void $ createAddCommit creator [alice] >>= sendAndConsumeCommitBundle newClient <- createMLSClient def alice diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index 8c6ce11355d..2598c10ad5b 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -25,18 +25,26 @@ import qualified Data.Set as Set import MLS.Util import Notifications import SetupHelpers +import Test.Version import Testlib.Prelude -testGetMLSOne2One :: HasCallStack => Domain -> App () -testGetMLSOne2One otherDomain = do +testGetMLSOne2One :: HasCallStack => Version5 -> Domain -> App () +testGetMLSOne2One v otherDomain = withVersion5 v $ do [alice, bob] <- createAndConnectUsers [OwnDomain, otherDomain] + let assertConvData conv = do + conv %. "epoch" `shouldMatchInt` 0 + case v of + Version5 -> conv %. "cipher_suite" `shouldMatchInt` 1 + NoVersion5 -> assertFieldMissing conv "cipher_suite" + conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 conv %. "type" `shouldMatchInt` 2 shouldBeEmpty (conv %. "members.others") conv %. "members.self.conversation_role" `shouldMatch` "wire_member" conv %. "members.self.qualified_id" `shouldMatch` (alice %. "qualified_id") + assertConvData conv convId <- conv %. "qualified_id" @@ -47,7 +55,7 @@ testGetMLSOne2One otherDomain = do conv2 %. "type" `shouldMatchInt` 2 conv2 %. "qualified_id" `shouldMatch` convId - conv2 %. "epoch" `shouldMatch` (conv %. "epoch") + assertConvData conv2 testMLSOne2OneOtherMember :: HasCallStack => One2OneScenario -> App () testMLSOne2OneOtherMember scenario = do @@ -220,8 +228,9 @@ one2OneScenarioConvDomain One2OneScenarioLocal = OwnDomain one2OneScenarioConvDomain One2OneScenarioLocalConv = OwnDomain one2OneScenarioConvDomain One2OneScenarioRemoteConv = OtherDomain -testMLSOne2One :: HasCallStack => One2OneScenario -> App () -testMLSOne2One scenario = do +testMLSOne2One :: HasCallStack => Ciphersuite -> One2OneScenario -> App () +testMLSOne2One suite scenario = do + setMLSCiphersuite suite alice <- randomUser OwnDomain def let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario diff --git a/integration/test/Test/Version.hs b/integration/test/Test/Version.hs index 31295918468..40c4dfeb14d 100644 --- a/integration/test/Test/Version.hs +++ b/integration/test/Test/Version.hs @@ -17,6 +17,19 @@ instance TestCases Versioned' where MkTestCase "[version=v6]" (Versioned' (ExplicitVersion 6)) ] +-- | Used to test endpoints that have changed after version 5 +data Version5 = Version5 | NoVersion5 + +instance TestCases Version5 where + testCases = + [ MkTestCase "[version=versioned]" NoVersion5, + MkTestCase "[version=v5]" Version5 + ] + +withVersion5 :: Version5 -> App a -> App a +withVersion5 Version5 = withAPIVersion 5 +withVersion5 NoVersion5 = id + testVersion :: Versioned' -> App () testVersion (Versioned' v) = withModifiedBackend def {brigCfg = setField "optSettings.setDisabledAPIVersions" ([] :: [String])} diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index 5faa2b97f05..62eda62cba2 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -178,6 +178,13 @@ fieldEquals a fieldSelector b = do Just f -> f `isEqual` b +assertFieldMissing :: (HasCallStack, MakesValue a) => a -> String -> App () +assertFieldMissing x k = do + mValue <- lookupField x k + case mValue of + Nothing -> pure () + Just _ -> assertFailureWithJSON x $ "Field \"" <> k <> "\" should be missing from object:" + 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/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 432ea013b98..61a98694401 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 @@ -86,5 +86,16 @@ testObject_ConversationCreated2 = nonCreatorMembers = Set.fromList [], messageTimer = Nothing, receiptMode = Nothing, - protocol = ProtocolMLS (ConversationMLSData (GroupId "group") (Epoch 3) (Just (UTCTime (fromGregorian 2020 8 29) 0)) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) + protocol = + ProtocolMLS + ( ConversationMLSData + (GroupId "group") + ( Just + ( ActiveMLSConversationData + (Epoch 3) + (UTCTime (fromGregorian 2020 8 29) 0) + MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) + ) + ) } diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 120ffe6a921..1165445df2f 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -104,11 +104,11 @@ import Data.Range (Range, fromRange, rangedSchema) import Data.SOP import Data.Schema import Data.Set qualified as Set +import Data.Singletons import Data.Text qualified as Text import Data.UUID qualified as UUID import Data.UUID.V5 qualified as UUIDV5 import Imports -import Servant.API import System.Random (randomRIO) import Wire.API.Conversation.Member import Wire.API.Conversation.Protocol @@ -155,9 +155,9 @@ defConversationMetadata mCreator = cnvmReceiptMode = Nothing } -accessRolesVersionedSchema :: Version -> ObjectSchema SwaggerDoc (Set AccessRole) -accessRolesVersionedSchema v = - if v > V2 then accessRolesSchema else accessRolesSchemaV2 +accessRolesVersionedSchema :: Maybe Version -> ObjectSchema SwaggerDoc (Set AccessRole) +accessRolesVersionedSchema (Just v) | v < V3 = accessRolesSchemaV2 +accessRolesVersionedSchema _ = accessRolesSchema accessRolesSchema :: ObjectSchema SwaggerDoc (Set AccessRole) accessRolesSchema = field "access_role" (set schema) @@ -266,12 +266,12 @@ cnvReceiptMode :: Conversation -> Maybe ReceiptMode cnvReceiptMode = cnvmReceiptMode . cnvMetadata instance ToSchema Conversation where - schema = conversationSchema V3 + schema = conversationSchema Nothing -instance ToSchema (Versioned 'V2 Conversation) where - schema = Versioned <$> unVersioned .= conversationSchema V2 +instance SingI v => ToSchema (Versioned v Conversation) where + schema = Versioned <$> unVersioned .= conversationSchema (Just (demote @v)) -conversationObjectSchema :: Version -> ObjectSchema SwaggerDoc Conversation +conversationObjectSchema :: Maybe Version -> ObjectSchema SwaggerDoc Conversation conversationObjectSchema v = Conversation <$> cnvQualifiedId .= field "qualified_id" schema @@ -279,14 +279,14 @@ conversationObjectSchema v = .= optional (field "id" (deprecatedSchema "qualified_id" schema)) <*> cnvMetadata .= conversationMetadataObjectSchema (accessRolesVersionedSchema v) <*> cnvMembers .= field "members" schema - <*> cnvProtocol .= protocolSchema + <*> cnvProtocol .= protocolSchema v conversationSchema :: - Version -> + Maybe Version -> ValueSchema NamedSwaggerDoc Conversation conversationSchema v = objectWithDocModifier - "Conversation" + ("Conversation" <> foldMap (Text.toUpper . versionText) v) (description ?~ "A conversation object as returned from the server") (conversationObjectSchema v) @@ -303,20 +303,26 @@ data CreateGroupConversation = CreateGroupConversation deriving (ToJSON, FromJSON, S.ToSchema) via Schema CreateGroupConversation instance ToSchema CreateGroupConversation where - schema = - objectWithDocModifier - "CreateGroupConversation" - (description ?~ "A created group-conversation object extended with a list of failed-to-add users") - $ CreateGroupConversation - <$> cgcConversation .= conversationObjectSchema V4 - <*> (toFlatList . cgcFailedToAdd) - .= field "failed_to_add" (fromFlatList <$> array schema) - where - toFlatList :: Map Domain (Set a) -> [Qualified a] - toFlatList m = - (\(d, s) -> flip Qualified d <$> Set.toList s) =<< Map.assocs m - fromFlatList :: Ord a => [Qualified a] -> Map Domain (Set a) - fromFlatList = fmap Set.fromList . indexQualified + schema = createGroupConversationSchema Nothing + +instance SingI v => ToSchema (Versioned v CreateGroupConversation) where + schema = Versioned <$> unVersioned .= createGroupConversationSchema (Just (demote @v)) + +createGroupConversationSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc CreateGroupConversation +createGroupConversationSchema v = + objectWithDocModifier + "CreateGroupConversation" + (description ?~ "A created group-conversation object extended with a list of failed-to-add users") + $ CreateGroupConversation + <$> cgcConversation .= conversationObjectSchema v + <*> (toFlatList . cgcFailedToAdd) + .= field "failed_to_add" (fromFlatList <$> array schema) + where + toFlatList :: Map Domain (Set a) -> [Qualified a] + toFlatList m = + (\(d, s) -> flip Qualified d <$> Set.toList s) =<< Map.assocs m + fromFlatList :: Ord a => [Qualified a] -> Map Domain (Set a) + fromFlatList = fmap Set.fromList . indexQualified -- | Limited view of a 'Conversation'. Is used to inform users with an invite -- link about the conversation. @@ -365,7 +371,7 @@ instance ToSchema (Versioned 'V2 (ConversationList Conversation)) where schema = Versioned <$> unVersioned - .= conversationListSchema (conversationSchema V2) + .= conversationListSchema (conversationSchema (Just V2)) conversationListSchema :: forall a. @@ -427,13 +433,13 @@ data ConversationsResponse = ConversationsResponse deriving (FromJSON, ToJSON, S.ToSchema) via Schema ConversationsResponse conversationsResponseSchema :: - Version -> + Maybe Version -> ValueSchema NamedSwaggerDoc ConversationsResponse conversationsResponseSchema v = let notFoundDoc = description ?~ "These conversations either don't exist or are deleted." failedDoc = description ?~ "The server failed to fetch these conversations, most likely due to network issues while contacting a remote server" in objectWithDocModifier - "ConversationsResponse" + ("ConversationsResponse" <> foldMap (Text.toUpper . versionText) v) (description ?~ "Response object for getting metadata of a list of conversations") $ ConversationsResponse <$> crFound .= field "found" (array (conversationSchema v)) @@ -441,10 +447,10 @@ conversationsResponseSchema v = <*> crFailed .= fieldWithDocModifier "failed" failedDoc (array schema) instance ToSchema ConversationsResponse where - schema = conversationsResponseSchema V3 + schema = conversationsResponseSchema Nothing -instance ToSchema (Versioned 'V2 ConversationsResponse) where - schema = Versioned <$> unVersioned .= conversationsResponseSchema V2 +instance SingI v => ToSchema (Versioned v ConversationsResponse) where + schema = Versioned <$> unVersioned .= conversationsResponseSchema (Just (demote @v)) -------------------------------------------------------------------------------- -- Conversation properties @@ -659,18 +665,19 @@ data NewConv = NewConv instance ToSchema NewConv where schema = - newConvSchema $ + newConvSchema Nothing $ maybe_ (optField "access_role" (set schema)) instance ToSchema (Versioned 'V2 NewConv) where - schema = Versioned <$> unVersioned .= newConvSchema accessRolesSchemaOptV2 + schema = Versioned <$> unVersioned .= newConvSchema (Just V2) accessRolesSchemaOptV2 newConvSchema :: + Maybe Version -> ObjectSchema SwaggerDoc (Maybe (Set AccessRole)) -> ValueSchema NamedSwaggerDoc NewConv -newConvSchema sch = +newConvSchema v sch = objectWithDocModifier - "NewConv" + ("NewConv" <> foldMap (Text.toUpper . versionText) v) (description ?~ "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'") $ NewConv <$> newConvUsers @@ -831,22 +838,18 @@ data ConversationAccessData = ConversationAccessData deriving (Arbitrary) via (GenericUniform ConversationAccessData) deriving (FromJSON, ToJSON, S.ToSchema) via Schema ConversationAccessData -conversationAccessDataSchema :: Version -> ValueSchema NamedSwaggerDoc ConversationAccessData +conversationAccessDataSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc ConversationAccessData conversationAccessDataSchema v = - object ("ConversationAccessData" <> suffix) $ + object ("ConversationAccessData" <> foldMap (Text.toUpper . versionText) v) $ ConversationAccessData <$> cupAccess .= field "access" (set schema) <*> cupAccessRoles .= accessRolesVersionedSchema v - where - suffix - | v == maxBound = "" - | otherwise = toUrlPiece v instance ToSchema ConversationAccessData where - schema = conversationAccessDataSchema V3 + schema = conversationAccessDataSchema Nothing instance ToSchema (Versioned 'V2 ConversationAccessData) where - schema = Versioned <$> unVersioned .= conversationAccessDataSchema V2 + schema = Versioned <$> unVersioned .= conversationAccessDataSchema (Just V2) data ConversationReceiptModeUpdate = ConversationReceiptModeUpdate { cruReceiptMode :: ReceiptMode diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 321c106b6de..c0060347b7b 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -31,22 +31,29 @@ module Wire.API.Conversation.Protocol conversationMLSData, protocolSchema, ConversationMLSData (..), + ActiveMLSConversationData (..), + optionalActiveMLSConversationDataSchema, + cnvmlsEpoch, ProtocolUpdate (..), ) where +import Control.Applicative import Control.Arrow import Control.Lens (Traversal', makePrisms, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.Json.Util import Data.OpenApi qualified as S import Data.Schema import Data.Time.Clock import Imports +import Test.QuickCheck 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.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.Arbitrary data ProtocolTag = ProtocolProteusTag | ProtocolMLSTag | ProtocolMixedTag @@ -58,44 +65,132 @@ instance S.ToSchema ProtocolTag data ConversationMLSData = ConversationMLSData { -- | The MLS group ID associated to the conversation. 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 + -- | Information available once the conversation is active (epoch > 0). + cnvmlsActiveData :: Maybe ActiveMLSConversationData } deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via GenericUniform ConversationMLSData deriving (ToJSON, FromJSON) via Schema ConversationMLSData -mlsDataSchema :: ObjectSchema SwaggerDoc ConversationMLSData -mlsDataSchema = +arbitraryActiveData :: Gen (Maybe ActiveMLSConversationData) +arbitraryActiveData = do + epoch <- arbitrary + if epoch == Epoch 0 + then pure Nothing + else + fmap Just $ + ActiveMLSConversationData epoch <$> arbitrary <*> arbitrary + +instance Arbitrary ConversationMLSData where + arbitrary = ConversationMLSData <$> arbitrary <*> arbitraryActiveData + +cnvmlsEpoch :: ConversationMLSData -> Epoch +cnvmlsEpoch = maybe (Epoch 0) (.epoch) . cnvmlsActiveData + +mlsDataSchema :: Maybe Version -> ObjectSchema SwaggerDoc ConversationMLSData +mlsDataSchema v = ConversationMLSData <$> cnvmlsGroupId .= fieldWithDocModifier "group_id" (description ?~ "A base64-encoded MLS group ID") schema - <*> cnvmlsEpoch + <*> cnvmlsActiveData .= optionalActiveMLSConversationDataSchema v + +optionalActiveMLSConversationDataSchema :: + Maybe Version -> + ObjectSchema SwaggerDoc (Maybe ActiveMLSConversationData) +optionalActiveMLSConversationDataSchema (Just v) + | v < V6 = + -- legacy serialisation + mk + <$> maybe (Epoch 0) (.epoch) + .= fieldWithDocModifier + "epoch" + (description ?~ "The epoch number of the corresponding MLS group") + schema + <*> fmap (.epochTimestamp) + .= maybe_ + ( optFieldWithDocModifier + "epoch_timestamp" + (description ?~ "The timestamp of the epoch number") + utcTimeSchema + ) + <*> maybe MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 (.ciphersuite) + .= fieldWithDocModifier + "cipher_suite" + (description ?~ "The cipher suite of the corresponding MLS group") + schema + where + mk :: Epoch -> Maybe UTCTime -> CipherSuiteTag -> Maybe ActiveMLSConversationData + mk (Epoch 0) _ _ = Nothing + mk epoch ts cs = ActiveMLSConversationData epoch <$> ts <*> pure cs +optionalActiveMLSConversationDataSchema _ = + mk + <$> maybe (Epoch 0) (.epoch) + .= fieldWithDocModifier + "epoch" + (description ?~ "The epoch number of the corresponding MLS group") + schema + <*> fmap (.epochTimestamp) + .= maybe_ + ( optFieldWithDocModifier + "epoch_timestamp" + (description ?~ "The timestamp of the epoch number") + utcTimeSchema + ) + <*> fmap (.ciphersuite) + .= maybe_ + ( optFieldWithDocModifier + "cipher_suite" + (description ?~ "The cipher suite of the corresponding MLS group") + schema + ) + where + mk :: Epoch -> Maybe UTCTime -> Maybe CipherSuiteTag -> Maybe ActiveMLSConversationData + mk (Epoch 0) _ _ = Nothing + mk epoch ts cs = ActiveMLSConversationData epoch <$> ts <*> cs + +instance ToSchema ConversationMLSData where + schema = object "ConversationMLSData" (mlsDataSchema Nothing) + +instance ToSchema (Versioned 'V5 ConversationMLSData) where + schema = Versioned <$> object "ConversationMLSDataV5" (unVersioned .= mlsDataSchema (Just V5)) + +-- TODO: Fix API compatibility +data ActiveMLSConversationData = ActiveMLSConversationData + { -- | The current epoch number of the corresponding MLS group. + epoch :: Epoch, + -- | The time stamp of the epoch. + epochTimestamp :: UTCTime, + -- | The cipher suite to be used in the MLS group. + ciphersuite :: CipherSuiteTag + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via GenericUniform ActiveMLSConversationData + deriving (ToJSON, FromJSON) via Schema ActiveMLSConversationData + +instance ToSchema ActiveMLSConversationData where + schema = object "ActiveMLSConversationData" activeMLSConversationDataSchema + +activeMLSConversationDataSchema :: ObjectSchema SwaggerDoc ActiveMLSConversationData +activeMLSConversationDataSchema = + ActiveMLSConversationData + <$> (.epoch) .= fieldWithDocModifier "epoch" (description ?~ "The epoch number of the corresponding MLS group") schema - <*> cnvmlsEpochTimestamp + <*> (.epochTimestamp) .= fieldWithDocModifier "epoch_timestamp" (description ?~ "The timestamp of the epoch number") - schemaEpochTimestamp - <*> cnvmlsCipherSuite + utcTimeSchema + <*> (.ciphersuite) .= 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 = ProtocolProteus @@ -145,16 +240,16 @@ deriving via (Schema ProtocolTag) instance ToJSON ProtocolTag protocolTagSchema :: ObjectSchema SwaggerDoc ProtocolTag protocolTagSchema = fmap (fromMaybe ProtocolProteusTag) (optField "protocol" schema) -protocolSchema :: ObjectSchema SwaggerDoc Protocol -protocolSchema = +protocolSchema :: Maybe Version -> ObjectSchema SwaggerDoc Protocol +protocolSchema v = snd <$> (protocolTag &&& id) .= bind (fst .= protocolTagSchema) - (snd .= dispatch protocolDataSchema) + (snd .= dispatch (protocolDataSchema v)) instance ToSchema Protocol where - schema = object "Protocol" protocolSchema + schema = object "Protocol" (protocolSchema Nothing) deriving via (Schema Protocol) instance FromJSON Protocol @@ -162,10 +257,10 @@ deriving via (Schema Protocol) instance ToJSON Protocol deriving via (Schema Protocol) instance S.ToSchema Protocol -protocolDataSchema :: ProtocolTag -> ObjectSchema SwaggerDoc Protocol -protocolDataSchema ProtocolProteusTag = tag _ProtocolProteus (pure ()) -protocolDataSchema ProtocolMLSTag = tag _ProtocolMLS mlsDataSchema -protocolDataSchema ProtocolMixedTag = tag _ProtocolMixed mlsDataSchema +protocolDataSchema :: Maybe Version -> ProtocolTag -> ObjectSchema SwaggerDoc Protocol +protocolDataSchema _ ProtocolProteusTag = tag _ProtocolProteus (pure ()) +protocolDataSchema v ProtocolMLSTag = tag _ProtocolMLS (mlsDataSchema v) +protocolDataSchema v ProtocolMixedTag = tag _ProtocolMixed (mlsDataSchema v) newtype ProtocolUpdate = ProtocolUpdate {unProtocolUpdate :: ProtocolTag} deriving (Show, Eq, Generic) diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index 7b3dcca7b63..f06e8d62973 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -393,10 +393,10 @@ taggedEventDataSchema = ConvAccessUpdate -> tag _EdConvAccessUpdate - (unnamed (conversationAccessDataSchema V2)) + (unnamed (conversationAccessDataSchema (Just V2))) ConvCodeUpdate -> tag _EdConvCodeUpdate (unnamed schema) ConvConnect -> tag _EdConnect (unnamed schema) - ConvCreate -> tag _EdConversation (unnamed (conversationSchema V2)) + ConvCreate -> tag _EdConversation (unnamed (conversationSchema (Just V2))) ConvMessageTimerUpdate -> tag _EdConvMessageTimerUpdate (unnamed schema) ConvReceiptModeUpdate -> tag _EdConvReceiptModeUpdate (unnamed schema) OtrMessageAdd -> tag _EdOtrMessage (unnamed schema) diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index 043984800ac..ca8a5552f0f 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -21,6 +21,7 @@ module Wire.API.MLS.SubConversation where +import Control.Applicative import Control.Lens (makePrisms, (?~)) import Control.Lens.Tuple (_1) import Control.Monad.Except @@ -28,20 +29,19 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) 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.Qualified import Data.Schema hiding (HasField) import Data.Text qualified as T -import Data.Time.Clock import GHC.Records import Imports import Servant (FromHttpApiData (..), ToHttpApiData (toQueryParam)) import Test.QuickCheck -import Wire.API.MLS.CipherSuite +import Wire.API.Conversation.Protocol import Wire.API.MLS.Credential -import Wire.API.MLS.Epoch import Wire.API.MLS.Group +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.Arbitrary -- | An MLS subconversation ID, which identifies a subconversation within a @@ -75,33 +75,29 @@ data PublicSubConversation = PublicSubConversation { pscParentConvId :: Qualified ConvId, 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, + pscActiveData :: Maybe ActiveMLSConversationData, pscMembers :: [ClientIdentity] } deriving (Eq, Show) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema PublicSubConversation) +publicSubConversationSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc PublicSubConversation +publicSubConversationSchema v = + objectWithDocModifier + ("PublicSubConversation" <> foldMap (T.toUpper . versionText) v) + (description ?~ "An MLS subconversation") + $ PublicSubConversation + <$> pscParentConvId .= field "parent_qualified_id" schema + <*> pscSubConvId .= field "subconv_id" schema + <*> pscGroupId .= field "group_id" schema + <*> pscActiveData .= optionalActiveMLSConversationDataSchema v + <*> pscMembers .= field "members" (array schema) + instance ToSchema PublicSubConversation where - schema = - objectWithDocModifier - "PublicSubConversation" - (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 + schema = publicSubConversationSchema Nothing + +instance ToSchema (Versioned 'V5 PublicSubConversation) where + schema = Versioned <$> unVersioned .= publicSubConversationSchema (Just V5) data ConvOrSubTag = ConvTag | SubConvTag deriving (Eq, Enum, Bounded) 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 d07f48258dc..c68dc51a76b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -43,6 +43,7 @@ import Wire.API.Routes.Public import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Galley.Feature import Wire.API.Routes.QualifiedCapture +import Wire.API.Routes.Version import Wire.API.Team import Wire.API.Team.Feature import Wire.API.Team.Member @@ -226,7 +227,7 @@ type InternalAPIBase = :> "conversations" :> "connect" :> ReqBody '[Servant.JSON] Connect - :> ConversationVerb + :> ConversationVerb 'V6 Conversation ) -- This endpoint is meant for testing membership of a conversation :<|> Named 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 08da1c6adee..064dd35f673 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 @@ -48,9 +48,7 @@ import Wire.API.Routes.Version import Wire.API.Routes.Versioned import Wire.API.Team.Feature -type ConversationResponse = ResponseForExistedCreated Conversation - --- | A type similar to 'ConversationResponse' introduced to allow for a failure +-- | A type similar to 'ResponseForExistedCreated' introduced to allow for a failure -- to add remote members while creating a conversation or due to involved -- backends forming an incomplete graph. data CreateGroupConversationResponse @@ -72,50 +70,26 @@ instance type ConversationHeaders = '[DescHeader "Location" "Conversation ID" ConvId] -type ConversationVerb = - MultiVerb - 'POST - '[JSON] - '[ WithHeaders - ConversationHeaders - Conversation - (Respond 200 "Conversation existed" Conversation), - WithHeaders - ConversationHeaders - Conversation - (Respond 201 "Conversation created" Conversation) - ] - ConversationResponse +type family ConversationResponse r -type CreateGroupConversationVerb = - MultiVerb - 'POST - '[JSON] - '[ WithHeaders - ConversationHeaders - Conversation - (Respond 200 "Conversation existed" Conversation), - WithHeaders - ConversationHeaders - CreateGroupConversation - (Respond 201 "Conversation created" CreateGroupConversation) - ] - CreateGroupConversationResponse +type instance ConversationResponse Conversation = ResponseForExistedCreated Conversation + +type instance ConversationResponse CreateGroupConversation = CreateGroupConversationResponse -type ConversationV2Verb = +type ConversationVerb v r = MultiVerb 'POST '[JSON] '[ WithHeaders ConversationHeaders Conversation - (VersionedRespond 'V2 200 "Conversation existed" Conversation), + (VersionedRespond v 200 "Conversation existed" Conversation), WithHeaders ConversationHeaders - Conversation - (VersionedRespond 'V2 201 "Conversation created" Conversation) + r + (VersionedRespond v 201 "Conversation created" r) ] - ConversationResponse + (ConversationResponse r) type CreateConversationCodeVerb = MultiVerb @@ -177,9 +151,22 @@ type ConversationAPI = :> MultiVerb1 'GET '[JSON] (VersionedRespond 'V2 200 "Conversation" Conversation) ) :<|> Named - "get-conversation" + "get-conversation@v5" ( Summary "Get a conversation by ID" :> From 'V3 + :> Until 'V6 + :> MakesFederatedCall 'Galley "get-conversations" + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvAccessDenied + :> ZLocalUser + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> MultiVerb1 'GET '[JSON] (VersionedRespond 'V5 200 "Conversation" Conversation) + ) + :<|> Named + "get-conversation" + ( Summary "Get a conversation by ID" + :> From 'V6 :> MakesFederatedCall 'Galley "get-conversations" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied @@ -340,10 +327,30 @@ type ConversationAPI = ) ) :<|> Named - "list-conversations" + "list-conversations@v5" ( Summary "Get conversation metadata for a list of conversation ids" :> MakesFederatedCall 'Galley "get-conversations" :> From 'V3 + :> Until 'V6 + :> ZLocalUser + :> "conversations" + :> "list" + :> ReqBody '[JSON] ListConversations + :> MultiVerb1 + 'POST + '[JSON] + ( VersionedRespond + 'V5 + 200 + "Conversation page" + ConversationsResponse + ) + ) + :<|> Named + "list-conversations" + ( Summary "Get conversation metadata for a list of conversation ids" + :> MakesFederatedCall 'Galley "get-conversations" + :> From 'V6 :> ZLocalUser :> "conversations" :> "list" @@ -389,7 +396,7 @@ type ConversationAPI = :> ZOptConn :> "conversations" :> VersionedReqBody 'V2 '[Servant.JSON] NewConv - :> ConversationV2Verb + :> ConversationVerb 'V2 Conversation ) :<|> Named "create-group-conversation@v3" @@ -413,16 +420,17 @@ type ConversationAPI = :> ZOptConn :> "conversations" :> ReqBody '[Servant.JSON] NewConv - :> ConversationVerb + :> ConversationVerb 'V3 Conversation ) :<|> Named - "create-group-conversation" + "create-group-conversation@v5" ( Summary "Create a new conversation" :> MakesFederatedCall 'Brig "api-version" :> MakesFederatedCall 'Brig "get-not-fully-connected-backends" :> MakesFederatedCall 'Galley "on-conversation-created" :> MakesFederatedCall 'Galley "on-conversation-updated" :> From 'V4 + :> Until 'V6 :> CanThrow 'ConvAccessDenied :> CanThrow 'MLSNonEmptyMemberList :> CanThrow 'MLSNotEnabled @@ -437,7 +445,31 @@ type ConversationAPI = :> ZOptConn :> "conversations" :> ReqBody '[Servant.JSON] NewConv - :> CreateGroupConversationVerb + :> ConversationVerb 'V5 CreateGroupConversation + ) + :<|> Named + "create-group-conversation" + ( Summary "Create a new conversation" + :> MakesFederatedCall 'Brig "api-version" + :> MakesFederatedCall 'Brig "get-not-fully-connected-backends" + :> MakesFederatedCall 'Galley "on-conversation-created" + :> MakesFederatedCall 'Galley "on-conversation-updated" + :> From 'V6 + :> CanThrow 'ConvAccessDenied + :> CanThrow 'MLSNonEmptyMemberList + :> CanThrow 'MLSNotEnabled + :> CanThrow 'NotConnected + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'MissingLegalholdConsent + :> CanThrow NonFederatingBackends + :> CanThrow UnreachableBackends + :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" + :> ZLocalUser + :> ZOptConn + :> "conversations" + :> ReqBody '[Servant.JSON] NewConv + :> ConversationVerb 'V6 CreateGroupConversation ) :<|> Named "create-self-conversation@v2" @@ -446,21 +478,50 @@ type ConversationAPI = :> ZLocalUser :> "conversations" :> "self" - :> ConversationV2Verb + :> ConversationVerb 'V2 Conversation ) :<|> Named - "create-self-conversation" + "create-self-conversation@v5" ( Summary "Create a self-conversation" :> From 'V3 + :> Until 'V6 :> ZLocalUser :> "conversations" :> "self" - :> ConversationVerb + :> ConversationVerb 'V5 Conversation ) :<|> Named - "get-mls-self-conversation" + "create-self-conversation" + ( Summary "Create a self-conversation" + :> From 'V6 + :> ZLocalUser + :> "conversations" + :> "self" + :> ConversationVerb 'V6 Conversation + ) + :<|> Named + "get-mls-self-conversation@v5" ( Summary "Get the user's MLS self-conversation" :> From 'V5 + :> Until 'V6 + :> ZLocalUser + :> "conversations" + :> "mls-self" + :> CanThrow 'MLSNotEnabled + :> MultiVerb1 + 'GET + '[JSON] + ( VersionedRespond + 'V5 + 200 + "The MLS self-conversation" + Conversation + ) + ) + :<|> Named + "get-mls-self-conversation" + ( Summary "Get the user's MLS self-conversation" + :> From 'V6 :> ZLocalUser :> "conversations" :> "mls-self" @@ -586,7 +647,7 @@ type ConversationAPI = :> "conversations" :> "one2one" :> VersionedReqBody 'V2 '[JSON] NewConv - :> ConversationV2Verb + :> ConversationVerb 'V2 Conversation ) :<|> Named "create-one-to-one-conversation" @@ -608,7 +669,20 @@ type ConversationAPI = :> "conversations" :> "one2one" :> ReqBody '[JSON] NewConv - :> ConversationVerb + :> ConversationVerb 'V3 Conversation + ) + :<|> Named + "get-one-to-one-mls-conversation@v5" + ( Summary "Get an MLS 1:1 conversation" + :> From 'V5 + :> Until 'V6 + :> ZLocalUser + :> CanThrow 'MLSNotEnabled + :> CanThrow 'NotConnected + :> "conversations" + :> "one2one" + :> QualifiedCapture "usr" UserId + :> MultiVerb1 'GET '[JSON] (VersionedRespond 'V5 200 "MLS 1-1 conversation" Conversation) ) :<|> Named "get-one-to-one-mls-conversation" diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index fe6362cafec..f672efe25b7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -31,6 +31,7 @@ module Wire.API.Routes.Version -- * Version Version (..), versionInt, + versionText, VersionNumber (..), VersionExp (..), supportedVersions, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index c530bf3234b..8940de2043c 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -389,12 +389,26 @@ tests = testGroup "Golden: Conversation_user V2" $ testObjects [ (Versioned @'V2 Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_1, "testObject_Conversation_v2_user_1.json"), - (Versioned @'V2 Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_2, "testObject_Conversation_v2_user_2.json") + (Versioned @'V2 Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_2, "testObject_Conversation_v2_user_2.json"), + (Versioned @'V2 Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_3, "testObject_Conversation_v2_user_3.json"), + (Versioned @'V2 Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_4, "testObject_Conversation_v2_user_4.json"), + (Versioned @'V2 Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_5, "testObject_Conversation_v2_user_5.json") + ], + testGroup "Golden: Conversation_user V5" $ + testObjects + [ (Versioned @'V5 Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_1, "testObject_Conversation_v5_user_1.json"), + (Versioned @'V5 Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_2, "testObject_Conversation_v5_user_2.json"), + (Versioned @'V5 Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_3, "testObject_Conversation_v5_user_3.json"), + (Versioned @'V5 Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_4, "testObject_Conversation_v5_user_4.json"), + (Versioned @'V5 Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_5, "testObject_Conversation_v5_user_5.json") ], testGroup "Golden: Conversation_user" $ testObjects [ (Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_1, "testObject_Conversation_user_1.json"), - (Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_2, "testObject_Conversation_user_2.json") + (Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_2, "testObject_Conversation_user_2.json"), + (Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_3, "testObject_Conversation_user_3.json"), + (Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_4, "testObject_Conversation_user_4.json"), + (Test.Wire.API.Golden.Generated.Conversation_user.testObject_Conversation_user_5, "testObject_Conversation_user_5.json") ], testGroup "Golden: NewConv_user V2" $ testObjects 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 516f8baae58..daf78e839bb 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 @@ -17,18 +17,28 @@ -- 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.Generated.Conversation_user where +module Test.Wire.API.Golden.Generated.Conversation_user + ( testObject_Conversation_user_1, + testObject_Conversation_user_2, + testObject_Conversation_user_3, + testObject_Conversation_user_4, + testObject_Conversation_user_5, + ) +where import Data.Domain import Data.Id (Id (Id)) import Data.Misc (Milliseconds (Ms, ms)) import Data.Qualified import Data.Set qualified as Set +import Data.Time.Calendar +import Data.Time.Clock import Data.UUID qualified as UUID (fromString) import Imports import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role (parseRoleName) +import Wire.API.MLS.CipherSuite import Wire.API.Provider.Service (ServiceRef (ServiceRef, _serviceRefId, _serviceRefProvider)) domain :: Domain @@ -133,3 +143,152 @@ testObject_Conversation_user_2 = ] } } + +testObject_Conversation_user_3 :: Conversation +testObject_Conversation_user_3 = + Conversation + { cnvQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002"))) domain, + cnvMetadata = + ConversationMetadata + { cnvmType = SelfConv, + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001"))), + cnvmAccess = + [ InviteAccess, + InviteAccess, + CodeAccess, + LinkAccess, + InviteAccess, + PrivateAccess, + LinkAccess, + CodeAccess, + CodeAccess, + LinkAccess, + PrivateAccess, + InviteAccess + ], + cnvmAccessRoles = Set.fromList [TeamMemberAccessRole, GuestAccessRole, ServiceAccessRole], + cnvmName = Just "", + cnvmTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000000"))), + cnvmMessageTimer = Just (Ms {ms = 1319272593797015}), + cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = 2}) + }, + cnvMembers = + ConvMembers + { cmSelf = + Member + { memId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) domain, + memService = Nothing, + memOtrMutedStatus = Just (MutedStatus {fromMutedStatus = -1}), + memOtrMutedRef = Nothing, + memOtrArchived = False, + memOtrArchivedRef = Nothing, + memHidden = True, + memHiddenRef = Just "", + memConvRoleName = + fromJust (parseRoleName "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj") + }, + cmOthers = [] + }, + cnvProtocol = + ProtocolMLS + ( ConversationMLSData + (GroupId "test_group") + ( Just + ( ActiveMLSConversationData + (Epoch 42) + timestamp + MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) + ) + ) + } + where + timestamp :: UTCTime + timestamp = UTCTime (fromGregorian 2023 1 17) (secondsToDiffTime 42) + +testObject_Conversation_user_4 :: Conversation +testObject_Conversation_user_4 = + Conversation + { cnvQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002"))) domain, + cnvMetadata = + ConversationMetadata + { cnvmType = SelfConv, + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001"))), + cnvmAccess = + [ InviteAccess, + InviteAccess, + CodeAccess, + LinkAccess, + InviteAccess, + PrivateAccess, + LinkAccess, + CodeAccess, + CodeAccess, + LinkAccess, + PrivateAccess, + InviteAccess + ], + cnvmAccessRoles = Set.fromList [TeamMemberAccessRole, GuestAccessRole, ServiceAccessRole], + cnvmName = Just "", + cnvmTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000000"))), + cnvmMessageTimer = Just (Ms {ms = 1319272593797015}), + cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = 2}) + }, + cnvMembers = + ConvMembers + { cmSelf = + Member + { memId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) domain, + memService = Nothing, + memOtrMutedStatus = Just (MutedStatus {fromMutedStatus = -1}), + memOtrMutedRef = Nothing, + memOtrArchived = False, + memOtrArchivedRef = Nothing, + memHidden = True, + memHiddenRef = Just "", + memConvRoleName = + fromJust (parseRoleName "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj") + }, + cmOthers = [] + }, + cnvProtocol = + ProtocolMLS + ( ConversationMLSData + (GroupId "test_group") + Nothing + ) + } + +testObject_Conversation_user_5 :: Conversation +testObject_Conversation_user_5 = + Conversation + { cnvQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) domain, + cnvMetadata = + ConversationMetadata + { cnvmType = One2OneConv, + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))), + cnvmAccess = [], + cnvmAccessRoles = Set.empty, + cnvmName = Just " 0", + cnvmTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002"))), + cnvmMessageTimer = Nothing, + cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = -2}) + }, + cnvMembers = + ConvMembers + { cmSelf = + Member + { memId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) domain, + memService = Nothing, + memOtrMutedStatus = Nothing, + memOtrMutedRef = Nothing, + memOtrArchived = False, + memOtrArchivedRef = Just "", + memHidden = False, + memHiddenRef = Just "", + memConvRoleName = fromJust (parseRoleName "rhhdzf0j0njilixx0g0vzrp06b_5us") + }, + cmOthers = [] + }, + cnvProtocol = ProtocolProteus + } 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 9e41b29bf3a..ffb00e875b7 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 @@ -123,6 +123,8 @@ tests = [(testObject_ListConversations_1, "testObject_ListConversations_1.json")], testGroup "ConversationsResponse V2" $ testObjects [(Versioned @'V2 testObject_ConversationsResponse_1, "testObject_ConversationsResponse_v2_1.json")], + testGroup "ConversationsResponse V5" $ + testObjects [(Versioned @'V5 testObject_ConversationsResponse_1, "testObject_ConversationsResponse_v5_1.json")], testGroup "ConversationsResponse" $ testObjects [(testObject_ConversationsResponse_1, "testObject_ConversationsResponse_1.json")], testGroup "CreateScimToken" $ @@ -151,6 +153,15 @@ tests = (testObject_TeamSize_2, "testObject_TeamSize_2.json"), (testObject_TeamSize_3, "testObject_TeamSize_3.json") ], + testGroup "PublicSubConversationV5" $ + testObjects + [ ( Versioned @'V5 testObject_PublicSubConversation_1, + "testObject_PublicSubConversation_v5_1.json" + ), + ( Versioned @'V5 testObject_PublicSubConversation_2, + "testObject_PublicSubConversation_v5_2.json" + ) + ], testGroup "PublicSubConversation" $ testObjects [ (testObject_PublicSubConversation_1, "testObject_PublicSubConversation_1.json"), 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 0a7b0342409..8fa36355f5b 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 @@ -22,17 +22,11 @@ where import Data.Domain import Data.Id (Id (Id)) -import Data.Misc import Data.Qualified -import Data.Set qualified as Set -import Data.Time.Calendar -import Data.Time.Clock import Data.UUID qualified as UUID import Imports +import Test.Wire.API.Golden.Generated.Conversation_user import Wire.API.Conversation -import Wire.API.Conversation.Protocol -import Wire.API.Conversation.Role -import Wire.API.MLS.CipherSuite domain :: Domain domain = Domain "golden.example.com" @@ -40,7 +34,7 @@ domain = Domain "golden.example.com" testObject_ConversationsResponse_1 :: ConversationsResponse testObject_ConversationsResponse_1 = ConversationsResponse - { crFound = [conv1, conv2], + { crFound = [testObject_Conversation_user_5, testObject_Conversation_user_3], crNotFound = [ Qualified (Id (fromJust (UUID.fromString "00000018-0000-0020-0000-000e00000002"))) domain, Qualified (Id (fromJust (UUID.fromString "00000018-0000-0020-0000-111111111112"))) (Domain "golden2.example.com") @@ -50,95 +44,3 @@ testObject_ConversationsResponse_1 = Qualified (Id (fromJust (UUID.fromString "99999999-0000-0020-0000-111111111112"))) (Domain "golden3.example.com") ] } - -conv1 :: Conversation -conv1 = - Conversation - { cnvQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) domain, - cnvMetadata = - ConversationMetadata - { cnvmType = One2OneConv, - cnvmCreator = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))), - cnvmAccess = [], - cnvmAccessRoles = Set.empty, - cnvmName = Just " 0", - cnvmTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002"))), - cnvmMessageTimer = Nothing, - cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = -2}) - }, - cnvMembers = - ConvMembers - { cmSelf = - Member - { memId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) domain, - memService = Nothing, - memOtrMutedStatus = Nothing, - memOtrMutedRef = Nothing, - memOtrArchived = False, - memOtrArchivedRef = Just "", - memHidden = False, - memHiddenRef = Just "", - memConvRoleName = fromJust (parseRoleName "rhhdzf0j0njilixx0g0vzrp06b_5us") - }, - cmOthers = [] - }, - cnvProtocol = ProtocolProteus - } - -conv2 :: Conversation -conv2 = - Conversation - { cnvQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002"))) domain, - cnvMetadata = - ConversationMetadata - { cnvmType = SelfConv, - cnvmCreator = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001"))), - cnvmAccess = - [ InviteAccess, - InviteAccess, - CodeAccess, - LinkAccess, - InviteAccess, - PrivateAccess, - LinkAccess, - CodeAccess, - CodeAccess, - LinkAccess, - PrivateAccess, - InviteAccess - ], - cnvmAccessRoles = Set.fromList [TeamMemberAccessRole, GuestAccessRole, ServiceAccessRole], - cnvmName = Just "", - cnvmTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000000"))), - cnvmMessageTimer = Just (Ms {ms = 1319272593797015}), - cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = 2}) - }, - cnvMembers = - ConvMembers - { cmSelf = - Member - { memId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) domain, - memService = Nothing, - memOtrMutedStatus = Just (MutedStatus {fromMutedStatus = -1}), - memOtrMutedRef = Nothing, - memOtrArchived = False, - memOtrArchivedRef = Nothing, - memHidden = True, - memHiddenRef = Just "", - memConvRoleName = - fromJust (parseRoleName "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj") - }, - cmOthers = [] - }, - 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 72810686d4b..eda0dcb51aa 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 @@ -28,9 +28,9 @@ import Data.Time.Calendar import Data.Time.Clock import Data.UUID qualified as UUID import Imports +import Wire.API.Conversation.Protocol 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 @@ -56,9 +56,13 @@ testObject_PublicSubConversation_1 = convId subConvId1 (GroupId "test_group") - (Epoch 5) - (Just (UTCTime day fromMidnight)) - MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ( Just + ( ActiveMLSConversationData + (Epoch 5) + (UTCTime day fromMidnight) + MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) + ) [] where fromMidnight :: DiffTime @@ -72,9 +76,7 @@ testObject_PublicSubConversation_2 = convId subConvId2 (GroupId "test_group_2") - (Epoch 0) Nothing - MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 [mkClientIdentity user cid] where user :: Qualified UserId diff --git a/libs/wire-api/test/golden/testObject_Conversation_user_3.json b/libs/wire-api/test/golden/testObject_Conversation_user_3.json new file mode 100644 index 00000000000..bdc0413d26a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Conversation_user_3.json @@ -0,0 +1,60 @@ +{ + "access": [ + "invite", + "invite", + "code", + "link", + "invite", + "private", + "link", + "code", + "code", + "link", + "private", + "invite" + ], + "access_role": [ + "team_member", + "guest", + "service" + ], + "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", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj", + "hidden": true, + "hidden_ref": "", + "id": "00000000-0000-0001-0000-000100000001", + "otr_archived": false, + "otr_archived_ref": null, + "otr_muted_ref": null, + "otr_muted_status": -1, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0001-0000-000100000001" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": 1319272593797015, + "name": "", + "protocol": "mls", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0000-0000-000000000002" + }, + "receipt_mode": 2, + "team": "00000000-0000-0001-0000-000200000000", + "type": 1 +} diff --git a/libs/wire-api/test/golden/testObject_Conversation_user_4.json b/libs/wire-api/test/golden/testObject_Conversation_user_4.json new file mode 100644 index 00000000000..3d6616cf602 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Conversation_user_4.json @@ -0,0 +1,58 @@ +{ + "access": [ + "invite", + "invite", + "code", + "link", + "invite", + "private", + "link", + "code", + "code", + "link", + "private", + "invite" + ], + "access_role": [ + "team_member", + "guest", + "service" + ], + "creator": "00000000-0000-0000-0000-000200000001", + "epoch": 0, + "group_id": "dGVzdF9ncm91cA==", + "id": "00000000-0000-0000-0000-000000000002", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj", + "hidden": true, + "hidden_ref": "", + "id": "00000000-0000-0001-0000-000100000001", + "otr_archived": false, + "otr_archived_ref": null, + "otr_muted_ref": null, + "otr_muted_status": -1, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0001-0000-000100000001" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": 1319272593797015, + "name": "", + "protocol": "mls", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0000-0000-000000000002" + }, + "receipt_mode": 2, + "team": "00000000-0000-0001-0000-000200000000", + "type": 1 +} diff --git a/libs/wire-api/test/golden/testObject_Conversation_user_5.json b/libs/wire-api/test/golden/testObject_Conversation_user_5.json new file mode 100644 index 00000000000..e9c08330a13 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Conversation_user_5.json @@ -0,0 +1,39 @@ +{ + "access": [], + "access_role": [], + "creator": "00000001-0000-0001-0000-000200000001", + "id": "00000001-0000-0000-0000-000000000000", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "rhhdzf0j0njilixx0g0vzrp06b_5us", + "hidden": false, + "hidden_ref": "", + "id": "00000001-0000-0001-0000-000100000000", + "otr_archived": false, + "otr_archived_ref": "", + "otr_muted_ref": null, + "otr_muted_status": null, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000001-0000-0001-0000-000100000000" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": null, + "name": " 0", + "protocol": "proteus", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000001-0000-0000-0000-000000000000" + }, + "receipt_mode": -2, + "team": "00000001-0000-0001-0000-000100000002", + "type": 2 +} diff --git a/libs/wire-api/test/golden/testObject_Conversation_v2_user_3.json b/libs/wire-api/test/golden/testObject_Conversation_v2_user_3.json new file mode 100644 index 00000000000..7f4502a0adb --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Conversation_v2_user_3.json @@ -0,0 +1,61 @@ +{ + "access": [ + "invite", + "invite", + "code", + "link", + "invite", + "private", + "link", + "code", + "code", + "link", + "private", + "invite" + ], + "access_role": "non_activated", + "access_role_v2": [ + "team_member", + "guest", + "service" + ], + "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", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj", + "hidden": true, + "hidden_ref": "", + "id": "00000000-0000-0001-0000-000100000001", + "otr_archived": false, + "otr_archived_ref": null, + "otr_muted_ref": null, + "otr_muted_status": -1, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0001-0000-000100000001" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": 1319272593797015, + "name": "", + "protocol": "mls", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0000-0000-000000000002" + }, + "receipt_mode": 2, + "team": "00000000-0000-0001-0000-000200000000", + "type": 1 +} diff --git a/libs/wire-api/test/golden/testObject_Conversation_v2_user_4.json b/libs/wire-api/test/golden/testObject_Conversation_v2_user_4.json new file mode 100644 index 00000000000..6609a65f3af --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Conversation_v2_user_4.json @@ -0,0 +1,60 @@ +{ + "access": [ + "invite", + "invite", + "code", + "link", + "invite", + "private", + "link", + "code", + "code", + "link", + "private", + "invite" + ], + "access_role": "non_activated", + "access_role_v2": [ + "team_member", + "guest", + "service" + ], + "cipher_suite": 1, + "creator": "00000000-0000-0000-0000-000200000001", + "epoch": 0, + "group_id": "dGVzdF9ncm91cA==", + "id": "00000000-0000-0000-0000-000000000002", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj", + "hidden": true, + "hidden_ref": "", + "id": "00000000-0000-0001-0000-000100000001", + "otr_archived": false, + "otr_archived_ref": null, + "otr_muted_ref": null, + "otr_muted_status": -1, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0001-0000-000100000001" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": 1319272593797015, + "name": "", + "protocol": "mls", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0000-0000-000000000002" + }, + "receipt_mode": 2, + "team": "00000000-0000-0001-0000-000200000000", + "type": 1 +} diff --git a/libs/wire-api/test/golden/testObject_Conversation_v2_user_5.json b/libs/wire-api/test/golden/testObject_Conversation_v2_user_5.json new file mode 100644 index 00000000000..90e837c0700 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Conversation_v2_user_5.json @@ -0,0 +1,40 @@ +{ + "access": [], + "access_role": "private", + "access_role_v2": [], + "creator": "00000001-0000-0001-0000-000200000001", + "id": "00000001-0000-0000-0000-000000000000", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "rhhdzf0j0njilixx0g0vzrp06b_5us", + "hidden": false, + "hidden_ref": "", + "id": "00000001-0000-0001-0000-000100000000", + "otr_archived": false, + "otr_archived_ref": "", + "otr_muted_ref": null, + "otr_muted_status": null, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000001-0000-0001-0000-000100000000" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": null, + "name": " 0", + "protocol": "proteus", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000001-0000-0000-0000-000000000000" + }, + "receipt_mode": -2, + "team": "00000001-0000-0001-0000-000100000002", + "type": 2 +} diff --git a/libs/wire-api/test/golden/testObject_Conversation_v5_user_1.json b/libs/wire-api/test/golden/testObject_Conversation_v5_user_1.json new file mode 100644 index 00000000000..e9c08330a13 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Conversation_v5_user_1.json @@ -0,0 +1,39 @@ +{ + "access": [], + "access_role": [], + "creator": "00000001-0000-0001-0000-000200000001", + "id": "00000001-0000-0000-0000-000000000000", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "rhhdzf0j0njilixx0g0vzrp06b_5us", + "hidden": false, + "hidden_ref": "", + "id": "00000001-0000-0001-0000-000100000000", + "otr_archived": false, + "otr_archived_ref": "", + "otr_muted_ref": null, + "otr_muted_status": null, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000001-0000-0001-0000-000100000000" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": null, + "name": " 0", + "protocol": "proteus", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000001-0000-0000-0000-000000000000" + }, + "receipt_mode": -2, + "team": "00000001-0000-0001-0000-000100000002", + "type": 2 +} diff --git a/libs/wire-api/test/golden/testObject_Conversation_v5_user_2.json b/libs/wire-api/test/golden/testObject_Conversation_v5_user_2.json new file mode 100644 index 00000000000..bc75e888889 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Conversation_v5_user_2.json @@ -0,0 +1,70 @@ +{ + "access": [ + "invite", + "invite", + "code", + "link", + "invite", + "private", + "link", + "code", + "code", + "link", + "private", + "invite" + ], + "access_role": [ + "team_member", + "guest", + "service" + ], + "creator": "00000000-0000-0000-0000-000200000001", + "id": "00000000-0000-0000-0000-000000000002", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [ + { + "conversation_role": "r1rg526serx51g15n99y1bw_9q0qrcwck3jxl7ocjsjqcoux7d1zbkz9nnczy92t2oyogxrx3cyh_b8yv44l61mx9uzdnv6", + "id": "00000001-0000-0001-0000-000100000001", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000001-0000-0001-0000-000100000001" + }, + "service": { + "id": "00000001-0000-0000-0000-000000000000", + "provider": "00000001-0000-0000-0000-000000000001" + }, + "status": 0 + } + ], + "self": { + "conversation_role": "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj", + "hidden": true, + "hidden_ref": "", + "id": "00000000-0000-0001-0000-000100000001", + "otr_archived": false, + "otr_archived_ref": null, + "otr_muted_ref": null, + "otr_muted_status": -1, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0001-0000-000100000001" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": 1319272593797015, + "name": "", + "protocol": "proteus", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0000-0000-000000000002" + }, + "receipt_mode": null, + "team": null, + "type": 1 +} diff --git a/libs/wire-api/test/golden/testObject_Conversation_v5_user_3.json b/libs/wire-api/test/golden/testObject_Conversation_v5_user_3.json new file mode 100644 index 00000000000..bdc0413d26a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Conversation_v5_user_3.json @@ -0,0 +1,60 @@ +{ + "access": [ + "invite", + "invite", + "code", + "link", + "invite", + "private", + "link", + "code", + "code", + "link", + "private", + "invite" + ], + "access_role": [ + "team_member", + "guest", + "service" + ], + "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", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj", + "hidden": true, + "hidden_ref": "", + "id": "00000000-0000-0001-0000-000100000001", + "otr_archived": false, + "otr_archived_ref": null, + "otr_muted_ref": null, + "otr_muted_status": -1, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0001-0000-000100000001" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": 1319272593797015, + "name": "", + "protocol": "mls", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0000-0000-000000000002" + }, + "receipt_mode": 2, + "team": "00000000-0000-0001-0000-000200000000", + "type": 1 +} diff --git a/libs/wire-api/test/golden/testObject_Conversation_v5_user_4.json b/libs/wire-api/test/golden/testObject_Conversation_v5_user_4.json new file mode 100644 index 00000000000..430ac682cee --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Conversation_v5_user_4.json @@ -0,0 +1,59 @@ +{ + "access": [ + "invite", + "invite", + "code", + "link", + "invite", + "private", + "link", + "code", + "code", + "link", + "private", + "invite" + ], + "access_role": [ + "team_member", + "guest", + "service" + ], + "cipher_suite": 1, + "creator": "00000000-0000-0000-0000-000200000001", + "epoch": 0, + "group_id": "dGVzdF9ncm91cA==", + "id": "00000000-0000-0000-0000-000000000002", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj", + "hidden": true, + "hidden_ref": "", + "id": "00000000-0000-0001-0000-000100000001", + "otr_archived": false, + "otr_archived_ref": null, + "otr_muted_ref": null, + "otr_muted_status": -1, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0001-0000-000100000001" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": 1319272593797015, + "name": "", + "protocol": "mls", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0000-0000-000000000002" + }, + "receipt_mode": 2, + "team": "00000000-0000-0001-0000-000200000000", + "type": 1 +} diff --git a/libs/wire-api/test/golden/testObject_Conversation_v5_user_5.json b/libs/wire-api/test/golden/testObject_Conversation_v5_user_5.json new file mode 100644 index 00000000000..e9c08330a13 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Conversation_v5_user_5.json @@ -0,0 +1,39 @@ +{ + "access": [], + "access_role": [], + "creator": "00000001-0000-0001-0000-000200000001", + "id": "00000001-0000-0000-0000-000000000000", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "rhhdzf0j0njilixx0g0vzrp06b_5us", + "hidden": false, + "hidden_ref": "", + "id": "00000001-0000-0001-0000-000100000000", + "otr_archived": false, + "otr_archived_ref": "", + "otr_muted_ref": null, + "otr_muted_status": null, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000001-0000-0001-0000-000100000000" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": null, + "name": " 0", + "protocol": "proteus", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000001-0000-0000-0000-000000000000" + }, + "receipt_mode": -2, + "team": "00000001-0000-0001-0000-000100000002", + "type": 2 +} diff --git a/libs/wire-api/test/golden/testObject_ConversationsResponse_v5_1.json b/libs/wire-api/test/golden/testObject_ConversationsResponse_v5_1.json new file mode 100644 index 00000000000..1156816764e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ConversationsResponse_v5_1.json @@ -0,0 +1,123 @@ +{ + "failed": [ + { + "domain": "golden.example.com", + "id": "00000018-4444-0020-0000-000e00000002" + }, + { + "domain": "golden3.example.com", + "id": "99999999-0000-0020-0000-111111111112" + } + ], + "found": [ + { + "access": [], + "access_role": [], + "creator": "00000001-0000-0001-0000-000200000001", + "id": "00000001-0000-0000-0000-000000000000", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "rhhdzf0j0njilixx0g0vzrp06b_5us", + "hidden": false, + "hidden_ref": "", + "id": "00000001-0000-0001-0000-000100000000", + "otr_archived": false, + "otr_archived_ref": "", + "otr_muted_ref": null, + "otr_muted_status": null, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000001-0000-0001-0000-000100000000" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": null, + "name": " 0", + "protocol": "proteus", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000001-0000-0000-0000-000000000000" + }, + "receipt_mode": -2, + "team": "00000001-0000-0001-0000-000100000002", + "type": 2 + }, + { + "access": [ + "invite", + "invite", + "code", + "link", + "invite", + "private", + "link", + "code", + "code", + "link", + "private", + "invite" + ], + "access_role": [ + "team_member", + "guest", + "service" + ], + "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", + "last_event_time": "1970-01-01T00:00:00.000Z", + "members": { + "others": [], + "self": { + "conversation_role": "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj", + "hidden": true, + "hidden_ref": "", + "id": "00000000-0000-0001-0000-000100000001", + "otr_archived": false, + "otr_archived_ref": null, + "otr_muted_ref": null, + "otr_muted_status": -1, + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0001-0000-000100000001" + }, + "service": null, + "status": 0, + "status_ref": "0.0", + "status_time": "1970-01-01T00:00:00.000Z" + } + }, + "message_timer": 1319272593797015, + "name": "", + "protocol": "mls", + "qualified_id": { + "domain": "golden.example.com", + "id": "00000000-0000-0000-0000-000000000002" + }, + "receipt_mode": 2, + "team": "00000000-0000-0001-0000-000200000000", + "type": 1 + } + ], + "not_found": [ + { + "domain": "golden.example.com", + "id": "00000018-0000-0020-0000-000e00000002" + }, + { + "domain": "golden2.example.com", + "id": "00000018-0000-0020-0000-111111111112" + } + ] +} diff --git a/libs/wire-api/test/golden/testObject_PublicSubConversation_2.json b/libs/wire-api/test/golden/testObject_PublicSubConversation_2.json index a918c3161ba..64914b82b12 100644 --- a/libs/wire-api/test/golden/testObject_PublicSubConversation_2.json +++ b/libs/wire-api/test/golden/testObject_PublicSubConversation_2.json @@ -1,7 +1,5 @@ { - "cipher_suite": 1, "epoch": 0, - "epoch_timestamp": null, "group_id": "dGVzdF9ncm91cF8y", "members": [ { diff --git a/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_1.json b/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_1.json new file mode 100644 index 00000000000..05ce835507a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_1.json @@ -0,0 +1,12 @@ +{ + "cipher_suite": 1, + "epoch": 5, + "epoch_timestamp": "2023-01-17T00:00:42Z", + "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_v5_2.json b/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_2.json new file mode 100644 index 00000000000..ac57e7e8e1b --- /dev/null +++ b/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_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/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 2df5b92e5c8..b998a108339 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -70,6 +70,7 @@ import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P import Wire.API.Conversation hiding (Conversation, Member) +import Wire.API.Conversation qualified as Public import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation @@ -116,7 +117,7 @@ createGroupConversationUpToV3 :: Local UserId -> Maybe ConnId -> NewConv -> - Sem r ConversationResponse + Sem r (ConversationResponse Public.Conversation) createGroupConversationUpToV3 lusr conn newConv = mapError UnreachableBackendsLegacy $ do conv <- @@ -291,13 +292,13 @@ createProteusSelfConversation :: Member P.TinyLog r ) => Local UserId -> - Sem r ConversationResponse + Sem r (ConversationResponse Public.Conversation) createProteusSelfConversation lusr = do let lcnv = fmap Data.selfConv lusr c <- E.getConversation (tUnqualified lcnv) maybe (create lcnv) (conversationExisted lusr) c where - create :: Local ConvId -> Sem r ConversationResponse + create :: Local ConvId -> Sem r (ConversationResponse Public.Conversation) create lcnv = do let nc = NewConversation @@ -332,7 +333,7 @@ createOne2OneConversation :: Local UserId -> ConnId -> NewConv -> - Sem r ConversationResponse + Sem r (ConversationResponse Public.Conversation) createOne2OneConversation lusr zcon j = mapError @UnreachableBackends @UnreachableBackendsLegacy UnreachableBackendsLegacy $ do let allUsers = newConvMembers lusr j @@ -402,7 +403,7 @@ createLegacyOne2OneConversationUnchecked :: Maybe (Range 1 256 Text) -> Maybe TeamId -> Local UserId -> - Sem r ConversationResponse + Sem r (ConversationResponse Public.Conversation) createLegacyOne2OneConversationUnchecked self zcon name mtid other = do lcnv <- localOne2OneConvId self other let meta = @@ -445,7 +446,7 @@ createOne2OneConversationUnchecked :: Maybe (Range 1 256 Text) -> Maybe TeamId -> Qualified UserId -> - Sem r ConversationResponse + Sem r (ConversationResponse Public.Conversation) createOne2OneConversationUnchecked self zcon name mtid other = do let create = foldQualified @@ -471,7 +472,7 @@ createOne2OneConversationLocally :: Maybe (Range 1 256 Text) -> Maybe TeamId -> Qualified UserId -> - Sem r ConversationResponse + Sem r (ConversationResponse Public.Conversation) createOne2OneConversationLocally lcnv self zcon name mtid other = do mc <- E.getConversation (tUnqualified lcnv) case mc of @@ -501,7 +502,7 @@ createOne2OneConversationRemotely :: Maybe (Range 1 256 Text) -> Maybe TeamId -> Qualified UserId -> - Sem r ConversationResponse + Sem r (ConversationResponse Public.Conversation) createOne2OneConversationRemotely _ _ _ _ _ _ = throw FederationNotImplemented @@ -523,7 +524,7 @@ createConnectConversation :: Local UserId -> Maybe ConnId -> Connect -> - Sem r ConversationResponse + Sem r (ConversationResponse Public.Conversation) createConnectConversation lusr conn j = do lrecipient <- ensureLocal lusr (cRecipient j) n <- rangeCheckedMaybe (cName j) @@ -645,7 +646,7 @@ conversationCreated :: ) => Local UserId -> Data.Conversation -> - Sem r ConversationResponse + Sem r (ConversationResponse Public.Conversation) conversationCreated lusr cnv = Created <$> conversationView lusr cnv -- | The return set contains all the remote users that could not be contacted. diff --git a/services/galley/src/Galley/API/MLS/Commit/Core.hs b/services/galley/src/Galley/API/MLS/Commit/Core.hs index 0386d3d4738..1c7b97f6b89 100644 --- a/services/galley/src/Galley/API/MLS/Commit/Core.hs +++ b/services/galley/src/Galley/API/MLS/Commit/Core.hs @@ -31,6 +31,7 @@ import Data.Qualified import Data.Time import Galley.API.Error import Galley.API.MLS.Conversation +import Galley.API.MLS.IncomingMessage import Galley.API.MLS.Proposal import Galley.API.MLS.Types import Galley.Effects @@ -59,6 +60,7 @@ import Wire.API.Federation.Version import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.Credential +import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.User.Client import Wire.NotificationSubsystem @@ -99,9 +101,10 @@ getCommitData :: ClientIdentity -> Local ConvOrSubConv -> Epoch -> - Commit -> + CipherSuiteTag -> + IncomingBundle -> Sem r ProposalAction -getCommitData senderIdentity lConvOrSub epoch commit = do +getCommitData senderIdentity lConvOrSub epoch ciphersuite bundle = do let convOrSub = tUnqualified lConvOrSub groupId = cnvmlsGroupId convOrSub.mlsMeta @@ -110,8 +113,11 @@ getCommitData senderIdentity lConvOrSub epoch commit = do if epoch == Epoch 0 then addProposedClient senderIdentity else mempty - proposals <- traverse (derefOrCheckProposal convOrSub.mlsMeta groupId epoch) commit.proposals - action <- applyProposals convOrSub.mlsMeta groupId proposals + proposals <- + traverse + (derefOrCheckProposal epoch ciphersuite groupId) + bundle.commit.value.proposals + action <- applyProposals ciphersuite 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 364feaf5ce2..52b5a447ca8 100644 --- a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs @@ -42,6 +42,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.Error +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.Credential import Wire.API.MLS.LeafNode @@ -70,8 +71,11 @@ getExternalCommitData :: Sem r ExternalCommitAction getExternalCommitData senderIdentity lConvOrSub epoch commit = do let convOrSub = tUnqualified lConvOrSub - curEpoch = cnvmlsEpoch convOrSub.mlsMeta groupId = cnvmlsGroupId convOrSub.mlsMeta + activeData <- + note (mlsProtocolError "The first commit in a group cannot be external") $ + cnvmlsActiveData convOrSub.mlsMeta + let curEpoch = activeData.epoch when (epoch /= curEpoch) $ throwS @'MLSStaleMessage when (epoch == Epoch 0) $ throw $ @@ -95,7 +99,7 @@ getExternalCommitData senderIdentity lConvOrSub epoch commit = do evalState convOrSub.indexMap $ do -- process optional removal - propAction <- applyProposals convOrSub.mlsMeta groupId proposals + propAction <- applyProposals activeData.ciphersuite groupId proposals removedIndex <- case cmAssocs (paRemove propAction) of [(cid, idx)] | cid /= senderIdentity -> @@ -130,11 +134,12 @@ processExternalCommit :: ) => ClientIdentity -> Local ConvOrSubConv -> + CipherSuiteTag -> Epoch -> ExternalCommitAction -> Maybe UpdatePath -> Sem r () -processExternalCommit senderIdentity lConvOrSub epoch action updatePath = do +processExternalCommit senderIdentity lConvOrSub ciphersuite epoch action updatePath = do let convOrSub = tUnqualified lConvOrSub -- only members can join a subconversation @@ -148,10 +153,9 @@ processExternalCommit senderIdentity lConvOrSub epoch action updatePath = do <$> note (mlsProtocolError "External commits need an update path") updatePath - 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 + case validateLeafNode ciphersuite (Just senderIdentity) extra leafNode.value of Left errMsg -> throw $ mlsProtocolError ("Tried to add invalid LeafNode: " <> errMsg) diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index 8a1bbe7fe21..4e7974de17d 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -54,6 +54,7 @@ import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.LeaveReason +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.Credential import Wire.API.MLS.Proposal qualified as Proposal @@ -76,15 +77,15 @@ processInternalCommit :: ClientIdentity -> Maybe ConnId -> Local ConvOrSubConv -> + CipherSuiteTag -> Epoch -> ProposalAction -> Commit -> Sem r [LocalConversationUpdate] -processInternalCommit senderIdentity con lConvOrSub epoch action commit = do +processInternalCommit senderIdentity con lConvOrSub ciphersuite epoch action commit = do let convOrSub = tUnqualified lConvOrSub qusr = cidQualifiedUser senderIdentity cm = convOrSub.members - suite = cnvmlsCipherSuite convOrSub.mlsMeta newUserClients = Map.assocs (paAdd action) -- check all pending proposals are referenced in the commit @@ -154,7 +155,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 suite >>= \case + getClientInfo lConvOrSub qtarget ciphersuite >>= \case Left _e -> pure (Just qtarget) Right clientInfo -> do let allClients = Set.map ciId clientInfo @@ -192,7 +193,6 @@ processInternalCommit senderIdentity con lConvOrSub epoch action commit = do createSubConversation cnv sub - convOrSub.mlsMeta.cnvmlsCipherSuite convOrSub.mlsMeta.cnvmlsGroupId pure [] Conv _ diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 39c897665af..a4add447b06 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -204,39 +204,40 @@ postMLSCommitBundleToLocalConv :: Local ConvOrSubConvId -> Sem r [LocalConversationUpdate] postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do - 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 + lConvOrSub <- fetchConvOrSub qusr bundle.groupId ctype lConvOrSubId + let convOrSub = tUnqualified lConvOrSub + + ciphersuite <- + note (mlsProtocolError "Unsupported ciphersuite") $ + cipherSuiteTag bundle.groupInfo.value.groupContext.cipherSuite + + case convOrSub.mlsMeta.cnvmlsActiveData of -- 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 + Nothing -> do + case convOrSub.id of + Conv cid -> setConversationCipherSuite cid ciphersuite + SubConv cid sub -> setSubConversationCipherSuite cid sub ciphersuite + -- otherwise, make sure the ciphersuite matches + Just activeData -> do + unless (ciphersuite == activeData.ciphersuite) $ + throw $ + mlsProtocolError "GroupInfo ciphersuite does not match conversation" + unless (bundle.epoch == activeData.epoch) $ throwS @'MLSStaleMessage senderIdentity <- getSenderIdentity qusr c bundle.sender lConvOrSub (events, newClients) <- case bundle.sender of SenderMember _index -> do -- extract added/removed clients from bundle - action <- getCommitData senderIdentity lConvOrSub bundle.epoch bundle.commit.value + action <- getCommitData senderIdentity lConvOrSub bundle.epoch ciphersuite bundle + -- process additions and removals events <- processInternalCommit senderIdentity conn lConvOrSub + ciphersuite bundle.epoch action bundle.commit.value @@ -251,6 +252,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do processExternalCommit senderIdentity lConvOrSub + ciphersuite bundle.epoch action bundle.commit.value.path @@ -405,11 +407,15 @@ postMLSMessageToLocalConv qusr c con msg ctype convOrSubId = do throwS @'MLSUnsupportedMessage -- reject application messages older than 2 epochs + -- FUTUREWORK: consider rejecting this message if the conversation epoch is 0 let epochInt :: Epoch -> Integer epochInt = fromIntegral . epochNumber - when - (epochInt msg.epoch < epochInt convOrSub.mlsMeta.cnvmlsEpoch - 2) - $ throwS @'MLSStaleMessage + case convOrSub.mlsMeta.cnvmlsActiveData of + Nothing -> throw $ mlsProtocolError "Application messages at epoch 0 are not supported" + Just activeData -> + when + (epochInt msg.epoch < epochInt activeData.epoch - 2) + $ throwS @'MLSStaleMessage propagateMessage qusr (Just c) lConvOrSub con msg.rawMessage (tUnqualified lConvOrSub).members pure [] @@ -489,7 +495,7 @@ fetchConvOrSub qusr groupId ctype convOrSubId = for convOrSubId $ \case c <- getMLSConv qusr Nothing ctype lconv msubconv <- getSubConversation convId sconvId subconv <- case msubconv of - Nothing -> pure $ newSubConversationFromParent lconv sconvId (mcMLSData c) + Nothing -> pure $ newSubConversationFromParent lconv sconvId Just subconv -> do when (groupId /= subconv.scMLSData.cnvmlsGroupId) $ throw (mlsProtocolError "The message group ID does not match the subconversation") diff --git a/services/galley/src/Galley/API/MLS/One2One.hs b/services/galley/src/Galley/API/MLS/One2One.hs index a5b01e129a3..462c0beb66f 100644 --- a/services/galley/src/Galley/API/MLS/One2One.hs +++ b/services/galley/src/Galley/API/MLS/One2One.hs @@ -36,7 +36,6 @@ 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 import Wire.API.User @@ -92,9 +91,7 @@ localMLSOne2OneConversationMetadata convId = mlsData = ConversationMLSData { cnvmlsGroupId = groupId, - cnvmlsEpoch = Epoch 0, - cnvmlsEpochTimestamp = Nothing, - cnvmlsCipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + cnvmlsActiveData = Nothing } in (metadata, mlsData) diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs index 4df31ac4c97..cbaba7f43db 100644 --- a/services/galley/src/Galley/API/MLS/Proposal.hs +++ b/services/galley/src/Galley/API/MLS/Proposal.hs @@ -60,6 +60,7 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.Error import Wire.API.MLS.AuthenticatedContent +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage import Wire.API.MLS.LeafNode @@ -146,28 +147,28 @@ derefOrCheckProposal :: Member (State IndexMap) r, Member (ErrorS 'MLSProposalNotFound) r ) => - ConversationMLSData -> - GroupId -> Epoch -> + CipherSuiteTag -> + GroupId -> ProposalOrRef -> Sem r Proposal -derefOrCheckProposal _mlsMeta groupId epoch (Ref ref) = do +derefOrCheckProposal epoch _ciphersuite groupId (Ref ref) = do p <- getProposal groupId epoch ref >>= noteS @'MLSProposalNotFound pure p.value -derefOrCheckProposal mlsMeta _ _ (Inline p) = do +derefOrCheckProposal _epoch ciphersuite _ (Inline p) = do im <- get - checkProposal mlsMeta im p + checkProposal ciphersuite im p pure p checkProposal :: ( Member (Error MLSProtocolError) r, Member (ErrorS 'MLSInvalidLeafNodeIndex) r ) => - ConversationMLSData -> + CipherSuiteTag -> IndexMap -> Proposal -> Sem r () -checkProposal mlsMeta im p = case p of +checkProposal ciphersuite im p = case p of AddProposal kp -> do (cs, _lifetime) <- either @@ -175,7 +176,7 @@ checkProposal mlsMeta im p = case p of pure $ validateKeyPackage Nothing kp.value -- we are not checking lifetime constraints here - unless (mlsMeta.cnvmlsCipherSuite == cs) $ + unless (ciphersuite == cs) $ throw (mlsProtocolError "Key package ciphersuite does not match conversation") RemoveProposal idx -> do void $ noteS @'MLSInvalidLeafNodeIndex $ imLookup im idx @@ -194,13 +195,13 @@ applyProposals :: Member (ErrorS 'MLSUnsupportedProposal) r, Member (ErrorS 'MLSInvalidLeafNodeIndex) r ) => - ConversationMLSData -> + CipherSuiteTag -> GroupId -> [Proposal] -> Sem r ProposalAction -applyProposals mlsMeta groupId = +applyProposals ciphersuite groupId = -- proposals are sorted before processing - foldMap (applyProposal mlsMeta groupId) + foldMap (applyProposal ciphersuite groupId) . sortOn proposalProcessingStage applyProposal :: @@ -209,27 +210,27 @@ applyProposal :: Member (ErrorS 'MLSUnsupportedProposal) r, Member (ErrorS 'MLSInvalidLeafNodeIndex) r ) => - ConversationMLSData -> + CipherSuiteTag -> GroupId -> Proposal -> Sem r ProposalAction -applyProposal mlsMeta _groupId (AddProposal kp) = do +applyProposal ciphersuite _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) $ + unless (ciphersuite == 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 +applyProposal _ciphersuite _groupId (RemoveProposal idx) = do im <- get (cid, im') <- noteS @'MLSInvalidLeafNodeIndex $ imRemoveClient im idx put im' pure (paRemoveClient cid idx) -applyProposal _mlsMeta _groupId _ = pure mempty +applyProposal _activeData _groupId _ = pure mempty processProposal :: HasProposalEffects r => @@ -245,21 +246,23 @@ processProposal :: Sem r () processProposal qusr lConvOrSub groupId epoch pub prop = do 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 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") + case cnvmlsActiveData mlsMeta of + Nothing -> throw $ mlsProtocolError "Bare proposals at epoch 0 are not supported" + Just activeData -> do + -- Check if the epoch number matches that of a conversation + unless (epoch == activeData.epoch) $ throwS @'MLSStaleMessage - -- FUTUREWORK: validate the member's conversation role - 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 + -- FUTUREWORK: validate the member's conversation role + checkProposal activeData.ciphersuite (tUnqualified lConvOrSub).indexMap prop.value + when (isExternal pub.sender) $ checkExternalProposalUser qusr prop.value + let propRef = + authContentRef + activeData.ciphersuite + (incomingMessageAuthenticatedContent pub) + storeProposal groupId epoch propRef ProposalOriginClient prop getKeyPackageIdentity :: Member (ErrorS 'MLSUnsupportedProposal) r => diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 2b70748035a..53c9a4f2a97 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -87,9 +87,11 @@ createAndSendRemoveProposals :: -- conversation/subconversation client maps. ClientMap -> Sem r () -createAndSendRemoveProposals lConvOrSubConv indices qusr cm = do +createAndSendRemoveProposals lConvOrSubConv indices qusr cm = void . runError @() $ do let meta = (tUnqualified lConvOrSubConv).mlsMeta - mKeyPair <- getMLSRemovalKey (csSignatureScheme (cnvmlsCipherSuite meta)) + activeData <- note () $ cnvmlsActiveData meta + let cs = activeData.ciphersuite + mKeyPair <- getMLSRemovalKey (csSignatureScheme cs) 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) @@ -108,7 +110,7 @@ createAndSendRemoveProposals lConvOrSubConv indices qusr cm = do storeProposal (cnvmlsGroupId meta) (cnvmlsEpoch meta) - (publicMessageRef (cnvmlsCipherSuite meta) pmsg) + (publicMessageRef cs pmsg) ProposalOriginBackend proposal propagateMessage qusr Nothing lConvOrSubConv Nothing msg cm diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 37a400a9672..af4df8a7482 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -120,7 +120,7 @@ getLocalSubConversation qusr lconv sconv = do msub <- Eff.getSubConversation (tUnqualified lconv) sconv sub <- case msub of Nothing -> do - (mlsMeta, mlsProtocol) <- noteS @'ConvNotFound (mlsMetadata c) + (_mlsMeta, mlsProtocol) <- noteS @'ConvNotFound (mlsMetadata c) case mlsProtocol of MLSMigrationMixed -> throwS @'MLSSubConvUnsupportedConvType @@ -128,7 +128,7 @@ getLocalSubConversation qusr lconv sconv = do -- deriving this deterministically to prevent race conditions with -- multiple threads creating the subconversation - pure (newSubConversationFromParent lconv sconv mlsMeta) + pure (newSubConversationFromParent lconv sconv) Just sub -> pure sub pure (toPublicSubConv (tUntagged (qualifyAs lconv sub))) @@ -263,10 +263,6 @@ deleteLocalSubConversation qusr lcnvId scnvId dsc = do lConvOrSubId = qualifyAs lcnvId (SubConv cnvId scnvId) cnv <- getConversationAndCheckMembership qusr lcnvId - (mlsMeta, _mlsProtocol) <- noteS @'ConvNotFound (mlsMetadata cnv) - - let cs = cnvmlsCipherSuite mlsMeta - withCommitLock lConvOrSubId (dscGroupId dsc) (dscEpoch dsc) $ do sconv <- Eff.getSubConversation cnvId scnvId @@ -287,7 +283,7 @@ deleteLocalSubConversation qusr lcnvId scnvId dsc = do $ nextGenGroupId gid -- the following overwrites any prior information about the subconversation - void $ Eff.createSubConversation cnvId scnvId cs newGid + void $ Eff.createSubConversation cnvId scnvId newGid deleteRemoteSubConversation :: ( Members diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index 5cfce7bd88a..3274956f1bd 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -30,7 +30,6 @@ 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 @@ -148,17 +147,15 @@ data SubConversation = SubConversation } deriving (Eq, Show) -newSubConversation :: ConvId -> SubConvId -> CipherSuiteTag -> GroupId -> SubConversation -newSubConversation convId subConvId suite groupId = +newSubConversation :: ConvId -> SubConvId -> GroupId -> SubConversation +newSubConversation convId subConvId groupId = SubConversation { scParentConvId = convId, scSubConvId = subConvId, scMLSData = ConversationMLSData { cnvmlsGroupId = groupId, - cnvmlsEpoch = Epoch 0, - cnvmlsEpochTimestamp = Nothing, - cnvmlsCipherSuite = suite + cnvmlsActiveData = Nothing }, scMembers = mkClientMap [], scIndexMap = mempty @@ -167,15 +164,13 @@ newSubConversation convId subConvId suite groupId = newSubConversationFromParent :: Local ConvId -> SubConvId -> - ConversationMLSData -> SubConversation -newSubConversationFromParent lconv sconv mlsMeta = +newSubConversationFromParent lconv sconv = let groupId = convToGroupId . groupIdParts RegularConv $ flip SubConv sconv <$> tUntagged lconv - suite = cnvmlsCipherSuite mlsMeta - in newSubConversation (tUnqualified lconv) sconv suite groupId + in newSubConversation (tUnqualified lconv) sconv groupId toPublicSubConv :: Qualified SubConversation -> PublicSubConversation toPublicSubConv (Qualified (SubConversation {..}) domain) = @@ -184,9 +179,7 @@ toPublicSubConv (Qualified (SubConversation {..}) domain) = { pscParentConvId = Qualified scParentConvId domain, pscSubConvId = scSubConvId, pscGroupId = cnvmlsGroupId scMLSData, - pscEpoch = cnvmlsEpoch scMLSData, - pscEpochTimestamp = cnvmlsEpochTimestamp scMLSData, - pscCipherSuite = cnvmlsCipherSuite scMLSData, + pscActiveData = cnvmlsActiveData scMLSData, pscMembers = members } @@ -215,14 +208,8 @@ 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} - } +convOrSubConvActivate :: ActiveMLSConversationData -> ConvOrSubConv -> ConvOrSubConv +convOrSubConvActivate activeData (Conv c) = + Conv $ c {mcMLSData = (mcMLSData c) {cnvmlsActiveData = Just activeData}} +convOrSubConvActivate activeData (SubConv c s) = + SubConv c $ s {scMLSData = (scMLSData s) {cnvmlsActiveData = Just activeData}} diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 6341091e356..487e6893c85 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -34,6 +34,7 @@ conversationAPI = mkNamedAPI @"get-unqualified-conversation" getUnqualifiedConversation <@> mkNamedAPI @"get-unqualified-conversation-legalhold-alias" getUnqualifiedConversation <@> mkNamedAPI @"get-conversation@v2" (callsFed (exposeAnnotations getConversation)) + <@> mkNamedAPI @"get-conversation@v5" (callsFed (exposeAnnotations getConversation)) <@> mkNamedAPI @"get-conversation" (callsFed (exposeAnnotations getConversation)) <@> mkNamedAPI @"get-conversation-roles" getConversationRoles <@> mkNamedAPI @"get-group-info" (callsFed (exposeAnnotations getGroupInfo)) @@ -43,13 +44,17 @@ conversationAPI = <@> mkNamedAPI @"get-conversations" getConversations <@> mkNamedAPI @"list-conversations@v1" (callsFed (exposeAnnotations listConversations)) <@> mkNamedAPI @"list-conversations@v2" (callsFed (exposeAnnotations listConversations)) + <@> mkNamedAPI @"list-conversations@v5" (callsFed (exposeAnnotations listConversations)) <@> mkNamedAPI @"list-conversations" (callsFed (exposeAnnotations listConversations)) <@> mkNamedAPI @"get-conversation-by-reusable-code" getConversationByReusableCode <@> mkNamedAPI @"create-group-conversation@v2" (callsFed (exposeAnnotations createGroupConversationUpToV3)) <@> mkNamedAPI @"create-group-conversation@v3" (callsFed (exposeAnnotations createGroupConversationUpToV3)) + <@> mkNamedAPI @"create-group-conversation@v5" (callsFed (exposeAnnotations createGroupConversation)) <@> mkNamedAPI @"create-group-conversation" (callsFed (exposeAnnotations createGroupConversation)) <@> mkNamedAPI @"create-self-conversation@v2" createProteusSelfConversation + <@> mkNamedAPI @"create-self-conversation@v5" createProteusSelfConversation <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation + <@> mkNamedAPI @"get-mls-self-conversation@v5" getMLSSelfConversationWithError <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError <@> mkNamedAPI @"get-subconversation" (callsFed getSubConversation) <@> mkNamedAPI @"leave-subconversation" (callsFed leaveSubConversation) @@ -57,6 +62,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@v5" getMLSOne2OneConversation <@> mkNamedAPI @"get-one-to-one-mls-conversation" getMLSOne2OneConversation <@> mkNamedAPI @"add-members-to-conversation-unqualified" (callsFed addMembersUnqualified) <@> mkNamedAPI @"add-members-to-conversation-unqualified2" (callsFed addMembersUnqualifiedV2) diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 0c8d4df22fb..0e9b8b1ee4a 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -1048,7 +1048,7 @@ conversationExisted :: ) => Local UserId -> Data.Conversation -> - Sem r ConversationResponse + Sem r (ConversationResponse Conversation) conversationExisted lusr cnv = Existed <$> conversationView lusr cnv getLocalUsers :: Domain -> NonEmpty (Qualified UserId) -> [UserId] diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index e685085b0a0..2fb941c3e7b 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -77,18 +77,11 @@ createMLSSelfConversation lusr = do } meta = ncMetadata nc gid = convToGroupId . groupIdParts meta.cnvmType . fmap Conv . tUntagged . qualifyAs lusr $ cnv - -- FUTUREWORK: Stop hard-coding the cipher suite - -- - -- 'CipherSuite 1' corresponds to - -- 'MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519'. - cs = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 proto = ProtocolMLS ConversationMLSData { cnvmlsGroupId = gid, - cnvmlsEpoch = Epoch 0, - cnvmlsEpochTimestamp = Nothing, - cnvmlsCipherSuite = cs + cnvmlsActiveData = Nothing } retry x5 . batch $ do setType BatchLogged @@ -104,8 +97,7 @@ createMLSSelfConversation lusr = do cnvmTeam meta, cnvmMessageTimer meta, cnvmReceiptMode meta, - Just gid, - Just cs + Just gid ) (lmems, rmems) <- addMembers cnv (ncUsers nc) @@ -122,27 +114,16 @@ createMLSSelfConversation lusr = do createConversation :: Local ConvId -> NewConversation -> Client Conversation createConversation lcnv nc = do let meta = ncMetadata nc - (proto, mgid, mep, mcs) = case ncProtocol nc of - BaseProtocolProteusTag -> (ProtocolProteus, Nothing, Nothing, Nothing) + (proto, mgid) = case ncProtocol nc of + BaseProtocolProteusTag -> (ProtocolProteus, Nothing) BaseProtocolMLSTag -> let gid = convToGroupId . groupIdParts meta.cnvmType $ Conv <$> tUntagged lcnv - ep = Epoch 0 - cs = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 in ( ProtocolMLS ConversationMLSData { cnvmlsGroupId = gid, - cnvmlsEpoch = ep, - cnvmlsEpochTimestamp = Nothing, - cnvmlsCipherSuite = cs + cnvmlsActiveData = Nothing }, - Just gid, - Just ep, - -- FUTUREWORK: Make the cipher suite be a record field in - -- 'NewConversation' instead of hard-coding it here. - -- - -- 'CipherSuite 1' corresponds to - -- 'MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519'. - Just cs + Just gid ) retry x5 . batch $ do setType BatchLogged @@ -159,9 +140,7 @@ createConversation lcnv nc = do cnvmMessageTimer meta, cnvmReceiptMode meta, baseProtocolToProtocol (ncProtocol nc), - mgid, - mep, - mcs + mgid ) for_ (cnvmTeam meta) $ \tid -> addPrepQuery Cql.insertTeamConv (tid, tUnqualified lcnv) (lmems, rmems) <- addMembers (tUnqualified lcnv) (ncUsers nc) @@ -355,15 +334,12 @@ toConversationMLSData :: Maybe GroupId -> Maybe Epoch -> Maybe UTCTime -> Maybe 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 - toTimestamp (Just (Epoch 0)) _ = Nothing - toTimestamp (Just _) ts = ts + <*> pure + ( ActiveMLSConversationData + <$> mepoch + <*> mtimestamp + <*> mcs + ) toConv :: ConvId -> diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index ef6e26f5a4a..588053ada63 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -240,8 +240,8 @@ 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, 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" +insertConv :: PrepQuery W (ConvId, ConvType, Maybe UserId, C.Set Access, C.Set AccessRole, Maybe Text, Maybe TeamId, Maybe Milliseconds, Maybe ReceiptMode, ProtocolTag, Maybe GroupId) () +insertConv = "insert into conversation (conv, type, creator, access, access_roles_v2, name, team, message_timer, receipt_mode, protocol, group_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" insertMLSSelfConv :: PrepQuery @@ -255,18 +255,17 @@ insertMLSSelfConv :: Maybe TeamId, Maybe Milliseconds, Maybe ReceiptMode, - Maybe GroupId, - Maybe CipherSuiteTag + Maybe GroupId ) () insertMLSSelfConv = fromString $ "insert into conversation (conv, type, creator, access, \ \ access_roles_v2, name, team, message_timer, receipt_mode,\ - \ protocol, group_id, cipher_suite) values \ + \ protocol, group_id) values \ \ (?, ?, ?, ?, ?, ?, ?, ?, ?, " <> show (fromEnum ProtocolMLSTag) - <> ", ?, ?)" + <> ", ?)" updateToMixedConv :: PrepQuery W (ConvId, ProtocolTag, GroupId, Epoch, CipherSuiteTag) () updateToMixedConv = @@ -344,8 +343,8 @@ deleteUserConv = "delete from user where user = ? and conv = ?" 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) () -insertSubConversation = "INSERT INTO subconversation (conv_id, subconv_id, cipher_suite, epoch, group_id, public_group_state) VALUES (?, ?, ?, ?, ?, ?)" +insertSubConversation :: PrepQuery W (ConvId, SubConvId, Epoch, GroupId, Maybe GroupInfoData) () +insertSubConversation = "INSERT INTO subconversation (conv_id, subconv_id, epoch, group_id, public_group_state) VALUES (?, ?, ?, ?, ?)" updateSubConvGroupInfo :: PrepQuery W (ConvId, SubConvId, Maybe GroupInfoData) () updateSubConvGroupInfo = "INSERT INTO subconversation (conv_id, subconv_id, public_group_state) VALUES (?, ?, ?)" diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index 4a00cf0a29e..4d775d02b99 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -26,7 +26,6 @@ import Control.Error.Util import Control.Monad.Trans.Maybe import Data.Id import Data.Map qualified as Map -import Data.Time.Clock import Galley.API.MLS.Types import Galley.Cassandra.Conversation.MLS import Galley.Cassandra.Queries qualified as Cql @@ -48,9 +47,11 @@ 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 + let activeData = + ActiveMLSConversationData + <$> mEpoch + <*> fmap writetimeToUTC mEpochWritetime + <*> mSuite groupId <- hoistMaybe mGroupId (cm, im) <- lift $ lookupMLSClientLeafIndices groupId pure $ @@ -60,9 +61,7 @@ selectSubConversation convId subConvId = runMaybeT $ do scMLSData = ConversationMLSData { cnvmlsGroupId = groupId, - cnvmlsEpoch = epoch, - cnvmlsEpochTimestamp = epochTimestamp epoch epochWritetime, - cnvmlsCipherSuite = suite + cnvmlsActiveData = activeData }, scMembers = cm, scIndexMap = im @@ -71,20 +70,19 @@ selectSubConversation convId subConvId = runMaybeT $ do insertSubConversation :: ConvId -> SubConvId -> - CipherSuiteTag -> GroupId -> Client SubConversation -insertSubConversation convId subConvId suite groupId = do +insertSubConversation convId subConvId groupId = do retry x5 ( write Cql.insertSubConversation ( params LocalQuorum - (convId, subConvId, suite, Epoch 0, groupId, Nothing) + (convId, subConvId, Epoch 0, groupId, Nothing) ) ) - pure (newSubConversation convId subConvId suite groupId) + pure (newSubConversation convId subConvId groupId) updateSubConvGroupInfo :: ConvId -> SubConvId -> Maybe GroupInfoData -> Client () updateSubConvGroupInfo convId subConvId mGroupInfo = @@ -115,13 +113,21 @@ listSubConversations cid = do subs <- retry x1 (query Cql.listSubConversations (params LocalQuorum (Identity cid))) pure . Map.fromList $ do (subId, cs, epoch, ts, gid) <- subs + let activeData = case (epoch, ts) of + (Epoch 0, _) -> Nothing + (_, Writetime t) -> + Just + ActiveMLSConversationData + { epoch = epoch, + epochTimestamp = t, + ciphersuite = cs + } + pure ( subId, ConversationMLSData { cnvmlsGroupId = gid, - cnvmlsEpoch = epoch, - cnvmlsEpochTimestamp = epochTimestamp epoch ts, - cnvmlsCipherSuite = cs + cnvmlsActiveData = activeData } ) @@ -133,9 +139,9 @@ interpretSubConversationStoreToCassandra :: Sem (SubConversationStore ': r) a -> Sem r a interpretSubConversationStoreToCassandra = interpret $ \case - CreateSubConversation convId subConvId suite groupId -> do + CreateSubConversation convId subConvId groupId -> do logEffect "SubConversationStore.CreateSubConversation" - embedClient (insertSubConversation convId subConvId suite groupId) + embedClient (insertSubConversation convId subConvId groupId) GetSubConversation convId subConvId -> do logEffect "SubConversationStore.GetSubConversation" embedClient (selectSubConversation convId subConvId) @@ -160,10 +166,3 @@ interpretSubConversationStoreToCassandra = interpret $ \case DeleteSubConversation convId subConvId -> do logEffect "SubConversationStore.DeleteSubConversation" embedClient (deleteSubConversation convId subConvId) - --------------------------------------------------------------------------------- --- Utilities - -epochTimestamp :: Epoch -> Writetime Epoch -> Maybe UTCTime -epochTimestamp (Epoch 0) _ = Nothing -epochTimestamp _ (Writetime t) = Just t diff --git a/services/galley/src/Galley/Effects/SubConversationStore.hs b/services/galley/src/Galley/Effects/SubConversationStore.hs index 2179781b134..cc25e5ac4a2 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 -> GroupId -> SubConversationStore m SubConversation + CreateSubConversation :: ConvId -> SubConvId -> 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 9c61172ec0c..dcb01c32c56 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -57,7 +57,6 @@ 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.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation @@ -1868,7 +1867,7 @@ testAddClientSubConvFailure = do assertEqual "The subconversation epoch has moved beyond 1" (Epoch 1) - (pscEpoch finalSub) + (fromJust (pscActiveData finalSub)).epoch -- FUTUREWORK: implement the following test @@ -1969,9 +1968,7 @@ testGetRemoteSubConv isAMember = do { pscParentConvId = qconv, pscSubConvId = sconv, pscGroupId = GroupId "deadbeef", - pscEpoch = Epoch 0, - pscEpochTimestamp = Nothing, - pscCipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519, + pscActiveData = Nothing, pscMembers = [] } let mock = do @@ -2073,7 +2070,8 @@ testJoinDeletedSubConvWithRemoval = do responseJsonError =<< getSubConv (qUnqualified bob) qcnv subConvId