mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-02 12:16:55 +01:00
Merge pull request #6207 from vector-im/feature/adm/sdk-signout-all-devices
SDK - Logout all devices
This commit is contained in:
commit
ccb4f2d1dd
1
changelog.d/6191.sdk
Normal file
1
changelog.d/6191.sdk
Normal file
@ -0,0 +1 @@
|
||||
Add support for MSC2457 - opting in or out of logging out all devices when changing password
|
@ -21,5 +21,6 @@ data class LoginFlowResult(
|
||||
val ssoIdentityProviders: List<SsoIdentityProvider>?,
|
||||
val isLoginAndRegistrationSupported: Boolean,
|
||||
val homeServerUrl: String,
|
||||
val isOutdatedHomeserver: Boolean
|
||||
val isOutdatedHomeserver: Boolean,
|
||||
val isLogoutDevicesSupported: Boolean
|
||||
)
|
||||
|
@ -72,7 +72,9 @@ interface LoginWizard {
|
||||
* Confirm the new password, once the user has checked their email
|
||||
* When this method succeed, tha account password will be effectively modified.
|
||||
*
|
||||
* @param newPassword the desired new password
|
||||
* @param newPassword the desired new password.
|
||||
* @param logoutAllDevices defaults to true, all devices will be logged out. False values will only be taken into account
|
||||
* if [org.matrix.android.sdk.api.auth.data.LoginFlowResult.isLogoutDevicesSupported] is true.
|
||||
*/
|
||||
suspend fun resetPasswordMailConfirmed(newPassword: String)
|
||||
suspend fun resetPasswordMailConfirmed(newPassword: String, logoutAllDevices: Boolean = true)
|
||||
}
|
||||
|
@ -24,13 +24,13 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||
interface AccountService {
|
||||
/**
|
||||
* Ask the homeserver to change the password.
|
||||
*
|
||||
* @param password Current password.
|
||||
* @param newPassword New password
|
||||
* @param logoutAllDevices defaults to true, all devices will be logged out. False values will only be taken into account
|
||||
* if [org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities.canControlLogoutDevices] is true.
|
||||
*/
|
||||
suspend fun changePassword(
|
||||
password: String,
|
||||
newPassword: String
|
||||
)
|
||||
suspend fun changePassword(password: String, newPassword: String, logoutAllDevices: Boolean = true)
|
||||
|
||||
/**
|
||||
* Deactivate the account.
|
||||
|
@ -54,7 +54,12 @@ data class HomeServerCapabilities(
|
||||
/**
|
||||
* True if the home server support threading.
|
||||
*/
|
||||
val canUseThreading: Boolean = false
|
||||
val canUseThreading: Boolean = false,
|
||||
|
||||
/**
|
||||
* True if the home server supports controlling the logout of all devices when changing password.
|
||||
*/
|
||||
val canControlLogoutDevices: Boolean = false
|
||||
) {
|
||||
|
||||
enum class RoomCapabilitySupport {
|
||||
|
@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.auth.login.DefaultLoginWizard
|
||||
import org.matrix.android.sdk.internal.auth.login.DirectLoginTask
|
||||
import org.matrix.android.sdk.internal.auth.registration.DefaultRegistrationWizard
|
||||
import org.matrix.android.sdk.internal.auth.version.Versions
|
||||
import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
|
||||
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
|
||||
import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk
|
||||
import org.matrix.android.sdk.internal.di.Unauthenticated
|
||||
@ -292,7 +293,8 @@ internal class DefaultAuthenticationService @Inject constructor(
|
||||
ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
|
||||
isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
|
||||
homeServerUrl = homeServerUrl,
|
||||
isOutdatedHomeserver = !versions.isSupportedBySdk()
|
||||
isOutdatedHomeserver = !versions.isSupportedBySdk(),
|
||||
isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -121,12 +121,13 @@ internal class DefaultLoginWizard(
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
}
|
||||
|
||||
override suspend fun resetPasswordMailConfirmed(newPassword: String) {
|
||||
override suspend fun resetPasswordMailConfirmed(newPassword: String, logoutAllDevices: Boolean) {
|
||||
val resetPasswordData = pendingSessionData.resetPasswordData ?: throw IllegalStateException("Developer error - Must call resetPassword first")
|
||||
val param = ResetPasswordMailConfirmed.create(
|
||||
pendingSessionData.clientSecret,
|
||||
resetPasswordData.addThreePidRegistrationResponse.sid,
|
||||
newPassword
|
||||
newPassword,
|
||||
logoutAllDevices
|
||||
)
|
||||
|
||||
executeRequest(null) {
|
||||
|
@ -30,13 +30,17 @@ internal data class ResetPasswordMailConfirmed(
|
||||
|
||||
// the new password
|
||||
@Json(name = "new_password")
|
||||
val newPassword: String? = null
|
||||
val newPassword: String? = null,
|
||||
|
||||
@Json(name = "logout_devices")
|
||||
val logoutDevices: Boolean? = null
|
||||
) {
|
||||
companion object {
|
||||
fun create(clientSecret: String, sid: String, newPassword: String): ResetPasswordMailConfirmed {
|
||||
fun create(clientSecret: String, sid: String, newPassword: String, logoutDevices: Boolean?): ResetPasswordMailConfirmed {
|
||||
return ResetPasswordMailConfirmed(
|
||||
auth = AuthParams.createForResetPassword(clientSecret, sid),
|
||||
newPassword = newPassword
|
||||
newPassword = newPassword,
|
||||
logoutDevices = logoutDevices
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +58,7 @@ internal data class HomeServerVersion(
|
||||
val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
|
||||
val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
|
||||
val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
|
||||
val r0_6_1 = HomeServerVersion(major = 0, minor = 6, patch = 1)
|
||||
val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
|
||||
}
|
||||
}
|
||||
|
@ -111,6 +111,15 @@ private fun Versions.doesServerSeparatesAddAndBind(): Boolean {
|
||||
unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate if the server supports MSC2457 `logout_devices` parameter when setting a new password.
|
||||
*
|
||||
* @return true if logout_devices is supported
|
||||
*/
|
||||
internal fun Versions.doesServerSupportLogoutDevices(): Boolean {
|
||||
return getMaxVersion() >= HomeServerVersion.r0_6_1
|
||||
}
|
||||
|
||||
private fun Versions.getMaxVersion(): HomeServerVersion {
|
||||
return supportedVersions
|
||||
?.mapNotNull { HomeServerVersion.parse(it) }
|
||||
|
@ -48,6 +48,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027
|
||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028
|
||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029
|
||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030
|
||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031
|
||||
import org.matrix.android.sdk.internal.util.Normalizer
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@ -62,7 +63,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
||||
override fun equals(other: Any?) = other is RealmSessionStoreMigration
|
||||
override fun hashCode() = 1000
|
||||
|
||||
val schemaVersion = 30L
|
||||
val schemaVersion = 31L
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
|
||||
@ -97,5 +98,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
||||
if (oldVersion < 28) MigrateSessionTo028(realm).perform()
|
||||
if (oldVersion < 29) MigrateSessionTo029(realm).perform()
|
||||
if (oldVersion < 30) MigrateSessionTo030(realm).perform()
|
||||
if (oldVersion < 31) MigrateSessionTo031(realm).perform()
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,8 @@ internal object HomeServerCapabilitiesMapper {
|
||||
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
|
||||
defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
|
||||
roomVersions = mapRoomVersion(entity.roomVersionsJson),
|
||||
canUseThreading = entity.canUseThreading
|
||||
canUseThreading = entity.canUseThreading,
|
||||
canControlLogoutDevices = entity.canControlLogoutDevices
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||
* Migrating to:
|
||||
* Live location sharing aggregated summary: adding new field userId.
|
||||
*/
|
||||
internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 28) {
|
||||
internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 29) {
|
||||
|
||||
override fun doMigrate(realm: DynamicRealm) {
|
||||
realm.schema.get("LiveLocationShareAggregatedSummaryEntity")
|
||||
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.database.migration
|
||||
|
||||
import io.realm.DynamicRealm
|
||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
|
||||
import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
|
||||
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||
|
||||
internal class MigrateSessionTo031(realm: DynamicRealm) : RealmMigrator(realm, 31) {
|
||||
|
||||
override fun doMigrate(realm: DynamicRealm) {
|
||||
realm.schema.get("HomeServerCapabilitiesEntity")
|
||||
?.addField(HomeServerCapabilitiesEntityFields.CAN_CONTROL_LOGOUT_DEVICES, Boolean::class.java)
|
||||
?.forceRefreshOfHomeServerCapabilities()
|
||||
}
|
||||
}
|
@ -29,7 +29,8 @@ internal open class HomeServerCapabilitiesEntity(
|
||||
var lastVersionIdentityServerSupported: Boolean = false,
|
||||
var defaultIdentityServerUrl: String? = null,
|
||||
var lastUpdatedTimestamp: Long = 0L,
|
||||
var canUseThreading: Boolean = false
|
||||
var canUseThreading: Boolean = false,
|
||||
var canControlLogoutDevices: Boolean = false
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
|
@ -29,13 +29,17 @@ internal data class ChangePasswordParams(
|
||||
val auth: UserPasswordAuth? = null,
|
||||
|
||||
@Json(name = "new_password")
|
||||
val newPassword: String? = null
|
||||
val newPassword: String? = null,
|
||||
|
||||
@Json(name = "logout_devices")
|
||||
val logoutDevices: Boolean = true
|
||||
) {
|
||||
companion object {
|
||||
fun create(userId: String, oldPassword: String, newPassword: String): ChangePasswordParams {
|
||||
fun create(userId: String, oldPassword: String, newPassword: String, logoutAllDevices: Boolean): ChangePasswordParams {
|
||||
return ChangePasswordParams(
|
||||
auth = UserPasswordAuth(user = userId, password = oldPassword),
|
||||
newPassword = newPassword
|
||||
newPassword = newPassword,
|
||||
logoutDevices = logoutAllDevices
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,8 @@ import javax.inject.Inject
|
||||
internal interface ChangePasswordTask : Task<ChangePasswordTask.Params, Unit> {
|
||||
data class Params(
|
||||
val password: String,
|
||||
val newPassword: String
|
||||
val newPassword: String,
|
||||
val logoutAllDevices: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
@ -37,7 +38,7 @@ internal class DefaultChangePasswordTask @Inject constructor(
|
||||
) : ChangePasswordTask {
|
||||
|
||||
override suspend fun execute(params: ChangePasswordTask.Params) {
|
||||
val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword)
|
||||
val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword, params.logoutAllDevices)
|
||||
try {
|
||||
executeRequest(globalErrorReceiver) {
|
||||
accountAPI.changePassword(changePasswordParams)
|
||||
|
@ -25,8 +25,8 @@ internal class DefaultAccountService @Inject constructor(
|
||||
private val deactivateAccountTask: DeactivateAccountTask
|
||||
) : AccountService {
|
||||
|
||||
override suspend fun changePassword(password: String, newPassword: String) {
|
||||
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
|
||||
override suspend fun changePassword(password: String, newPassword: String, logoutAllDevices: Boolean) {
|
||||
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword, logoutAllDevices))
|
||||
}
|
||||
|
||||
override suspend fun deactivateAccount(eraseAllData: Boolean, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) {
|
||||
|
@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.orTrue
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||
import org.matrix.android.sdk.internal.auth.version.Versions
|
||||
import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
|
||||
import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
|
||||
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
|
||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
|
||||
@ -142,6 +143,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||
|
||||
if (getVersionResult != null) {
|
||||
homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk()
|
||||
homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices()
|
||||
}
|
||||
|
||||
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
|
||||
|
@ -444,10 +444,11 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
currentJob = viewModelScope.launch {
|
||||
runCatching { safeLoginWizard.resetPassword(action.email) }.fold(
|
||||
onSuccess = {
|
||||
val state = awaitState()
|
||||
setState {
|
||||
copy(
|
||||
isLoading = false,
|
||||
resetState = ResetState(email = action.email, newPassword = action.newPassword)
|
||||
resetState = createResetState(action, state.selectedHomeserver)
|
||||
)
|
||||
}
|
||||
_viewEvents.post(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
|
||||
@ -460,6 +461,12 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun createResetState(action: OnboardingAction.ResetPassword, selectedHomeserverState: SelectedHomeserverState) = ResetState(
|
||||
email = action.email,
|
||||
newPassword = action.newPassword,
|
||||
supportsLogoutAllDevices = selectedHomeserverState.isLogoutDevicesSupported
|
||||
)
|
||||
|
||||
private fun handleResetPasswordMailConfirmed() {
|
||||
setState { copy(isLoading = true) }
|
||||
currentJob = viewModelScope.launch {
|
||||
|
@ -71,6 +71,7 @@ data class SelectedHomeserverState(
|
||||
val upstreamUrl: String? = null,
|
||||
val preferredLoginMode: LoginMode = LoginMode.Unknown,
|
||||
val supportedLoginTypes: List<String> = emptyList(),
|
||||
val isLogoutDevicesSupported: Boolean = false,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
@ -88,6 +89,7 @@ data class PersonalizationState(
|
||||
data class ResetState(
|
||||
val email: String? = null,
|
||||
val newPassword: String? = null,
|
||||
val supportsLogoutAllDevices: Boolean = false
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
|
@ -53,7 +53,8 @@ class StartAuthenticationFlowUseCase @Inject constructor(
|
||||
userFacingUrl = config.homeServerUri.toString(),
|
||||
upstreamUrl = authFlow.homeServerUrl,
|
||||
preferredLoginMode = preferredLoginMode,
|
||||
supportedLoginTypes = authFlow.supportedLoginTypes
|
||||
supportedLoginTypes = authFlow.supportedLoginTypes,
|
||||
isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported
|
||||
)
|
||||
|
||||
private fun matrixOrgUrl() = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
|
||||
|
@ -178,9 +178,10 @@ class VectorSettingsGeneralFragment @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val homeServerCapabilities = session.homeServerCapabilitiesService().getHomeServerCapabilities()
|
||||
// Password
|
||||
// Hide the preference if password can not be updated
|
||||
if (session.homeServerCapabilitiesService().getHomeServerCapabilities().canChangePassword) {
|
||||
if (homeServerCapabilities.canChangePassword) {
|
||||
mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
onPasswordUpdateClick()
|
||||
false
|
||||
|
@ -65,6 +65,7 @@ private val A_DIRECT_LOGIN = OnboardingAction.AuthenticateAction.LoginDirect("@a
|
||||
private const val A_HOMESERVER_URL = "https://edited-homeserver.org"
|
||||
private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
|
||||
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password)
|
||||
private val SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES = SelectedHomeserverState(isLogoutDevicesSupported = true)
|
||||
private const val AN_EMAIL = "hello@example.com"
|
||||
private const val A_PASSWORD = "a-password"
|
||||
|
||||
@ -478,6 +479,7 @@ class OnboardingViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `given can successfully reset password, when resetting password, then emits reset done event`() = runTest {
|
||||
viewModelWith(initialState.copy(selectedHomeserver = SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES))
|
||||
val test = viewModel.test()
|
||||
fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL)
|
||||
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
|
||||
@ -488,7 +490,10 @@ class OnboardingViewModelTest {
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false, resetState = ResetState(AN_EMAIL, A_PASSWORD)) }
|
||||
{
|
||||
val resetState = ResetState(AN_EMAIL, A_PASSWORD, supportsLogoutAllDevices = true)
|
||||
copy(isLoading = false, resetState = resetState)
|
||||
}
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
|
||||
.finish()
|
||||
|
@ -128,7 +128,8 @@ class StartAuthenticationFlowUseCaseTest {
|
||||
ssoIdentityProviders = SSO_IDENTITY_PROVIDERS,
|
||||
isLoginAndRegistrationSupported = true,
|
||||
homeServerUrl = A_DECLARED_HOMESERVER_URL,
|
||||
isOutdatedHomeserver = false
|
||||
isOutdatedHomeserver = false,
|
||||
isLogoutDevicesSupported = false
|
||||
)
|
||||
|
||||
private fun expectedResult(
|
||||
|
Loading…
x
Reference in New Issue
Block a user