diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/PushManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/PushManagerImpl.kt index 1f00b22e9a..d9ce889da3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/PushManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/PushManagerImpl.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.platform.manager +import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.core.data.util.decodeFromStringOrNull import com.bitwarden.data.manager.DispatcherManager @@ -13,6 +14,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.NotificationLogoutData import com.x8bit.bitwarden.data.platform.manager.model.NotificationPayload import com.x8bit.bitwarden.data.platform.manager.model.NotificationType import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData +import com.x8bit.bitwarden.data.platform.manager.model.PushNotificationLogOutReason import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData @@ -44,12 +46,14 @@ private val PUSH_TOKEN_UPDATE_DELAY: Duration = 7.days /** * Primary implementation of [PushManager]. */ +@Suppress("LongParameterList") class PushManagerImpl @Inject constructor( private val authDiskSource: AuthDiskSource, private val pushDiskSource: PushDiskSource, private val pushService: PushService, private val clock: Clock, private val json: Json, + private val featureFlagManager: FeatureFlagManager, dispatcherManager: DispatcherManager, ) : PushManager { private val ioScope = CoroutineScope(dispatcherManager.io) @@ -157,8 +161,15 @@ class PushManagerImpl @Inject constructor( .decodeFromString( string = notification.payload, ) - .userId - ?.let { mutableLogoutSharedFlow.tryEmit(NotificationLogoutData(it)) } + .takeUnless { + featureFlagManager.getFeatureFlag(FlagKey.NoLogoutOnKdfChange) && + it.pushNotificationLogOutReason == + PushNotificationLogOutReason.KDF_CHANGE + } + ?.userId + ?.let { + mutableLogoutSharedFlow.tryEmit(NotificationLogoutData(userId = it)) + } } NotificationType.SYNC_CIPHER_CREATE, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt index 59c93a0652..8f62e2ff66 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt @@ -302,6 +302,7 @@ object PlatformManagerModule { dispatcherManager: DispatcherManager, clock: Clock, json: Json, + featureFlagManager: FeatureFlagManager, ): PushManager = PushManagerImpl( authDiskSource = authDiskSource, pushDiskSource = pushDiskSource, @@ -309,6 +310,7 @@ object PlatformManagerModule { dispatcherManager = dispatcherManager, clock = clock, json = json, + featureFlagManager = featureFlagManager, ) @Provides diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/NotificationPayload.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/NotificationPayload.kt index 111197aa1d..558884f1b3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/NotificationPayload.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/NotificationPayload.kt @@ -50,9 +50,15 @@ sealed class NotificationPayload { */ @Serializable data class UserNotification( - @JsonNames("UserId", "userId") override val userId: String?, + @JsonNames("UserId", "userId") + override val userId: String?, + @Contextual - @JsonNames("Date", "date") val date: ZonedDateTime?, + @JsonNames("Date", "date") + val date: ZonedDateTime?, + + @JsonNames("PushNotificationLogOutReason", "pushNotificationLogOutReason") + val pushNotificationLogOutReason: PushNotificationLogOutReason?, ) : NotificationPayload() /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/PushNotificationLogOutReason.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/PushNotificationLogOutReason.kt new file mode 100644 index 0000000000..805f342a9a --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/PushNotificationLogOutReason.kt @@ -0,0 +1,11 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +import kotlinx.serialization.SerialName + +/** + * Enumerated values to represent the possible reasons for a log out push notification + */ +enum class PushNotificationLogOutReason { + @SerialName("0") + KDF_CHANGE, +} diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/PushManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/PushManagerTest.kt index 0d1f7a86f2..269f8b78ec 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/PushManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/PushManagerTest.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.platform.manager import app.cash.turbine.test +import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.util.asFailure import com.bitwarden.core.data.util.asSuccess import com.bitwarden.core.di.CoreModule @@ -26,6 +27,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.SyncSendDeleteData import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -55,6 +57,10 @@ class PushManagerTest { coEvery { putDeviceToken(any()) } returns Unit.asSuccess() } + private val mockFeatureFlagManager = mockk(relaxed = true) { + every { getFeatureFlag(FlagKey.NoLogoutOnKdfChange) } returns false + } + private lateinit var pushManager: PushManager @BeforeEach @@ -66,6 +72,7 @@ class PushManagerTest { dispatcherManager = dispatcherManager, clock = clock, json = CoreModule.providesJson(), + featureFlagManager = mockFeatureFlagManager, ) } @@ -135,6 +142,28 @@ class PushManagerTest { } } + @Test + @Suppress("MaxLineLength") + fun `onMessageReceived with logout with kdf change as reason should not emit to logoutFlow`() = + runTest { + every { + mockFeatureFlagManager.getFeatureFlag(FlagKey.NoLogoutOnKdfChange) + } returns true + + val accountTokens = AccountTokensJson( + accessToken = "accessToken", + refreshToken = "refreshToken", + ) + authDiskSource.storeAccountTokens(userId, accountTokens) + authDiskSource.userState = + UserStateJson(userId, mapOf(userId to mockk())) + + pushManager.logoutFlow.test { + pushManager.onMessageReceived(LOGOUT_KDF_NOTIFICATION_MAP) + expectNoEvents() + } + } + @Nested inner class LoggedOutUserState { @BeforeEach @@ -156,6 +185,18 @@ class PushManagerTest { } } + @Test + fun `onMessageReceived with logout with KDF reason do not emits to logoutFlow`() = + runTest { + every { + mockFeatureFlagManager.getFeatureFlag(FlagKey.NoLogoutOnKdfChange) + } returns true + pushManager.logoutFlow.test { + pushManager.onMessageReceived(LOGOUT_KDF_NOTIFICATION_MAP) + expectNoEvents() + } + } + @Test fun `onMessageReceived with ciphers emits to fullSyncFlow`() = runTest { pushManager.fullSyncFlow.test { @@ -517,6 +558,14 @@ class PushManagerTest { } } + @Test + fun `onMessageReceived with logout with kdf reason does nothing`() = runTest { + pushManager.logoutFlow.test { + pushManager.onMessageReceived(LOGOUT_KDF_NOTIFICATION_MAP) + expectNoEvents() + } + } + @Test fun `onMessageReceived with sync ciphers does nothing`() = runTest { pushManager.fullSyncFlow.test { @@ -575,6 +624,18 @@ class PushManagerTest { } } + @Test + fun `onMessageReceived with logout with kdf reason does not emit to logoutFlow`() = + runTest { + every { + mockFeatureFlagManager.getFeatureFlag(FlagKey.NoLogoutOnKdfChange) + } returns true + pushManager.logoutFlow.test { + pushManager.onMessageReceived(LOGOUT_KDF_NOTIFICATION_MAP) + expectNoEvents() + } + } + @Test fun `onMessageReceived with sync ciphers emits to fullSyncFlow`() = runTest { pushManager.fullSyncFlow.test { @@ -908,6 +969,16 @@ private val LOGOUT_NOTIFICATION_MAP = mapOf( }""", ) +private val LOGOUT_KDF_NOTIFICATION_MAP = mapOf( + "contextId" to "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type" to "11", + "payload" to """{ + "UserId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "Date": "2023-10-27T12:00:00.000Z", + "PushNotificationLogOutReason": "0" + }""", +) + private val SYNC_CIPHER_CREATE_NOTIFICATION_MAP = mapOf( "contextId" to "801f459d-8e51-47d0-b072-3f18c9f66f64", "type" to "1", diff --git a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt index afb8d2e8df..6b950e1563 100644 --- a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt +++ b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt @@ -34,6 +34,7 @@ sealed class FlagKey { CredentialExchangeProtocolExport, ForceUpdateKdfSettings, CipherKeyEncryption, + NoLogoutOnKdfChange, ) } } @@ -80,6 +81,14 @@ sealed class FlagKey { override val defaultValue: Boolean = false } + /** + * Data object holding the feature flag key for the No Logout On KDF Change feature. + */ + data object NoLogoutOnKdfChange : FlagKey() { + override val keyName: String = "pm-23995-no-logout-on-kdf-change" + override val defaultValue: Boolean = false + } + //region Dummy keys for testing /** * Data object holding the key for a [Boolean] flag to be used in tests. diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt index 84d29a3602..fbad9acb4d 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt @@ -28,6 +28,7 @@ fun FlagKey.ListItemContent( FlagKey.CredentialExchangeProtocolExport, FlagKey.CipherKeyEncryption, FlagKey.ForceUpdateKdfSettings, + FlagKey.NoLogoutOnKdfChange, -> { @Suppress("UNCHECKED_CAST") BooleanFlagItem( @@ -73,6 +74,7 @@ private fun FlagKey.getDisplayLabel(): String = when (this) { FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export) FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption) FlagKey.ForceUpdateKdfSettings -> stringResource(BitwardenString.force_update_kdf_settings) + FlagKey.NoLogoutOnKdfChange -> stringResource(BitwardenString.avoid_logout_on_kdf_change) FlagKey.BitwardenAuthenticationEnabled -> { stringResource(BitwardenString.bitwarden_authentication_enabled) } diff --git a/ui/src/main/res/values/strings_non_localized.xml b/ui/src/main/res/values/strings_non_localized.xml index 065a3501de..4495a77df6 100644 --- a/ui/src/main/res/values/strings_non_localized.xml +++ b/ui/src/main/res/values/strings_non_localized.xml @@ -37,6 +37,7 @@ LastPass (.json) Aegis (.json) Force update KDF settings + Avoid logout on KDF change