From e1bb3a4b5d52af0d1575ee0768b4235dc45eeb58 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:15:53 -0500 Subject: [PATCH] [PM-27118] Restrict Credential Exchange import based on Personal Ownership policy (#6220) --- .../settings/vault/VaultSettingsViewModel.kt | 28 +++++++---- .../vault/VaultSettingsViewModelTest.kt | 47 ++++++++++++++++++- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt index 56767b2ed1..9e3bd1fc60 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt @@ -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, private val firstTimeActionManager: FirstTimeActionManager, private val featureFlagManager: FeatureFlagManager, + private val policyManager: PolicyManager, ) : BaseViewModel( 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() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt index 14d82498a6..c9f942301e 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt @@ -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>() + private val policyManager = mockk { + every { getActivePolicies(any()) } returns emptyList() + every { getActivePoliciesFlow(any()) } returns mutablePoliciesFlow + } private val mutableSnackbarSharedFlow = bufferedMutableSharedFlow() private val snackbarRelayManager = mockk> { @@ -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, ) }