From 320e9eac390d6c35561f494b45dd989a5670ae29 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 21 Sep 2022 11:53:28 +0200 Subject: [PATCH 01/17] Adding changelog entry --- changelog.d/7190.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7190.wip diff --git a/changelog.d/7190.wip b/changelog.d/7190.wip new file mode 100644 index 0000000000..3c70666d91 --- /dev/null +++ b/changelog.d/7190.wip @@ -0,0 +1 @@ +[Device management] Sign out a session From 5380c3078042e48b8609333ff2b28e57db32aeb0 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 22 Sep 2022 17:26:04 +0200 Subject: [PATCH 02/17] Adding signout action and corresponding use cases --- .../v2/overview/SessionOverviewAction.kt | 1 + .../v2/overview/SessionOverviewViewEvent.kt | 5 ++ .../v2/overview/SessionOverviewViewModel.kt | 33 +++++++++++ .../InterceptSignoutFlowResponseUseCase.kt | 59 +++++++++++++++++++ .../v2/signout/SignoutSessionResult.kt | 32 ++++++++++ .../v2/signout/SignoutSessionUseCase.kt | 40 +++++++++++++ 6 files changed, 170 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt index 1118c5dc5b..cea0281570 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt @@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class SessionOverviewAction : VectorViewModelAction { object VerifySession : SessionOverviewAction() + object SignoutSession : SessionOverviewAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewEvent.kt index 8508b395ad..ac64e3dfe6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewEvent.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewEvent.kt @@ -17,9 +17,14 @@ package im.vector.app.features.settings.devices.v2.overview import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse sealed class SessionOverviewViewEvent : VectorViewEvents { object ShowVerifyCurrentSession : SessionOverviewViewEvent() data class ShowVerifyOtherSession(val deviceId: String) : SessionOverviewViewEvent() object PromptResetSecrets : SessionOverviewViewEvent() + data class RequestReAuth( + val registrationFlowResponse: RegistrationFlowResponse, + val lastErrorCode: String? + ) : SessionOverviewViewEvent() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 69556e039e..2f3116f40f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -25,14 +25,22 @@ 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.platform.VectorViewModel +import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.IsCurrentSessionUseCase +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase 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.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import kotlin.coroutines.Continuation class SessionOverviewViewModel @AssistedInject constructor( @Assisted val initialState: SessionOverviewViewState, @@ -40,6 +48,9 @@ class SessionOverviewViewModel @AssistedInject constructor( private val isCurrentSessionUseCase: IsCurrentSessionUseCase, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, + private val signoutSessionUseCase: SignoutSessionUseCase, + private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + private val pendingAuthHandler: PendingAuthHandler, ) : VectorViewModel(initialState) { companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() @@ -83,6 +94,7 @@ class SessionOverviewViewModel @AssistedInject constructor( override fun handle(action: SessionOverviewAction) { when (action) { is SessionOverviewAction.VerifySession -> handleVerifySessionAction() + SessionOverviewAction.SignoutSession -> handleSignoutSession() } } @@ -108,4 +120,25 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleVerifyOtherSession(deviceId: String) { _viewEvents.post(SessionOverviewViewEvent.ShowVerifyOtherSession(deviceId)) } + + // TODO add unit tests + private fun handleSignoutSession() = withState { state -> + // TODO should we do something different when it is current session? + viewModelScope.launch { + signoutSessionUseCase.execute(state.deviceId, object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { + is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) + is SignoutSessionResult.Completed -> Unit // TODO refresh devices list? + post event to close the associated screen + } + } + }) + } + } + + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation + _viewEvents.post(SessionOverviewViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt new file mode 100644 index 0000000000..4e69452ab3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.signout + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.login.ReAuthHelper +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserPasswordAuth +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import javax.inject.Inject +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume + +// TODO add unit tests +class InterceptSignoutFlowResponseUseCase @Inject constructor( + private val reAuthHelper: ReAuthHelper, + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute( + flowResponse: RegistrationFlowResponse, + errCode: String?, + promise: Continuation + ): SignoutSessionResult { + 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 + } else { + SignoutSessionResult.ReAuthNeeded( + pendingAuth = DefaultBaseAuth(session = flowResponse.session), + uiaContinuation = promise, + flowResponse = flowResponse, + errCode = errCode + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt new file mode 100644 index 0000000000..fa1fb31b66 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.signout + +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, + val flowResponse: RegistrationFlowResponse, + val errCode: String? + ) : SignoutSessionResult() + + object Completed : SignoutSessionResult() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt new file mode 100644 index 0000000000..b86dab13cd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.signout + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.util.awaitCallback +import javax.inject.Inject + +// TODO add unit tests +class SignoutSessionUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + suspend fun execute(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { + return deleteDevice(deviceId, userInteractiveAuthInterceptor) + } + + private suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { + awaitCallback { matrixCallback -> + activeSessionHolder.getActiveSession() + .cryptoService() + .deleteDevice(deviceId, userInteractiveAuthInterceptor, matrixCallback) + } + } +} From 7e81aa6193bbf7ad05dff06b325259d17f2fc21c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 23 Sep 2022 10:13:48 +0200 Subject: [PATCH 03/17] ReAuth process --- .../v2/overview/SessionOverviewAction.kt | 5 ++- .../v2/overview/SessionOverviewFragment.kt | 38 +++++++++++++++++++ .../v2/overview/SessionOverviewViewModel.kt | 25 +++++++++++- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt index cea0281570..c0d428b951 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 New Vector Ltd + * Copyright (c) 2022 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,4 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class SessionOverviewAction : VectorViewModelAction { object VerifySession : SessionOverviewAction() object SignoutSession : SessionOverviewAction() + object SsoAuthDone : SessionOverviewAction() + data class PasswordAuthDone(val password: String) : SessionOverviewAction() + object ReAuthCancelled : SessionOverviewAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index ccf68a18bc..c369065292 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.overview +import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem @@ -30,13 +31,16 @@ import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.databinding.FragmentSessionOverviewBinding +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import javax.inject.Inject /** @@ -92,6 +96,7 @@ class SessionOverviewFragment : is SessionOverviewViewEvent.PromptResetSecrets -> { navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET) } + is SessionOverviewViewEvent.RequestReAuth -> askForReAuthentication(it) } } } @@ -157,4 +162,37 @@ class SessionOverviewFragment : views.sessionOverviewInfo.isVisible = false } } + + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(SessionOverviewAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(SessionOverviewAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(SessionOverviewAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(SessionOverviewAction.ReAuthCancelled) + } + } + + /** + * Launch the re auth activity to get credentials. + */ + private fun askForReAuthentication(reAuthReq: SessionOverviewViewEvent.RequestReAuth) { + ReAuthActivity.newIntent( + requireContext(), + reAuthReq.registrationFlowResponse, + reAuthReq.lastErrorCode, + getString(R.string.devices_delete_dialog_title) + ).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 2f3116f40f..7dab404167 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -28,6 +28,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.IsCurrentSessionUseCase 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.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import kotlinx.coroutines.flow.distinctUntilChanged @@ -40,6 +41,7 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import timber.log.Timber import kotlin.coroutines.Continuation class SessionOverviewViewModel @AssistedInject constructor( @@ -91,10 +93,14 @@ class SessionOverviewViewModel @AssistedInject constructor( } } + // TODO add unit tests override fun handle(action: SessionOverviewAction) { when (action) { is SessionOverviewAction.VerifySession -> handleVerifySessionAction() SessionOverviewAction.SignoutSession -> handleSignoutSession() + SessionOverviewAction.SsoAuthDone -> handleSsoAuthDone() + is SessionOverviewAction.PasswordAuthDone -> handlePasswordAuthDone(action) + SessionOverviewAction.ReAuthCancelled -> handleReAuthCancelled() } } @@ -129,7 +135,11 @@ class SessionOverviewViewModel @AssistedInject constructor( override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) - is SignoutSessionResult.Completed -> Unit // TODO refresh devices list? + post event to close the associated screen + is SignoutSessionResult.Completed -> { + Timber.d("signout completed") + // TODO check if it is called after a reAuth + // TODO refresh devices list? + post event to close the associated screen + } } } }) @@ -137,8 +147,21 @@ class SessionOverviewViewModel @AssistedInject constructor( } private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + Timber.d("onReAuthNeeded") pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation _viewEvents.post(SessionOverviewViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)) } + + private fun handleSsoAuthDone() { + pendingAuthHandler.ssoAuthDone() + } + + private fun handlePasswordAuthDone(action: SessionOverviewAction.PasswordAuthDone) { + pendingAuthHandler.passwordAuthDone(action.password) + } + + private fun handleReAuthCancelled() { + pendingAuthHandler.reAuthCancelled() + } } From b2b3ee1fe5923d3c2ffacb7ae3d9afa0db519bb7 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 23 Sep 2022 16:11:55 +0200 Subject: [PATCH 04/17] Adding button to trigger sign out --- .../src/main/res/values/strings.xml | 1 + .../src/main/res/values/styles_buttons.xml | 4 ++ .../v2/overview/SessionOverviewFragment.kt | 33 +++++++----- .../v2/overview/SessionOverviewViewEvent.kt | 3 ++ .../v2/overview/SessionOverviewViewModel.kt | 51 +++++++++++++++---- .../overview/SessionOverviewViewNavigator.kt | 6 +++ .../res/layout/fragment_session_overview.xml | 13 +++++ 7 files changed, 89 insertions(+), 22 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 4d9ea008d5..6e68b09ee7 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3290,6 +3290,7 @@ No unverified sessions found. No inactive sessions found. Clear Filter + Sign out of this session Session details Application, device, and activity information. Session name diff --git a/library/ui-styles/src/main/res/values/styles_buttons.xml b/library/ui-styles/src/main/res/values/styles_buttons.xml index 702f427cc0..db78fcf338 100644 --- a/library/ui-styles/src/main/res/values/styles_buttons.xml +++ b/library/ui-styles/src/main/res/values/styles_buttons.xml @@ -41,6 +41,10 @@ 24sp + +