mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
[PM-9407] Confirm overwrite existing passkey on item listing (#3540)
This commit is contained in:
parent
d9f506dd8f
commit
815e779475
9 changed files with 243 additions and 16 deletions
|
@ -41,6 +41,7 @@ import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingConten
|
|||
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwritePasskeyConfirmationDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
|
@ -172,6 +173,15 @@ fun VaultItemListingScreen(
|
|||
)
|
||||
}
|
||||
},
|
||||
onConfirmOverwriteExistingPasskey = remember(viewModel) {
|
||||
{ cipherId ->
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ConfirmOverwriteExistingPasskeyClick(
|
||||
cipherViewId = cipherId,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
VaultItemListingScaffold(
|
||||
|
@ -188,6 +198,7 @@ private fun VaultItemListingDialogs(
|
|||
dialogState: VaultItemListingState.DialogState?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onDismissFido2ErrorDialog: () -> Unit,
|
||||
onConfirmOverwriteExistingPasskey: (cipherViewId: String) -> Unit,
|
||||
) {
|
||||
when (dialogState) {
|
||||
is VaultItemListingState.DialogState.Error -> BitwardenBasicDialog(
|
||||
|
@ -210,6 +221,13 @@ private fun VaultItemListingDialogs(
|
|||
onDismissRequest = onDismissFido2ErrorDialog,
|
||||
)
|
||||
|
||||
is VaultItemListingState.DialogState.OverwritePasskeyConfirmationPrompt -> {
|
||||
BitwardenOverwritePasskeyConfirmationDialog(
|
||||
onConfirmClick = { onConfirmOverwriteExistingPasskey(dialogState.cipherViewId) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -199,6 +199,9 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
is VaultItemListingsAction.AddVaultItemClick -> handleAddVaultItemClick()
|
||||
is VaultItemListingsAction.RefreshClick -> handleRefreshClick()
|
||||
is VaultItemListingsAction.RefreshPull -> handleRefreshPull()
|
||||
is VaultItemListingsAction.ConfirmOverwriteExistingPasskeyClick -> {
|
||||
handleConfirmOverwriteExistingPasskeyClick(action)
|
||||
}
|
||||
|
||||
VaultItemListingsAction.UserVerificationLockOut -> {
|
||||
handleUserVerificationLockOut()
|
||||
|
@ -255,6 +258,18 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
vaultRepository.sync()
|
||||
}
|
||||
|
||||
private fun handleConfirmOverwriteExistingPasskeyClick(
|
||||
action: VaultItemListingsAction.ConfirmOverwriteExistingPasskeyClick,
|
||||
) {
|
||||
clearDialogState()
|
||||
getCipherViewOrNull(action.cipherViewId)
|
||||
?.let { registerFido2Credential(it) }
|
||||
?: run {
|
||||
showFido2ErrorDialog()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUserVerificationLockOut() {
|
||||
fido2CredentialManager.isUserVerified = false
|
||||
showFido2ErrorDialog()
|
||||
|
@ -385,6 +400,21 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
showFido2ErrorDialog()
|
||||
return
|
||||
}
|
||||
|
||||
if (cipherView.isActiveWithFido2Credentials) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState
|
||||
.DialogState
|
||||
.OverwritePasskeyConfirmationPrompt(cipherViewId = action.id),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
registerFido2Credential(cipherView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerFido2Credential(cipherView: CipherView) {
|
||||
val credentialRequest = state
|
||||
.fido2CredentialRequest
|
||||
?: run {
|
||||
|
@ -1209,6 +1239,12 @@ data class VaultItemListingState(
|
|||
data class Loading(
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays the overwrite passkey confirmation prompt to the user.
|
||||
*/
|
||||
@Parcelize
|
||||
data class OverwritePasskeyConfirmationPrompt(val cipherViewId: String) : DialogState()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1687,6 +1723,13 @@ sealed class VaultItemListingsAction {
|
|||
*/
|
||||
data object UserVerificationNotSupported : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* The user has confirmed overwriting the existing cipher's passkey.
|
||||
*/
|
||||
data class ConfirmOverwriteExistingPasskeyClick(
|
||||
val cipherViewId: String,
|
||||
) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [VaultItemListingViewModel] itself might send.
|
||||
*/
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.autofill.util
|
|||
import com.bitwarden.vault.CipherType
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
|
@ -139,7 +140,10 @@ class CipherViewExtensionsTest {
|
|||
@Test
|
||||
fun `isActiveWithFido2Credentials should return true when type is login, deleted date is null, and fido2 credentials are not null`() {
|
||||
assertTrue(
|
||||
createMockCipherView(number = 1)
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
.isActiveWithFido2Credentials,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -46,10 +46,7 @@ fun createMockCipherView(
|
|||
totp: String? = "mockTotp-$number",
|
||||
folderId: String? = "mockId-$number",
|
||||
clock: Clock = FIXED_CLOCK,
|
||||
fido2Credentials: List<Fido2Credential>? = createMockSdkFido2CredentialList(
|
||||
number = 1,
|
||||
clock = clock,
|
||||
),
|
||||
fido2Credentials: List<Fido2Credential>? = null,
|
||||
): CipherView =
|
||||
CipherView(
|
||||
id = "mockId-$number",
|
||||
|
|
|
@ -30,6 +30,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
|||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockUriView
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
|
@ -1338,7 +1339,10 @@ class SearchViewModelTest : BaseViewModelTest() {
|
|||
autofillSelectionData = AUTOFILL_SELECTION_DATA,
|
||||
shouldFinishWhenComplete = true,
|
||||
)
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
val cipherView = createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
val ciphers = listOf(cipherView)
|
||||
val expectedViewState = SearchState.ViewState.Content(
|
||||
displayItems = listOf(createMockDisplayItemForCipher(number = 1)),
|
||||
|
|
|
@ -554,10 +554,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
),
|
||||
totpCode = "mockTotp-1",
|
||||
canViewPassword = true,
|
||||
fido2CredentialCreationDateTime = R.string.created_xy.asText(
|
||||
"10/27/23",
|
||||
"12:00 PM",
|
||||
),
|
||||
fido2CredentialCreationDateTime = null,
|
||||
)
|
||||
.copy(totp = "mockTotp-1"),
|
||||
),
|
||||
|
|
|
@ -1665,6 +1665,40 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `OverwritePasskeyConfirmationPrompt should display based on dialog state and send ConfirmOverwriteExistingPasskeyClick on Ok click`() {
|
||||
val stateWithDialog = DEFAULT_STATE
|
||||
.copy(
|
||||
dialogState = VaultItemListingState.DialogState.OverwritePasskeyConfirmationPrompt(
|
||||
cipherViewId = "mockCipherViewId",
|
||||
),
|
||||
)
|
||||
|
||||
mutableStateFlow.value = stateWithDialog
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Overwrite passkey?")
|
||||
.assertIsDisplayed()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
composeTestRule
|
||||
.onNodeWithText("This item already contains a passkey. Are you sure you want to overwrite the current passkey?")
|
||||
.assertIsDisplayed()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ConfirmOverwriteExistingPasskeyClick(
|
||||
cipherViewId = "mockCipherViewId",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
||||
|
|
|
@ -37,6 +37,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
|||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
|
||||
|
@ -294,7 +295,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
fun `ItemClick for vault item when autofill should post to the AutofillSelectionManager`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
val cipherView = createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
coEvery {
|
||||
vaultRepository.getDecryptedFido2CredentialAutofillViews(
|
||||
cipherViewList = listOf(cipherView),
|
||||
|
@ -391,12 +395,58 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ItemClick for vault item during FIDO 2 registration should show overwrite passkey confirmation when selected cipher has existing passkey`() {
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val cipherView = createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = createMockFido2CredentialRequest(number = 1),
|
||||
)
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(cipherView),
|
||||
folderViewList = emptyList(),
|
||||
collectionViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyCreateOptionsOrNull(any())
|
||||
} returns createMockPublicKeyCredentialCreationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(VaultItemListingsAction.ItemClick(cipherView.id.orEmpty()))
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.OverwritePasskeyConfirmationPrompt(
|
||||
cipherViewId = cipherView.id!!,
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ItemClick for vault item during FIDO 2 registration should show loading dialog, then request user verification when required`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
val cipherView = createMockCipherView(number = 1, fido2Credentials = null)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = createMockFido2CredentialRequest(number = 1),
|
||||
)
|
||||
|
@ -1168,8 +1218,14 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
setupMockUri()
|
||||
|
||||
val cipherView1 = createMockCipherView(number = 1)
|
||||
val cipherView2 = createMockCipherView(number = 2)
|
||||
val cipherView1 = createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
val cipherView2 = createMockCipherView(
|
||||
number = 2,
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
vaultRepository.getDecryptedFido2CredentialAutofillViews(
|
||||
|
@ -1239,8 +1295,14 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
setupMockUri()
|
||||
|
||||
val cipherView1 = createMockCipherView(number = 1)
|
||||
val cipherView2 = createMockCipherView(number = 2)
|
||||
val cipherView1 = createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
val cipherView2 = createMockCipherView(
|
||||
number = 2,
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
vaultRepository.getDecryptedFido2CredentialAutofillViews(
|
||||
|
@ -2292,6 +2354,71 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConfirmOverwriteExistingPasskeyClick should check if user is verified`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = createMockFido2CredentialRequest(number = 1),
|
||||
)
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(cipherView),
|
||||
folderViewList = emptyList(),
|
||||
collectionViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyCreateOptionsOrNull(any())
|
||||
} returns createMockPublicKeyCredentialCreationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
)
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ConfirmOverwriteExistingPasskeyClick(
|
||||
cipherViewId = cipherView.id!!,
|
||||
),
|
||||
)
|
||||
|
||||
verify { fido2CredentialManager.isUserVerified }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ConfirmOverwriteExistingPasskeyClick should display Fido2ErrorDialog when getSelectedCipher returns null`() = runTest {
|
||||
setupMockUri()
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = createMockFido2CredentialRequest(number = 1),
|
||||
)
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(cipherView),
|
||||
folderViewList = emptyList(),
|
||||
collectionViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ConfirmOverwriteExistingPasskeyClick(
|
||||
cipherViewId = "invalidId",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||
R.string.an_error_has_occurred.asText(),
|
||||
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun createSavedStateHandleWithVaultItemListingType(
|
||||
vaultItemListingType: VaultItemListingType,
|
||||
|
|
|
@ -18,6 +18,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
|||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -456,6 +457,7 @@ class VaultItemListingDataExtensionsTest {
|
|||
isDeleted = false,
|
||||
cipherType = CipherType.LOGIN,
|
||||
folderId = "mockId-1",
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
.copy(reprompt = CipherRepromptType.PASSWORD),
|
||||
createMockCipherView(
|
||||
|
@ -463,6 +465,7 @@ class VaultItemListingDataExtensionsTest {
|
|||
isDeleted = false,
|
||||
cipherType = CipherType.CARD,
|
||||
folderId = "mockId-1",
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 2),
|
||||
),
|
||||
)
|
||||
val fido2CredentialAutofillViews = listOf(
|
||||
|
|
Loading…
Reference in a new issue