From 1a2beea770e6fb3bfcafe2d485ccf1eaacc103a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Mon, 5 May 2025 14:56:58 +0100 Subject: [PATCH] [PM-18092] Update cipher delete restore permissions (#5075) Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> --- .../data/platform/manager/model/FlagKey.kt | 10 + .../sdk/model/Fido2CredentialStoreImpl.kt | 13 +- .../data/vault/repository/VaultRepository.kt | 9 + .../vault/repository/VaultRepositoryImpl.kt | 37 +++ .../util/VaultSdkCipherExtensions.kt | 38 +++ .../components/FeatureFlagListItems.kt | 2 + .../feature/addedit/VaultAddEditViewModel.kt | 25 +- .../ui/vault/feature/item/VaultItemScreen.kt | 2 +- .../vault/feature/item/VaultItemViewModel.kt | 37 ++- .../feature/item/model/VaultItemStateData.kt | 1 + .../feature/item/util/CipherViewExtensions.kt | 2 + .../vault/util/VaultAddItemStateExtensions.kt | 1 + .../main/res/values/strings_non_localized.xml | 1 + .../data/platform/manager/FlagKeyTest.kt | 6 + .../datasource/disk/VaultDiskSourceTest.kt | 4 + .../datasource/sdk/model/CipherViewUtil.kt | 1 + .../sdk/model/VaultSdkCipherUtil.kt | 14 ++ .../debugmenu/DebugMenuViewModelTest.kt | 2 + .../addedit/VaultAddEditViewModelTest.kt | 163 ++++++++++++- .../addedit/util/CipherViewExtensionsTest.kt | 2 + .../vault/feature/item/VaultItemScreenTest.kt | 2 + .../feature/item/VaultItemViewModelTest.kt | 227 ++++++++++++++++++ .../item/util/CipherViewExtensionsTest.kt | 15 ++ .../feature/item/util/VaultItemTestUtil.kt | 4 + .../util/VaultAddItemStateExtensionsTest.kt | 6 + gradle/libs.versions.toml | 2 +- .../network/model/SyncResponseJson.kt | 18 ++ .../network/service/CiphersServiceTest.kt | 8 + .../network/service/SyncServiceTest.kt | 4 + .../network/model/SyncResponseCipherUtil.kt | 13 + 30 files changed, 655 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt index d40fbb5a8b..9d6908202f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt @@ -41,6 +41,7 @@ sealed class FlagKey { ChromeAutofill, MobileErrorReporting, FlightRecorder, + RestrictCipherItemDeletion, PreAuthSettings, ) } @@ -194,6 +195,15 @@ sealed class FlagKey { override val isRemotelyConfigured: Boolean = true } + /** + * Data object holding the feature flag key to enable the restriction of cipher item deletion + */ + data object RestrictCipherItemDeletion : FlagKey() { + override val keyName: String = "pm-15493-restrict-item-deletion-to-can-manage-permission" + override val defaultValue: Boolean = false + override val isRemotelyConfigured: Boolean = false + } + /** * Data object holding the feature flag key to enable the settings menu before login. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/Fido2CredentialStoreImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/Fido2CredentialStoreImpl.kt index 9fec401442..dc636be44b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/Fido2CredentialStoreImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/Fido2CredentialStoreImpl.kt @@ -4,6 +4,7 @@ import com.bitwarden.core.annotation.OmitFromCoverage import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.sdk.Fido2CredentialStore import com.bitwarden.vault.Cipher +import com.bitwarden.vault.CipherListView import com.bitwarden.vault.CipherView import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials @@ -24,15 +25,23 @@ class Fido2CredentialStoreImpl( /** * Return all active ciphers that contain FIDO 2 credentials. */ - override suspend fun allCredentials(): List { + override suspend fun allCredentials(): List { val syncResult = vaultRepository.syncForResult() if (syncResult is SyncVaultDataResult.Error) { syncResult.throwable ?.let { throw it } ?: throw IllegalStateException("Sync failed.") } - return vaultRepository.ciphersStateFlow.value.data + val activeCipherIds = vaultRepository.ciphersStateFlow.value.data ?.filter { it.isActiveWithFido2Credentials } + ?.map { it.id } + ?: emptyList() + + return vaultRepository.ciphersListViewStateFlow.value.data + ?.filter { clv -> + activeCipherIds + .contains(clv.id) + } ?: emptyList() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt index c4a8f8df5e..26a5157e55 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt @@ -8,6 +8,7 @@ import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.sdk.Fido2CredentialStore import com.bitwarden.send.SendType import com.bitwarden.send.SendView +import com.bitwarden.vault.CipherListView import com.bitwarden.vault.CipherView import com.bitwarden.vault.CollectionView import com.bitwarden.vault.FolderView @@ -65,6 +66,14 @@ interface VaultRepository : CipherManager, VaultLockManager { */ val ciphersStateFlow: StateFlow>> + /** + * Flow that represents all ciphers for the active user. + * + * Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself + * must be collected in order to trigger state changes. + */ + val ciphersListViewStateFlow: StateFlow>> + /** * Flow that represents all collections for the active user. * diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index cd00ac65a2..09f0c20444 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -30,6 +30,7 @@ import com.bitwarden.sdk.Fido2CredentialStore import com.bitwarden.send.Send 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 @@ -169,6 +170,9 @@ class VaultRepositoryImpl( private val mutableCiphersStateFlow = MutableStateFlow>>(DataState.Loading) + private val mutableCiphersListViewStateFlow = + MutableStateFlow>>(DataState.Loading) + private val mutableFoldersStateFlow = MutableStateFlow>>(DataState.Loading) @@ -214,6 +218,9 @@ class VaultRepositoryImpl( override val ciphersStateFlow: StateFlow>> get() = mutableCiphersStateFlow.asStateFlow() + override val ciphersListViewStateFlow: StateFlow>> + get() = mutableCiphersListViewStateFlow.asStateFlow() + override val domainsStateFlow: StateFlow> get() = mutableDomainsStateFlow.asStateFlow() @@ -260,6 +267,15 @@ class VaultRepositoryImpl( } .launchIn(unconfinedScope) + mutableCiphersListViewStateFlow + .observeWhenSubscribedAndUnlocked( + userStateFlow = authDiskSource.userStateFlow, + vaultUnlockFlow = vaultUnlockDataStateFlow, + ) { activeUserId -> + observeVaultDiskCiphersToCipherListView(activeUserId) + } + .launchIn(unconfinedScope) + // Setup domains MutableStateFlow mutableDomainsStateFlow .observeWhenSubscribedAndLoggedIn( @@ -1071,6 +1087,27 @@ class VaultRepositoryImpl( .map { it.orLoadingIfNotSynced(userId = userId) } .onEach { mutableCiphersStateFlow.value = it } + private fun observeVaultDiskCiphersToCipherListView( + userId: String, + ): Flow>> = + vaultDiskSource + .getCiphers(userId = userId) + .onStart { mutableCiphersListViewStateFlow.updateToPendingOrLoading() } + .map { + waitUntilUnlocked(userId = userId) + vaultSdkSource + .decryptCipherListCollection( + userId = userId, + cipherList = it.toEncryptedSdkCipherList(), + ) + .fold( + onSuccess = { ciphers -> DataState.Loaded(ciphers.sortAlphabetically()) }, + onFailure = { throwable -> DataState.Error(throwable) }, + ) + } + .map { it.orLoadingIfNotSynced(userId = userId) } + .onEach { mutableCiphersListViewStateFlow.value = it } + private fun observeVaultDiskDomains( userId: String, ): Flow> = diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt index e0cb740834..765456f53e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt @@ -15,6 +15,8 @@ import com.bitwarden.network.model.UriMatchTypeJson import com.bitwarden.vault.Attachment import com.bitwarden.vault.Card import com.bitwarden.vault.Cipher +import com.bitwarden.vault.CipherListView +import com.bitwarden.vault.CipherPermissions import com.bitwarden.vault.CipherRepromptType import com.bitwarden.vault.CipherType import com.bitwarden.vault.CipherView @@ -68,6 +70,7 @@ fun Cipher.toEncryptedNetworkCipherResponse(): SyncResponseJson.Cipher = notes = notes, reprompt = reprompt.toNetworkRepromptType(), passwordHistory = passwordHistory?.toEncryptedNetworkPasswordHistoryList(), + permissions = permissions?.toEncryptedNetworkCipherPermissions(), type = type.toNetworkCipherType(), login = login?.toEncryptedNetworkLogin(), secureNote = secureNote?.toEncryptedNetworkSecureNote(), @@ -299,6 +302,17 @@ private fun PasswordHistory.toEncryptedNetworkPasswordHistory(): SyncResponseJso lastUsedDate = ZonedDateTime.ofInstant(lastUsedDate, ZoneOffset.UTC), ) +/** + * Converts a Bitwarden SDK [CipherPermissions] object to a corresponding + * [SyncResponseJson.Cipher.CipherPermissions] object. + */ +@Suppress("MaxLineLength") +private fun CipherPermissions.toEncryptedNetworkCipherPermissions(): SyncResponseJson.Cipher.CipherPermissions = + SyncResponseJson.Cipher.CipherPermissions( + delete = delete, + restore = restore, + ) + /** * Converts a Bitwarden SDK [CipherRepromptType] object to a corresponding * [CipherRepromptTypeJson] object. @@ -357,6 +371,7 @@ fun SyncResponseJson.Cipher.toEncryptedSdkCipher(): Cipher = attachments = attachments?.toSdkAttachmentList(), fields = fields?.toSdkFieldList(), passwordHistory = passwordHistory?.toSdkPasswordHistoryList(), + permissions = permissions?.toSdkPermissions(), creationDate = creationDate.toInstant(), deletedDate = deletedDate?.toInstant(), revisionDate = revisionDate.toInstant(), @@ -531,6 +546,16 @@ fun SyncResponseJson.Cipher.PasswordHistory.toSdkPasswordHistory(): PasswordHist lastUsedDate = lastUsedDate.toInstant(), ) +/** + * Transforms a [SyncResponseJson.Cipher.CipherPermissions] into + * a corresponding Bitwarden SDK [CipherPermissions]. + */ +fun SyncResponseJson.Cipher.CipherPermissions.toSdkPermissions(): CipherPermissions = + CipherPermissions( + delete = delete, + restore = restore, + ) + /** * Transforms a [CipherTypeJson] to the corresponding Bitwarden SDK [CipherType]. */ @@ -588,3 +613,16 @@ fun List.sortAlphabetically(): List { }, ) } + +/** + * Sorts the data in alphabetical order by name. Using lexicographical sorting but giving + * precedence to special characters over letters and digits. + */ +@JvmName("toAlphabeticallySortedCipherListView") +fun List.sortAlphabetically(): List { + return this.sortedWith( + comparator = { cipher1, cipher2 -> + SpecialCharWithPrecedenceComparator.compare(cipher1.name, cipher2.name) + }, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt index 217db5c782..967f019e0f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt @@ -41,6 +41,7 @@ fun FlagKey.ListItemContent( FlagKey.ChromeAutofill, FlagKey.MobileErrorReporting, FlagKey.FlightRecorder, + FlagKey.RestrictCipherItemDeletion, FlagKey.PreAuthSettings, -> { @Suppress("UNCHECKED_CAST") @@ -102,5 +103,6 @@ private fun FlagKey.getDisplayLabel(): String = when (this) { FlagKey.ChromeAutofill -> stringResource(R.string.enable_chrome_autofill) FlagKey.MobileErrorReporting -> stringResource(R.string.enable_error_reporting_dialog) FlagKey.FlightRecorder -> stringResource(R.string.enable_flight_recorder) + FlagKey.RestrictCipherItemDeletion -> stringResource(R.string.restrict_item_deletion) FlagKey.PreAuthSettings -> stringResource(R.string.enable_pre_auth_settings) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index bd0ffc23eb..f18d65eafc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -26,12 +26,14 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResu import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement import com.x8bit.bitwarden.data.autofill.fido2.util.getCreatePasskeyCredentialRequestOrNull import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials +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.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType +import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull @@ -119,6 +121,7 @@ class VaultAddEditViewModel @Inject constructor( private val organizationEventManager: OrganizationEventManager, private val networkConnectionManager: NetworkConnectionManager, private val firstTimeActionManager: FirstTimeActionManager, + private val featureFlagManager: FeatureFlagManager, ) : BaseViewModel( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] @@ -1719,6 +1722,10 @@ class VaultAddEditViewModel @Inject constructor( vaultData: VaultData?, userData: UserState?, ): VaultAddEditState { + val restrictCipherItemDeletionEnabled = featureFlagManager + .getFeatureFlag( + FlagKey.RestrictCipherItemDeletion, + ) val internalVaultData = vaultData ?: VaultData( cipherViewList = emptyList(), @@ -1737,10 +1744,15 @@ class VaultAddEditViewModel @Inject constructor( currentAccount = userData?.activeAccount, vaultAddEditType = vaultAddEditType, ) { currentAccount, cipherView -> - - val canDelete = internalVaultData - .collectionViewList - .hasDeletePermissionInAtLeastOneCollection(cipherView?.collectionIds) + val canDelete = if (restrictCipherItemDeletionEnabled && + cipherView?.permissions?.delete != null + ) { + cipherView.permissions?.delete == true + } else { + internalVaultData + .collectionViewList + .hasDeletePermissionInAtLeastOneCollection(cipherView?.collectionIds) + } val canAssignToCollections = internalVaultData .collectionViewList @@ -2241,16 +2253,21 @@ data class VaultAddEditState( * This is only present when editing a pre-existing cipher. * @property name Represents the name for the item type. This is an abstract property * that must be overridden to save the item. + * @property isUnlockWithPasswordEnabled Indicates whether the user is allowed to + * unlock with a password. * @property masterPasswordReprompt Indicates if a master password reprompt is required. * @property favorite Indicates whether this item is marked as a favorite. * @property customFieldData Additional custom fields associated with the item. * @property notes Any additional notes or comments associated with the item. + * @property selectedCollectionId The ID of the collection that this item belongs to. * @property selectedFolderId The ID of the folder that this item belongs to. * @property availableFolders The list of folders that this item could be added too. * @property selectedOwnerId The ID of the owner associated with the item. * @property availableOwners A list of available owners. * @property hasOrganizations Indicates if the user is part of any organizations. * @property canDelete Indicates whether the current user can delete the item. + * @property canAssignToCollections Indicates whether the current user can assign the + * item to a collection. */ @Parcelize data class Common( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt index f5fdf71b88..cad2f2ce93 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt @@ -166,7 +166,7 @@ fun VaultItemScreen( { viewModel.trySendAction(VaultItemAction.Common.CloseClick) } }, actions = { - if (state.isCipherDeleted && state.canDelete) { + if (state.canRestore) { BitwardenTextButton( label = stringResource(id = R.string.restore), onClick = remember(viewModel) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index 66904a2142..2476790915 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -17,8 +17,10 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager +import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -70,6 +72,7 @@ class VaultItemViewModel @Inject constructor( private val organizationEventManager: OrganizationEventManager, private val environmentRepository: EnvironmentRepository, private val settingsRepository: SettingsRepository, + private val featureFlagManager: FeatureFlagManager, ) : BaseViewModel( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] ?: run { @@ -105,6 +108,10 @@ class VaultItemViewModel @Inject constructor( vaultRepository.collectionsStateFlow, vaultRepository.foldersStateFlow, ) { cipherViewState, userState, authCodeState, collectionsState, folderState -> + val restrictCipherItemDeletionEnabled = featureFlagManager + .getFeatureFlag( + FlagKey.RestrictCipherItemDeletion, + ) val totpCodeData = authCodeState.data?.let { TotpCodeItemData( periodSeconds = it.periodSeconds, @@ -126,10 +133,24 @@ class VaultItemViewModel @Inject constructor( } .mapNullable { val cipherView = cipherViewState.data - val canDelete = collectionsState.data - .hasDeletePermissionInAtLeastOneCollection( + val canDelete = if (restrictCipherItemDeletionEnabled && + cipherView?.permissions?.delete != null + ) { + cipherView.permissions?.delete == true + } else { + collectionsState.data.hasDeletePermissionInAtLeastOneCollection( collectionIds = cipherView?.collectionIds, ) + } + + val canRestore = if (restrictCipherItemDeletionEnabled && + cipherView?.permissions?.restore != null + ) { + cipherView.permissions?.restore == true && + cipherView.deletedDate != null + } else { + canDelete && cipherView?.deletedDate != null + } val canAssignToCollections = collectionsState.data .canAssignToCollections(cipherView?.collectionIds) @@ -167,6 +188,7 @@ class VaultItemViewModel @Inject constructor( cipher = cipherView, totpCodeItemData = totpCodeData, canDelete = canDelete, + canRestore = canRestore, canAssociateToCollections = canAssignToCollections, canEdit = canEdit, relatedLocations = relatedLocations, @@ -1200,6 +1222,7 @@ class VaultItemViewModel @Inject constructor( hasMasterPassword = account.hasMasterPassword, totpCodeItemData = this.data?.totpCodeItemData, canDelete = this.data?.canDelete == true, + canRestore = this.data?.canRestore == true, canAssignToCollections = this.data?.canAssociateToCollections == true, canEdit = this.data?.canEdit == true, baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl, @@ -1496,6 +1519,14 @@ data class VaultItemState( ?.common ?.canDelete == true + /** + * Whether or not the cipher can be deleted. + */ + val canRestore: Boolean + get() = viewState.asContentOrNull() + ?.common + ?.canRestore == true + val canAssignToCollections: Boolean get() = viewState.asContentOrNull() ?.common @@ -1555,6 +1586,7 @@ data class VaultItemState( * @property currentCipher The cipher that is currently being viewed (nullable). * @property attachments A list of attachments associated with the cipher. * @property canDelete Indicates if the cipher can be deleted. + * @property canRestore Indicates if the cipher can be restored. * @property canAssignToCollections Indicates if the cipher can be assigned to * collections. * @property favorite Indicates that the cipher is favorite. @@ -1572,6 +1604,7 @@ data class VaultItemState( val currentCipher: CipherView? = null, val attachments: List?, val canDelete: Boolean, + val canRestore: Boolean, val canAssignToCollections: Boolean, val canEdit: Boolean, val favorite: Boolean, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt index 862e81c1b0..1519cd57ad 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt @@ -17,6 +17,7 @@ data class VaultItemStateData( val cipher: CipherView?, val totpCodeItemData: TotpCodeItemData?, val canDelete: Boolean, + val canRestore: Boolean, val canAssociateToCollections: Boolean, val canEdit: Boolean, val relatedLocations: ImmutableList, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt index 15ad34b16a..e696eaa4cd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt @@ -45,6 +45,7 @@ fun CipherView.toViewState( totpCodeItemData: TotpCodeItemData?, clock: Clock = Clock.systemDefaultZone(), canDelete: Boolean, + canRestore: Boolean, canAssignToCollections: Boolean, canEdit: Boolean, baseIconUrl: String, @@ -98,6 +99,7 @@ fun CipherView.toViewState( } .orEmpty(), canDelete = canDelete, + canRestore = canRestore, canAssignToCollections = canAssignToCollections, canEdit = canEdit, favorite = this.favorite, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt index efcf83e7df..c52556c363 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt @@ -40,6 +40,7 @@ fun VaultAddEditState.ViewState.Content.toCipherView(): CipherView = attachments = common.originalCipher?.attachments, organizationUseTotp = common.originalCipher?.organizationUseTotp ?: false, passwordHistory = toPasswordHistory(), + permissions = common.originalCipher?.permissions, creationDate = common.originalCipher?.creationDate ?: Instant.now(), deletedDate = common.originalCipher?.deletedDate, revisionDate = common.originalCipher?.revisionDate ?: Instant.now(), diff --git a/app/src/main/res/values/strings_non_localized.xml b/app/src/main/res/values/strings_non_localized.xml index fe27475cb4..212c0c69e8 100644 --- a/app/src/main/res/values/strings_non_localized.xml +++ b/app/src/main/res/values/strings_non_localized.xml @@ -26,6 +26,7 @@ Enable chrome autofill Enable error reporting dialog Enable flight recorder + Restrict item deletion Enabled pre-auth settings diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FlagKeyTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FlagKeyTest.kt index 9685e16e72..6d2ca7b61e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FlagKeyTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FlagKeyTest.kt @@ -69,6 +69,10 @@ class FlagKeyTest { FlagKey.FlightRecorder.keyName, "enable-pm-flight-recorder", ) + assertEquals( + FlagKey.RestrictCipherItemDeletion.keyName, + "pm-15493-restrict-item-deletion-to-can-manage-permission", + ) assertEquals( FlagKey.PreAuthSettings.keyName, "enable-pm-prelogin-settings", @@ -94,6 +98,7 @@ class FlagKeyTest { FlagKey.ChromeAutofill, FlagKey.MobileErrorReporting, FlagKey.FlightRecorder, + FlagKey.RestrictCipherItemDeletion, FlagKey.PreAuthSettings, ).all { !it.defaultValue @@ -129,6 +134,7 @@ class FlagKeyTest { listOf( FlagKey.FlightRecorder, FlagKey.PreAuthSettings, + FlagKey.RestrictCipherItemDeletion, ).all { !it.isRemotelyConfigured }, diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt index a3313d796d..c2539aae1f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt @@ -326,6 +326,10 @@ private const val CIPHER_JSON = """ "lastUsedDate": "2023-10-27T12:00:00.000Z" } ], + "permissions": { + "delete": true, + "restore": true + }, "revisionDate": "2023-10-27T12:00:00.000Z", "type": 1, "login": { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt index e7fd31143a..570a3e45f8 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt @@ -83,6 +83,7 @@ fun createMockCipherView( sshKey = sshKey.takeIf { cipherType == CipherType.SSH_KEY }, favorite = false, passwordHistory = listOf(createMockPasswordHistoryView(number = number, clock)), + permissions = createMockSdkCipherPermissions(), reprompt = repromptType, secureNote = createMockSecureNoteView().takeIf { cipherType == CipherType.SECURE_NOTE }, edit = true, diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt index 2920e071f1..bcd0f8e461 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk.model import com.bitwarden.vault.Attachment import com.bitwarden.vault.Card import com.bitwarden.vault.Cipher +import com.bitwarden.vault.CipherPermissions import com.bitwarden.vault.CipherRepromptType import com.bitwarden.vault.CipherType import com.bitwarden.vault.Field @@ -53,6 +54,7 @@ fun createMockSdkCipher(number: Int, clock: Clock = FIXED_CLOCK): Cipher = sshKey = createMockSdkSshKey(number = number), favorite = false, passwordHistory = listOf(createMockSdkPasswordHistory(number = number, clock = clock)), + permissions = createMockSdkCipherPermissions(), reprompt = CipherRepromptType.NONE, secureNote = createMockSdkSecureNote(), edit = false, @@ -78,6 +80,18 @@ fun createMockSdkPasswordHistory(number: Int, clock: Clock): PasswordHistory = lastUsedDate = clock.instant(), ) +/** + * Create a mock [CipherPermissions] with a given [delete] and [restore] permissions. + */ +fun createMockSdkCipherPermissions( + delete: Boolean = true, + restore: Boolean = true, +): CipherPermissions = + CipherPermissions( + delete = delete, + restore = restore, + ) + /** * Create a mock [Identity] with a given [number]. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt index 8cacdd734e..8aa1aaac96 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt @@ -135,6 +135,7 @@ private val DEFAULT_MAP_VALUE: ImmutableMap, Any> = persistentMapOf FlagKey.ChromeAutofill to true, FlagKey.MobileErrorReporting to true, FlagKey.FlightRecorder to true, + FlagKey.RestrictCipherItemDeletion to true, FlagKey.PreAuthSettings to true, ) @@ -154,6 +155,7 @@ private val UPDATED_MAP_VALUE: ImmutableMap, Any> = persistentMapOf FlagKey.ChromeAutofill to false, FlagKey.MobileErrorReporting to false, FlagKey.FlightRecorder to false, + FlagKey.RestrictCipherItemDeletion to false, FlagKey.PreAuthSettings to false, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index 9795f6ec4a..572fa8ce86 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -38,6 +38,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CreateCredentialRequest import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +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.SpecialCircumstanceManager @@ -46,6 +47,7 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState +import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager @@ -56,6 +58,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createEditCollectionV import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createEditExceptPasswordsCollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createManageCollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipherPermissions import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createViewCollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createViewExceptPasswordsCollectionView @@ -190,6 +193,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { every { shouldShowAddLoginCoachMarkFlow } returns mutableShouldShowAddLoginCoachMarkFlow } + private val featureFlagManager: FeatureFlagManager = mockk { + every { getFeatureFlag(key = FlagKey.RestrictCipherItemDeletion) } returns false + } + @BeforeEach fun setup() { mockkStatic(CipherView::toViewState) @@ -1146,10 +1153,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { .copy(shouldExitOnSave = true) val mockProviderCreateCredentialRequest: ProviderCreateCredentialRequest = - mockk(relaxed = true) { - every { callingAppInfo } returns mockk(relaxed = true) - every { callingRequest } returns mockk(relaxed = true) - } + mockk(relaxed = true) { + every { callingAppInfo } returns mockk(relaxed = true) + every { callingRequest } returns mockk(relaxed = true) + } every { ProviderCreateCredentialRequest.fromBundle(any()) @@ -1304,6 +1311,152 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } + @Test + fun `in edit mode, canDelete should be false when cipher permission is false`() = + runTest { + val cipherView = createMockCipherView(1) + .copy( + permissions = createMockSdkCipherPermissions( + delete = false, + restore = false, + ), + ) + val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID) + val stateWithName = createVaultAddItemState( + vaultAddEditType = vaultAddEditType, + commonContentViewState = createCommonContentViewState( + name = "mockName-1", + originalCipher = cipherView, + customFieldData = listOf( + VaultAddEditState.Custom.HiddenField( + itemId = "testId", + name = "mockName-1", + value = "mockValue-1", + ), + ), + notes = "mockNotes-1", + canDelete = false, + ), + ) + every { + featureFlagManager.getFeatureFlag(FlagKey.RestrictCipherItemDeletion) + } returns true + + every { + cipherView.toViewState( + isClone = false, + isIndividualVaultDisabled = false, + totpData = null, + resourceManager = resourceManager, + clock = fixedClock, + canDelete = false, + canAssignToCollections = true, + ) + } returns stateWithName.viewState + + mutableVaultDataFlow.value = DataState.Loaded( + data = createVaultData( + cipherView = cipherView, + collectionViewList = listOf( + createEditCollectionView(number = 1), + ), + ), + ) + + createAddVaultItemViewModel( + createSavedStateHandleWithState( + state = stateWithName, + vaultAddEditType = vaultAddEditType, + vaultItemCipherType = VaultItemCipherType.LOGIN, + ), + ) + + verify { + cipherView.toViewState( + isClone = false, + isIndividualVaultDisabled = false, + totpData = null, + resourceManager = resourceManager, + clock = fixedClock, + canDelete = false, + canAssignToCollections = true, + ) + } + } + + @Test + fun `in edit mode, canDelete should be true when cipher permission is true`() = + runTest { + val cipherView = createMockCipherView(1) + .copy( + permissions = createMockSdkCipherPermissions( + delete = true, + restore = false, + ), + ) + val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID) + val stateWithName = createVaultAddItemState( + vaultAddEditType = vaultAddEditType, + commonContentViewState = createCommonContentViewState( + name = "mockName-1", + originalCipher = cipherView, + customFieldData = listOf( + VaultAddEditState.Custom.HiddenField( + itemId = "testId", + name = "mockName-1", + value = "mockValue-1", + ), + ), + notes = "mockNotes-1", + canDelete = true, + ), + ) + every { + featureFlagManager.getFeatureFlag(FlagKey.RestrictCipherItemDeletion) + } returns true + + every { + cipherView.toViewState( + isClone = false, + isIndividualVaultDisabled = false, + totpData = null, + resourceManager = resourceManager, + clock = fixedClock, + canDelete = true, + canAssignToCollections = true, + ) + } returns stateWithName.viewState + + mutableVaultDataFlow.value = DataState.Loaded( + data = createVaultData( + cipherView = cipherView, + collectionViewList = listOf( + createEditCollectionView(number = 1), + ), + ), + ) + + createAddVaultItemViewModel( + createSavedStateHandleWithState( + state = stateWithName, + vaultAddEditType = vaultAddEditType, + vaultItemCipherType = VaultItemCipherType.LOGIN, + ), + ) + + verify { + cipherView.toViewState( + isClone = false, + isIndividualVaultDisabled = false, + totpData = null, + resourceManager = resourceManager, + clock = fixedClock, + canDelete = true, + canAssignToCollections = true, + ) + } + } + @Suppress("MaxLineLength") @Test fun `in edit mode, canDelete should be false when cipher is in a collection the user cannot manage`() = @@ -3243,6 +3396,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { organizationEventManager = organizationEventManager, networkConnectionManager = networkConnectionManager, firstTimeActionManager = firstTimeActionManager, + featureFlagManager = featureFlagManager, ) } @@ -4582,6 +4736,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { organizationEventManager = organizationEventManager, networkConnectionManager = networkConnectionManager, firstTimeActionManager = firstTimeActionManager, + featureFlagManager = featureFlagManager, ) private fun createVaultData( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt index 9ea2ffe5c9..f0bbffe7b3 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt @@ -25,6 +25,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipherPermissions import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState @@ -636,6 +637,7 @@ private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView( lastUsedDate = FIXED_CLOCK.instant(), ), ), + permissions = createMockSdkCipherPermissions(), creationDate = FIXED_CLOCK.instant(), deletedDate = null, revisionDate = FIXED_CLOCK.instant(), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index da04f017ae..0cdfafd57d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -3190,6 +3190,7 @@ private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common = ), ), canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, favorite = false, @@ -3278,6 +3279,7 @@ private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common = requiresCloneConfirmation = false, attachments = emptyList(), canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, favorite = false, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index e1d3dd5be1..fcfadc90cb 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -20,9 +20,11 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.Organization import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState +import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -30,6 +32,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentReposito import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipherPermissions import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem import com.x8bit.bitwarden.data.vault.repository.VaultRepository @@ -102,6 +105,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { every { edit } returns true every { folderId } returns null every { organizationId } returns null + every { permissions } returns null + every { deletedDate } returns null } private val mockEnvironmentRepository = FakeEnvironmentRepository() private val mutableIsIconLoadingDisabledFlow = MutableStateFlow(false) @@ -109,6 +114,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { every { isIconLoadingDisabled } returns false every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow } + private val featureFlagManager: FeatureFlagManager = mockk { + every { getFeatureFlag(key = FlagKey.RestrictCipherItemDeletion) } returns false + } @BeforeEach fun setup() { @@ -198,6 +206,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -228,6 +237,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -251,6 +261,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -300,6 +311,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -338,6 +350,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -387,6 +400,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -441,6 +455,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -485,6 +500,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -524,6 +540,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -567,6 +584,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -613,6 +631,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -670,6 +689,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -708,6 +728,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -731,6 +752,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -762,6 +784,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -831,6 +854,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -891,6 +915,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -963,6 +988,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -993,6 +1019,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1013,6 +1040,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1037,6 +1065,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1082,6 +1111,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1120,6 +1150,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1160,6 +1191,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -1197,6 +1229,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1222,6 +1255,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1252,6 +1286,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1276,6 +1311,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1316,6 +1352,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1348,6 +1385,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1376,6 +1414,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1430,6 +1469,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1460,6 +1500,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1484,6 +1525,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1522,6 +1564,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1552,6 +1595,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1576,6 +1620,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1624,6 +1669,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1686,6 +1732,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1758,6 +1805,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = null, @@ -1931,6 +1979,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -1976,6 +2025,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), @@ -2022,6 +2072,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), @@ -2045,6 +2096,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), @@ -2078,6 +2130,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), @@ -2098,6 +2151,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), @@ -2124,6 +2178,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), @@ -2180,6 +2235,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, totpCodeItemData = createTotpCodeData(), @@ -2207,6 +2263,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2251,6 +2308,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2282,6 +2340,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2302,6 +2361,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2335,6 +2395,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2356,6 +2417,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2391,6 +2453,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2415,6 +2478,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2453,6 +2517,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2492,6 +2557,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2524,6 +2590,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2544,6 +2611,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2572,6 +2640,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2592,6 +2661,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2624,6 +2694,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2644,6 +2715,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2675,6 +2747,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2695,6 +2768,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2727,6 +2801,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2747,6 +2822,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2775,6 +2851,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2795,6 +2872,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2827,6 +2905,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2846,6 +2925,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2877,6 +2957,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2909,6 +2990,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2947,6 +3029,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -2981,6 +3064,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -3002,6 +3086,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -3035,6 +3120,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -3055,6 +3141,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -3091,6 +3178,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -3132,6 +3220,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -3173,6 +3262,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -3328,6 +3418,138 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, + canAssignToCollections = true, + canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = persistentListOf( + VaultItemLocation.Organization("mockOrganizationName"), + VaultItemLocation.Collection("mockName-1"), + VaultItemLocation.Folder("mockName-1"), + ), + ) + } returns viewState + mutableUserStateFlow.value = DEFAULT_USER_STATE.copy( + accounts = listOf( + DEFAULT_USER_ACCOUNT.copy( + organizations = listOf( + Organization( + id = "mockOrganizationId", + name = "mockOrganizationName", + shouldManageResetPassword = false, + shouldUseKeyConnector = false, + role = OrganizationType.OWNER, + keyConnectorUrl = null, + userIsClaimedByOrganization = true, + ), + ), + ), + ), + ) + + val viewModel = createViewModel(state = null) + + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) + mutableCollectionsStateFlow.value = DataState.Loaded( + listOf(createMockCollectionView(number = 1)), + ) + mutableFoldersStateFlow.value = DataState.Loaded( + listOf(createMockFolderView(number = 1)), + ) + + assertEquals( + DEFAULT_STATE.copy(viewState = viewState), + viewModel.stateFlow.value, + ) + } + + @Test + @Suppress("MaxLineLength") + fun `on VaultDataReceive with Loaded and nonnull false permission data should update the ViewState with cipher permissions`() { + val viewState = mockk() + every { + featureFlagManager.getFeatureFlag(FlagKey.RestrictCipherItemDeletion) + } returns true + every { mockCipherView.organizationId } returns "mockOrganizationId" + every { mockCipherView.collectionIds } returns listOf("mockId-1") + every { mockCipherView.folderId } returns "mockId-1" + every { + mockCipherView.permissions + } returns createMockSdkCipherPermissions(delete = false, restore = false) + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = false, + canRestore = false, + canAssignToCollections = true, + canEdit = true, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = false, + relatedLocations = persistentListOf( + VaultItemLocation.Organization("mockOrganizationName"), + VaultItemLocation.Collection("mockName-1"), + VaultItemLocation.Folder("mockName-1"), + ), + ) + } returns viewState + mutableUserStateFlow.value = DEFAULT_USER_STATE.copy( + accounts = listOf( + DEFAULT_USER_ACCOUNT.copy( + organizations = listOf( + Organization( + id = "mockOrganizationId", + name = "mockOrganizationName", + shouldManageResetPassword = false, + shouldUseKeyConnector = false, + role = OrganizationType.OWNER, + keyConnectorUrl = null, + userIsClaimedByOrganization = true, + ), + ), + ), + ), + ) + + val viewModel = createViewModel(state = null) + + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) + mutableCollectionsStateFlow.value = DataState.Loaded( + listOf(createMockCollectionView(number = 1)), + ) + mutableFoldersStateFlow.value = DataState.Loaded( + listOf(createMockFolderView(number = 1)), + ) + + assertEquals( + DEFAULT_STATE.copy(viewState = viewState), + viewModel.stateFlow.value, + ) + } + + @Test + @Suppress("MaxLineLength") + fun `on VaultDataReceive with Loaded and nonnull true permission data should update the ViewState with cipher permissions`() { + val viewState = mockk() + every { + featureFlagManager.getFeatureFlag(FlagKey.RestrictCipherItemDeletion) + } returns true + every { mockCipherView.organizationId } returns "mockOrganizationId" + every { mockCipherView.deletedDate } returns Instant.MIN + every { mockCipherView.collectionIds } returns listOf("mockId-1") + every { mockCipherView.folderId } returns "mockId-1" + every { mockCipherView.permissions } returns createMockSdkCipherPermissions() + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -3401,6 +3623,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -3446,6 +3669,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -3486,6 +3710,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, totpCodeItemData = null, canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, @@ -3568,6 +3793,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { organizationEventManager = eventManager, environmentRepository = environmentRepository, settingsRepository = settingsRepository, + featureFlagManager = featureFlagManager, ) private fun createViewState( @@ -3748,6 +3974,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { ), ), canDelete = true, + canRestore = false, canAssignToCollections = true, canEdit = true, favorite = false, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt index e4e416863d..491c61c5cb 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt @@ -68,6 +68,7 @@ class CipherViewExtensionsTest { ), clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -109,6 +110,7 @@ class CipherViewExtensionsTest { ), clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -143,6 +145,7 @@ class CipherViewExtensionsTest { ), clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -177,6 +180,7 @@ class CipherViewExtensionsTest { ), clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -217,6 +221,7 @@ class CipherViewExtensionsTest { ), clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -247,6 +252,7 @@ class CipherViewExtensionsTest { totpCodeItemData = null, clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -275,6 +281,7 @@ class CipherViewExtensionsTest { totpCodeItemData = null, clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -306,6 +313,7 @@ class CipherViewExtensionsTest { totpCodeItemData = null, clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -347,6 +355,7 @@ class CipherViewExtensionsTest { totpCodeItemData = null, clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -393,6 +402,7 @@ class CipherViewExtensionsTest { totpCodeItemData = null, clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -441,6 +451,7 @@ class CipherViewExtensionsTest { totpCodeItemData = null, clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -473,6 +484,7 @@ class CipherViewExtensionsTest { totpCodeItemData = null, clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -503,6 +515,7 @@ class CipherViewExtensionsTest { totpCodeItemData = null, clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -550,6 +563,7 @@ class CipherViewExtensionsTest { totpCodeItemData = null, clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", @@ -577,6 +591,7 @@ class CipherViewExtensionsTest { totpCodeItemData = null, clock = fixedClock, canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, baseIconUrl = "https://example.com/", diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt index 1ff60e47ed..3656269aa9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt @@ -15,6 +15,7 @@ import com.bitwarden.vault.SshKeyView import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList import com.bitwarden.ui.util.asText +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipherPermissions import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData @@ -153,6 +154,7 @@ fun createCipherView(type: CipherType, isEmpty: Boolean): CipherView = ), ) .takeUnless { isEmpty }, + permissions = createMockSdkCipherPermissions(), creationDate = Instant.ofEpochSecond(1_000L), deletedDate = null, revisionDate = Instant.ofEpochSecond(1_000L), @@ -174,6 +176,7 @@ fun createCommonContent( requiresCloneConfirmation = false, attachments = emptyList(), canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, favorite = false, @@ -236,6 +239,7 @@ fun createCommonContent( ), ), canDelete = true, + canRestore = true, canAssignToCollections = true, canEdit = true, favorite = false, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt index 6430fc086a..bd6caa18be 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt @@ -110,6 +110,7 @@ class VaultAddItemStateExtensionsTest { attachments = null, fields = emptyList(), passwordHistory = null, + permissions = null, creationDate = Instant.MIN, deletedDate = null, revisionDate = Instant.MIN, @@ -294,6 +295,7 @@ class VaultAddItemStateExtensionsTest { ), ), passwordHistory = null, + permissions = null, creationDate = Instant.MIN, deletedDate = null, revisionDate = Instant.MIN, @@ -426,6 +428,7 @@ class VaultAddItemStateExtensionsTest { attachments = null, fields = emptyList(), passwordHistory = null, + permissions = null, creationDate = Instant.MIN, deletedDate = null, revisionDate = Instant.MIN, @@ -612,6 +615,7 @@ class VaultAddItemStateExtensionsTest { attachments = null, fields = emptyList(), passwordHistory = null, + permissions = null, creationDate = Instant.MIN, deletedDate = null, revisionDate = Instant.MIN, @@ -754,6 +758,7 @@ class VaultAddItemStateExtensionsTest { attachments = null, fields = emptyList(), passwordHistory = null, + permissions = null, creationDate = Instant.MIN, deletedDate = null, revisionDate = Instant.MIN, @@ -972,6 +977,7 @@ private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView( lastUsedDate = Instant.MIN, ), ), + permissions = null, creationDate = Instant.MIN, deletedDate = null, revisionDate = Instant.MIN, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f3da53320..366cac4e16 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ androidxSplash = "1.1.0-rc01" androidXAppCompat = "1.7.0" androdixAutofill = "1.1.0" androidxWork = "2.10.0" -bitwardenSdk = "1.0.0-20250225.125021-120" +bitwardenSdk = "1.0.0-20250418.073303-151" crashlytics = "3.0.3" detekt = "1.23.8" firebaseBom = "33.12.0" diff --git a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt index 2deb103872..e34d041b45 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt @@ -455,6 +455,9 @@ data class SyncResponseJson( @SerialName("passwordHistory") val passwordHistory: List?, + @SerialName("permissions") + val permissions: CipherPermissions?, + @SerialName("revisionDate") @Contextual val revisionDate: ZonedDateTime, @@ -769,6 +772,21 @@ data class SyncResponseJson( val lastUsedDate: ZonedDateTime, ) + /** + * Represents a cipher permissions in the vault response. + * + * @property delete whether the delete permissions is active. + * @property restore whether the restore permissions is active. + */ + @Serializable + data class CipherPermissions( + @SerialName("delete") + val delete: Boolean, + + @SerialName("restore") + val restore: Boolean, + ) + /** * Represents a secure note in the vault response. * diff --git a/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt index deede80a63..1a19983f7d 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt @@ -375,6 +375,10 @@ private const val CREATE_ATTACHMENT_SUCCESS_JSON = """ "lastUsedDate": "2023-10-27T12:00:00.00Z" } ], + "permissions": { + "delete": true, + "restore": true + }, "revisionDate": "2023-10-27T12:00:00.00Z", "type": 1, "login": { @@ -498,6 +502,10 @@ private const val CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON = """ "lastUsedDate": "2023-10-27T12:00:00.00Z" } ], + "permissions": { + "delete": true, + "restore": true + }, "revisionDate": "2023-10-27T12:00:00.00Z", "type": 1, "login": { diff --git a/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt index e7a0511388..c676fee3a5 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt @@ -222,6 +222,10 @@ private const val SYNC_SUCCESS_JSON = """ "lastUsedDate": "2023-10-27T12:00:00.00Z" } ], + "permissions": { + "delete": true, + "restore": true + }, "revisionDate": "2023-10-27T12:00:00.00Z", "type": 1, "login": { diff --git a/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt b/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt index 62bb9b3867..db18adda38 100644 --- a/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt +++ b/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt @@ -41,6 +41,7 @@ fun createMockCipher( sshKey = createMockSshKey(number = number), isFavorite = false, passwordHistory = listOf(createMockPasswordHistory(number = number)), + permissions = createMockCipherPermissions(), reprompt = CipherRepromptTypeJson.NONE, secureNote = createMockSecureNote(), shouldEdit = false, @@ -109,6 +110,18 @@ fun createMockPasswordHistory(number: Int): SyncResponseJson.Cipher.PasswordHist lastUsedDate = MOCK_ZONED_DATE_TIME, ) +/** + * Create a mock [SyncResponseJson.Cipher.CipherPermissions]. + */ +fun createMockCipherPermissions( + delete: Boolean = true, + restore: Boolean = true, +): SyncResponseJson.Cipher.CipherPermissions = + SyncResponseJson.Cipher.CipherPermissions( + delete = delete, + restore = restore, + ) + /** * Create a mock [SyncResponseJson.Cipher.SecureNote]. */