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
This commit is contained in:
anonymous authored and dasien committed 2026-03-25 13:08:14 -04:00
1 parent d0809a7c07
commit 5dcaf6e4a8
8 files changed
+62 -5

No files matched your search

@@ -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
}
@@ -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,
@@ -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
@@ -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 {
@@ -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
},
),
)
@@ -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<SnackbarRelay>,
) : BaseViewModel<CollectionAddEditState, CollectionAddEditEvent, CollectionAddEditAction>(
@@ -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,
@@ -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,
@@ -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,
)
/**