Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 54 additions & 12 deletions app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -382,39 +384,49 @@ 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<AccountInfo.Valid>().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
}
return false
}

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()
Expand All @@ -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
}

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}")
false
}

is AutoVersionAuthScopeUseCase.Result.Success -> {
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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,7 +36,6 @@ data class AutomatedLoginViaSSO(

@Singleton
class IntentsProcessor @Inject internal constructor(
private val nomadProfilesFeatureConfig: NomadProfilesFeatureConfig,
private val nomadIntentSignatureValidator: NomadIntentSignatureValidator
) {

Expand All @@ -62,7 +60,11 @@ class IntentsProcessor @Inject internal constructor(
?.let { Json.decodeFromString<Parameters>(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
}

Expand All @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -822,16 +825,16 @@ 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()

val handled = viewModel.handleIntentsThatAreNotDeepLinks(mockedIntent())
advanceUntilIdle()

assertTrue(handled)
assertTrue(arrangement.automatedLoginManager.pendingMoveToBackgroundAfterSync)
assertFalse(handled)
assertFalse(arrangement.automatedLoginManager.pendingMoveToBackgroundAfterSync)
}

@Test
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -374,18 +365,15 @@ class IntentsProcessorTest {

class Arrangement {
internal val intent: Intent = mockk()
private val nomadProfilesFeatureConfig = mockk<NomadProfilesFeatureConfig>()
private var configurationSignatureKeys: List<String>? = 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
Expand All @@ -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()
}
Expand Down
Loading