[PM-14843] Allow deletion of items in collections with manage permission (#4299)

This commit is contained in:
Patrick Honkonen 2024-11-15 11:10:32 -05:00 committed by GitHub
parent 13210343db
commit d125fab0b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 708 additions and 853 deletions

View file

@ -53,6 +53,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDefaultAddTypeContent
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toItemType import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toItemType
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.validateCipherOrReturnErrorState import com.x8bit.bitwarden.ui.vault.feature.addedit.util.validateCipherOrReturnErrorState
import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections
import com.x8bit.bitwarden.ui.vault.feature.util.hasDeletePermissionInAtLeastOneCollection
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView
import com.x8bit.bitwarden.ui.vault.model.TotpData import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
@ -1579,27 +1581,13 @@ class VaultAddEditViewModel @Inject constructor(
vaultAddEditType = vaultAddEditType, vaultAddEditType = vaultAddEditType,
) { currentAccount, cipherView -> ) { currentAccount, cipherView ->
// Deletion is not allowed when the item is in a collection that the user val canDelete = vaultData
// does not have "manage" permission for. .collectionViewList
val canDelete = vaultData.collectionViewList .hasDeletePermissionInAtLeastOneCollection(cipherView?.collectionIds)
.none {
val isItemInCollection = cipherView
?.collectionIds
?.contains(it.id) == true
isItemInCollection && !it.manage val canAssignToCollections = vaultData
} .collectionViewList
.canAssignToCollections(cipherView?.collectionIds)
// Assigning to a collection is not allowed when the item is in a collection
// that the user does not have "manage" and "edit" permission for.
val canAssignToCollections = vaultData.collectionViewList
.none {
val isItemInCollection = cipherView
?.collectionIds
?.contains(it.id) == true
isItemInCollection && (!it.manage || it.readOnly)
}
// Derive the view state from the current Cipher for Edit mode // Derive the view state from the current Cipher for Edit mode
// or use the current state for Add // or use the current state for Add

View file

@ -28,6 +28,8 @@ import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemStateData import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemStateData
import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections
import com.x8bit.bitwarden.ui.vault.feature.util.hasDeletePermissionInAtLeastOneCollection
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -103,29 +105,15 @@ class VaultItemViewModel @Inject constructor(
// we map it to the appropriate value below. // we map it to the appropriate value below.
} }
.mapNullable { .mapNullable {
// Deletion is not allowed when the item is in a collection that the user val canDelete = collectionsState
// does not have "manage" permission for. .data
val canDelete = collectionsState.data .hasDeletePermissionInAtLeastOneCollection(
?.none { collectionIds = cipherViewState.data?.collectionIds,
val itemIsInCollection = cipherViewState.data )
?.collectionIds
?.contains(it.id) == true
itemIsInCollection && !it.manage val canAssignToCollections = collectionsState
} .data
?: true .canAssignToCollections(cipherViewState.data?.collectionIds)
// Assigning to a collection is not allowed when the item is in a collection
// that the user does not have "manage" and "edit" permission for.
val canAssignToCollections = collectionsState.data
?.none {
val itemIsInCollection = cipherViewState.data
?.collectionIds
?.contains(it.id) == true
itemIsInCollection && !it.manage && it.readOnly
}
?: true
VaultItemStateData( VaultItemStateData(
cipher = cipherViewState.data, cipher = cipherViewState.data,

View file

@ -88,3 +88,36 @@ fun String.toCollectionDisplayName(list: List<CollectionView>): String {
return collectionName return collectionName
} }
/**
* Checks if the user has delete permission in at least one collection.
*
* Deletion is allowed when the item is in any collection that the user has "manage" permission for.
*/
fun List<CollectionView>?.hasDeletePermissionInAtLeastOneCollection(collectionIds: List<String>?) =
this
?.takeUnless { it.isEmpty() }
?.any {
collectionIds
?.contains(it.id)
?.let { isInCollection -> !isInCollection || it.manage }
?: true
}
?: true
/**
* Checks if the user has permission to assign an item to a collection.
*
* Assigning to a collection is not allowed when the item is in a collection that the user does not
* have "manage" and "edit" permission for.
*/
fun List<CollectionView>?.canAssignToCollections(currentCollectionIds: List<String>?) =
this
?.none {
val itemIsInCollection = currentCollectionIds
?.contains(it.id)
?: false
itemIsInCollection && (!it.manage || it.readOnly)
}
?: true

View file

@ -1259,6 +1259,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
manage = true, manage = true,
readOnly = false, readOnly = false,
), ),
createMockCollectionView(
number = 2,
manage = false,
readOnly = true,
),
), ),
), ),
) )

View file

@ -3,6 +3,8 @@ package com.x8bit.bitwarden.ui.vault.feature.util
import com.bitwarden.vault.CollectionView import com.bitwarden.vault.CollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class CollectionViewExtensionsTest { class CollectionViewExtensionsTest {
@ -66,4 +68,102 @@ class CollectionViewExtensionsTest {
collectionName.toCollectionDisplayName(collectionList), collectionName.toCollectionDisplayName(collectionList),
) )
} }
@Suppress("MaxLineLength")
@Test
fun `hasDeletePermissionInAtLeastOneCollection should return true if the user has manage permission in at least one collection`() {
val collectionList: List<CollectionView> = listOf(
createMockCollectionView(number = 1, manage = true),
createMockCollectionView(number = 2, manage = false),
)
val collectionIds = listOf("mockId-1", "mockId-2")
assertTrue(collectionList.hasDeletePermissionInAtLeastOneCollection(collectionIds))
}
@Suppress("MaxLineLength")
@Test
fun `hasDeletePermissionInAtLeastOneCollection should return false if the user does not have manage permission in at least one collection`() {
val collectionList: List<CollectionView> = listOf(
createMockCollectionView(number = 1, manage = false),
createMockCollectionView(number = 2, manage = false),
)
val collectionIds = listOf("mockId-1", "mockId-2")
assertFalse(collectionList.hasDeletePermissionInAtLeastOneCollection(collectionIds))
}
@Suppress("MaxLineLength")
@Test
fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionView list is null`() {
val collectionIds = listOf("mockId-1", "mockId-2")
assertTrue(null.hasDeletePermissionInAtLeastOneCollection(collectionIds))
}
@Suppress("MaxLineLength")
@Test
fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionIds list is null`() {
val collectionList: List<CollectionView> = listOf(
createMockCollectionView(number = 1, manage = true),
createMockCollectionView(number = 2, manage = false),
)
assertTrue(collectionList.hasDeletePermissionInAtLeastOneCollection(null))
}
@Suppress("MaxLineLength")
@Test
fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionIds list is empty`() {
val collectionList: List<CollectionView> = listOf(
createMockCollectionView(number = 1, manage = true),
createMockCollectionView(number = 2, manage = false),
)
assertTrue(collectionList.hasDeletePermissionInAtLeastOneCollection(emptyList()))
}
@Suppress("MaxLineLength")
@Test
fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionView list is empty`() {
val collectionIds = listOf("mockId-1", "mockId-2")
assertTrue(
emptyList<CollectionView>().hasDeletePermissionInAtLeastOneCollection(
collectionIds,
),
)
}
@Suppress("MaxLineLength")
@Test
fun `canAssociateToCollections should return true if the user has edit and manage permission`() {
val collectionList: List<CollectionView> = listOf(
createMockCollectionView(number = 1, manage = true, readOnly = false),
)
val collectionIds = listOf("mockId-1", "mockId-2")
assertTrue(collectionList.canAssignToCollections(collectionIds))
}
@Suppress("MaxLineLength")
@Test
fun `canAssociateToCollections should return false if the user does not have manage or edit permission in at least one collection`() {
val collectionList: List<CollectionView> = listOf(
createMockCollectionView(number = 1, manage = false, readOnly = true),
createMockCollectionView(number = 2, manage = false, readOnly = true),
)
val collectionIds = listOf("mockId-1", "mockId-2")
assertFalse(collectionList.canAssignToCollections(collectionIds))
}
@Test
fun `canAssociateToCollections should return true if the collectionView list is null`() {
val collectionIds = listOf("mockId-1", "mockId-2")
assertTrue(null.canAssignToCollections(collectionIds))
}
@Test
fun `canAssociateToCollections should return true if the collectionIds list is null`() {
val collectionList: List<CollectionView> = listOf(
createMockCollectionView(number = 1, manage = true, readOnly = false),
createMockCollectionView(number = 2, manage = false),
)
assertTrue(collectionList.canAssignToCollections(null))
}
} }