Skip to content
Open
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
@@ -0,0 +1,35 @@
package com.x8bit.bitwarden.data.vault.manager

import com.bitwarden.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.manager.model.VaultMigrationData
import kotlinx.coroutines.flow.StateFlow

/**
* Manages the migration of personal vault items to organization collections.
* This interface provides a way to check if migration is needed and track migration state.
*/
interface VaultMigrationManager {
/**
* Flow that emits when conditions are met for the user to migrate their personal vault.
* Updated after each sync to reflect current policy and vault state.
*/
val vaultMigrationDataStateFlow: StateFlow<VaultMigrationData>

/**
* Verifies if the user should migrate their personal vault to organization collections
* based on active policies, feature flags, and the provided cipher list.
*
* @param userId The ID of the user to check for migration
* @param cipherList List of ciphers from the sync response to check for personal items.
*/
fun verifyAndUpdateMigrationState(userId: String, cipherList: List<SyncResponseJson.Cipher>)

/**
* Checks if the user should migrate their vault based on policies, feature flags,
* network connectivity, and whether they have personal items.
*
* @param hasPersonalItems Callback to check if the user has personal items.
* @return true if migration conditions are met, false otherwise.
*/
fun shouldMigrateVault(hasPersonalItems: () -> Boolean): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.x8bit.bitwarden.data.vault.manager

import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.vault.manager.model.VaultMigrationData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

/**
* Default implementation of [VaultMigrationManager].
*/
class VaultMigrationManagerImpl(
private val authDiskSource: AuthDiskSource,
private val policyManager: PolicyManager,
private val featureFlagManager: FeatureFlagManager,
private val connectionManager: NetworkConnectionManager,
) : VaultMigrationManager {
private val mutableVaultMigrationDataStateFlow =
MutableStateFlow<VaultMigrationData>(value = VaultMigrationData.NoMigrationRequired)

override val vaultMigrationDataStateFlow: StateFlow<VaultMigrationData>
get() = mutableVaultMigrationDataStateFlow.asStateFlow()

override fun verifyAndUpdateMigrationState(
userId: String,
cipherList: List<SyncResponseJson.Cipher>,
) {
mutableVaultMigrationDataStateFlow.update {
if (shouldMigrateVault { cipherList.any { it.organizationId == null } }) {
val orgId = policyManager.getPersonalOwnershipPolicyOrganizationId()
val orgName = authDiskSource
.getOrganizations(userId = userId)
?.firstOrNull { it.id == orgId }
?.name

if (orgId != null && orgName != null) {
VaultMigrationData.MigrationRequired(
organizationId = orgId,
organizationName = orgName,
)
} else {
VaultMigrationData.NoMigrationRequired
}
} else {
VaultMigrationData.NoMigrationRequired
}
}
}

override fun shouldMigrateVault(hasPersonalItems: () -> Boolean): Boolean {
return policyManager
.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
.any() &&
featureFlagManager.getFeatureFlag(FlagKey.MigrateMyVaultToMyItems) &&
connectionManager.isNetworkConnected &&
hasPersonalItems()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class VaultSyncManagerImpl(
private val userLogoutManager: UserLogoutManager,
private val userStateManager: UserStateManager,
private val vaultLockManager: VaultLockManager,
private val vaultMigrationManager: VaultMigrationManager,
private val clock: Clock,
databaseSchemeManager: DatabaseSchemeManager,
pushManager: PushManager,
Expand Down Expand Up @@ -341,6 +342,12 @@ class VaultSyncManagerImpl(
)
vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse)
val itemsAvailable = syncResponse.ciphers?.isNotEmpty() == true
syncResponse.ciphers?.let {
vaultMigrationManager.verifyAndUpdateMigrationState(
userId = userId,
cipherList = it,
)
}
SyncVaultDataResult.Success(itemsAvailable = itemsAvailable)
}
},
Expand Down Expand Up @@ -402,6 +409,11 @@ class VaultSyncManagerImpl(
.onStart { mutableDecryptCipherListResultFlow.updateToPendingOrLoading() }
.map {
vaultLockManager.waitUntilUnlocked(userId = userId)
// Verify migration state after unlock with the cipher list from disk
vaultMigrationManager.verifyAndUpdateMigrationState(
userId = userId,
cipherList = it,
)
vaultSdkSource
.decryptCipherListWithFailures(
userId = userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import com.x8bit.bitwarden.data.auth.manager.UserStateManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
Expand All @@ -37,6 +39,8 @@ import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManagerImpl
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManagerImpl
import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManager
import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManagerImpl
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManagerImpl
import dagger.Module
Expand All @@ -55,6 +59,20 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object VaultManagerModule {

@Provides
@Singleton
fun provideVaultMigrationManager(
authDiskSource: AuthDiskSource,
policyManager: PolicyManager,
featureFlagManager: FeatureFlagManager,
connectionManager: NetworkConnectionManager,
): VaultMigrationManager = VaultMigrationManagerImpl(
authDiskSource = authDiskSource,
policyManager = policyManager,
featureFlagManager = featureFlagManager,
connectionManager = connectionManager,
)

@Provides
@Singleton
fun provideCipherManager(
Expand Down Expand Up @@ -193,6 +211,7 @@ object VaultManagerModule {
userLogoutManager: UserLogoutManager,
userStateManager: UserStateManager,
vaultLockManager: VaultLockManager,
vaultMigrationManager: VaultMigrationManager,
clock: Clock,
databaseSchemeManager: DatabaseSchemeManager,
pushManager: PushManager,
Expand All @@ -206,6 +225,7 @@ object VaultManagerModule {
userLogoutManager = userLogoutManager,
userStateManager = userStateManager,
vaultLockManager = vaultLockManager,
vaultMigrationManager = vaultMigrationManager,
clock = clock,
databaseSchemeManager = databaseSchemeManager,
pushManager = pushManager,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.x8bit.bitwarden.data.vault.manager.model

/**
* Represents vault migration state with organization metadata.
*/
sealed class VaultMigrationData {
/**
* User should migrate personal vault items to the specified organization.
*/
data class MigrationRequired(
val organizationId: String,
val organizationName: String,
) : VaultMigrationData()

/**
* No migration required.
*/
data object NoMigrationRequired : VaultMigrationData()
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,4 @@ interface VaultRepository :
* `null` if the item cannot be found.
*/
fun getVaultListItemStateFlow(itemId: String): StateFlow<DataState<CipherListView?>>

/**
* Checks if there are any personal vault items (items without an organization ID) in the vault.
*
* @return `true` if there are personal vault items, `false` otherwise.
*/
fun hasPersonalVaultItems(): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,4 @@ class VaultRepositoryImpl(
organizationKeys = organizationKeys,
)
}

override fun hasPersonalVaultItems(): Boolean {
val vaultData = vaultSyncManager.vaultDataStateFlow.value.data ?: return false
return vaultData.decryptCipherListResult.successes.any { it.organizationId.isNullOrEmpty() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import com.x8bit.bitwarden.ui.vault.feature.exportitems.exportItemsGraph
import com.x8bit.bitwarden.ui.vault.feature.exportitems.navigateToExportItemsGraph
import com.x8bit.bitwarden.ui.vault.feature.exportitems.verifypassword.navigateToVerifyPassword
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListingAsRoot
import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.MigrateToMyItemsRoute
import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.navigateToMigrateToMyItems
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
Expand Down Expand Up @@ -153,6 +155,13 @@ fun RootNavScreen(
RootNavState.OnboardingAutoFillSetup -> SetupAutofillRoute.AsRoot
RootNavState.OnboardingBrowserAutofillSetup -> SetupBrowserAutofillRoute.AsRoot
RootNavState.OnboardingStepsComplete -> SetupCompleteRoute
is RootNavState.MigrateToMyItems -> {
val migrateState = state as RootNavState.MigrateToMyItems
MigrateToMyItemsRoute(
organizationId = migrateState.organizationId,
organizationName = migrateState.organizationName,
)
}
}
val currentRoute = navController.currentDestination?.rootLevelRoute()

Expand Down Expand Up @@ -204,6 +213,14 @@ fun RootNavScreen(
navController.navigateToExpiredRegistrationLinkScreen()
}

is RootNavState.MigrateToMyItems -> {
navController.navigateToMigrateToMyItems(
organizationName = currentState.organizationName,
organizationId = currentState.organizationId,
navOptions = rootNavOptions,
)
}

RootNavState.RemovePassword -> navController.navigateToRemovePassword(rootNavOptions)
RootNavState.ResetPassword -> {
navController.navigateToResetPasswordScreen(rootNavOptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManager
import com.x8bit.bitwarden.data.vault.manager.model.VaultMigrationData
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
Expand All @@ -35,6 +37,7 @@ import javax.inject.Inject
class RootNavViewModel @Inject constructor(
private val authRepository: AuthRepository,
specialCircumstanceManager: SpecialCircumstanceManager,
vaultMigrationManager: VaultMigrationManager,
) : BaseViewModel<RootNavState, Unit, RootNavAction>(
initialState = RootNavState.Splash,
) {
Expand All @@ -43,11 +46,13 @@ class RootNavViewModel @Inject constructor(
authRepository.authStateFlow,
authRepository.userStateFlow,
specialCircumstanceManager.specialCircumstanceStateFlow,
) { authState, userState, specialCircumstance ->
vaultMigrationManager.vaultMigrationDataStateFlow,
) { authState, userState, specialCircumstance, shouldMigratePersonalVault ->
RootNavAction.Internal.UserStateUpdateReceive(
authState = authState,
userState = userState,
specialCircumstance = specialCircumstance,
shouldMigratePersonalVault = shouldMigratePersonalVault,
)
}
.onEach(::handleAction)
Expand All @@ -66,6 +71,7 @@ class RootNavViewModel @Inject constructor(
) {
val userState = action.userState
val specialCircumstance = action.specialCircumstance

val updatedRootNavState = when {
userState?.activeAccount?.trustedDevice?.isDeviceTrusted == false &&
authRepository.tdeLoginComplete != true &&
Expand Down Expand Up @@ -110,21 +116,31 @@ class RootNavViewModel @Inject constructor(
getOnboardingNavState(onboardingStatus = userState.activeAccount.onboardingStatus)
}

userState.activeAccount.isVaultUnlocked -> {
when (specialCircumstance) {
is SpecialCircumstance.AutofillSave -> {
RootNavState.VaultUnlockedForAutofillSave(
autofillSaveItem = specialCircumstance.autofillSaveItem,
)
}
userState.activeAccount.isVaultUnlocked &&
specialCircumstance is SpecialCircumstance.AutofillSave -> {
RootNavState.VaultUnlockedForAutofillSave(
autofillSaveItem = specialCircumstance.autofillSaveItem,
)
}

is SpecialCircumstance.AutofillSelection -> {
RootNavState.VaultUnlockedForAutofillSelection(
activeUserId = userState.activeAccount.userId,
type = specialCircumstance.autofillSelectionData.type,
)
}
userState.activeAccount.isVaultUnlocked &&
specialCircumstance is SpecialCircumstance.AutofillSelection -> {
RootNavState.VaultUnlockedForAutofillSelection(
activeUserId = userState.activeAccount.userId,
type = specialCircumstance.autofillSelectionData.type,
)
}

userState.activeAccount.isVaultUnlocked &&
action.shouldMigratePersonalVault is VaultMigrationData.MigrationRequired -> {
RootNavState.MigrateToMyItems(
organizationId = action.shouldMigratePersonalVault.organizationId,
organizationName = action.shouldMigratePersonalVault.organizationName,
)
}

userState.activeAccount.isVaultUnlocked -> {
when (specialCircumstance) {
is SpecialCircumstance.AddTotpLoginItem -> {
RootNavState.VaultUnlockedForNewTotp(
activeUserId = userState.activeAccount.userId,
Expand Down Expand Up @@ -207,6 +223,8 @@ class RootNavViewModel @Inject constructor(

is SpecialCircumstance.CredentialExchangeExport,
is SpecialCircumstance.RegistrationEvent,
is SpecialCircumstance.AutofillSave,
is SpecialCircumstance.AutofillSelection,
-> {
throw IllegalStateException(
"Special circumstance should have been already handled.",
Expand Down Expand Up @@ -317,6 +335,15 @@ sealed class RootNavState : Parcelable {
@Parcelize
data object VaultLocked : RootNavState()

/**
* App should show MigrateToMyItems screen.
*/
@Parcelize
data class MigrateToMyItems(
val organizationId: String,
val organizationName: String,
) : RootNavState()

/**
* App should show vault unlocked nav graph for the given [activeUserId].
*/
Expand Down Expand Up @@ -494,6 +521,7 @@ sealed class RootNavAction {
val authState: AuthState,
val userState: UserState?,
val specialCircumstance: SpecialCircumstance?,
val shouldMigratePersonalVault: VaultMigrationData,
) : RootNavAction()
}
}
Loading
Loading