[PM-26736] Prevent logout notification on KDF change (#6038)

This commit is contained in:
André Bispo 2025-10-16 19:54:56 +01:00 committed by GitHub
parent 714f7cfadc
commit a70b2172cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 117 additions and 4 deletions

View File

@ -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<NotificationPayload.UserNotification>(
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,

View File

@ -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

View File

@ -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()
/**

View File

@ -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,
}

View File

@ -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<FeatureFlagManager>(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<AccountJson>()))
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",

View File

@ -34,6 +34,7 @@ sealed class FlagKey<out T : Any> {
CredentialExchangeProtocolExport,
ForceUpdateKdfSettings,
CipherKeyEncryption,
NoLogoutOnKdfChange,
)
}
}
@ -80,6 +81,14 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key for the No Logout On KDF Change feature.
*/
data object NoLogoutOnKdfChange : FlagKey<Boolean>() {
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.

View File

@ -28,6 +28,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.CredentialExchangeProtocolExport,
FlagKey.CipherKeyEncryption,
FlagKey.ForceUpdateKdfSettings,
FlagKey.NoLogoutOnKdfChange,
-> {
@Suppress("UNCHECKED_CAST")
BooleanFlagItem(
@ -73,6 +74,7 @@ private fun <T : Any> FlagKey<T>.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)
}

View File

@ -37,6 +37,7 @@
<string name="import_format_label_lastpass_json">LastPass (.json)</string>
<string name="import_format_label_aegis_json">Aegis (.json)</string>
<string name="force_update_kdf_settings">Force update KDF settings</string>
<string name="avoid_logout_on_kdf_change">Avoid logout on KDF change</string>
<!-- endregion Debug Menu -->
</resources>