Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2-features/WPB-26018
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add option to manually finish Proteus->MLS for single conversations migration.
22 changes: 7 additions & 15 deletions docs/src/understand/team-feature-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:

Expand All @@ -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
```
81 changes: 80 additions & 1 deletion integration/test/Test/MLS.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
10 changes: 7 additions & 3 deletions libs/wire-api/src/Wire/API/Team/Feature.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions tools/db/migrate-features/src/Work.hs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ writeFeatures
( MlsMigrationConfig @Covered
(fmap unOptionalUTCTime mls_migration_start_time)
(fmap unOptionalUTCTime mls_migration_finalise_regardless_after)
Nothing
)
}

Expand Down
Loading