[PM-23696] Hide cards from export when policy is enabled. (#5520)

This commit is contained in:
André Bispo 2025-07-15 16:21:39 +01:00 committed by GitHub
parent 33cfaa5e95
commit f26d54a2e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 187 additions and 11 deletions

View File

@ -9,6 +9,7 @@ import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.send.SendType
import com.bitwarden.send.SendView
import com.bitwarden.vault.CipherListView
import com.bitwarden.vault.CipherType
import com.bitwarden.vault.CipherView
import com.bitwarden.vault.CollectionView
import com.bitwarden.vault.FolderView
@ -262,6 +263,12 @@ interface VaultRepository : CipherManager, VaultLockManager {
/**
* Attempt to get the user's vault data for export.
*
* @param format The export format to use.
* @param restrictedTypes A list of restricted types to export.
*/
suspend fun exportVaultDataToString(format: ExportFormat): ExportVaultDataResult
suspend fun exportVaultDataToString(
format: ExportFormat,
restrictedTypes: List<CipherType>,
): ExportVaultDataResult
}

View File

@ -931,7 +931,10 @@ class VaultRepositoryImpl(
}
}
override suspend fun exportVaultDataToString(format: ExportFormat): ExportVaultDataResult {
override suspend fun exportVaultDataToString(
format: ExportFormat,
restrictedTypes: List<CipherType>,
): ExportVaultDataResult {
val userId = activeUserId
?: return ExportVaultDataResult.Error(error = NoActiveUserException())
val folders = vaultDiskSource
@ -945,7 +948,11 @@ class VaultRepositoryImpl(
.firstOrNull()
.orEmpty()
.map { it.toEncryptedSdkCipher() }
.filter { it.collectionIds.isEmpty() && it.deletedDate == null }
.filter {
it.collectionIds.isEmpty() &&
it.deletedDate == null &&
!restrictedTypes.contains(it.type)
}
return vaultSdkSource
.exportVaultDataToString(

View File

@ -9,6 +9,7 @@ import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.bitwarden.vault.CipherType
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@ -16,7 +17,9 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
@ -41,15 +44,16 @@ private const val KEY_STATE = "state"
/**
* Manages application state for the Export Vault screen.
*/
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LongParameterList")
@HiltViewModel
class ExportVaultViewModel @Inject constructor(
private val authRepository: AuthRepository,
policyManager: PolicyManager,
private val policyManager: PolicyManager,
savedStateHandle: SavedStateHandle,
private val vaultRepository: VaultRepository,
private val fileManager: FileManager,
private val clock: Clock,
private val featureFlagManager: FeatureFlagManager,
) : BaseViewModel<ExportVaultState, ExportVaultEvent, ExportVaultAction>(
initialState = savedStateHandle[KEY_STATE]
?: ExportVaultState(
@ -433,6 +437,7 @@ class ExportVaultViewModel @Inject constructor(
state.passwordInput
},
),
restrictedTypes = getRestrictedItemTypes(),
)
sendAction(
@ -454,6 +459,22 @@ class ExportVaultViewModel @Inject constructor(
)
}
}
private fun getRestrictedItemTypes(): List<CipherType> {
val isRemoveCardPolicyFeatureEnabled =
featureFlagManager.getFeatureFlag(FlagKey.RemoveCardPolicy)
if (!isRemoveCardPolicyFeatureEnabled) {
return emptyList()
}
val hasActiveRestrictItemTypesPolicy =
policyManager.getActivePolicies(type = PolicyTypeJson.RESTRICT_ITEM_TYPES).isNotEmpty()
if (!hasActiveRestrictItemTypesPolicy) {
return emptyList()
}
return listOf(CipherType.CARD)
}
}
/**

View File

@ -15,6 +15,7 @@ import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.exporters.ExportFormat
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.network.model.CipherTypeJson
import com.bitwarden.network.model.CreateFileSendResponse
import com.bitwarden.network.model.CreateSendJsonResponse
import com.bitwarden.network.model.FolderJsonRequest
@ -41,6 +42,7 @@ import com.bitwarden.network.service.SyncService
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.send.SendType
import com.bitwarden.send.SendView
import com.bitwarden.vault.CipherType
import com.bitwarden.vault.CipherView
import com.bitwarden.vault.CollectionView
import com.bitwarden.vault.Folder
@ -4405,7 +4407,63 @@ class VaultRepositoryTest {
} returns "TestResult".asSuccess()
val expected = ExportVaultDataResult.Success(vaultData = "TestResult")
val result = vaultRepository.exportVaultDataToString(format = format)
val result = vaultRepository.exportVaultDataToString(
format = format,
restrictedTypes = emptyList(),
)
coVerify {
vaultSdkSource.exportVaultDataToString(
userId = userId,
ciphers = listOf(userCipher.toEncryptedSdkCipher()),
folders = listOf(createMockSdkFolder(1)),
format = ExportFormat.Json,
)
}
assertEquals(
expected,
result,
)
}
@Test
fun `exportVaultDataToString with restrictedTypes should filter out restricted cipher types`() =
runTest {
val format = ExportFormat.Json
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val userCipher = createMockCipher(1).copy(
type = CipherTypeJson.LOGIN,
collectionIds = null,
deletedDate = null,
)
val userCipherCard = createMockCipher(2).copy(
type = CipherTypeJson.CARD,
collectionIds = null,
deletedDate = null,
)
val deletedCipher = createMockCipher(2).copy(collectionIds = null)
val orgCipher = createMockCipher(3).copy(deletedDate = null)
coEvery {
vaultDiskSource.getCiphersFlow(userId)
} returns flowOf(listOf(userCipher, userCipherCard, deletedCipher, orgCipher))
coEvery {
vaultDiskSource.getFolders(userId)
} returns flowOf(listOf(createMockFolder(1)))
coEvery {
vaultSdkSource.exportVaultDataToString(userId, any(), any(), format)
} returns "TestResult".asSuccess()
val expected = ExportVaultDataResult.Success(vaultData = "TestResult")
val result = vaultRepository.exportVaultDataToString(
format = format,
restrictedTypes = listOf(CipherType.CARD),
)
coVerify {
vaultSdkSource.exportVaultDataToString(
@ -4442,7 +4500,10 @@ class VaultRepositoryTest {
} returns error.asFailure()
val expected = ExportVaultDataResult.Error(error = error)
val result = vaultRepository.exportVaultDataToString(format = format)
val result = vaultRepository.exportVaultDataToString(
format = format,
restrictedTypes = emptyList(),
)
assertEquals(
expected,

View File

@ -9,6 +9,7 @@ import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.network.model.createMockPolicy
import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.util.asText
import com.bitwarden.vault.CipherType
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
@ -18,8 +19,10 @@ import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
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.FlagKey
import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
@ -49,6 +52,9 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
every {
getActivePolicies(type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT)
} returns emptyList()
every {
getActivePolicies(type = PolicyTypeJson.RESTRICT_ITEM_TYPES)
} returns emptyList()
}
private val clock: Clock = Clock.fixed(
@ -57,10 +63,27 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
)
private val vaultRepository: VaultRepository = mockk {
coEvery { exportVaultDataToString(any()) } returns ExportVaultDataResult.Success("data")
coEvery {
exportVaultDataToString(
format = any(),
restrictedTypes = emptyList(),
)
} returns ExportVaultDataResult.Success("data")
coEvery {
exportVaultDataToString(
format = any(),
restrictedTypes = listOf(CipherType.CARD),
)
} returns ExportVaultDataResult.Success("data")
}
private val fileManager: FileManager = mockk()
private val featureFlagManager: FeatureFlagManager = mockk {
every {
getFeatureFlag(FlagKey.RemoveCardPolicy)
} returns false
}
@Test
fun `initial state should be correct`() = runTest {
every {
@ -106,7 +129,62 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked)
coVerify {
vaultRepository.exportVaultDataToString(any())
vaultRepository.exportVaultDataToString(any(), emptyList())
}
}
@Suppress("MaxLineLength")
@Test
fun `ConfirmExportVaultClicked correct password should call exportVaultDataToString with restricted item types when policy and feature flags enabled`() {
val password = "password"
coEvery {
authRepository.validatePassword(
password = password,
)
} returns ValidatePasswordResult.Success(isValid = true)
every {
policyManager.getActivePolicies(type = PolicyTypeJson.RESTRICT_ITEM_TYPES)
} returns listOf(createMockPolicy())
every {
featureFlagManager.getFeatureFlag(FlagKey.RemoveCardPolicy)
} returns true
val viewModel = createViewModel()
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password))
viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked)
coVerify {
vaultRepository.exportVaultDataToString(
format = any(),
restrictedTypes = listOf(CipherType.CARD),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `ConfirmExportVaultClicked correct password should call exportVaultDataToString without restricted item types when policy is disabled and feature flag is enabled`() {
val password = "password"
coEvery {
authRepository.validatePassword(
password = password,
)
} returns ValidatePasswordResult.Success(isValid = true)
every {
featureFlagManager.getFeatureFlag(FlagKey.RemoveCardPolicy)
} returns true
val viewModel = createViewModel()
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password))
viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked)
coVerify {
vaultRepository.exportVaultDataToString(
format = any(),
restrictedTypes = listOf(),
)
}
}
@ -135,7 +213,7 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
coVerify {
vaultRepository.exportVaultDataToString(any())
vaultRepository.exportVaultDataToString(any(), emptyList())
}
}
@ -168,7 +246,7 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
coVerify(exactly = 0) {
vaultRepository.exportVaultDataToString(any())
vaultRepository.exportVaultDataToString(any(), emptyList())
}
}
@ -214,6 +292,7 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
authRepository.validatePassword(password)
vaultRepository.exportVaultDataToString(
format = ExportFormat.EncryptedJson(filePassword),
restrictedTypes = emptyList(),
)
}
}
@ -746,6 +825,7 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
fileManager = fileManager,
vaultRepository = vaultRepository,
clock = clock,
featureFlagManager = featureFlagManager,
)
}