Skip to content

[PM-15970] Allow assigning collections if user has correct permissions #4461

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.vault.feature.util

import com.bitwarden.vault.CollectionView
import com.x8bit.bitwarden.ui.vault.feature.util.model.CollectionPermission

private const val COLLECTION_DIVIDER: String = "/"

Expand Down Expand Up @@ -110,15 +111,43 @@ fun List<CollectionView>?.hasDeletePermissionInAtLeastOneCollection(
* 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.
* have "manage" permission for and is also in a collection they cannot view the passwords in.
*
* E.g., If an item is in A collection with "view except passwords" or "edit except passwords"
* permission and in another with "manage" permission, the user **cannot** assign the item to other
* collections. Conversely, if an item is in a collection with "manage" permission and another with
* "view" or "edit" permission, the user **can** assign the item to other collections.
*/
fun List<CollectionView>?.canAssignToCollections(currentCollectionIds: List<String>?) =
this
?.none {
val itemIsInCollection = currentCollectionIds
?.contains(it.id)
?: false

itemIsInCollection && (!it.manage || it.readOnly)
}
?: true
fun List<CollectionView>?.canAssignToCollections(currentCollectionIds: List<String>?): Boolean {
if (this.isNullOrEmpty()) return true
if (currentCollectionIds.isNullOrEmpty()) return true

// Verify user can MANAGE at least one collection the item is in.
return this
.any {
currentCollectionIds.contains(it.id) &&
it.permission == CollectionPermission.MANAGE
} &&

// Verify user does not have "edit except password" or "view except passwords"
// permission in any collection the item is not in.
this
.none {
currentCollectionIds.contains(it.id) &&
(it.permission == CollectionPermission.EDIT_EXCEPT_PASSWORD ||
it.permission == CollectionPermission.VIEW_EXCEPT_PASSWORDS)
}
}

/**
* Determines the user's permission level for a given [CollectionView].
*/
val CollectionView.permission: CollectionPermission
get() = when {
manage -> CollectionPermission.MANAGE
readOnly && hidePasswords -> CollectionPermission.VIEW_EXCEPT_PASSWORDS
readOnly -> CollectionPermission.VIEW
!readOnly && hidePasswords -> CollectionPermission.EDIT_EXCEPT_PASSWORD
// !readOnly is the only other possible condition, which resolves to EDIT permission
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ‘

else -> CollectionPermission.EDIT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.x8bit.bitwarden.ui.vault.feature.util.model

/**
* Represents the permission levels a user can be assigned to a collection.
*/
enum class CollectionPermission {
VIEW,
VIEW_EXCEPT_PASSWORDS,
EDIT,
EDIT_EXCEPT_PASSWORD,
MANAGE,
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,64 @@ fun createMockCollectionView(
name: String? = null,
readOnly: Boolean = false,
manage: Boolean = true,
hidePasswords: Boolean = false,
): CollectionView =
CollectionView(
id = "mockId-$number",
organizationId = "mockOrganizationId-$number",
hidePasswords = false,
hidePasswords = hidePasswords,
name = name ?: "mockName-$number",
externalId = "mockExternalId-$number",
readOnly = readOnly,
manage = manage,
)

/**
* Create a [CollectionView] configured to reflect MANAGE permission.
*/
fun createManageCollectionView(number: Int) = createMockCollectionView(
number = number,
manage = true,
readOnly = false,
hidePasswords = false,
)

/**
* Create a [CollectionView] configured to reflect EDIT permission.
*/
fun createEditCollectionView(number: Int) = createMockCollectionView(
number = number,
manage = false,
readOnly = false,
hidePasswords = false,
)

/**
* Create a [CollectionView] configured to reflect EDIT_EXCEPT_PASSWORDS permission.
*/
fun createEditExceptPasswordsCollectionView(number: Int) = createMockCollectionView(
number = number,
manage = false,
readOnly = false,
hidePasswords = true,
)

/**
* Create a [CollectionView] configured to reflect VIEW permission.
*/
fun createViewCollectionView(number: Int) = createMockCollectionView(
number = number,
manage = false,
readOnly = true,
hidePasswords = false,
)

/**
* Create a [CollectionView] configured to reflect VIEW_EXCEPT_PASSWORDS permission.
*/
fun createViewExceptPasswordsCollectionView(number: Int) = createMockCollectionView(
number = number,
manage = false,
readOnly = true,
hidePasswords = true,
)
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,13 @@ import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRep
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createEditCollectionView
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.createMockCollectionView
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
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
Expand Down Expand Up @@ -1182,18 +1186,15 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
resourceManager = resourceManager,
clock = fixedClock,
canDelete = false,
canAssignToCollections = true,
canAssignToCollections = false,
)
} returns stateWithName.viewState

mutableVaultDataFlow.value = DataState.Loaded(
data = createVaultData(
cipherView = cipherView,
collectionViewList = listOf(
createMockCollectionView(
number = 1,
manage = false,
),
createEditCollectionView(number = 1),
),
),
)
Expand Down Expand Up @@ -1223,6 +1224,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
fun `in edit mode, canDelete should be true when cipher is in a collection the user can manage`() =
runTest {
val cipherView = createMockCipherView(1)
.copy(collectionIds = listOf("mockId-1", "mockId-2"))
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
val stateWithName = createVaultAddItemState(
vaultAddEditType = vaultAddEditType,
Expand Down Expand Up @@ -1258,16 +1260,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
data = createVaultData(
cipherView = cipherView,
collectionViewList = listOf(
createMockCollectionView(
number = 1,
manage = true,
readOnly = false,
),
createMockCollectionView(
number = 2,
manage = false,
readOnly = true,
),
createManageCollectionView(number = 1),
createViewCollectionView(number = 2),
),
),
)
Expand All @@ -1294,7 +1288,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {

@Suppress("MaxLineLength")
@Test
fun `in edit mode, canAssociateToCollections should be false when cipher is in a collection the user cannot manage or edit`() =
fun `in edit mode, canAssociateToCollections should be false when cipher is in a collection with view permission`() =
runTest {
val cipherView = createMockCipherView(1)
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
Expand Down Expand Up @@ -1332,11 +1326,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
data = createVaultData(
cipherView = cipherView,
collectionViewList = listOf(
createMockCollectionView(
number = 1,
readOnly = true,
manage = false,
),
createViewCollectionView(number = 1),
),
),
)
Expand All @@ -1363,9 +1353,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {

@Suppress("MaxLineLength")
@Test
fun `in edit mode, canAssociateToCollections should be false when cipher is in a collection the user cannot manage but can edit`() =
fun `in edit mode, canAssociateToCollections should be false when cipher is in a collection with manage permission and a collection with edit, except password permission`() =
runTest {
val cipherView = createMockCipherView(1)
.copy(collectionIds = listOf("mockId-1", "mockId-2"))
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
val stateWithName = createVaultAddItemState(
vaultAddEditType = vaultAddEditType,
Expand All @@ -1392,7 +1383,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
totpData = null,
resourceManager = resourceManager,
clock = fixedClock,
canDelete = false,
canDelete = true,
canAssignToCollections = false,
)
} returns stateWithName.viewState
Expand All @@ -1401,11 +1392,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
data = createVaultData(
cipherView = cipherView,
collectionViewList = listOf(
createMockCollectionView(
number = 1,
manage = false,
readOnly = false,
),
createManageCollectionView(number = 1),
createEditExceptPasswordsCollectionView(number = 2),
),
),
)
Expand All @@ -1424,17 +1412,18 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
totpData = null,
resourceManager = resourceManager,
clock = fixedClock,
canDelete = false,
canDelete = true,
canAssignToCollections = false,
)
}
}

@Suppress("MaxLineLength")
@Test
fun `in edit mode, canAssociateToCollections should be false when cipher is in a collection the user can manage but cannot edit`() =
fun `in edit mode, canAssociateToCollections should be false when cipher is in a collection with manage permission and a collection with view, except password permission`() =
runTest {
val cipherView = createMockCipherView(1)
.copy(collectionIds = listOf("mockId-1", "mockId-2"))
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
val stateWithName = createVaultAddItemState(
vaultAddEditType = vaultAddEditType,
Expand Down Expand Up @@ -1470,11 +1459,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
data = createVaultData(
cipherView = cipherView,
collectionViewList = listOf(
createMockCollectionView(
number = 1,
manage = true,
readOnly = true,
),
createManageCollectionView(number = 1),
createViewExceptPasswordsCollectionView(number = 2),
),
),
)
Expand Down
Loading