Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Session manager] Multi-session signout (PSG-857) #7456

Merged
merged 25 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
97cfc7d
Adding changelog entry
Oct 20, 2022
f45cc71
Adding new menu entry for multi signout
Oct 20, 2022
1ed92e5
Adding overflow menu capability in sessions list header view
Oct 20, 2022
ae4a728
Handling press on multi signout action in other sessions list screen
Oct 20, 2022
810c93c
Handling press on multi signout action from header menu in other sess…
Oct 20, 2022
7e836c0
Updating the action title to include sessions number
Oct 24, 2022
bb262f0
Adding new "delete_devices" request API
Oct 24, 2022
1bda543
Calling signout multi sessions use case in other sessions screen
Oct 24, 2022
0f8e591
Calling signout multi sessions use case in main screen for other sess…
Oct 24, 2022
727c746
Adding confirmation dialog before signout process
Oct 25, 2022
a968ac0
Adding unit tests for signout sessions use case
Oct 25, 2022
5bcf2ac
Adding unit tests for other sessions list view model
Oct 25, 2022
880ee40
Adding unit tests about reAuth actions for devices view model
Oct 25, 2022
a3df90a
Adding unit tests about multi signout action for devices view model
Oct 25, 2022
e0d511a
Fixing a name of a mocked component
Oct 25, 2022
4b0b335
Fixing code quality issues
Oct 25, 2022
76e2b6b
Removing some TODOs
Oct 25, 2022
db42d1c
Fix post rebase unit tests
Oct 26, 2022
ef5aaf7
Fix forbidden usage of AlertDialog
Oct 26, 2022
d2d9da3
Exclude the current session from other sessions and security recommen…
Oct 26, 2022
3c7ba85
Removing unused string
Oct 26, 2022
5515cd3
Use SHOW_AS_ACTION_IF_ROOM tag
Oct 26, 2022
1d2b8e7
Adding min size annotation to task params
Nov 7, 2022
45050e8
Removing error formatting from ViewModel
Nov 7, 2022
6d26208
Moving UI auth interceptor into use case
Nov 7, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.matrix.android.sdk.internal.crypto.tasks

import androidx.annotation.Size
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.uia.UiaResult
Expand All @@ -31,7 +32,7 @@ import javax.inject.Inject

internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
data class Params(
val deviceIds: List<String>,
@Size(min = 1) val deviceIds: List<String>,
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
val userAuthParam: UIABaseAuth?
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,26 @@ import com.airbnb.mvrx.Success
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.auth.PendingAuthHandler
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber
import javax.net.ssl.HttpsURLConnection
import kotlin.coroutines.Continuation

class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState,
activeSessionHolder: ActiveSessionHolder,
private val stringProvider: StringProvider,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
Expand Down Expand Up @@ -146,16 +137,14 @@ class DevicesViewModel @AssistedInject constructor(
if (deviceIds.isEmpty()) {
return@launch
}
val signoutResult = signout(deviceIds)
val result = signout(deviceIds)
setLoading(false)

if (signoutResult.isSuccess) {
val error = result.exceptionOrNull()
if (error == null) {
onSignoutSuccess()
} else {
when (val failure = signoutResult.exceptionOrNull()) {
null -> onSignoutSuccess()
else -> onSignoutFailure(failure)
}
onSignoutFailure(error)
}
}
}
Expand All @@ -167,16 +156,9 @@ class DevicesViewModel @AssistedInject constructor(
.orEmpty()
}

private suspend fun signout(deviceIds: List<String>) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) {
is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result)
is SignoutSessionResult.Completed -> Unit
}
}
})
private suspend fun signout(deviceIds: List<String>) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded)

private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) {
private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
Timber.d("onReAuthNeeded")
pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation
Expand All @@ -195,12 +177,7 @@ class DevicesViewModel @AssistedInject constructor(

private fun onSignoutFailure(failure: Throwable) {
Timber.e("signout failure", failure)
val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {
stringProvider.getString(R.string.authentication_error)
} else {
stringProvider.getString(R.string.matrix_error)
}
_viewEvents.post(DevicesViewEvent.SignoutError(Exception(failureMessage)))
_viewEvents.post(DevicesViewEvent.SignoutError(failure))
}

private fun handleSsoAuthDone() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class OtherSessionsFragment :
} else {
viewState.devices.invoke()?.isNotEmpty().orFalse()
}
val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER
val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER
multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT)
changeTextColorOfDestructiveAction(multiSignoutItem)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,26 @@ import com.airbnb.mvrx.Success
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.auth.PendingAuthHandler
import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber
import javax.net.ssl.HttpsURLConnection
import kotlin.coroutines.Continuation

class OtherSessionsViewModel @AssistedInject constructor(
@Assisted private val initialState: OtherSessionsViewState,
activeSessionHolder: ActiveSessionHolder,
private val stringProvider: StringProvider,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val signoutSessionsUseCase: SignoutSessionsUseCase,
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
private val pendingAuthHandler: PendingAuthHandler,
refreshDevicesUseCase: RefreshDevicesUseCase
) : VectorSessionsListViewModel<OtherSessionsViewState, OtherSessionsAction, OtherSessionsViewEvents>(
Expand Down Expand Up @@ -173,16 +162,14 @@ class OtherSessionsViewModel @AssistedInject constructor(
if (deviceIds.isEmpty()) {
return@launch
}
val signoutResult = signout(deviceIds)
val result = signout(deviceIds)
setLoading(false)

if (signoutResult.isSuccess) {
val error = result.exceptionOrNull()
if (error == null) {
onSignoutSuccess()
} else {
when (val failure = signoutResult.exceptionOrNull()) {
null -> onSignoutSuccess()
else -> onSignoutFailure(failure)
}
onSignoutFailure(error)
}
}
}
Expand All @@ -195,16 +182,9 @@ class OtherSessionsViewModel @AssistedInject constructor(
}.mapNotNull { it.deviceInfo.deviceId }
}

private suspend fun signout(deviceIds: List<String>) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) {
is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result)
is SignoutSessionResult.Completed -> Unit
}
}
})
private suspend fun signout(deviceIds: List<String>) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded)

private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) {
private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
Timber.d("onReAuthNeeded")
pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation
Expand All @@ -223,12 +203,7 @@ class OtherSessionsViewModel @AssistedInject constructor(

private fun onSignoutFailure(failure: Throwable) {
Timber.e("signout failure", failure)
val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {
stringProvider.getString(R.string.authentication_error)
} else {
stringProvider.getString(R.string.matrix_error)
}
_viewEvents.post(OtherSessionsViewEvents.SignoutError(Exception(failureMessage)))
_viewEvents.post(OtherSessionsViewEvents.SignoutError(failure))
}

private fun handleSsoAuthDone() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,42 +21,33 @@ import com.airbnb.mvrx.Success
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.auth.PendingAuthHandler
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber
import javax.net.ssl.HttpsURLConnection
import kotlin.coroutines.Continuation

class SessionOverviewViewModel @AssistedInject constructor(
@Assisted val initialState: SessionOverviewViewState,
private val stringProvider: StringProvider,
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
private val signoutSessionUseCase: SignoutSessionUseCase,
private val signoutSessionsUseCase: SignoutSessionsUseCase,
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
private val pendingAuthHandler: PendingAuthHandler,
private val activeSessionHolder: ActiveSessionHolder,
Expand Down Expand Up @@ -154,30 +145,21 @@ class SessionOverviewViewModel @AssistedInject constructor(
private fun handleSignoutOtherSession(deviceId: String) {
viewModelScope.launch {
setLoading(true)
val signoutResult = signout(deviceId)
val result = signout(deviceId)
setLoading(false)

if (signoutResult.isSuccess) {
val error = result.exceptionOrNull()
if (error == null) {
onSignoutSuccess()
} else {
when (val failure = signoutResult.exceptionOrNull()) {
null -> onSignoutSuccess()
else -> onSignoutFailure(failure)
}
onSignoutFailure(error)
}
}
}

private suspend fun signout(deviceId: String) = signoutSessionUseCase.execute(deviceId, object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) {
is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result)
is SignoutSessionResult.Completed -> Unit
}
}
})
private suspend fun signout(deviceId: String) = signoutSessionsUseCase.execute(listOf(deviceId), this::onReAuthNeeded)

private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) {
private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
Timber.d("onReAuthNeeded")
pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation
Expand All @@ -196,12 +178,7 @@ class SessionOverviewViewModel @AssistedInject constructor(

private fun onSignoutFailure(failure: Throwable) {
Timber.e("signout failure", failure)
val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {
stringProvider.getString(R.string.authentication_error)
} else {
stringProvider.getString(R.string.matrix_error)
}
_viewEvents.post(SessionOverviewViewEvent.SignoutError(Exception(failureMessage)))
_viewEvents.post(SessionOverviewViewEvent.SignoutError(failure))
}

private fun handleSsoAuthDone() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,16 @@ class InterceptSignoutFlowResponseUseCase @Inject constructor(
flowResponse: RegistrationFlowResponse,
errCode: String?,
promise: Continuation<UIABaseAuth>
): SignoutSessionResult {
): SignoutSessionsReAuthNeeded? {
return if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) {
UserPasswordAuth(
session = null,
user = activeSessionHolder.getActiveSession().myUserId,
password = reAuthHelper.data
).let { promise.resume(it) }

SignoutSessionResult.Completed
null
} else {
SignoutSessionResult.ReAuthNeeded(
SignoutSessionsReAuthNeeded(
pendingAuth = DefaultBaseAuth(session = flowResponse.session),
uiaContinuation = promise,
flowResponse = flowResponse,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,9 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import kotlin.coroutines.Continuation

sealed class SignoutSessionResult {
data class ReAuthNeeded(
val pendingAuth: UIABaseAuth,
val uiaContinuation: Continuation<UIABaseAuth>,
val flowResponse: RegistrationFlowResponse,
val errCode: String?
) : SignoutSessionResult()

object Completed : SignoutSessionResult()
}
data class SignoutSessionsReAuthNeeded(
val pendingAuth: UIABaseAuth,
val uiaContinuation: Continuation<UIABaseAuth>,
val flowResponse: RegistrationFlowResponse,
val errCode: String?
)
Loading