Stop showing the sliding sync proxy alert to any remaining users.

The session will simply fail to restore and the restoration token deleted.
This commit is contained in:
Doug
2025-07-30 12:18:10 +01:00
committed by Doug
parent df335bd2b1
commit 5762e1c537
10 changed files with 89 additions and 128 deletions
@@ -74,7 +74,6 @@ extension ClientProxyMock {
ignoreUserReturnValue = .success(())
unignoreUserReturnValue = .success(())
needsSlidingSyncMigration = false
slidingSyncVersion = .native
trackRecentlyVisitedRoomReturnValue = .success(())
@@ -2093,11 +2093,6 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
set(value) { underlyingHomeserver = value }
}
var underlyingHomeserver: String!
var needsSlidingSyncMigration: Bool {
get { return underlyingNeedsSlidingSyncMigration }
set(value) { underlyingNeedsSlidingSyncMigration = value }
}
var underlyingNeedsSlidingSyncMigration: Bool!
var slidingSyncVersion: SlidingSyncVersion {
get { return underlyingSlidingSyncVersion }
set(value) { underlyingSlidingSyncVersion = value }
@@ -133,10 +133,6 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
setupRoomListSubscriptions()
updateRooms()
Task {
await checkSlidingSyncMigration()
}
}
// MARK: - Public
@@ -318,24 +314,6 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
state.rooms = rooms
}
/// Check whether we can inform the user about potential migrations
/// or have him logout as his proxy is no longer available
private func checkSlidingSyncMigration() async {
guard userSession.clientProxy.needsSlidingSyncMigration else {
return
}
// The proxy is no longer supported so a logout is needed.
// Delay setting the alert otherwise it automatically gets dismissed. Same as the crashed last run one
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.state.bindings.alertInfo = AlertInfo(id: UUID(),
title: L10n.bannerMigrateToNativeSlidingSyncAppForceLogoutTitle(InfoPlistReader.main.bundleDisplayName),
primaryButton: .init(title: L10n.bannerMigrateToNativeSlidingSyncAction) { [weak self] in
self?.actionsSubject.send(.logoutWithoutConfirmation)
})
}
}
private func markRoomAsFavourite(_ roomID: String, isFavourite: Bool) async {
guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Failed retrieving room for identifier: \(roomID)")
@@ -150,7 +150,6 @@ class ClientProxy: ClientProxyProtocol {
private let sendQueueStatusSubject = CurrentValueSubject<Bool, Never>(false)
init(client: ClientProtocol,
needsSlidingSyncMigration: Bool,
networkMonitor: NetworkMonitorProtocol,
appSettings: AppSettings) async throws {
self.client = client
@@ -165,8 +164,6 @@ class ClientProxy: ClientProxyProtocol {
secureBackupController = SecureBackupController(encryption: client.encryption())
self.needsSlidingSyncMigration = needsSlidingSyncMigration
let configuredAppService = try await ClientProxyServices(client: client,
actionsSubject: actionsSubject,
notificationSettings: notificationSettings,
@@ -271,7 +268,6 @@ class ClientProxy: ClientProxyProtocol {
client.homeserver()
}
let needsSlidingSyncMigration: Bool
var slidingSyncVersion: SlidingSyncVersion {
client.slidingSyncVersion()
}
@@ -341,11 +337,6 @@ class ClientProxy: ClientProxyProtocol {
}
func startSync() {
guard !needsSlidingSyncMigration else {
MXLog.warning("Ignoring request, this client needs to be migrated to native sliding sync.")
return
}
guard !hasEncounteredAuthError else {
MXLog.warning("Ignoring request, this client has an unknown token.")
return
@@ -86,9 +86,6 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
var homeserver: String { get }
// TODO: This is a temporary value, in the future we should throw a migration error
// when decoding a session that contains a sliding sync proxy URL instead of restoring it.
var needsSlidingSyncMigration: Bool { get }
var slidingSyncVersion: SlidingSyncVersion { get }
var canDeactivateAccount: Bool { get }
@@ -54,8 +54,12 @@ class KeychainController: KeychainControllerProtocol {
guard let tokenData = try restorationTokenKeychain.getData(username) else {
return nil
}
return try JSONDecoder().decode(RestorationToken.self, from: tokenData)
} catch RestorationTokenError.slidingSyncProxyNotSupported {
MXLog.error("Unsupported user restore token (contains sliding sync proxy). Deleting token.")
removeRestorationTokenForUsername(username)
return nil
} catch {
MXLog.error("Failed retrieving user restore token")
return nil
@@ -112,8 +116,7 @@ class KeychainController: KeychainControllerProtocol {
let restorationToken = RestorationToken(session: session,
sessionDirectories: oldToken.sessionDirectories,
passphrase: oldToken.passphrase,
pusherNotificationClientIdentifier: oldToken.pusherNotificationClientIdentifier,
slidingSyncProxyURLString: oldToken.slidingSyncProxyURLString)
pusherNotificationClientIdentifier: oldToken.pusherNotificationClientIdentifier)
setRestorationToken(restorationToken, forUsername: session.userId)
}
@@ -8,19 +8,16 @@
import Foundation
import MatrixRustSDK
enum RestorationTokenError: Error {
case slidingSyncProxyNotSupported
}
struct RestorationToken: Equatable {
let session: MatrixRustSDK.Session
let sessionDirectories: SessionDirectories
let passphrase: String?
let pusherNotificationClientIdentifier: String?
/// The sliding sync proxy URL that was previously encoded in the Session.
/// This is temporary to help make a nicer user migration flow. In the future
/// we will throw when decoding sessions with a sliding sync proxy URL.
let slidingSyncProxyURLString: String?
/// Whether the token is for a session that is using the now unsupported sliding sync proxy.
var needsSlidingSyncMigration: Bool { slidingSyncProxyURLString != nil }
enum CodingKeys: CodingKey {
case session
case sessionDirectory
@@ -34,7 +31,7 @@ extension RestorationToken: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let sessionWrapper = try container.decode(SessionWrapper.self, forKey: .session)
let session = try container.decode(Session.self, forKey: .session)
let dataDirectory = try container.decodeIfPresent(URL.self, forKey: .sessionDirectory)
let cacheDirectory = try container.decodeIfPresent(URL.self, forKey: .cacheDirectory)
@@ -45,19 +42,18 @@ extension RestorationToken: Codable {
SessionDirectories(dataDirectory: dataDirectory)
}
} else {
SessionDirectories(userID: sessionWrapper.session.userId)
SessionDirectories(userID: session.userId)
}
self = try .init(session: sessionWrapper.session,
self = try .init(session: session,
sessionDirectories: sessionDirectories,
passphrase: container.decodeIfPresent(String.self, forKey: .passphrase),
pusherNotificationClientIdentifier: container.decodeIfPresent(String.self, forKey: .pusherNotificationClientIdentifier),
slidingSyncProxyURLString: sessionWrapper.slidingSyncProxyURLString)
pusherNotificationClientIdentifier: container.decodeIfPresent(String.self, forKey: .pusherNotificationClientIdentifier))
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(SessionWrapper(session: session, slidingSyncProxyURLString: slidingSyncProxyURLString), forKey: .session)
try container.encode(session, forKey: .session)
try container.encode(sessionDirectories.dataDirectory, forKey: .sessionDirectory)
try container.encode(sessionDirectories.cacheDirectory, forKey: .cacheDirectory)
try container.encode(passphrase, forKey: .passphrase)
@@ -65,33 +61,20 @@ extension RestorationToken: Codable {
}
}
/// Temporary struct to smooth the forced migration by keeping a user session.
/// In the future we can remove this and throw a migration error when the URL
/// is decoded to a non-nil value.
private struct SessionWrapper {
let session: MatrixRustSDK.Session
let slidingSyncProxyURLString: String?
}
extension SessionWrapper: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MatrixRustSDK.Session.CodingKeys.self)
session = try Session(from: decoder)
// TODO: In the future we should decode this in the Session and throw a migration error if it contains a value.
slidingSyncProxyURLString = try container.decodeIfPresent(String.self, forKey: .slidingSyncProxy)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: MatrixRustSDK.Session.CodingKeys.self)
try session.encode(to: encoder)
try container.encode(slidingSyncProxyURLString, forKey: .slidingSyncProxy)
}
}
extension MatrixRustSDK.Session: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// The SDK no longer supports sliding sync proxies. We must bail here otherwise the
// session would be restored using native sliding sync, but if the proxy was still
// active its sync loop would be stealing keys away from us.
//
// Note: As tempting as it is to use container.contains(.slidingSyncProxy), that will always
// return true as our coding keys have resulted in the key being encoded with a nil value 🤦‍♂️.
if (try? container.decodeIfPresent(String.self, forKey: .slidingSyncProxy)) != nil {
throw RestorationTokenError.slidingSyncProxyNotSupported
}
self = try .init(accessToken: container.decode(String.self, forKey: .accessToken),
refreshToken: container.decodeIfPresent(String.self, forKey: .refreshToken),
userId: container.decode(String.self, forKey: .userId),
@@ -64,13 +64,12 @@ class UserSessionStore: UserSessionStoreProtocol {
do {
let session = try client.session()
let userID = try client.userId()
let clientProxy = try await setupProxyForClient(client, needsSlidingSyncMigration: false)
let clientProxy = try await setupProxyForClient(client)
keychainController.setRestorationToken(RestorationToken(session: session,
sessionDirectories: sessionDirectories,
passphrase: passphrase,
pusherNotificationClientIdentifier: clientProxy.pusherNotificationClientIdentifier,
slidingSyncProxyURLString: nil),
pusherNotificationClientIdentifier: clientProxy.pusherNotificationClientIdentifier),
forUsername: userID)
MXLog.info("Set up session for user \(userID) at: \(sessionDirectories)")
@@ -141,7 +140,7 @@ class UserSessionStore: UserSessionStoreProtocol {
Task(priority: .low) { await appHooks.remoteSettingsHook.updateCache(using: client) }
return try await .success(setupProxyForClient(client, needsSlidingSyncMigration: credentials.restorationToken.needsSlidingSyncMigration))
return try await .success(setupProxyForClient(client))
} catch UserSessionStoreError.failedSettingUpClientProxy(let error) {
// If this has failed, there is likely something wrong with the creation of the sync service
// There is nothing we can do, but at the same time we don't want the user to the get logged out
@@ -153,10 +152,9 @@ class UserSessionStore: UserSessionStoreProtocol {
}
}
private func setupProxyForClient(_ client: ClientProtocol, needsSlidingSyncMigration: Bool) async throws -> ClientProxyProtocol {
private func setupProxyForClient(_ client: ClientProtocol) async throws -> ClientProxyProtocol {
do {
return try await ClientProxy(client: client,
needsSlidingSyncMigration: needsSlidingSyncMigration,
networkMonitor: networkMonitor,
appSettings: appSettings)
} catch {
+31 -27
View File
@@ -6,6 +6,7 @@
//
@testable import ElementX
import KeychainAccess
import XCTest
class KeychainControllerTests: XCTestCase {
@@ -33,8 +34,7 @@ class KeychainControllerTests: XCTestCase {
slidingSyncVersion: .native),
sessionDirectories: .init(),
passphrase: "passphrase",
pusherNotificationClientIdentifier: "pusherClientID",
slidingSyncProxyURLString: "https://my.sync.proxy")
pusherNotificationClientIdentifier: "pusherClientID")
keychain.setRestorationToken(restorationToken, forUsername: username)
// Then the restoration token should be stored in the keychain.
@@ -53,8 +53,7 @@ class KeychainControllerTests: XCTestCase {
slidingSyncVersion: .native),
sessionDirectories: .init(),
passphrase: "passphrase",
pusherNotificationClientIdentifier: "pusherClientID",
slidingSyncProxyURLString: "https://my.sync.proxy")
pusherNotificationClientIdentifier: "pusherClientID")
keychain.setRestorationToken(restorationToken, forUsername: username)
XCTAssertEqual(keychain.restorationTokens().count, 1, "The keychain should have 1 restoration token.")
XCTAssertEqual(keychain.restorationTokenForUsername(username), restorationToken, "The initial restoration token should match the value that was stored.")
@@ -79,8 +78,7 @@ class KeychainControllerTests: XCTestCase {
slidingSyncVersion: .native),
sessionDirectories: .init(),
passphrase: "passphrase",
pusherNotificationClientIdentifier: "pusherClientID",
slidingSyncProxyURLString: "https://my.sync.proxy")
pusherNotificationClientIdentifier: "pusherClientID")
keychain.setRestorationToken(restorationToken, forUsername: "@test\(index):example.com")
}
XCTAssertEqual(keychain.restorationTokens().count, 5, "The keychain should have 5 restoration tokens.")
@@ -104,8 +102,7 @@ class KeychainControllerTests: XCTestCase {
slidingSyncVersion: .native),
sessionDirectories: .init(),
passphrase: "passphrase",
pusherNotificationClientIdentifier: "pusherClientID",
slidingSyncProxyURLString: "https://my.sync.proxy")
pusherNotificationClientIdentifier: "pusherClientID")
keychain.setRestorationToken(restorationToken, forUsername: "@test\(index):example.com")
}
XCTAssertEqual(keychain.restorationTokens().count, 5, "The keychain should have 5 restoration tokens.")
@@ -122,27 +119,34 @@ class KeychainControllerTests: XCTestCase {
XCTAssertNotNil(keychain.restorationTokenForUsername("@test4:example.com"), "The restoration token should not have been deleted.")
}
func testSimplifiedSlidingSyncRestorationToken() {
// Given an empty keychain.
XCTAssertTrue(keychain.restorationTokens().isEmpty, "The keychain should be empty to begin with.")
func testUnsupportedRestorationToken() {
// Given a keychain with an unsupported restoration token with a sliding sync proxy URL value.
let underlyingKeychain = Keychain(service: KeychainControllerService.tests.restorationTokenID,
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
// Note: We assert with this underlying keychain's keys as keychain.restorationTokens() triggers the deletion that we're testing.
XCTAssertTrue(underlyingKeychain.allKeys().isEmpty, "The keychain should be empty to begin with.")
// When adding an restoration token that doesn't contain a sliding sync proxy (e.g. for SSS).
let username = "@test:example.com"
let restorationToken = RestorationToken(session: .init(accessToken: "accessToken",
refreshToken: "refreshToken",
userId: "userId",
deviceId: "deviceId",
homeserverUrl: "homeserverUrl",
oidcData: "oidcData",
slidingSyncVersion: .native),
sessionDirectories: .init(),
passphrase: "passphrase",
pusherNotificationClientIdentifier: "pusherClientID",
slidingSyncProxyURLString: nil)
keychain.setRestorationToken(restorationToken, forUsername: username)
do {
let unsupportedToken = RestorationTokenV1(session: SessionV1(accessToken: "1234",
refreshToken: nil,
userId: "@test:example.com",
deviceId: "D3V1C3",
homeserverUrl: "https://matrix.example.com",
oidcData: nil,
slidingSyncVersion: .proxy(url: "https://sync.example.com")))
let tokenData = try JSONEncoder().encode(unsupportedToken)
try underlyingKeychain.set(tokenData, key: "@test:example.com")
XCTAssertEqual(underlyingKeychain.allKeys().count, 1)
} catch {
XCTFail("Failed storing user restore token with error: \(error)")
}
// Then decoding the restoration token from the keychain should still work.
XCTAssertEqual(keychain.restorationTokenForUsername(username), restorationToken, "The retrieved restoration token should match the value that was stored.")
// When attempting to retrieve the unsupported token.
let retrievedToken = keychain.restorationTokenForUsername("@test:example.com")
// Then nothing should be returned and the restoration token should be automatically removed.
XCTAssertNil(retrievedToken, "The token should not be decoded.")
XCTAssertTrue(underlyingKeychain.allKeys().isEmpty, "The keychain should be empty again.")
}
func testAddPINCode() throws {
+28 -15
View File
@@ -11,6 +11,31 @@ import XCTest
import MatrixRustSDK
class RestorationTokenTests: XCTestCase {
func testDecodeTokenV1WithSlidingSyncProxy() throws {
// Given an encoded restoration token that contains a session with a sliding sync proxy.
let originalToken = RestorationTokenV1(session: SessionV1(accessToken: "1234",
refreshToken: nil,
userId: "@user:example.com",
deviceId: "D3V1C3",
homeserverUrl: "https://matrix.example.com",
oidcData: nil,
slidingSyncVersion: .proxy(url: "https://sync.example.com")))
let data = try JSONEncoder().encode(originalToken)
// When decoding the data to the current restoration token format.
XCTAssertThrowsError(try JSONDecoder().decode(RestorationToken.self, from: data)) { error in
// Then an error should be thrown as it is no longer supported.
switch error {
case RestorationTokenError.slidingSyncProxyNotSupported:
break
default:
XCTFail("Unexpected error thrown: \(error)")
}
}
}
/// Honestly not sure if this test is needed or not – it is quite possible that `RestorationTokenV1` was only encoded at a time
/// when the sliding sync proxy was the only option. But lets keep the test for completeness of the session directory migration coverage.
func testDecodeFromTokenV1() throws {
// Given an encoded restoration token in the original format that only contains a Session from the SDK.
let originalToken = RestorationTokenV1(session: SessionV1(accessToken: "1234",
@@ -19,7 +44,7 @@ class RestorationTokenTests: XCTestCase {
deviceId: "D3V1C3",
homeserverUrl: "https://matrix.example.com",
oidcData: nil,
slidingSyncVersion: .proxy(url: "https://sync.example.com")))
slidingSyncVersion: .native))
let data = try JSONEncoder().encode(originalToken)
// When decoding the data to the current restoration token format.
@@ -33,10 +58,6 @@ class RestorationTokenTests: XCTestCase {
"The session directory should match the original location set by the Rust SDK from our base directory.")
XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: "@user_example.com"),
"The cache directory should be derived from the session directory but in the caches directory.")
XCTAssertEqual(decodedToken.slidingSyncProxyURLString, "https://sync.example.com",
"The original sliding sync URL should be preserved in order to trigger the migration prompt.")
XCTAssertTrue(decodedToken.needsSlidingSyncMigration, "The migration flag should be set to true.")
}
func testDecodeFromTokenV4() throws {
@@ -48,7 +69,7 @@ class RestorationTokenTests: XCTestCase {
deviceId: "D3V1C3",
homeserverUrl: "https://matrix.example.com",
oidcData: "data-from-mas",
slidingSyncVersion: .proxy(url: "https://sync.example.com")),
slidingSyncVersion: .native),
sessionDirectory: .sessionsBaseDirectory.appending(component: sessionDirectoryName),
passphrase: "passphrase",
pusherNotificationClientIdentifier: "pusher-identifier")
@@ -66,10 +87,6 @@ class RestorationTokenTests: XCTestCase {
"The session directory should not be changed.")
XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: sessionDirectoryName),
"The cache directory should be derived from the session directory but in the caches directory.")
XCTAssertEqual(decodedToken.slidingSyncProxyURLString, "https://sync.example.com",
"The original sliding sync URL should be preserved in order to trigger the migration prompt.")
XCTAssertTrue(decodedToken.needsSlidingSyncMigration, "The migration flag should be set to true.")
}
func testDecodeFromTokenV5() throws {
@@ -100,9 +117,6 @@ class RestorationTokenTests: XCTestCase {
"The session directory should not be changed.")
XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, originalToken.cacheDirectory,
"The cache directory should not be changed.")
XCTAssertNil(decodedToken.slidingSyncProxyURLString, "No sliding sync proxy URL should be decoded for native sliding sync.")
XCTAssertFalse(decodedToken.needsSlidingSyncMigration, "The migration flag should not be set.")
}
func testDecodeFromCurrentToken() throws {
@@ -116,8 +130,7 @@ class RestorationTokenTests: XCTestCase {
slidingSyncVersion: .native),
sessionDirectories: .init(),
passphrase: "passphrase",
pusherNotificationClientIdentifier: "pusher-identifier",
slidingSyncProxyURLString: nil)
pusherNotificationClientIdentifier: "pusher-identifier")
let data = try JSONEncoder().encode(originalToken)
// When decoding the data.