[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 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.BaseViewModel
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -25,6 +28,7 @@ class VaultSettingsViewModel @Inject constructor(
snackbarRelayManager: SnackbarRelayManager<SnackbarRelay>,
private val firstTimeActionManager: FirstTimeActionManager,
private val featureFlagManager: FeatureFlagManager,
private val policyManager: PolicyManager,
) : BaseViewModel<VaultSettingsState, VaultSettingsEvent, VaultSettingsAction>(
initialState = run {
val firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState
@ -55,7 +59,13 @@ class VaultSettingsViewModel @Inject constructor(
featureFlagManager
.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)
.launchIn(viewModelScope)
}
@ -80,8 +90,8 @@ class VaultSettingsViewModel @Inject constructor(
handleSnackbarDataReceived(action)
}
is VaultSettingsAction.Internal.ImportFeatureUpdated -> {
handleImportFeatureUpdated(action)
is VaultSettingsAction.Internal.CredentialExchangeAvailabilityChanged -> {
handleCredentialExchangeAvailabilityChanged(action)
}
}
}
@ -92,8 +102,8 @@ class VaultSettingsViewModel @Inject constructor(
sendEvent(VaultSettingsEvent.ShowSnackbar(action.data))
}
private fun handleImportFeatureUpdated(
action: VaultSettingsAction.Internal.ImportFeatureUpdated,
private fun handleCredentialExchangeAvailabilityChanged(
action: VaultSettingsAction.Internal.CredentialExchangeAvailabilityChanged,
) {
mutableStateFlow.update { it.copy(showImportItemsChevron = action.isEnabled) }
}
@ -128,7 +138,9 @@ class VaultSettingsViewModel @Inject constructor(
}
private fun handleImportItemsClicked() {
if (featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport)) {
if (featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport) &&
policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP).isEmpty()
) {
sendEvent(VaultSettingsEvent.NavigateToImportItems)
} else {
sendEvent(VaultSettingsEvent.NavigateToImportVault)
@ -218,9 +230,9 @@ sealed class 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,
) : Internal()

View File

@ -3,12 +3,15 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import app.cash.turbine.test
import com.bitwarden.core.data.manager.model.FlagKey
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.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
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.ui.platform.model.SnackbarRelay
import io.mockk.every
@ -38,6 +41,11 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
getFeatureFlagFlow(FlagKey.CredentialExchangeProtocolImport)
} 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 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")
@Test
fun `ImportItemsClick should emit NavigateToImportItems when CredentialExchangeProtocolImport is enabled`() =
@ -160,21 +187,36 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
}
@Test
fun `CredentialExchangeProtocolImport flag changes should update state accordingly`() {
fun `showImportItemsChevron should display based on feature flag and policies`() {
val viewModel = createViewModel()
// Verify chevron is shown when feature flag is enabled and no policies (default state)
assertEquals(
viewModel.stateFlow.value,
VaultSettingsState(showImportActionCard = true, showImportItemsChevron = true),
)
// 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),
)
// Verify chevron is hidden when feature flag is enabled and policies exist
mutableFeatureFlagFlow.tryEmit(true)
mutablePoliciesFlow.tryEmit(listOf(mockk()))
assertEquals(
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,
snackbarRelayManager = snackbarRelayManager,
featureFlagManager = featureFlagManager,
policyManager = policyManager,
)
}