From 5dcaf6e4a8a8a93f691f5b0866aa6d65ad7d2f79 Mon Sep 17 00:00:00 2001 From: anonymous <> Date: Wed, 25 Mar 2026 13:08:14 -0400 Subject: [PATCH] fix: Grant creating user manage access and fix permission checks - Add organizationUserId to SyncResponseJson and Organization domain model to identify the current user's org membership ID - Include creating user with manage access in collection create request, matching web client behavior - Add limitCollectionCreation/limitCollectionDeletion to org model - Fix FAB visibility: use canManageCollections computed property that checks role (Owner/Admin) in addition to permissions flags, matching web client logic: !limitCollectionCreation || isAdmin || permissions --- .../data/auth/repository/model/Organization.kt | 16 +++++++++++++++- .../util/SyncResponseJsonExtensions.kt | 3 +++ .../data/vault/manager/CollectionManager.kt | 2 ++ .../vault/manager/CollectionManagerImpl.kt | 18 +++++++++++++++++- .../collections/CollectionsViewModel.kt | 7 ++++--- .../addedit/CollectionAddEditViewModel.kt | 6 ++++++ .../auth/repository/model/OrganizationUtil.kt | 6 ++++++ .../network/model/SyncResponseJson.kt | 9 +++++++++ 8 files changed, 62 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/Organization.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/Organization.kt index 4f82532550..2f95f01a35 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/Organization.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/Organization.kt @@ -31,7 +31,21 @@ data class Organization( val limitItemDeletion: Boolean, val shouldUseEvents: Boolean, val maxCollections: Int?, + val limitCollectionCreation: Boolean, + val limitCollectionDeletion: Boolean, + val organizationUserId: String?, val canCreateNewCollections: Boolean, val canEditAnyCollection: Boolean, val canDeleteAnyCollection: Boolean, -) +) { + /** + * Whether the user can create new collections in this organization, accounting for + * the organization's role and limitCollectionCreation setting. + * Matches web client logic: `!limitCollectionCreation || isAdmin || permissions.createNewCollections` + */ + val canManageCollections: Boolean + get() = !limitCollectionCreation || + role == OrganizationType.ADMIN || + role == OrganizationType.OWNER || + canCreateNewCollections +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensions.kt index e6caf6b70f..00f4e45a63 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensions.kt @@ -29,6 +29,9 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization? = limitItemDeletion = this.limitItemDeletion, shouldUseEvents = this.shouldUseEvents, maxCollections = this.maxCollections, + organizationUserId = this.organizationUserId, + limitCollectionCreation = this.limitCollectionCreation, + limitCollectionDeletion = this.limitCollectionDeletion, canCreateNewCollections = this.permissions.canCreateNewCollections, canEditAnyCollection = this.permissions.canEditAnyCollection, canDeleteAnyCollection = this.permissions.canDeleteAnyCollection, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CollectionManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CollectionManager.kt index 7e96e625f5..6f1103dbed 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CollectionManager.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CollectionManager.kt @@ -11,9 +11,11 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateCollectionResult interface CollectionManager { /** * Attempt to create a collection in the given [organizationId]. + * The [organizationUserId] is used to grant the creating user manage access. */ suspend fun createCollection( organizationId: String, + organizationUserId: String?, collectionView: CollectionView, ): CreateCollectionResult diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CollectionManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CollectionManagerImpl.kt index 202425efa5..26ffc1df44 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CollectionManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CollectionManagerImpl.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.manager import com.bitwarden.collections.CollectionView import com.bitwarden.core.data.util.flatMap +import com.bitwarden.network.model.CollectionAccessSelectionJson import com.bitwarden.network.model.CollectionJsonRequest import com.bitwarden.network.model.UpdateCollectionResponseJson import com.bitwarden.network.service.CollectionService @@ -27,16 +28,31 @@ class CollectionManagerImpl( override suspend fun createCollection( organizationId: String, + organizationUserId: String?, collectionView: CollectionView, ): CreateCollectionResult { val userId = activeUserId ?: return CreateCollectionResult.Error(error = NoActiveUserException()) + // Grant the creating user manage access, matching web client behavior. + val users = organizationUserId?.let { + listOf( + CollectionAccessSelectionJson( + id = it, + readOnly = false, + hidePasswords = false, + manage = true, + ), + ) + } return vaultSdkSource .encryptCollection(userId = userId, collectionView = collectionView) .flatMap { collectionService.createCollection( organizationId = organizationId, - body = CollectionJsonRequest(name = it.name), + body = CollectionJsonRequest( + name = it.name, + users = users, + ), ) } .onSuccess { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/collections/CollectionsViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/collections/CollectionsViewModel.kt index 8bf9ed9802..9caf2c3b19 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/collections/CollectionsViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/collections/CollectionsViewModel.kt @@ -12,6 +12,7 @@ import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.concat +import com.bitwarden.network.model.OrganizationType import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.Organization import com.x8bit.bitwarden.data.vault.repository.VaultRepository @@ -88,7 +89,7 @@ class CollectionsViewModel @Inject constructor( // For now, use the first org with create permission. // TODO: If multiple orgs, show org picker (pending G1 decision). val org = authRepository.organizations.firstOrNull { - it.canCreateNewCollections + it.canManageCollections } if (org != null) { sendEvent(CollectionsEvent.NavigateToAddCollectionScreen(org.id)) @@ -128,7 +129,7 @@ class CollectionsViewModel @Inject constructor( collectionList = vaultDataState.data .toDisplayItems(organizations), showAddButton = organizations.any { - org -> org.canCreateNewCollections + org -> org.canManageCollections }, ), ) @@ -165,7 +166,7 @@ class CollectionsViewModel @Inject constructor( collectionList = vaultDataState.data .toDisplayItems(organizations), showAddButton = organizations.any { - org -> org.canCreateNewCollections + org -> org.canManageCollections }, ), ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/collections/addedit/CollectionAddEditViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/collections/addedit/CollectionAddEditViewModel.kt index c5750b7aaf..9260d43a69 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/collections/addedit/CollectionAddEditViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/collections/addedit/CollectionAddEditViewModel.kt @@ -12,6 +12,7 @@ import com.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText +import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.CreateCollectionResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteCollectionResult @@ -38,6 +39,7 @@ private const val SLASH_CHAR = "/" @Suppress("TooManyFunctions", "LargeClass") class CollectionAddEditViewModel @Inject constructor( savedStateHandle: SavedStateHandle, + private val authRepository: AuthRepository, private val vaultRepository: VaultRepository, private val relayManager: SnackbarRelayManager, ) : BaseViewModel( @@ -168,8 +170,12 @@ class CollectionAddEditViewModel @Inject constructor( viewModelScope.launch { when (collectionAddEditType) { is CollectionAddEditType.AddItem -> { + val org = authRepository.organizations.find { + it.id == collectionAddEditType.organizationId + } val result = vaultRepository.createCollection( organizationId = collectionAddEditType.organizationId, + organizationUserId = org?.organizationUserId, collectionView = CollectionView( id = null, organizationId = collectionAddEditType.organizationId, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/model/OrganizationUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/model/OrganizationUtil.kt index 3f421fc1f9..ea47429626 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/model/OrganizationUtil.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/model/OrganizationUtil.kt @@ -18,6 +18,9 @@ fun createMockOrganization( limitItemDeletion: Boolean = false, shouldUseEvents: Boolean = false, maxCollections: Int? = null, + organizationUserId: String? = null, + limitCollectionCreation: Boolean = false, + limitCollectionDeletion: Boolean = false, canCreateNewCollections: Boolean = false, canEditAnyCollection: Boolean = false, canDeleteAnyCollection: Boolean = false, @@ -33,6 +36,9 @@ fun createMockOrganization( limitItemDeletion = limitItemDeletion, shouldUseEvents = shouldUseEvents, maxCollections = maxCollections, + organizationUserId = organizationUserId, + limitCollectionCreation = limitCollectionCreation, + limitCollectionDeletion = limitCollectionDeletion, canCreateNewCollections = canCreateNewCollections, canEditAnyCollection = canEditAnyCollection, canDeleteAnyCollection = canDeleteAnyCollection, 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 c3bf4abeca..adc0a81093 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt @@ -337,6 +337,9 @@ data class SyncResponseJson( @SerialName("userId") val userId: String?, + @SerialName("organizationUserId") + val organizationUserId: String?, + @SerialName("useEvents") val shouldUseEvents: Boolean, @@ -368,6 +371,12 @@ data class SyncResponseJson( @SerialName("limitItemDeletion") val limitItemDeletion: Boolean = false, + + @SerialName("limitCollectionCreation") + val limitCollectionCreation: Boolean = false, + + @SerialName("limitCollectionDeletion") + val limitCollectionDeletion: Boolean = false, ) /**