From 4e57f306d36549e01deb194cdb7971fde4a02090 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:56:50 -0400 Subject: [PATCH] [PM-26330] Correct owner data when individual vault is disabled (#5968) --- .../addedit/util/CipherViewExtensions.kt | 36 ++++- .../addedit/util/CipherViewExtensionsTest.kt | 145 ++++++++++++++---- 2 files changed, 146 insertions(+), 35 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt index eee507b926..9103ed151e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit.util +import com.bitwarden.collections.CollectionType import com.bitwarden.collections.CollectionView import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.ui.platform.resource.BitwardenString @@ -139,12 +140,22 @@ fun VaultAddEditState.ViewState.appendFolderAndOwnerData( .toSelectedOwnerId(cipherView = currentContentState.common.originalCipher) ?: collectionViewList .firstOrNull { it.id == currentContentState.common.selectedCollectionId } + ?.organizationId + ?: collectionViewList + .getDefaultCollectionViewOrNull( + isIndividualVaultDisabled = isIndividualVaultDisabled, + ) ?.organizationId, availableOwners = activeAccount.toAvailableOwners( collectionViewList = collectionViewList, cipherView = currentContentState.common.originalCipher, isIndividualVaultDisabled = isIndividualVaultDisabled, - selectedCollectionId = currentContentState.common.selectedCollectionId, + selectedCollectionId = currentContentState.common.selectedCollectionId + ?: collectionViewList + .getDefaultCollectionViewOrNull( + isIndividualVaultDisabled = isIndividualVaultDisabled, + ) + ?.id, ), isUnlockWithPasswordEnabled = activeAccount.hasMasterPassword, hasOrganizations = activeAccount.organizations.isNotEmpty(), @@ -153,6 +164,29 @@ fun VaultAddEditState.ViewState.appendFolderAndOwnerData( } ?: this } +/** + * Retrieves the default user collection from a list of [CollectionView]s, but only if the + * individual vault is disabled. + * + * This is used to pre-select the default collection for a new item when the user is part of an + * organization and the "Individual Vault" policy is enabled, which prevents them from creating + * items in their personal vault. + * + * @param isIndividualVaultDisabled A boolean indicating if the policy disabling the individual + * vault is active. + * + * @return The [CollectionView] corresponding to the default user collection if the individual vault + * is disabled, otherwise `null`. + */ +fun List.getDefaultCollectionViewOrNull( + isIndividualVaultDisabled: Boolean, +): CollectionView? = + if (isIndividualVaultDisabled) { + firstOrNull { it.type == CollectionType.DEFAULT_USER_COLLECTION } + } else { + null + } + /** * Validates a [CipherView] otherwise returning a [VaultAddEditState.ViewState.Error]. */ diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt index d16ddc8385..4f637bc2ed 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit.util +import com.bitwarden.collections.CollectionType import com.bitwarden.data.repository.model.Environment import com.bitwarden.network.model.OrganizationType import com.bitwarden.ui.platform.resource.BitwardenString @@ -453,7 +454,18 @@ class CipherViewExtensionsTest { @Test fun `appendFolderAndOwnerData should append folder and owner data`() { - val viewState = createSecureNoteViewState(withFolderAndOwnerData = false) + val viewState = createSecureNoteViewState( + availableOwners = listOf( + USER_OWNER, + ORGANIZATION_OWNER, + ), + availableFolders = listOf( + NO_FOLDER_ITEM, + MOCK_FOLDER_ITEM, + ), + selectedFolderId = null, + selectedOwnerId = null, + ) val account = createAccount() val folderView = listOf(createMockFolderView(number = 1)) val collectionList = listOf(createMockCollectionView(number = 1)) @@ -466,15 +478,74 @@ class CipherViewExtensionsTest { resourceManager = resourceManager, ) + val expected = createSecureNoteViewState( + availableOwners = listOf( + USER_OWNER, + ORGANIZATION_OWNER, + ), + availableFolders = listOf( + NO_FOLDER_ITEM, + MOCK_FOLDER_ITEM, + ), + selectedFolderId = MOCK_FOLDER_ITEM.id, + selectedOwnerId = ORGANIZATION_OWNER.id, + ) assertEquals( - createSecureNoteViewState(withFolderAndOwnerData = true), + expected, + result, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `appendFolderAndOwnerData should append correct owner data if individual vault is disabled`() { + val mockCipherView = createMockCipherView(number = 1, organizationId = null) + val viewState = createSecureNoteViewState( + cipherView = mockCipherView, + availableOwners = listOf(USER_OWNER, ORGANIZATION_OWNER), + availableFolders = emptyList(), + selectedOwnerId = null, + selectedFolderId = null, + ) + val account = createAccount() + val collectionList = listOf( + createMockCollectionView( + number = 1, + type = CollectionType.DEFAULT_USER_COLLECTION, + ), + ) + val result = viewState.appendFolderAndOwnerData( + folderViewList = emptyList(), + collectionViewList = collectionList, + activeAccount = account, + isIndividualVaultDisabled = true, + resourceManager = resourceManager, + ) + + val expected = createSecureNoteViewState( + cipherView = mockCipherView, + availableOwners = listOf(ORGANIZATION_OWNER), + availableFolders = listOf(NO_FOLDER_ITEM), + ) + + assertEquals( + expected, result, ) } private fun createSecureNoteViewState( cipherView: CipherView = createMockCipherView(number = 1), - withFolderAndOwnerData: Boolean, + availableOwners: List = listOf( + USER_OWNER, + ORGANIZATION_OWNER, + ), + availableFolders: List = listOf( + NO_FOLDER_ITEM, + MOCK_FOLDER_ITEM, + ), + selectedFolderId: String? = availableFolders.firstOrNull()?.id, + selectedOwnerId: String? = availableOwners.firstOrNull()?.id, ): VaultAddEditState.ViewState.Content = VaultAddEditState.ViewState.Content( common = VaultAddEditState.ViewState.Content.Common( @@ -504,39 +575,21 @@ class CipherViewExtensionsTest { availableOwners = emptyList(), ) .let { - if (withFolderAndOwnerData) { + if (availableOwners.isNotEmpty()) { it.copy( - selectedFolderId = "mockId-1", - selectedOwnerId = "mockOrganizationId-1", - availableFolders = listOf( - VaultAddEditState.Folder( - id = null, - name = "No Folder", - ), - VaultAddEditState.Folder( - id = "mockId-1", - name = "mockName-1", - ), - ), + selectedOwnerId = selectedOwnerId, hasOrganizations = true, - availableOwners = listOf( - VaultAddEditState.Owner( - id = null, - name = "activeEmail", - collections = emptyList(), - ), - VaultAddEditState.Owner( - id = "mockOrganizationId-1", - name = "organizationName", - collections = listOf( - VaultCollection( - id = "mockId-1", - name = "mockName-1", - isSelected = true, - ), - ), - ), - ), + availableOwners = availableOwners, + ) + } else { + it + } + } + .let { + if (availableFolders.isNotEmpty()) { + it.copy( + selectedFolderId = selectedFolderId, + availableFolders = availableFolders, ) } else { it @@ -734,3 +787,27 @@ private val DEFAULT_SSH_KEY_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.c ) private const val TEST_ID = "testID" +private val NO_FOLDER_ITEM = VaultAddEditState.Folder( + id = null, + name = "No Folder", +) +private val MOCK_FOLDER_ITEM = VaultAddEditState.Folder( + id = "mockId-1", + name = "mockName-1", +) +private val ORGANIZATION_OWNER = VaultAddEditState.Owner( + id = "mockOrganizationId-1", + name = "organizationName", + collections = listOf( + VaultCollection( + id = "mockId-1", + name = "mockName-1", + isSelected = true, + ), + ), +) +private val USER_OWNER = VaultAddEditState.Owner( + id = null, + name = "activeEmail", + collections = emptyList(), +)