diff --git a/changelog.d/2-features/WPB-26018 b/changelog.d/2-features/WPB-26018 new file mode 100644 index 00000000000..a1c120f96b9 --- /dev/null +++ b/changelog.d/2-features/WPB-26018 @@ -0,0 +1 @@ +Add option to manually finish Proteus->MLS for single conversations migration. diff --git a/docs/src/understand/team-feature-settings.md b/docs/src/understand/team-feature-settings.md index 0d07d3b5aff..b7c52cfd41f 100644 --- a/docs/src/understand/team-feature-settings.md +++ b/docs/src/understand/team-feature-settings.md @@ -171,7 +171,7 @@ galley: ## MLS Migration The MLS migration configuration determines client behaviour related to -migration from Proteus to MSL, and defines the criteria enforced by the backend +migration from Proteus to MLS, and defines the criteria enforced by the backend when a conversation is finally migrated to MLS. The settings are the following: @@ -183,20 +183,13 @@ The settings are the following: started. - `finaliseRegardlessAfter`: timestamp of the date by which the migration must be finalised. -- `usersThreshold`: percentage of migrated users needed for migration to - finalise (0-100). -- `clientsThreshold`: percentage of migrated clients needed for migration to - finalise (0-100). +- `allowManualMigration`: allows clients to manually finalise MLS migration for + single conversations. The default is `false`. All of the migration finalisation values are technically optional, but at least -one of them must be specified for the configuration to be valid. If -`finaliseRegardlessAfter` is not set, `usersThreshold` or `clientsThreshold` -should be specified. In case both `usersThreshold` and `clientsThreshold` are -specified, even if one of them is set to 0, both have to be fulfilled for the -migration to be finalised. - -The `finaliseRegardlessAfter` timestamp determines a time after which the -threshold criteria are dropped, and finalisation is allowed in any case. +one of them must be specified for the configuration to be valid. The +`finaliseRegardlessAfter` timestamp determines a time after which the threshold +criteria are dropped, and finalisation is allowed in any case. An example configuration follows: @@ -215,7 +208,6 @@ galley: config: startTime: "2024-05-16T00:00:00.000Z" finaliseRegardlessAfter: "2024-10-17T00:00:00.000Z" - usersThreshold: 100 - clientsThreshold: 50 + allowManualMigration: false lockStatus: locked ``` diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index ca8dc088b64..fb8f81a1ef9 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -394,7 +394,9 @@ testMLSProtocolUpgrade secondDomain = do void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed void $ withWebSocket bob $ \ws -> do - -- charlie is added to the group + -- charlie is added to the group, but he does not become part of the + -- conversation, since the Proteus part of the conversation is the source of + -- truth in mixed conversations void $ uploadNewKeyPackage def charlie1 void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed awaitMatch isNewMLSMessageNotif ws @@ -426,6 +428,83 @@ testMLSProtocolUpgrade secondDomain = do resp.json %. "protocol" `shouldMatch` "mls" resp.json %. "receipt_mode" `shouldMatchInt` 0 +testMLSProtocolUpgradeManual :: (HasCallStack) => Domain -> App () +testMLSProtocolUpgradeManual secondDomain = do + (alice, tid, _) <- createTeam OwnDomain 1 + I.patchTeamFeature OwnDomain tid "mls" (object ["status" .= "enabled"]) + >>= assertSuccess + bob <- randomUser secondDomain def + connectUsers [alice, bob] + + conv <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId + + bindResponse (putConversationProtocol bob conv "mixed") $ \resp -> + resp.status `shouldMatchInt` 200 + + convId <- + getConversation alice (convIdToQidObject conv) + >>= getJSON 200 + >>= objConvId + + updateReceiptMode alice convId (9 :: Int) >>= assertSuccess + charlie <- randomUser OwnDomain def + [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] + + -- alice creates MLS group and bob joins + createGroup def alice1 convId + void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + + void $ withWebSocket bob $ \ws -> do + -- charlie is added to the group, but he does not become part of the + -- conversation, since the Proteus part of the conversation is the source of + -- truth in mixed conversations + void $ uploadNewKeyPackage def charlie1 + void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + awaitMatch isNewMLSMessageNotif ws + + -- Without manual migration, the migration criteria must be satisfied. Since + -- bob does not support MLS, the migration from Mixed to MLS is rejected. + supportMLS alice + bindResponse (putConversationProtocol alice convId "mls") $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-migration-criteria-not-satisfied" + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "protocol" `shouldMatch` "mixed" + + -- With manual migration enabled, the migration can be forced even though not + -- all actual members of the Mixed conversation support MLS yet. + -- I.setTeamFeatureLockStatus alice tid "mlsMigration" "unlocked" + I.patchTeamFeature + OwnDomain + tid + "mlsMigration" + ( object + [ "lockStatus" .= "unlocked", + "config" .= object ["allowManualMigration" .= True] + ] + ) + >>= assertSuccess + withWebSockets [alice1, bob1] $ \wss -> do + bindResponse (putConversationProtocol bob convId "mls") $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "data.protocol" `shouldMatch` "mls" + for_ wss $ \ws -> do + n <- awaitMatch isNewMLSMessageNotif ws + msg <- asByteString (nPayload n %. "data") >>= showMessage def alice1 + let leafIndexCharlie = 2 + msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexCharlie + msg %. "message.content.sender.External" `shouldMatchInt` 0 + + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "protocol" `shouldMatch` "mls" + resp.json %. "receipt_mode" `shouldMatchInt` 0 + testAddUserSimple :: (HasCallStack) => Ciphersuite -> CredentialType -> App () testAddUserSimple suite ctype = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index a08c8675f7b..d185e833c1a 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -1449,7 +1449,8 @@ instance IsFeatureConfig MlsE2EIdConfig where data MlsMigrationConfigB t f = MlsMigrationConfig { startTime :: Wear t f (Maybe UTCTime), - finaliseRegardlessAfter :: Wear t f (Maybe UTCTime) + finaliseRegardlessAfter :: Wear t f (Maybe UTCTime), + allowManualMigration :: Wear t f (Maybe Bool) } deriving (BareB, Generic) @@ -1472,16 +1473,18 @@ deriving via (BarbieFeature MlsMigrationConfigB) instance (ToSchema MlsMigration deriving via (RenderableTypeName MlsMigrationConfig) instance (RenderableSymbol MlsMigrationConfig) instance Default MlsMigrationConfig where - def = MlsMigrationConfig Nothing Nothing + def = MlsMigrationConfig Nothing Nothing Nothing instance Arbitrary MlsMigrationConfig where arbitrary = do startTime <- fmap fromUTCTimeMillis <$> arbitrary finaliseRegardlessAfter <- fmap fromUTCTimeMillis <$> arbitrary + allowManualMigration <- arbitrary pure MlsMigrationConfig { startTime = startTime, - finaliseRegardlessAfter = finaliseRegardlessAfter + finaliseRegardlessAfter = finaliseRegardlessAfter, + allowManualMigration = allowManualMigration } instance (Typeable f, NestedMaybe f) => ToSchema (MlsMigrationConfigB Covered f) where @@ -1490,6 +1493,7 @@ instance (Typeable f, NestedMaybe f) => ToSchema (MlsMigrationConfigB Covered f) MlsMigrationConfig <$> startTime .= nestedMaybeField "startTime" (unnamed utcTimeSchema) <*> finaliseRegardlessAfter .= nestedMaybeField "finaliseRegardlessAfter" (unnamed utcTimeSchema) + <*> allowManualMigration .= nestedMaybeField "allowManualMigration" (unnamed schema) instance Default (LockableFeature MlsMigrationConfig) where def = defLockedFeature diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Migration.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Migration.hs index 0922d1588f2..9bbb80f0e48 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Migration.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Migration.hs @@ -59,10 +59,11 @@ checkMigrationCriteria :: Sem r Bool checkMigrationCriteria now conv ws | ws.status == FeatureStatusDisabled = pure False - | afterDeadline = pure True + | afterDeadline || manualMigrationEnabled = pure True | otherwise = unApAll $ mconcat [localUsersMigrated, remoteUsersMigrated] where afterDeadline = maybe False (now >=) ws.config.finaliseRegardlessAfter + manualMigrationEnabled = fromMaybe False ws.config.allowManualMigration containsMLS = Set.member BaseProtocolMLSTag diff --git a/tools/db/migrate-features/src/Work.hs b/tools/db/migrate-features/src/Work.hs index 8aef63d20dd..46837a47990 100644 --- a/tools/db/migrate-features/src/Work.hs +++ b/tools/db/migrate-features/src/Work.hs @@ -337,6 +337,7 @@ writeFeatures ( MlsMigrationConfig @Covered (fmap unOptionalUTCTime mls_migration_start_time) (fmap unOptionalUTCTime mls_migration_finalise_regardless_after) + Nothing ) }