Skip to content
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,8 @@
package com.x8bit.bitwarden.data.auth.manager

import com.bitwarden.data.manager.DispatcherManager
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.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
Expand All @@ -19,6 +21,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
Expand All @@ -39,6 +42,7 @@ class UserStateManagerImpl(
private val authDiskSource: AuthDiskSource,
firstTimeActionManager: FirstTimeActionManager,
vaultLockManager: VaultLockManager,
private val policyManager: PolicyManager,
dispatcherManager: DispatcherManager,
) : UserStateManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
Expand Down Expand Up @@ -110,6 +114,7 @@ class UserStateManagerImpl(
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeState,
getUserPolicies = ::existingPolicies,
)
}
.filterNot {
Expand All @@ -133,6 +138,7 @@ class UserStateManagerImpl(
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
getUserPolicies = ::existingPolicies,
),
)

Expand All @@ -159,4 +165,12 @@ class UserStateManagerImpl(
.getPinProtectedUserKeyEnvelope(userId = userId)
?.let { VaultUnlockType.PIN }
?: VaultUnlockType.MASTER_PASSWORD

private fun existingPolicies(
userId: String,
policyType: PolicyTypeJson,
): List<SyncResponseJson.Policy> = policyManager.getUserPolicies(
userId = userId,
type = policyType,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,13 @@ object AuthRepositoryModule {
authDiskSource: AuthDiskSource,
firstTimeActionManager: FirstTimeActionManager,
vaultLockManager: VaultLockManager,
policyManager: PolicyManager,
dispatcherManager: DispatcherManager,
): UserStateManager = UserStateManagerImpl(
authDiskSource = authDiskSource,
firstTimeActionManager = firstTimeActionManager,
vaultLockManager = vaultLockManager,
policyManager = policyManager,
dispatcherManager = dispatcherManager,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ data class UserState(
val isUsingKeyConnector: Boolean,
val onboardingStatus: OnboardingStatus,
val firstTimeState: FirstTimeState,
val isExportable: Boolean,
) {
/**
* Indicates that the user does or does not have a means to manually unlock the vault.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.repository.util
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
import com.bitwarden.network.model.KdfTypeJson
import com.bitwarden.network.model.OrganizationType
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.UserDecryptionOptionsJson
import com.bitwarden.ui.platform.base.util.toHexColorRepresentation
Expand Down Expand Up @@ -164,6 +165,7 @@ fun UserStateJson.toUserState(
isBiometricsEnabledProvider: (userId: String) -> Boolean,
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
isDeviceTrustedProvider: (userId: String) -> Boolean,
getUserPolicies: (userId: String, policy: PolicyTypeJson) -> List<SyncResponseJson.Policy>,
): UserState =
UserState(
activeUserId = this.activeUserId,
Expand Down Expand Up @@ -203,6 +205,19 @@ fun UserStateJson.toUserState(
hasManageResetPasswordPermission.takeIf { trustedDevice != null }
val needsMasterPassword = decryptionOptions?.hasMasterPassword == false &&
(tdeUserNeedsMasterPassword ?: (keyConnectorOptions == null))

val hasPersonalOwnershipRestrictedOrg = getUserPolicies(
userId,
PolicyTypeJson.PERSONAL_OWNERSHIP,
)
.any { it.isEnabled }

val hasPersonalVaultExportRestrictedOrg = getUserPolicies(
userId,
PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT,
)
.any { it.isEnabled }

UserState.Account(
userId = userId,
name = profile.name,
Expand Down Expand Up @@ -231,6 +246,8 @@ fun UserStateJson.toUserState(
// using the app prior to the release of the onboarding flow.
onboardingStatus = onboardingStatus ?: OnboardingStatus.COMPLETE,
firstTimeState = firstTimeState,
isExportable = !hasPersonalOwnershipRestrictedOrg &&
!hasPersonalVaultExportRestrictedOrg,
)
},
hasPendingAccountAddition = hasPendingAccountAddition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,12 @@ interface PolicyManager {
* Get all the policies of the given [type] that are enabled and applicable to the user.
*/
fun getActivePolicies(type: PolicyTypeJson): List<SyncResponseJson.Policy>

/**
* Get all the policies of the given [type] that are enabled and applicable to the [userId].
*/
fun getUserPolicies(
userId: String,
type: PolicyTypeJson,
): List<SyncResponseJson.Policy>
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ class PolicyManagerImpl(
}
?: emptyList()

override fun getUserPolicies(
userId: String,
type: PolicyTypeJson,
): List<SyncResponseJson.Policy> =
this
.filterPolicies(
userId = userId,
type = type,
policies = authDiskSource.getPolicies(userId = userId),
)
.orEmpty()

/**
* A helper method to filter policies.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,15 @@ class VaultSyncManagerImpl(
}
}

// Treat absent network policies as known empty data to
// distinguish between unknown null data.
// The user state update will trigger flows that depend on the latest policies.
// We must store the new policies first to prevent old data on UserState.
authDiskSource.storePolicies(
userId = userId,
policies = syncResponse.policies.orEmpty(),
)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a functional reason this was moved?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the UserState was being updated before the policies were updated, so the existingPolicies wasn't returning the most recent data.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add a comment explaining that order matters here

// Update user information with additional information from sync response
authDiskSource.userState = authDiskSource.userState?.toUpdatedUserStateJson(
syncResponse = syncResponse,
Expand All @@ -323,12 +332,6 @@ class VaultSyncManagerImpl(
unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse)
storeProfileData(syncResponse = syncResponse)

// Treat absent network policies as known empty data to
// distinguish between unknown null data.
authDiskSource.storePolicies(
userId = userId,
policies = syncResponse.policies.orEmpty(),
)
settingsDiskSource.storeLastSyncTime(
userId = userId,
lastSyncTime = clock.instant(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ fun RootNavScreen(
navController.navigateToVerifyPassword(
userId = currentState.userId,
navOptions = rootNavOptions,
hasOtherAccounts = false,
Copy link
Collaborator

Choose a reason for hiding this comment

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

So this can never be true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is on the RootNavState.CredentialExchangeExportSkipAccountSelection scenario and this is only called when only exists 1 valid account, so yes

)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ class RootNavViewModel @Inject constructor(
}

specialCircumstance is SpecialCircumstance.CredentialExchangeExport -> {
if (userState.accounts.size == 1) {
val exportableAccounts = userState.accounts.filter { it.isExportable }
if (exportableAccounts.size == 1) {
RootNavState.CredentialExchangeExportSkipAccountSelection(
userId = userState.accounts.first().userId,
userId = exportableAccounts.first().userId,
)
} else {
RootNavState.CredentialExchangeExport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ fun NavGraphBuilder.exportItemsGraph(
startDestination = SelectAccountRoute,
) {
selectAccountDestination(
onAccountSelected = {
navController.navigateToVerifyPassword(userId = it)
onAccountSelected = { userId, hasOtherAccounts ->
navController.navigateToVerifyPassword(
userId = userId,
hasOtherAccounts = hasOtherAccounts,
)
},
)
verifyPasswordDestination(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ data object SelectAccountRoute
* Add the [SelectAccountScreen] to the nav graph.
*/
fun NavGraphBuilder.selectAccountDestination(
onAccountSelected: (id: String) -> Unit,
onAccountSelected: (id: String, hasOtherAccounts: Boolean) -> Unit,
) {
composableWithRootPushTransitions<SelectAccountRoute> {
SelectAccountScreen(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import kotlinx.collections.immutable.persistentListOf
@Composable
@Suppress("LongMethod")
fun SelectAccountScreen(
onAccountSelected: (userId: String) -> Unit,
onAccountSelected: (userId: String, hasOtherAccounts: Boolean) -> Unit,
viewModel: SelectAccountViewModel = hiltViewModel(),
credentialExchangeCompletionManager: CredentialExchangeCompletionManager =
LocalCredentialExchangeCompletionManager.current,
Expand All @@ -74,7 +74,7 @@ fun SelectAccountScreen(
}

is SelectAccountEvent.NavigateToPasswordVerification -> {
onAccountSelected(event.userId)
onAccountSelected(event.userId, event.hasOtherAccounts)
}

is SelectAccountEvent.ValidateImportRequest -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ class SelectAccountViewModel @Inject constructor(

private fun handleAccountClick(action: SelectAccountAction.AccountClick) {
sendEvent(
SelectAccountEvent.NavigateToPasswordVerification(
action.userId,
event = SelectAccountEvent.NavigateToPasswordVerification(
userId = action.userId,
hasOtherAccounts = true,
),
)
}
Expand All @@ -119,19 +120,14 @@ class SelectAccountViewModel @Inject constructor(
val itemRestrictedOrgIds = action.itemRestrictedOrgs
.filter { it.isEnabled }
.map { it.organizationId }
val personalOwnershipRestrictedOrgIds = action
.personalOwnershipOrgs
.filter { it.isEnabled }
.map { it.organizationId }

val accountSelectionListItems = action.userState
?.accounts
.orEmpty()
// We only want accounts that do not restrict personal vault ownership
// or vault export
.filter { account ->
account
.organizations
.none { org -> org.id in personalOwnershipRestrictedOrgIds }
account.isExportable
}
.map { account ->
AccountSelectionListItem(
Expand Down Expand Up @@ -164,12 +160,10 @@ class SelectAccountViewModel @Inject constructor(
combine(
authRepository.userStateFlow,
policyManager.getActivePoliciesFlow(PolicyTypeJson.RESTRICT_ITEM_TYPES),
policyManager.getActivePoliciesFlow(PolicyTypeJson.PERSONAL_OWNERSHIP),
) { userState, itemRestrictedOrgs, personalOwnershipOrgs ->
) { userState, itemRestrictedOrgs ->
SelectAccountAction.Internal.SelectionDataReceive(
userState = userState,
itemRestrictedOrgs = itemRestrictedOrgs,
personalOwnershipOrgs = personalOwnershipOrgs,
)
}
.onEach(::handleAction)
Expand Down Expand Up @@ -255,7 +249,6 @@ sealed class SelectAccountAction {
data class SelectionDataReceive(
val userState: UserState?,
val itemRestrictedOrgs: List<SyncResponseJson.Policy>,
val personalOwnershipOrgs: List<SyncResponseJson.Policy>,
) : Internal()
}
}
Expand All @@ -282,5 +275,6 @@ sealed class SelectAccountEvent {
*/
data class NavigateToPasswordVerification(
val userId: String,
val hasOtherAccounts: Boolean,
) : SelectAccountEvent()
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import kotlinx.serialization.Serializable
@OmitFromCoverage
data class VerifyPasswordRoute(
val userId: String,
val hasOtherAccounts: Boolean,
) : Parcelable {

/**
Expand All @@ -38,6 +39,7 @@ data class VerifyPasswordRoute(
@OmitFromCoverage
data class VerifyPasswordArgs(
val userId: String,
val hasOtherAccounts: Boolean,
)

/**
Expand All @@ -47,6 +49,7 @@ fun SavedStateHandle.toVerifyPasswordArgs(): VerifyPasswordArgs {
val route = this.toRoute<VerifyPasswordRoute>()
return VerifyPasswordArgs(
userId = route.userId,
hasOtherAccounts = route.hasOtherAccounts,
)
}

Expand All @@ -70,10 +73,14 @@ fun NavGraphBuilder.verifyPasswordDestination(
*/
fun NavController.navigateToVerifyPassword(
userId: String,
hasOtherAccounts: Boolean,
navOptions: NavOptions? = null,
) {
navigate(
route = VerifyPasswordRoute(userId = userId),
route = VerifyPasswordRoute(
userId = userId,
hasOtherAccounts = hasOtherAccounts,
),
navOptions = navOptions,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,7 @@ class VerifyPasswordViewModel @Inject constructor(
?.firstOrNull { it.userId == args.userId }
?: throw IllegalStateException("Account not found")

val singleAccount = authRepository
.userStateFlow
.value
?.accounts
?.size == 1
val singleAccount = !args.hasOtherAccounts

val restrictedItemPolicyOrgIds = policyManager
.getActivePolicies(PolicyTypeJson.RESTRICT_ITEM_TYPES)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = DEFAULT_FIRST_TIME_STATE,
isExportable = true,
)

private val DEFAULT_USER_STATE = UserState(
Expand Down
Loading
Loading