From 64d34804858202caded3f488c9d116c75447b7af Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:54:43 +0100 Subject: [PATCH 1/2] feat: check if Nomad profiles feature is enabled before processing intents --- .../wire/android/ui/WireActivityViewModel.kt | 66 +++++++++++++---- .../util/lifecycle/IntentsProcessor.kt | 14 ++-- .../android/ui/WireActivityViewModelTest.kt | 70 ++++++++++++++++--- .../util/lifecycle/IntentsProcessorTest.kt | 40 ++++------- 4 files changed, 132 insertions(+), 58 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index e662a172b8..591ec8bee0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -72,6 +72,8 @@ import com.wire.kalium.logic.data.logout.LogoutReason import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.appVersioning.ObserveIfAppUpdateRequiredUseCase +import com.wire.kalium.logic.feature.auth.IsNomadProfilesEnabledUseCase +import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase import com.wire.kalium.logic.feature.client.ClearNewClientsForUserUseCase import com.wire.kalium.logic.feature.client.NewClientResult import com.wire.kalium.logic.feature.client.ObserveNewClientsUseCase @@ -382,21 +384,36 @@ class WireActivityViewModel @Inject constructor( // Returns whether an intent was handled, or if there was nothing to do @Suppress("ReturnCount") fun handleIntentsThatAreNotDeepLinks(intent: Intent?): Boolean { - if (!nomadProfilesFeatureConfig.isEnabled()) return false val result = intentsProcessor.get().invoke(intent) if (result != null) { + if (!nomadProfilesFeatureConfig.isEnabled()) { + appLogger.w("Nomad login ignored: local Nomad profiles flag is disabled") + return true + } viewModelScope.launch(dispatchers.io()) { + val serverLinks = result.backendConfig?.let { loadServerConfig(it) } + val backendConfigLoadFailed = result.backendConfig != null && serverLinks == null + if (backendConfigLoadFailed) { + sendAction(OnUnknownDeepLink) + return@launch + } + + if (!isNomadProfilesFlowEnabled(serverLinks)) { + return@launch + } + initValidSessionsFlowIfNeeded() if (validSessions.value.filterIsInstance().isNotEmpty()) { appLogger.w("Nomad login blocked: another session already exists") sendAction(ShowToast(R.string.nomad_login_blocked_message)) return@launch } + onAutomaticLoginParameters( - result.backendConfig, - result.ssoCode, - result.nomadProfilesHost, - ) + serverLinks = serverLinks, + ssoCode = result.ssoCode, + nomadServiceUrl = result.nomadProfilesHost, + ) } return true } @@ -404,17 +421,12 @@ class WireActivityViewModel @Inject constructor( } private fun onAutomaticLoginParameters( - backendConfigUrl: String?, + serverLinks: ServerConfig.Links?, ssoCode: String?, nomadServiceUrl: String?, ) { viewModelScope.launch(dispatchers.io()) { - // Load backend config - val serverLinks = backendConfigUrl?.let { loadServerConfig(it) } - - val backendConfigLoadFailed = backendConfigUrl != null && serverLinks == null - val nothingProvided = backendConfigUrl == null && ssoCode == null - if (backendConfigLoadFailed || nothingProvided) { + if (ssoCode == null) { sendAction(OnUnknownDeepLink) } else { automatedLoginManager.markPendingMoveToBackgroundAfterSync() @@ -431,6 +443,36 @@ class WireActivityViewModel @Inject constructor( } } + private suspend fun isNomadProfilesFlowEnabled(serverLinks: ServerConfig.Links?): Boolean { + if (serverLinks == null) { + appLogger.w("Nomad login ignored: missing server links") + return false + } + + when (val authScopeResult = coreLogic.get().versionedAuthenticationScope(serverLinks).invoke(null)) { + is AutoVersionAuthScopeUseCase.Result.Failure -> { + appLogger.w("Nomad login ignored: failed to create auth scope for backend ${serverLinks.api}") + return false + } + + is AutoVersionAuthScopeUseCase.Result.Success -> { + return when (val result = authScopeResult.authenticationScope.isNomadProfilesEnabled()) { + is IsNomadProfilesEnabledUseCase.Result.Failure -> { + appLogger.w("Nomad login ignored: failed to fetch server Nomad settings") + false + } + + is IsNomadProfilesEnabledUseCase.Result.Success -> { + if (!result.isEnabled) { + appLogger.w("Nomad login ignored: server does not support Nomad profiles") + } + result.isEnabled && nomadProfilesFeatureConfig.isEnabled() + } + } + } + } + } + fun dismissCustomBackendDialog() { globalAppState = globalAppState.copy(customBackendDialog = null) } diff --git a/app/src/main/kotlin/com/wire/android/util/lifecycle/IntentsProcessor.kt b/app/src/main/kotlin/com/wire/android/util/lifecycle/IntentsProcessor.kt index 28eac80538..cfe6f51ce8 100644 --- a/app/src/main/kotlin/com/wire/android/util/lifecycle/IntentsProcessor.kt +++ b/app/src/main/kotlin/com/wire/android/util/lifecycle/IntentsProcessor.kt @@ -18,7 +18,6 @@ package com.wire.android.util.lifecycle import android.content.Intent -import com.wire.android.config.NomadProfilesFeatureConfig import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import java.net.URI @@ -37,7 +36,6 @@ data class AutomatedLoginViaSSO( @Singleton class IntentsProcessor @Inject internal constructor( - private val nomadProfilesFeatureConfig: NomadProfilesFeatureConfig, private val nomadIntentSignatureValidator: NomadIntentSignatureValidator ) { @@ -62,7 +60,11 @@ class IntentsProcessor @Inject internal constructor( ?.let { Json.decodeFromString(it) } }.getOrNull() ?: return null - if (!nomadProfilesFeatureConfig.isEnabled() || parsed.nomadProfilesHost.isNullOrEmpty()) { + if (parsed.nomadProfilesHost.isNullOrEmpty()) { + return null + } + + if (!nomadIntentSignatureValidator.isValid(parsed.nomadProfilesHost, parsed.signatureNomadProfilesHost)) { return null } @@ -79,11 +81,7 @@ class IntentsProcessor @Inject internal constructor( ?.takeIf { val validBackend = parsed.backendConfig == null || isValidHttpsUrl(parsed.backendConfig) val validNomadProfileHost = isValidHttpsUrl(parsed.nomadProfilesHost) - val validSignature = nomadIntentSignatureValidator.isValid( - parsed.nomadProfilesHost, - parsed.signatureNomadProfilesHost - ) - validBackend && validNomadProfileHost && validSignature + validBackend && validNomadProfileHost } } diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 00bb5cb9d3..880f8ab6ae 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -73,6 +73,9 @@ import com.wire.kalium.logic.data.logout.LogoutReason import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.appVersioning.ObserveIfAppUpdateRequiredUseCase +import com.wire.kalium.logic.feature.auth.AuthenticationScope +import com.wire.kalium.logic.feature.auth.IsNomadProfilesEnabledUseCase +import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.client.ClearNewClientsForUserUseCase import com.wire.kalium.logic.feature.client.IsProfileQRCodeEnabledUseCase @@ -822,7 +825,7 @@ class WireActivityViewModelTest { } @Test - fun `given automated login intent with only backend config, when handling intents, then automated login manager pending flag is set`() = runTest { + fun `given automated login intent with only backend config, when handling intents, then intent is ignored`() = runTest { val (arrangement, viewModel) = Arrangement() .withAutomatedLoginIntent(backendConfig = "url") .arrange() @@ -830,8 +833,8 @@ class WireActivityViewModelTest { val handled = viewModel.handleIntentsThatAreNotDeepLinks(mockedIntent()) advanceUntilIdle() - assertTrue(handled) - assertTrue(arrangement.automatedLoginManager.pendingMoveToBackgroundAfterSync) + assertFalse(handled) + assertFalse(arrangement.automatedLoginManager.pendingMoveToBackgroundAfterSync) } @Test @@ -855,15 +858,45 @@ class WireActivityViewModelTest { fun `given nomad profiles disabled, when handling non deep link intents, then intent is ignored`() = runTest { val (arrangement, viewModel) = Arrangement() .withNomadProfilesEnabled(false) - .withAutomatedLoginIntent(ssoCode = "wire-b6261497-5b7d-4a57-8f4d-3a94e936b2c0") + .withAutomatedLoginIntent( + ssoCode = "wire-b6261497-5b7d-4a57-8f4d-3a94e936b2c0", + backendConfig = "url" + ) .arrange() - val handled = viewModel.handleIntentsThatAreNotDeepLinks(mockedIntent()) - advanceUntilIdle() + viewModel.actions.test { + val handled = viewModel.handleIntentsThatAreNotDeepLinks(mockedIntent()) + advanceUntilIdle() - assertEquals(false, handled) - assertEquals(false, arrangement.automatedLoginManager.pendingMoveToBackgroundAfterSync) - verify(exactly = 0) { arrangement.intentsProcessor(any()) } + assertEquals(true, handled) + assertEquals(false, arrangement.automatedLoginManager.pendingMoveToBackgroundAfterSync) + verify(exactly = 1) { arrangement.intentsProcessor(any()) } + coVerify(exactly = 0) { arrangement.isNomadProfilesEnabledUseCase.invoke() } + coVerify(exactly = 0) { arrangement.observeSessionsUseCase.invoke() } + expectNoEvents() + } + } + + @Test + fun `given server does not support nomad profiles, when handling automated login intent, then login is ignored`() = runTest { + val (arrangement, viewModel) = Arrangement() + .withServerNomadProfilesEnabled(false) + .withAutomatedLoginIntent( + ssoCode = "wire-b6261497-5b7d-4a57-8f4d-3a94e936b2c0", + backendConfig = "url" + ) + .arrange() + + viewModel.actions.test { + val handled = viewModel.handleIntentsThatAreNotDeepLinks(mockedIntent()) + advanceUntilIdle() + + assertTrue(handled) + assertFalse(arrangement.automatedLoginManager.pendingMoveToBackgroundAfterSync) + coVerify(exactly = 1) { arrangement.isNomadProfilesEnabledUseCase.invoke() } + coVerify(exactly = 0) { arrangement.observeSessionsUseCase.invoke() } + expectNoEvents() + } } @Test @@ -988,6 +1021,10 @@ class WireActivityViewModelTest { coEvery { observeSelfUserUseCase() } returns flowOf(SELF_USER) every { managedConfigurationsManager.persistentWebSocketEnforcedByMDM } returns persistentWebSocketEnforcedByMDMFlow every { nomadProfilesFeatureConfig.isEnabled() } returns true + every { coreLogic.versionedAuthenticationScope(any()) } returns autoVersionAuthScopeUseCase + coEvery { autoVersionAuthScopeUseCase(any()) } returns AutoVersionAuthScopeUseCase.Result.Success(authenticationScope) + every { authenticationScope.isNomadProfilesEnabled } returns isNomadProfilesEnabledUseCase + coEvery { isNomadProfilesEnabledUseCase() } returns IsNomadProfilesEnabledUseCase.Result.Success(true) coEvery { loginTypeSelector.canUseNewLogin(any()) } returns true } @@ -1021,6 +1058,15 @@ class WireActivityViewModelTest { @MockK private lateinit var coreLogic: CoreLogic + @MockK + private lateinit var autoVersionAuthScopeUseCase: AutoVersionAuthScopeUseCase + + @MockK + private lateinit var authenticationScope: AuthenticationScope + + @MockK + lateinit var isNomadProfilesEnabledUseCase: IsNomadProfilesEnabledUseCase + @MockK lateinit var servicesManager: ServicesManager @@ -1231,7 +1277,7 @@ class WireActivityViewModelTest { backendConfig: String? = null, ): Arrangement = apply { every { intentsProcessor(any()) } returns when { - ssoCode != null && backendConfig == null -> null + ssoCode == null || backendConfig == null -> null else -> AutomatedLoginViaSSO( ssoCode = ssoCode, backendConfig = backendConfig, @@ -1244,6 +1290,10 @@ class WireActivityViewModelTest { every { nomadProfilesFeatureConfig.isEnabled() } returns enabled } + fun withServerNomadProfilesEnabled(enabled: Boolean): Arrangement = apply { + coEvery { isNomadProfilesEnabledUseCase() } returns IsNomadProfilesEnabledUseCase.Result.Success(enabled) + } + fun withCanUseNewLogin(canUseNewLogin: Boolean): Arrangement = apply { coEvery { loginTypeSelector.canUseNewLogin(any()) } returns canUseNewLogin } diff --git a/app/src/test/kotlin/com/wire/android/util/lifecycle/IntentsProcessorTest.kt b/app/src/test/kotlin/com/wire/android/util/lifecycle/IntentsProcessorTest.kt index cbd3d411bf..d99304ca38 100644 --- a/app/src/test/kotlin/com/wire/android/util/lifecycle/IntentsProcessorTest.kt +++ b/app/src/test/kotlin/com/wire/android/util/lifecycle/IntentsProcessorTest.kt @@ -18,7 +18,6 @@ package com.wire.android.util.lifecycle import android.content.Intent -import com.wire.android.config.NomadProfilesFeatureConfig import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertEquals @@ -191,44 +190,36 @@ class IntentsProcessorTest { } @Test - fun `given nomad profiles feature disabled, skips the whole intent`() { + fun `given valid signed intent, returns AutomatedLoginViaSSO`() { val (arrangement, intentsProcessor) = Arrangement() - .withNomadProfilesFeatureEnabled(false) .withAutomatedLoginExtra( automatedLoginJson( "backendConfig" to FAKE_BACKEND_CONFIG, "ssoCode" to FAKE_SSO_CODE, "nomadProfilesHost" to FAKE_NOMAD_PROFILES_HOST, - "sigNomadProfilesHost" to IntentsProcessor.SKIP_SIGNATURE_VERIFICATION_TOKEN + "signatureNomadProfilesHost" to IntentsProcessor.SKIP_SIGNATURE_VERIFICATION_TOKEN ) ) .arrange() - assertNull(intentsProcessor(arrangement.intent)) + assertEquals( + AutomatedLoginViaSSO( + backendConfig = FAKE_BACKEND_CONFIG, + ssoCode = FAKE_SSO_CODE, + nomadProfilesHost = FAKE_NOMAD_PROFILES_HOST + ), + intentsProcessor(arrangement.intent) + ) } @Test - fun `given invalid nomadProfilesHost and feature disabled, skips the whole intent`() { + fun `given invalid signature, returns null before processing the rest of the intent`() { val (arrangement, intentsProcessor) = Arrangement() - .withNomadProfilesFeatureEnabled(false) .withAutomatedLoginExtra( automatedLoginJson( "backendConfig" to FAKE_BACKEND_CONFIG, "ssoCode" to FAKE_SSO_CODE, - "nomadProfilesHost" to "not a url" - ) - ) - .arrange() - assertNull(intentsProcessor(arrangement.intent)) - } - - @Test - fun `given only nomadProfilesHost and feature disabled, returns null`() { - val (arrangement, intentsProcessor) = Arrangement() - .withNomadProfilesFeatureEnabled(false) - .withAutomatedLoginExtra( - automatedLoginJson( "nomadProfilesHost" to FAKE_NOMAD_PROFILES_HOST, - "sigNomadProfilesHost" to IntentsProcessor.SKIP_SIGNATURE_VERIFICATION_TOKEN + "signatureNomadProfilesHost" to "invalid-signature" ) ) .arrange() @@ -374,18 +365,15 @@ class IntentsProcessorTest { class Arrangement { internal val intent: Intent = mockk() - private val nomadProfilesFeatureConfig = mockk() private var configurationSignatureKeys: List? = null private var isConfigurationSignatureEnforced = false init { every { intent.getStringExtra(any()) } returns null - every { nomadProfilesFeatureConfig.isEnabled() } returns true } fun arrange() = this to ( IntentsProcessor( - nomadProfilesFeatureConfig = nomadProfilesFeatureConfig, nomadIntentSignatureValidator = NomadIntentSignatureValidator( configurationSignatureKeys = configurationSignatureKeys ?: emptyList(), isConfigurationSignatureEnforced = isConfigurationSignatureEnforced @@ -397,10 +385,6 @@ class IntentsProcessorTest { every { intent.getStringExtra("automated_login") } returns json } - fun withNomadProfilesFeatureEnabled(enabled: Boolean) = apply { - every { nomadProfilesFeatureConfig.isEnabled() } returns enabled - } - fun withConfigurationSignatureKey(vararg key: String) = apply { configurationSignatureKeys = key.toList() } From c0235fe082e248bcb455f78cea1e2ec573dfe69b Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:06:39 +0100 Subject: [PATCH 2/2] detekt --- .../kotlin/com/wire/android/ui/WireActivityViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 591ec8bee0..68dfa1a4a3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -449,14 +449,14 @@ class WireActivityViewModel @Inject constructor( return false } - when (val authScopeResult = coreLogic.get().versionedAuthenticationScope(serverLinks).invoke(null)) { + return when (val authScopeResult = coreLogic.get().versionedAuthenticationScope(serverLinks).invoke(null)) { is AutoVersionAuthScopeUseCase.Result.Failure -> { appLogger.w("Nomad login ignored: failed to create auth scope for backend ${serverLinks.api}") - return false + false } is AutoVersionAuthScopeUseCase.Result.Success -> { - return when (val result = authScopeResult.authenticationScope.isNomadProfilesEnabled()) { + when (val result = authScopeResult.authenticationScope.isNomadProfilesEnabled()) { is IsNomadProfilesEnabledUseCase.Result.Failure -> { appLogger.w("Nomad login ignored: failed to fetch server Nomad settings") false