Add the continue button flow for TDE (#1248)

This commit is contained in:
David Perez 2024-04-10 10:12:11 -05:00 committed by Álison Fernandes
parent 403cfc94f0
commit 0a63d85457
9 changed files with 691 additions and 7 deletions

View file

@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
import com.x8bit.bitwarden.data.auth.repository.model.OrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
@ -123,6 +124,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
*/
suspend fun deleteAccount(password: String): DeleteAccountResult
/**
* Attempt to create a new user via SSO and log them into their account. Upon success the new
* user will also have the vault automatically unlocked for them.
*/
suspend fun createNewSsoUser(): NewSsoUserResult
/**
* Attempt to complete the trusted device login with the given [requestPrivateKey] and
* [asymmetricalKey]. This will unlock the vault and finish trusting the device.

View file

@ -38,6 +38,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
import com.x8bit.bitwarden.data.auth.repository.model.OrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
@ -332,6 +333,61 @@ class AuthRepositoryImpl(
)
}
@Suppress("ReturnCount")
override suspend fun createNewSsoUser(): NewSsoUserResult {
val account = authDiskSource.userState?.activeAccount ?: return NewSsoUserResult.Failure
val orgIdentifier = rememberedOrgIdentifier ?: return NewSsoUserResult.Failure
val userId = account.profile.userId
return organizationService
.getOrganizationAutoEnrollStatus(orgIdentifier)
.flatMap { orgAutoEnrollStatus ->
organizationService
.getOrganizationKeys(orgAutoEnrollStatus.organizationId)
.flatMap { organizationKeys ->
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
userId = userId,
orgPublicKey = organizationKeys.publicKey,
rememberDevice = authDiskSource.shouldTrustDevice,
)
}
.flatMap { keys ->
accountsService
.createAccountKeys(
publicKey = keys.publicKey,
encryptedPrivateKey = keys.privateKey,
)
.map { keys }
}
.flatMap { keys ->
organizationService
.organizationResetPasswordEnroll(
organizationId = orgAutoEnrollStatus.organizationId,
userId = userId,
passwordHash = null,
resetPasswordKey = keys.adminReset,
)
.map { keys }
}
.onSuccess { keys ->
authDiskSource.storePrivateKey(
userId = userId,
privateKey = keys.privateKey,
)
keys.deviceKey?.let { trustDeviceResponse ->
trustedDeviceManager.trustThisDevice(
userId = userId,
trustDeviceResponse = trustDeviceResponse,
)
}
vaultRepository.syncVaultState(userId = userId)
}
}
.fold(
onSuccess = { NewSsoUserResult.Success },
onFailure = { NewSsoUserResult.Failure },
)
}
@Suppress("ReturnCount")
override suspend fun completeTdeLogin(
requestPrivateKey: String,

View file

@ -0,0 +1,16 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Models result of creating a new user via SSO.
*/
sealed class NewSsoUserResult {
/**
* A new user has successfully been created.
*/
data object Success : NewSsoUserResult()
/**
* There was an error while truing to create the new user.
*/
data object Failure : NewSsoUserResult()
}

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.manager
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.crypto.Kdf
import com.bitwarden.sdk.ClientAuth
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import kotlinx.coroutines.flow.StateFlow
@ -55,4 +56,14 @@ interface VaultLockManager {
* Suspends until the vault for the given [userId] is unlocked.
*/
suspend fun waitUntilUnlocked(userId: String)
/**
* This will check the vault lock state for the given user and ensure that the
* [vaultUnlockDataStateFlow] is up-to-date.
*
* This is only meant to be used when the SDK unlocks the vault as a side-effect of some other
* function, such as [ClientAuth.makeRegisterTdeKeys]. When using the regular [unlockVault]
* functions, this is not necessary.
*/
suspend fun syncVaultState(userId: String)
}

View file

@ -187,6 +187,18 @@ class VaultLockManagerImpl(
.first { unlockedUserIds -> userId in unlockedUserIds }
}
override suspend fun syncVaultState(userId: String) {
// There is no proper way to query if the vault is actually unlocked or not but we can
// attempt to retrieve the user encryption key. If it fails, then the vault is locked and
// if it succeeds, then the vault is unlocked.
vaultSdkSource
.getUserEncryptionKey(userId = userId)
.fold(
onFailure = { setVaultToLocked(userId = userId) },
onSuccess = { setVaultToUnlocked(userId = userId) },
)
}
/**
* Increments the stored invalid unlock count for the given [userId] and automatically logs out
* if this new value is greater than [MAXIMUM_INVALID_UNLOCK_ATTEMPTS].

View file

@ -2,13 +2,17 @@ package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
@ -17,6 +21,7 @@ private const val KEY_STATE = "state"
/**
* Manages application state for the Trusted Device screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class TrustedDeviceViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@ -52,6 +57,37 @@ class TrustedDeviceViewModel @Inject constructor(
TrustedDeviceAction.ApproveWithDeviceClick -> handleApproveWithDeviceClick()
TrustedDeviceAction.ApproveWithPasswordClick -> handleApproveWithPasswordClick()
TrustedDeviceAction.NotYouClick -> handleNotYouClick()
is TrustedDeviceAction.Internal -> handleInternalAction(action)
}
}
private fun handleInternalAction(action: TrustedDeviceAction.Internal) {
when (action) {
is TrustedDeviceAction.Internal.ReceiveNewSsoUserResult -> {
handleReceiveNewSsoUserResult(action)
}
}
}
private fun handleReceiveNewSsoUserResult(
action: TrustedDeviceAction.Internal.ReceiveNewSsoUserResult,
) {
when (action.result) {
NewSsoUserResult.Failure -> {
mutableStateFlow.update {
it.copy(
dialogState = TrustedDeviceState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
),
)
}
}
NewSsoUserResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
// Should automatically navigate to a logged in state.
}
}
}
@ -68,7 +104,16 @@ class TrustedDeviceViewModel @Inject constructor(
}
private fun handleContinueClick() {
sendEvent(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()))
mutableStateFlow.update {
it.copy(
dialogState = TrustedDeviceState.DialogState.Loading(R.string.loading.asText()),
)
}
authRepository.shouldTrustDevice = state.isRemembered
viewModelScope.launch {
val result = authRepository.createNewSsoUser()
sendAction(TrustedDeviceAction.Internal.ReceiveNewSsoUserResult(result))
}
}
private fun handleApproveWithAdminClick() {
@ -196,4 +241,16 @@ sealed class TrustedDeviceAction {
* Indicates that the "Not you?" text was clicked.
*/
data object NotYouClick : TrustedDeviceAction()
/**
* Actions for internal use by the ViewModel.
*/
sealed class Internal : TrustedDeviceAction() {
/**
* Indicates a new SSO user result has been received.
*/
data class ReceiveNewSsoUserResult(
val result: NewSsoUserResult,
) : Internal()
}
}

View file

@ -5,10 +5,12 @@ import com.bitwarden.core.AuthRequestMethod
import com.bitwarden.core.AuthRequestResponse
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.RegisterKeyResponse
import com.bitwarden.core.RegisterTdeKeyResponse
import com.bitwarden.core.UpdatePasswordResponse
import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf
import com.bitwarden.crypto.RsaKeyPair
import com.bitwarden.crypto.TrustDeviceResponse
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
@ -55,6 +57,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
import com.x8bit.bitwarden.data.auth.repository.model.OrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
@ -733,6 +736,430 @@ class AuthRepositoryTest {
}
}
@Test
fun `createNewSsoUser when no active user returns failure`() = runTest {
fakeAuthDiskSource.userState = null
val result = repository.createNewSsoUser()
assertEquals(NewSsoUserResult.Failure, result)
}
@Test
fun `createNewSsoUser when remembered org identifier returns failure`() = runTest {
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.rememberedOrgIdentifier = null
val result = repository.createNewSsoUser()
assertEquals(NewSsoUserResult.Failure, result)
}
@Test
fun `createNewSsoUser when getOrganizationAutoEnrollStatus fails returns failure`() = runTest {
val orgIdentifier = "rememberedOrgIdentifier"
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
coEvery {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
} returns Throwable().asFailure()
val result = repository.createNewSsoUser()
assertEquals(NewSsoUserResult.Failure, result)
coVerify(exactly = 1) {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
}
}
@Test
fun `createNewSsoUser when getOrganizationKeys fails returns failure`() = runTest {
val orgIdentifier = "rememberedOrgIdentifier"
val orgId = "organizationId"
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
organizationId = orgId,
isResetPasswordEnabled = false,
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
coEvery {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
} returns orgAutoEnrollStatusResponse.asSuccess()
coEvery { organizationService.getOrganizationKeys(orgId) } returns Throwable().asFailure()
val result = repository.createNewSsoUser()
assertEquals(NewSsoUserResult.Failure, result)
coVerify(exactly = 1) {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
organizationService.getOrganizationKeys(orgId)
}
}
@Test
fun `createNewSsoUser when makeRegisterTdeKeysAndUnlockVault fails returns failure`() =
runTest {
val shouldTrustDevice = false
val orgIdentifier = "rememberedOrgIdentifier"
val orgId = "organizationId"
val orgPublicKey = "organizationPublicKey"
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
organizationId = orgId,
isResetPasswordEnabled = false,
)
val orgKeysResponse = OrganizationKeysResponseJson(
privateKey = "privateKey",
publicKey = orgPublicKey,
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
fakeAuthDiskSource.shouldTrustDevice = shouldTrustDevice
coEvery {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
} returns orgAutoEnrollStatusResponse.asSuccess()
coEvery {
organizationService.getOrganizationKeys(orgId)
} returns orgKeysResponse.asSuccess()
coEvery {
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
userId = USER_ID_1,
orgPublicKey = orgPublicKey,
rememberDevice = shouldTrustDevice,
)
} returns Throwable().asFailure()
val result = repository.createNewSsoUser()
assertEquals(NewSsoUserResult.Failure, result)
coVerify(exactly = 1) {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
organizationService.getOrganizationKeys(orgId)
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
userId = USER_ID_1,
orgPublicKey = orgPublicKey,
rememberDevice = shouldTrustDevice,
)
}
}
@Test
fun `createNewSsoUser when createAccountKeys fails returns failure`() = runTest {
val shouldTrustDevice = false
val orgIdentifier = "rememberedOrgIdentifier"
val orgId = "organizationId"
val orgPublicKey = "organizationPublicKey"
val userPrivateKey = "userPrivateKey"
val userPublicKey = "userPublicKey"
val userAdminReset = "userAdminReset"
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
organizationId = orgId,
isResetPasswordEnabled = false,
)
val orgKeysResponse = OrganizationKeysResponseJson(
privateKey = "privateKey",
publicKey = orgPublicKey,
)
val registerTdeKeyResponse = RegisterTdeKeyResponse(
privateKey = userPrivateKey,
publicKey = userPublicKey,
adminReset = userAdminReset,
deviceKey = null,
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
fakeAuthDiskSource.shouldTrustDevice = shouldTrustDevice
coEvery {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
} returns orgAutoEnrollStatusResponse.asSuccess()
coEvery {
organizationService.getOrganizationKeys(orgId)
} returns orgKeysResponse.asSuccess()
coEvery {
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
userId = USER_ID_1,
orgPublicKey = orgPublicKey,
rememberDevice = shouldTrustDevice,
)
} returns registerTdeKeyResponse.asSuccess()
coEvery {
accountsService.createAccountKeys(
publicKey = userPublicKey,
encryptedPrivateKey = userPrivateKey,
)
} returns Throwable().asFailure()
val result = repository.createNewSsoUser()
assertEquals(NewSsoUserResult.Failure, result)
coVerify(exactly = 1) {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
organizationService.getOrganizationKeys(orgId)
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
userId = USER_ID_1,
orgPublicKey = orgPublicKey,
rememberDevice = shouldTrustDevice,
)
accountsService.createAccountKeys(
publicKey = userPublicKey,
encryptedPrivateKey = userPrivateKey,
)
}
}
@Test
fun `createNewSsoUser when organizationResetPasswordEnroll fails returns failure`() = runTest {
val shouldTrustDevice = false
val orgIdentifier = "rememberedOrgIdentifier"
val orgId = "organizationId"
val orgPublicKey = "organizationPublicKey"
val userPrivateKey = "userPrivateKey"
val userPublicKey = "userPublicKey"
val userAdminReset = "userAdminReset"
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
organizationId = orgId,
isResetPasswordEnabled = false,
)
val orgKeysResponse = OrganizationKeysResponseJson(
privateKey = "privateKey",
publicKey = orgPublicKey,
)
val registerTdeKeyResponse = RegisterTdeKeyResponse(
privateKey = userPrivateKey,
publicKey = userPublicKey,
adminReset = userAdminReset,
deviceKey = null,
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
fakeAuthDiskSource.shouldTrustDevice = shouldTrustDevice
coEvery {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
} returns orgAutoEnrollStatusResponse.asSuccess()
coEvery {
organizationService.getOrganizationKeys(orgId)
} returns orgKeysResponse.asSuccess()
coEvery {
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
userId = USER_ID_1,
orgPublicKey = orgPublicKey,
rememberDevice = shouldTrustDevice,
)
} returns registerTdeKeyResponse.asSuccess()
coEvery {
accountsService.createAccountKeys(
publicKey = userPublicKey,
encryptedPrivateKey = userPrivateKey,
)
} returns Unit.asSuccess()
coEvery {
organizationService.organizationResetPasswordEnroll(
organizationId = orgId,
userId = USER_ID_1,
passwordHash = null,
resetPasswordKey = userAdminReset,
)
} returns Throwable().asFailure()
val result = repository.createNewSsoUser()
assertEquals(NewSsoUserResult.Failure, result)
coVerify(exactly = 1) {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
organizationService.getOrganizationKeys(orgId)
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
userId = USER_ID_1,
orgPublicKey = orgPublicKey,
rememberDevice = shouldTrustDevice,
)
accountsService.createAccountKeys(
publicKey = userPublicKey,
encryptedPrivateKey = userPrivateKey,
)
organizationService.organizationResetPasswordEnroll(
organizationId = orgId,
userId = USER_ID_1,
passwordHash = null,
resetPasswordKey = userAdminReset,
)
}
}
@Suppress("MaxLineLength")
@Test
fun `createNewSsoUser when shouldTrustDevice false should not trust the device returns Success`() =
runTest {
val shouldTrustDevice = false
val orgIdentifier = "rememberedOrgIdentifier"
val orgId = "organizationId"
val orgPublicKey = "organizationPublicKey"
val userPrivateKey = "userPrivateKey"
val userPublicKey = "userPublicKey"
val userAdminReset = "userAdminReset"
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
organizationId = orgId,
isResetPasswordEnabled = false,
)
val orgKeysResponse = OrganizationKeysResponseJson(
privateKey = "privateKey",
publicKey = orgPublicKey,
)
val registerTdeKeyResponse = RegisterTdeKeyResponse(
privateKey = userPrivateKey,
publicKey = userPublicKey,
adminReset = userAdminReset,
deviceKey = null,
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
fakeAuthDiskSource.shouldTrustDevice = shouldTrustDevice
coEvery {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
} returns orgAutoEnrollStatusResponse.asSuccess()
coEvery {
organizationService.getOrganizationKeys(orgId)
} returns orgKeysResponse.asSuccess()
coEvery {
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
userId = USER_ID_1,
orgPublicKey = orgPublicKey,
rememberDevice = shouldTrustDevice,
)
} returns registerTdeKeyResponse.asSuccess()
coEvery {
accountsService.createAccountKeys(
publicKey = userPublicKey,
encryptedPrivateKey = userPrivateKey,
)
} returns Unit.asSuccess()
coEvery {
organizationService.organizationResetPasswordEnroll(
organizationId = orgId,
userId = USER_ID_1,
passwordHash = null,
resetPasswordKey = userAdminReset,
)
} returns Unit.asSuccess()
coEvery { vaultRepository.syncVaultState(userId = USER_ID_1) } just runs
val result = repository.createNewSsoUser()
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = userPrivateKey)
assertEquals(NewSsoUserResult.Success, result)
coVerify(exactly = 1) {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
organizationService.getOrganizationKeys(orgId)
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
userId = USER_ID_1,
orgPublicKey = orgPublicKey,
rememberDevice = shouldTrustDevice,
)
accountsService.createAccountKeys(
publicKey = userPublicKey,
encryptedPrivateKey = userPrivateKey,
)
organizationService.organizationResetPasswordEnroll(
organizationId = orgId,
userId = USER_ID_1,
passwordHash = null,
resetPasswordKey = userAdminReset,
)
vaultRepository.syncVaultState(userId = USER_ID_1)
}
}
@Test
fun `createNewSsoUser when shouldTrustDevice true should trust the device returns Success`() =
runTest {
val shouldTrustDevice = true
val orgIdentifier = "rememberedOrgIdentifier"
val orgId = "organizationId"
val orgPublicKey = "organizationPublicKey"
val userPrivateKey = "userPrivateKey"
val userPublicKey = "userPublicKey"
val userAdminReset = "userAdminReset"
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
organizationId = orgId,
isResetPasswordEnabled = false,
)
val orgKeysResponse = OrganizationKeysResponseJson(
privateKey = "privateKey",
publicKey = orgPublicKey,
)
val trustDeviceResponse = mockk<TrustDeviceResponse>()
val registerTdeKeyResponse = RegisterTdeKeyResponse(
privateKey = userPrivateKey,
publicKey = userPublicKey,
adminReset = userAdminReset,
deviceKey = trustDeviceResponse,
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
fakeAuthDiskSource.shouldTrustDevice = shouldTrustDevice
coEvery {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
} returns orgAutoEnrollStatusResponse.asSuccess()
coEvery {
organizationService.getOrganizationKeys(orgId)
} returns orgKeysResponse.asSuccess()
coEvery {
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
userId = USER_ID_1,
orgPublicKey = orgPublicKey,
rememberDevice = shouldTrustDevice,
)
} returns registerTdeKeyResponse.asSuccess()
coEvery {
accountsService.createAccountKeys(
publicKey = userPublicKey,
encryptedPrivateKey = userPrivateKey,
)
} returns Unit.asSuccess()
coEvery {
organizationService.organizationResetPasswordEnroll(
organizationId = orgId,
userId = USER_ID_1,
passwordHash = null,
resetPasswordKey = userAdminReset,
)
} returns Unit.asSuccess()
coEvery {
trustedDeviceManager.trustThisDevice(
userId = USER_ID_1,
trustDeviceResponse = trustDeviceResponse,
)
} returns Unit.asSuccess()
coEvery { vaultRepository.syncVaultState(userId = USER_ID_1) } just runs
val result = repository.createNewSsoUser()
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = userPrivateKey)
assertEquals(NewSsoUserResult.Success, result)
coVerify(exactly = 1) {
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
organizationService.getOrganizationKeys(orgId)
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
userId = USER_ID_1,
orgPublicKey = orgPublicKey,
rememberDevice = shouldTrustDevice,
)
accountsService.createAccountKeys(
publicKey = userPublicKey,
encryptedPrivateKey = userPrivateKey,
)
organizationService.organizationResetPasswordEnroll(
organizationId = orgId,
userId = USER_ID_1,
passwordHash = null,
resetPasswordKey = userAdminReset,
)
trustedDeviceManager.trustThisDevice(
userId = USER_ID_1,
trustDeviceResponse = trustDeviceResponse,
)
vaultRepository.syncVaultState(userId = USER_ID_1)
}
}
@Test
fun `completeTdeLogin without active user fails`() = runTest {
val requestPrivateKey = "requestPrivateKey"

View file

@ -1285,6 +1285,46 @@ class VaultLockManagerTest {
assertTrue(vaultLockManager.isVaultUnlocked(userId = USER_ID))
}
@Suppress("MaxLineLength")
@Test
fun `syncVaultState with getUserEncryptionKey failure should update the users vault state to locked`() =
runTest {
coEvery {
vaultSdkSource.getUserEncryptionKey(userId = USER_ID)
} returns Throwable().asFailure()
// Begin in a locked state
assertFalse(vaultLockManager.isVaultUnlocked(userId = USER_ID))
vaultLockManager.syncVaultState(userId = USER_ID)
// Confirm the vault is still locked
assertFalse(vaultLockManager.isVaultUnlocked(userId = USER_ID))
coVerify(exactly = 1) {
vaultSdkSource.getUserEncryptionKey(userId = USER_ID)
}
}
@Suppress("MaxLineLength")
@Test
fun `syncVaultState with getUserEncryptionKey success should update the users vault state to unlocked`() =
runTest {
coEvery {
vaultSdkSource.getUserEncryptionKey(userId = USER_ID)
} returns "UserEncryptionKey".asSuccess()
// Begin in a locked state
assertFalse(vaultLockManager.isVaultUnlocked(userId = USER_ID))
vaultLockManager.syncVaultState(userId = USER_ID)
// Confirm the vault is unlocked
assertTrue(vaultLockManager.isVaultUnlocked(userId = USER_ID))
coVerify(exactly = 1) {
vaultSdkSource.getUserEncryptionKey(userId = USER_ID)
}
}
/**
* Resets the verification call count for the given [mock] while leaving all other mocked
* behavior in place.

View file

@ -2,13 +2,17 @@ package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -77,14 +81,68 @@ class TrustedDeviceViewModelTest : BaseViewModelTest() {
}
@Test
fun `on ContinueClick emits ShowToast`() = runTest {
val viewModel = createViewModel()
fun `on ContinueClick with createNewSsoUser failure should display the error dialog state`() =
runTest {
every { authRepository.shouldTrustDevice = true } just runs
coEvery { authRepository.createNewSsoUser() } returns NewSsoUserResult.Failure
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(TrustedDeviceAction.ContinueClick)
assertEquals(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()), awaitItem())
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.trySendAction(TrustedDeviceAction.ContinueClick)
assertEquals(
DEFAULT_STATE.copy(
dialogState = TrustedDeviceState.DialogState.Loading(
message = R.string.loading.asText(),
),
),
awaitItem(),
)
assertEquals(
DEFAULT_STATE.copy(
dialogState = TrustedDeviceState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
),
),
awaitItem(),
)
}
verify(exactly = 1) {
authRepository.shouldTrustDevice = true
}
coVerify(exactly = 1) {
authRepository.createNewSsoUser()
}
}
@Test
fun `on ContinueClick with createNewSsoUser success should display the loading dialog state`() =
runTest {
every { authRepository.shouldTrustDevice = true } just runs
coEvery { authRepository.createNewSsoUser() } returns NewSsoUserResult.Success
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.trySendAction(TrustedDeviceAction.ContinueClick)
assertEquals(
DEFAULT_STATE.copy(
dialogState = TrustedDeviceState.DialogState.Loading(
message = R.string.loading.asText(),
),
),
awaitItem(),
)
assertEquals(DEFAULT_STATE, awaitItem())
}
verify(exactly = 1) {
authRepository.shouldTrustDevice = true
}
coVerify(exactly = 1) {
authRepository.createNewSsoUser()
}
}
}
@Test
fun `on ApproveWithAdminClick emits NavigateToApproveWithAdmin`() = runTest {