[PM-27118] Restrict Credential Exchange import based on Personal Ownership policy (#6220)

This commit is contained in:
Patrick Honkonen 2025-12-03 15:15:53 -05:00 committed by GitHub
parent 1904c4ffb9
commit e1bb3a4b5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 65 additions and 10 deletions

View File

@ -2,14 +2,17 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.ui.platform.base.BackgroundEvent import com.bitwarden.ui.platform.base.BackgroundEvent
import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager import com.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -25,6 +28,7 @@ class VaultSettingsViewModel @Inject constructor(
snackbarRelayManager: SnackbarRelayManager<SnackbarRelay>, snackbarRelayManager: SnackbarRelayManager<SnackbarRelay>,
private val firstTimeActionManager: FirstTimeActionManager, private val firstTimeActionManager: FirstTimeActionManager,
private val featureFlagManager: FeatureFlagManager, private val featureFlagManager: FeatureFlagManager,
private val policyManager: PolicyManager,
) : BaseViewModel<VaultSettingsState, VaultSettingsEvent, VaultSettingsAction>( ) : BaseViewModel<VaultSettingsState, VaultSettingsEvent, VaultSettingsAction>(
initialState = run { initialState = run {
val firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState val firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState
@ -55,7 +59,13 @@ class VaultSettingsViewModel @Inject constructor(
featureFlagManager featureFlagManager
.getFeatureFlagFlow(key = FlagKey.CredentialExchangeProtocolImport) .getFeatureFlagFlow(key = FlagKey.CredentialExchangeProtocolImport)
.map { VaultSettingsAction.Internal.ImportFeatureUpdated(it) } .combine(
policyManager.getActivePoliciesFlow(type = PolicyTypeJson.PERSONAL_OWNERSHIP),
) { isEnabled, policies ->
VaultSettingsAction.Internal.CredentialExchangeAvailabilityChanged(
isEnabled = isEnabled && policies.isEmpty(),
)
}
.onEach(::sendAction) .onEach(::sendAction)
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
@ -80,8 +90,8 @@ class VaultSettingsViewModel @Inject constructor(
handleSnackbarDataReceived(action) handleSnackbarDataReceived(action)
} }
is VaultSettingsAction.Internal.ImportFeatureUpdated -> { is VaultSettingsAction.Internal.CredentialExchangeAvailabilityChanged -> {
handleImportFeatureUpdated(action) handleCredentialExchangeAvailabilityChanged(action)
} }
} }
} }
@ -92,8 +102,8 @@ class VaultSettingsViewModel @Inject constructor(
sendEvent(VaultSettingsEvent.ShowSnackbar(action.data)) sendEvent(VaultSettingsEvent.ShowSnackbar(action.data))
} }
private fun handleImportFeatureUpdated( private fun handleCredentialExchangeAvailabilityChanged(
action: VaultSettingsAction.Internal.ImportFeatureUpdated, action: VaultSettingsAction.Internal.CredentialExchangeAvailabilityChanged,
) { ) {
mutableStateFlow.update { it.copy(showImportItemsChevron = action.isEnabled) } mutableStateFlow.update { it.copy(showImportItemsChevron = action.isEnabled) }
} }
@ -128,7 +138,9 @@ class VaultSettingsViewModel @Inject constructor(
} }
private fun handleImportItemsClicked() { private fun handleImportItemsClicked() {
if (featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport)) { if (featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport) &&
policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP).isEmpty()
) {
sendEvent(VaultSettingsEvent.NavigateToImportItems) sendEvent(VaultSettingsEvent.NavigateToImportItems)
} else { } else {
sendEvent(VaultSettingsEvent.NavigateToImportVault) sendEvent(VaultSettingsEvent.NavigateToImportVault)
@ -218,9 +230,9 @@ sealed class VaultSettingsAction {
*/ */
sealed class Internal : VaultSettingsAction() { sealed class Internal : VaultSettingsAction() {
/** /**
* Indicates that the import feature flag has been updated. * Indicates that the CXF import feature availability has changed.
*/ */
data class ImportFeatureUpdated( data class CredentialExchangeAvailabilityChanged(
val isEnabled: Boolean, val isEnabled: Boolean,
) : Internal() ) : Internal()

View File

@ -3,12 +3,15 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import app.cash.turbine.test import app.cash.turbine.test
import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager import com.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
import io.mockk.every import io.mockk.every
@ -38,6 +41,11 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
getFeatureFlagFlow(FlagKey.CredentialExchangeProtocolImport) getFeatureFlagFlow(FlagKey.CredentialExchangeProtocolImport)
} returns mutableFeatureFlagFlow } returns mutableFeatureFlagFlow
} }
private val mutablePoliciesFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
private val policyManager = mockk<PolicyManager> {
every { getActivePolicies(any()) } returns emptyList()
every { getActivePoliciesFlow(any()) } returns mutablePoliciesFlow
}
private val mutableSnackbarSharedFlow = bufferedMutableSharedFlow<BitwardenSnackbarData>() private val mutableSnackbarSharedFlow = bufferedMutableSharedFlow<BitwardenSnackbarData>()
private val snackbarRelayManager = mockk<SnackbarRelayManager<SnackbarRelay>> { private val snackbarRelayManager = mockk<SnackbarRelayManager<SnackbarRelay>> {
@ -84,6 +92,25 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `ImportItemsClick should emit NavigateToImportVault when policy is not empty`() = runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport)
} returns true
every {
policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
} returns listOf(mockk())
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
assertEquals(
VaultSettingsEvent.NavigateToImportVault,
awaitItem(),
)
}
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `ImportItemsClick should emit NavigateToImportItems when CredentialExchangeProtocolImport is enabled`() = fun `ImportItemsClick should emit NavigateToImportItems when CredentialExchangeProtocolImport is enabled`() =
@ -160,21 +187,36 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `CredentialExchangeProtocolImport flag changes should update state accordingly`() { fun `showImportItemsChevron should display based on feature flag and policies`() {
val viewModel = createViewModel() val viewModel = createViewModel()
// Verify chevron is shown when feature flag is enabled and no policies (default state)
assertEquals( assertEquals(
viewModel.stateFlow.value, viewModel.stateFlow.value,
VaultSettingsState(showImportActionCard = true, showImportItemsChevron = true), VaultSettingsState(showImportActionCard = true, showImportItemsChevron = true),
) )
// Verify chevron is hidden when feature flag is disabled and no policies
mutableFeatureFlagFlow.tryEmit(false) mutableFeatureFlagFlow.tryEmit(false)
mutablePoliciesFlow.tryEmit(emptyList())
assertEquals( assertEquals(
viewModel.stateFlow.value, viewModel.stateFlow.value,
VaultSettingsState(showImportActionCard = true, showImportItemsChevron = false), VaultSettingsState(showImportActionCard = true, showImportItemsChevron = false),
) )
// Verify chevron is hidden when feature flag is enabled and policies exist
mutableFeatureFlagFlow.tryEmit(true) mutableFeatureFlagFlow.tryEmit(true)
mutablePoliciesFlow.tryEmit(listOf(mockk()))
assertEquals( assertEquals(
viewModel.stateFlow.value, viewModel.stateFlow.value,
VaultSettingsState(showImportActionCard = true, showImportItemsChevron = true), VaultSettingsState(showImportActionCard = true, showImportItemsChevron = false),
)
// Verify chevron is hidden when feature flag is disabled and no policies
mutableFeatureFlagFlow.tryEmit(false)
mutablePoliciesFlow.tryEmit(emptyList())
assertEquals(
viewModel.stateFlow.value,
VaultSettingsState(showImportActionCard = true, showImportItemsChevron = false),
) )
} }
@ -182,6 +224,7 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
firstTimeActionManager = firstTimeActionManager, firstTimeActionManager = firstTimeActionManager,
snackbarRelayManager = snackbarRelayManager, snackbarRelayManager = snackbarRelayManager,
featureFlagManager = featureFlagManager, featureFlagManager = featureFlagManager,
policyManager = policyManager,
) )
} }