From c9dc93eb6dbe2a2c51e696d11d2d4fa0616ea236 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:02:14 +0000 Subject: [PATCH] Resolve compile breaks from the organization SDK model changes `Repositories` gained an `organization` field (bitwarden/sdk-internal#1588). Android has no SDK-managed organization repository, so this is passed as `null` like the other repositories the client does not manage. `ProfileOrganization` was restructured from a flat record into nested `details`/`plan`/`membership`/`collectionManagement`/`provider`/`sso`/ `familySponsorship` records (bitwarden/sdk-internal#1588 and bitwarden/sdk-internal#1608). Every flat field in the sync response mapping moves to its new home; `userIsManagedByOrganization` is now `membership.userIsClaimedByOrganization`. Two new plan fields, `usePam` and `useInviteLinks`, have no counterpart in `SyncResponseJson.Profile.Organization` and are passed as `false`. `OrganizationMembership.userId` is no longer optional, so organizations without a user ID are now filtered out alongside those without a name. --- .../util/SyncResponseJsonExtensions.kt | 110 ++++++++----- .../manager/sdk/SdkRepositoryFactoryImpl.kt | 1 + .../model/ProfileOrganizationUtil.kt | 150 ++++++++++-------- .../util/SyncResponseJsonExtensionsTest.kt | 12 ++ 4 files changed, 172 insertions(+), 101 deletions(-) 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 44f6278765..cb5cea0422 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 @@ -7,10 +7,17 @@ import com.bitwarden.network.model.OrganizationType import com.bitwarden.network.model.ProductTierType import com.bitwarden.network.model.ProviderType import com.bitwarden.network.model.SyncResponseJson +import com.bitwarden.organizations.CollectionManagementSettings +import com.bitwarden.organizations.FamilySponsorship +import com.bitwarden.organizations.OrganizationDetails +import com.bitwarden.organizations.OrganizationMembership +import com.bitwarden.organizations.OrganizationPlan +import com.bitwarden.organizations.OrganizationProvider import com.bitwarden.organizations.OrganizationUserStatusType import com.bitwarden.organizations.OrganizationUserType import com.bitwarden.organizations.Permissions import com.bitwarden.organizations.ProfileOrganization +import com.bitwarden.organizations.SsoSettings import com.bitwarden.policies.Policy import com.bitwarden.policies.PolicyType import com.x8bit.bitwarden.data.auth.repository.model.Organization @@ -61,17 +68,28 @@ fun List.toSdkProfileOrganizations(): Lis /** * Maps the given [SyncResponseJson.Profile.Organization] to a [ProfileOrganization] or `null` if - * the [SyncResponseJson.Profile.Organization.name] is not present. + * the [SyncResponseJson.Profile.Organization.name] or + * [SyncResponseJson.Profile.Organization.userId] is not present. */ @Suppress("LongMethod") -private fun SyncResponseJson.Profile.Organization.toSdkProfileOrganization(): ProfileOrganization? = - this.name?.let { - ProfileOrganization( +private fun SyncResponseJson.Profile.Organization.toSdkProfileOrganization(): ProfileOrganization? { + val organizationName = this.name ?: return null + // The SDK requires a non-null user ID, so an organization without one cannot be mapped. + val memberUserId = this.userId ?: return null + return ProfileOrganization( + details = OrganizationDetails( id = this.id, - name = it, - status = this.status.toSdkOrganizationUserStatusType(), - type = this.type.toSdkOrganizationUserType(), + name = organizationName, enabled = this.isEnabled, + hasPublicAndPrivateKeys = this.hasPublicAndPrivateKeys, + ), + plan = OrganizationPlan( + productTierType = this.productTierType.toSdkProductTierType(), + seats = this.seats, + maxCollections = this.maxCollections, + maxStorageGb = this.maxStorageGb, + selfHost = this.isSelfHost, + usersGetPremium = this.shouldUsersGetPremium, usePolicies = this.shouldUsePolicies, useGroups = this.shouldUseGroups, useDirectory = this.shouldUseDirectory, @@ -87,49 +105,63 @@ private fun SyncResponseJson.Profile.Organization.toSdkProfileOrganization(): Pr useResetPassword = this.useResetPassword, useSecretsManager = this.useSecretsManager, usePasswordManager = this.usePasswordManager, + // Not provided by the sync response. + usePam = false, useActivateAutofillPolicy = this.useActivateAutofillPolicy, useAutomaticUserConfirmation = this.useAutomaticUserConfirmation, - selfHost = this.isSelfHost, - usersGetPremium = this.shouldUsersGetPremium, - seats = this.seats, - maxCollections = this.maxCollections, - maxStorageGb = this.maxStorageGb, - ssoBound = this.ssoBound, - identifier = this.identifier, + useAccessIntelligence = this.useAccessIntelligence, + useAdminSponsoredFamilies = this.useAdminSponsoredFamilies, + useDisableSmAdsForUsers = this.useDisableSmAdsForUsers, + usePhishingBlocker = this.usePhishingBlocker, + useMyItems = this.useMyItems, + // Not provided by the sync response. + useInviteLinks = false, + ), + membership = OrganizationMembership( + status = this.status.toSdkOrganizationUserStatusType(), + type = this.type.toSdkOrganizationUserType(), permissions = this.permissions.toSdkPermissions(), - resetPasswordEnrolled = this.resetPasswordEnrolled, - userId = this.userId, + userId = memberUserId, organizationUserId = this.organizationUserId, - hasPublicAndPrivateKeys = this.hasPublicAndPrivateKeys, - providerId = this.providerId, - providerName = this.providerName, - providerType = this.providerType?.toSdkProviderType(), - isProviderUser = this.isProviderUser, isMember = this.isMember, - familySponsorshipFriendlyName = this.familySponsorshipFriendlyName, - familySponsorshipAvailable = this.familySponsorshipAvailable, - productTierType = this.productTierType.toSdkProductTierType(), - keyConnectorEnabled = this.isKeyConnectorEnabled, - keyConnectorUrl = this.keyConnectorUrl, - familySponsorshipLastSyncDate = this.familySponsorshipLastSyncDate, - familySponsorshipValidUntil = this.familySponsorshipValidUntil, - familySponsorshipToDelete = this.familySponsorshipToDelete, + isProviderUser = this.isProviderUser, + resetPasswordEnrolled = this.resetPasswordEnrolled, + ssoBound = this.ssoBound, + userIsClaimedByOrganization = this.userIsClaimedByOrganization, accessSecretsManager = this.accessSecretsManager, + ), + collectionManagement = CollectionManagementSettings( limitCollectionCreation = this.limitCollectionCreation, limitCollectionDeletion = this.limitCollectionDeletion, limitItemDeletion = this.limitItemDeletion, allowAdminAccessToAllCollectionItems = this.allowAdminAccessToAllCollectionItems, - userIsManagedByOrganization = this.userIsClaimedByOrganization, - useAccessIntelligence = this.useAccessIntelligence, - useAdminSponsoredFamilies = this.useAdminSponsoredFamilies, - useDisableSmAdsForUsers = this.useDisableSmAdsForUsers, + ), + provider = this.providerId?.let { providerId -> + this.providerType?.let { providerType -> + OrganizationProvider( + id = providerId, + type = providerType.toSdkProviderType(), + name = this.providerName, + ) + } + }, + sso = SsoSettings( + enabled = this.ssoEnabled, + identifier = this.identifier, + memberDecryptionType = this.ssoMemberDecryptionType?.toSdkMemberDecryptionType(), + keyConnectorEnabled = this.isKeyConnectorEnabled, + keyConnectorUrl = this.keyConnectorUrl, + ), + familySponsorship = FamilySponsorship( + available = this.familySponsorshipAvailable, + friendlyName = this.familySponsorshipFriendlyName, + validUntil = this.familySponsorshipValidUntil, + lastSyncDate = this.familySponsorshipLastSyncDate, + toDelete = this.familySponsorshipToDelete, isAdminInitiated = this.isAdminInitiated, - ssoEnabled = this.ssoEnabled, - ssoMemberDecryptionType = this.ssoMemberDecryptionType?.toSdkMemberDecryptionType(), - usePhishingBlocker = this.usePhishingBlocker, - useMyItems = this.useMyItems, - ) - } + ), + ) +} /** * Convert the JSON data of the [Policy] object into [PolicyInformation] data. diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt index eeb6523fe3..12e13a93c5 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt @@ -42,6 +42,7 @@ class SdkRepositoryFactoryImpl( localUserDataKeyState = SdkLocalUserDataKeyStateRepository( authDiskSource = authDiskSource, ), + organization = null, organizationSharedKey = null, policy = null, send = null, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/model/ProfileOrganizationUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/model/ProfileOrganizationUtil.kt index 73c77d6aeb..610680516d 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/model/ProfileOrganizationUtil.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/model/ProfileOrganizationUtil.kt @@ -2,13 +2,20 @@ package com.x8bit.bitwarden.data.auth.repository.model +import com.bitwarden.organizations.CollectionManagementSettings +import com.bitwarden.organizations.FamilySponsorship import com.bitwarden.organizations.MemberDecryptionType +import com.bitwarden.organizations.OrganizationDetails +import com.bitwarden.organizations.OrganizationMembership +import com.bitwarden.organizations.OrganizationPlan +import com.bitwarden.organizations.OrganizationProvider import com.bitwarden.organizations.OrganizationUserStatusType import com.bitwarden.organizations.OrganizationUserType import com.bitwarden.organizations.Permissions import com.bitwarden.organizations.ProductTierType import com.bitwarden.organizations.ProfileOrganization import com.bitwarden.organizations.ProviderType +import com.bitwarden.organizations.SsoSettings import java.time.Instant /** @@ -37,7 +44,7 @@ fun createMockSdkProfileOrganization( identifier: String? = "mockIdentifier-$number", use2fa: Boolean = false, familySponsorshipToDelete: Boolean? = false, - userId: String? = "mockUserId-$number", + userId: String = "mockUserId-$number", shouldUseEvents: Boolean = false, familySponsorshipFriendlyName: String? = "mockFamilySponsorshipFriendlyName-$number", useTotp: Boolean = false, @@ -78,69 +85,88 @@ fun createMockSdkProfileOrganization( ssoMemberDecryptionType: MemberDecryptionType? = null, usePhishingBlocker: Boolean = false, useMyItems: Boolean = false, + usePam: Boolean = false, + useInviteLinks: Boolean = false, ): ProfileOrganization = ProfileOrganization( - id = id, - name = name, - status = status, - type = type, - enabled = enabled, - usePolicies = usePolicies, - useGroups = useGroups, - useDirectory = useDirectory, - useEvents = shouldUseEvents, - useTotp = useTotp, - use2fa = use2fa, - useApi = useApi, - useSso = useSso, - useOrganizationDomains = useOrganizationDomains, - useKeyConnector = shouldUseKeyConnector, - useScim = useScim, - useCustomPermissions = useCustomPermissions, - useResetPassword = useResetPassword, - useSecretsManager = useSecretsManager, - usePasswordManager = usePasswordManager, - useActivateAutofillPolicy = useActivateAutofillPolicy, - useAutomaticUserConfirmation = useAutomaticUserConfirmation, - selfHost = selfHost, - usersGetPremium = usersGetPremium, - seats = seats, - maxCollections = maxCollections, - maxStorageGb = maxStorageGb, - ssoBound = ssoBound, - identifier = identifier, - permissions = permissions, - resetPasswordEnrolled = resetPasswordEnrolled, - userId = userId, - organizationUserId = organizationUserId, - hasPublicAndPrivateKeys = hasPublicAndPrivateKeys, - providerId = providerId, - providerName = providerName, - providerType = providerType, - isProviderUser = isProviderUser, - isMember = isMember, - familySponsorshipFriendlyName = familySponsorshipFriendlyName, - familySponsorshipAvailable = familySponsorshipAvailable, - productTierType = productTierType, - keyConnectorEnabled = keyConnectorEnabled, - keyConnectorUrl = keyConnectorUrl, - familySponsorshipLastSyncDate = familySponsorshipLastSyncDate, - familySponsorshipValidUntil = familySponsorshipValidUntil, - familySponsorshipToDelete = familySponsorshipToDelete, - accessSecretsManager = accessSecretsManager, - limitCollectionCreation = limitCollectionCreation, - limitCollectionDeletion = limitCollectionDeletion, - limitItemDeletion = limitItemDeletion, - allowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems, - userIsManagedByOrganization = userIsClaimedByOrganization, - useAccessIntelligence = useAccessIntelligence, - useAdminSponsoredFamilies = useAdminSponsoredFamilies, - useDisableSmAdsForUsers = useDisableSmAdsForUsers, - isAdminInitiated = isAdminInitiated, - ssoEnabled = ssoEnabled, - ssoMemberDecryptionType = ssoMemberDecryptionType, - usePhishingBlocker = usePhishingBlocker, - useMyItems = useMyItems, + details = OrganizationDetails( + id = id, + name = name, + enabled = enabled, + hasPublicAndPrivateKeys = hasPublicAndPrivateKeys, + ), + plan = OrganizationPlan( + productTierType = productTierType, + seats = seats, + maxCollections = maxCollections, + maxStorageGb = maxStorageGb, + selfHost = selfHost, + usersGetPremium = usersGetPremium, + usePolicies = usePolicies, + useGroups = useGroups, + useDirectory = useDirectory, + useEvents = shouldUseEvents, + useTotp = useTotp, + use2fa = use2fa, + useApi = useApi, + useSso = useSso, + useOrganizationDomains = useOrganizationDomains, + useKeyConnector = shouldUseKeyConnector, + useScim = useScim, + useCustomPermissions = useCustomPermissions, + useResetPassword = useResetPassword, + useSecretsManager = useSecretsManager, + usePasswordManager = usePasswordManager, + usePam = usePam, + useActivateAutofillPolicy = useActivateAutofillPolicy, + useAutomaticUserConfirmation = useAutomaticUserConfirmation, + useAccessIntelligence = useAccessIntelligence, + useAdminSponsoredFamilies = useAdminSponsoredFamilies, + useDisableSmAdsForUsers = useDisableSmAdsForUsers, + usePhishingBlocker = usePhishingBlocker, + useMyItems = useMyItems, + useInviteLinks = useInviteLinks, + ), + membership = OrganizationMembership( + status = status, + type = type, + permissions = permissions, + userId = userId, + organizationUserId = organizationUserId, + isMember = isMember, + isProviderUser = isProviderUser, + resetPasswordEnrolled = resetPasswordEnrolled, + ssoBound = ssoBound, + userIsClaimedByOrganization = userIsClaimedByOrganization, + accessSecretsManager = accessSecretsManager, + ), + collectionManagement = CollectionManagementSettings( + limitCollectionCreation = limitCollectionCreation, + limitCollectionDeletion = limitCollectionDeletion, + limitItemDeletion = limitItemDeletion, + allowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems, + ), + provider = providerId + ?.let { id -> + providerType?.let { type -> + OrganizationProvider(id = id, type = type, name = providerName) + } + }, + sso = SsoSettings( + enabled = ssoEnabled, + identifier = identifier, + memberDecryptionType = ssoMemberDecryptionType, + keyConnectorEnabled = keyConnectorEnabled, + keyConnectorUrl = keyConnectorUrl, + ), + familySponsorship = FamilySponsorship( + available = familySponsorshipAvailable, + friendlyName = familySponsorshipFriendlyName, + validUntil = familySponsorshipValidUntil, + lastSyncDate = familySponsorshipLastSyncDate, + toDelete = familySponsorshipToDelete, + isAdminInitiated = isAdminInitiated, + ), ) /** diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensionsTest.kt index aab078a8ea..311c9edb84 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensionsTest.kt @@ -87,6 +87,18 @@ class SyncResponseJsonExtensionsTest { ) } + @Test + fun `toSdkProfileOrganizations should filter out organizations with null user IDs`() { + assertEquals( + listOf(createMockSdkProfileOrganization(number = 1)), + listOf( + createMockOrganizationNetwork(number = 1), + createMockOrganizationNetwork(number = 2, userId = null), + ) + .toSdkProfileOrganizations(), + ) + } + @Suppress("MaxLineLength") @Test fun `toSdkProfileOrganizations should return empty list when all organizations have null names`() {