Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,15 @@ object PlatformManagerModule {
dispatcherManager: DispatcherManager,
clock: Clock,
json: Json,
featureFlagManager: FeatureFlagManager,
): PushManager = PushManagerImpl(
authDiskSource = authDiskSource,
pushDiskSource = pushDiskSource,
pushService = pushService,
dispatcherManager = dispatcherManager,
clock = clock,
json = json,
featureFlagManager = featureFlagManager,
)

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -66,6 +72,7 @@ class PushManagerTest {
dispatcherManager = dispatcherManager,
clock = clock,
json = CoreModule.providesJson(),
featureFlagManager = mockFeatureFlagManager,
)
}

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ sealed class FlagKey<out T : Any> {
CredentialExchangeProtocolExport,
ForceUpdateKdfSettings,
CipherKeyEncryption,
NoLogoutOnKdfChange,
)
}
}
Expand Down Expand Up @@ -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>() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add this to the activePasswordManagerFlags above

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.CredentialExchangeProtocolExport,
FlagKey.CipherKeyEncryption,
FlagKey.ForceUpdateKdfSettings,
FlagKey.NoLogoutOnKdfChange,
-> {
@Suppress("UNCHECKED_CAST")
BooleanFlagItem(
Expand Down Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions ui/src/main/res/values/strings_non_localized.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Loading