[PM-13351] Prevent editing of TOTP key in 'can edit except passwords' collection (#4583)

This commit is contained in:
mpbw2
2025-01-30 15:12:28 -05:00
committed by GitHub
parent c09b02f1d0
commit 2195d07f78
2 changed files with 148 additions and 63 deletions

View File

@@ -125,69 +125,19 @@ fun LazyListScope.vaultAddEditLoginItems(
Spacer(modifier = Modifier.height(height = 8.dp))
}
coachMarkHighlightItem(
key = AddEditItemCoachMark.TOTP,
title = R.string.coachmark_2_of_3.asText(),
description = R.string.you_ll_only_need_to_set_up_authenticator_key.asText(),
onDismiss = onCoachMarkDismissed,
leftAction = {
CoachMarkActionText(
actionLabel = stringResource(R.string.back),
onActionClick = onPreviousCoachMark,
)
},
rightAction = {
CoachMarkActionText(
actionLabel = stringResource(R.string.next),
onActionClick = onNextCoachMark,
)
},
modifier = Modifier.standardHorizontalMargin(),
) {
if (loginState.totp != null) {
BitwardenTextFieldWithActions(
modifier = Modifier
.fillMaxWidth(),
label = stringResource(id = R.string.totp),
value = loginState.totp,
trailingIconContent = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_clear,
contentDescription = stringResource(id = R.string.delete),
onClick = loginItemTypeHandlers.onClearTotpKeyClick,
)
},
onValueChange = {},
readOnly = true,
singleLine = true,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_totp),
onClick = {
loginItemTypeHandlers.onCopyTotpKeyClick(loginState.totp)
},
)
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_camera,
contentDescription = stringResource(id = R.string.camera),
onClick = onTotpSetupClick,
)
},
textFieldTestTag = "LoginTotpEntry",
cardStyle = CardStyle.Full,
)
} else {
Spacer(modifier = Modifier.height(16.dp))
BitwardenOutlinedButton(
label = stringResource(id = R.string.setup_totp),
icon = rememberVectorPainter(id = R.drawable.ic_light_bulb),
onClick = onTotpSetupClick,
modifier = Modifier
.testTag("SetupTotpButton")
.fillMaxWidth(),
)
}
item(key = AddEditItemCoachMark.TOTP) {
TotpRow(
totpKey = loginState.totp,
canViewTotp = loginState.canViewPassword,
loginItemTypeHandlers = loginItemTypeHandlers,
onTotpSetupClick = onTotpSetupClick,
onPreviousCoachMark = onPreviousCoachMark,
onNextCoachMark = onNextCoachMark,
onCoachMarkDismissed = onCoachMarkDismissed,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
@@ -585,6 +535,100 @@ private fun CoachMarkScope<AddEditItemCoachMark>.PasswordRow(
}
}
@Suppress("LongMethod")
@Composable
private fun CoachMarkScope<AddEditItemCoachMark>.TotpRow(
totpKey: String?,
canViewTotp: Boolean,
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
onTotpSetupClick: () -> Unit,
onPreviousCoachMark: () -> Unit,
onNextCoachMark: () -> Unit,
onCoachMarkDismissed: () -> Unit,
modifier: Modifier = Modifier,
) {
CoachMarkHighlight(
key = AddEditItemCoachMark.TOTP,
title = stringResource(R.string.coachmark_2_of_3),
description = stringResource(R.string.you_ll_only_need_to_set_up_authenticator_key),
onDismiss = onCoachMarkDismissed,
leftAction = {
CoachMarkActionText(
actionLabel = stringResource(R.string.back),
onActionClick = onPreviousCoachMark,
)
},
rightAction = {
CoachMarkActionText(
actionLabel = stringResource(R.string.next),
onActionClick = onNextCoachMark,
)
},
modifier = modifier,
) {
if (totpKey != null) {
if (canViewTotp) {
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.totp),
value = totpKey,
trailingIconContent = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_clear,
contentDescription = stringResource(id = R.string.delete),
onClick = loginItemTypeHandlers.onClearTotpKeyClick,
)
},
onValueChange = {},
readOnly = true,
singleLine = true,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_totp),
onClick = {
loginItemTypeHandlers.onCopyTotpKeyClick(totpKey)
},
)
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_camera,
contentDescription = stringResource(id = R.string.camera),
onClick = onTotpSetupClick,
)
},
textFieldTestTag = "LoginTotpEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth(),
)
} else {
BitwardenTextField(
label = stringResource(id = R.string.totp),
value = totpKey,
cardStyle = CardStyle.Full,
textFieldTestTag = "LoginTotpEntry",
onValueChange = {},
readOnly = true,
enabled = false,
singleLine = true,
modifier = Modifier
.fillMaxWidth(),
)
}
} else {
Spacer(modifier = Modifier.height(16.dp))
BitwardenOutlinedButton(
label = stringResource(id = R.string.setup_totp),
icon = rememberVectorPainter(id = R.drawable.ic_light_bulb),
onClick = onTotpSetupClick,
isEnabled = canViewTotp,
modifier = Modifier
.testTag("SetupTotpButton")
.fillMaxWidth(),
)
}
}
}
@Composable
private fun PasskeyField(
creationDateTime: Text,

View File

@@ -49,6 +49,7 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
import com.x8bit.bitwarden.ui.util.assertScrollableNodeDoesNotExist
import com.x8bit.bitwarden.ui.util.isCoachMarkToolTip
import com.x8bit.bitwarden.ui.util.isProgressBar
import com.x8bit.bitwarden.ui.util.onAllNodesWithContentDescriptionAfterScroll
@@ -1164,6 +1165,46 @@ class VaultAddEditScreenTest : BaseComposeTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state the SetupTOTP button should be visible but disabled based on state`() {
mutableStateFlow.update { currentState ->
updateLoginType(currentState) {
copy(
canViewPassword = false,
totp = null,
)
}
}
composeTestRule
.onNodeWithTextAfterScroll(text = "Set up TOTP")
.assertIsDisplayed()
.assertIsNotEnabled()
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state the TOTP text field should be visible but disabled and the associated icon buttons should be invisible based on state`() {
mutableStateFlow.update { currentState ->
updateLoginType(currentState) {
copy(
canViewPassword = false,
totp = "TestCode",
)
}
}
composeTestRule
.onNodeWithTextAfterScroll(text = "TOTP")
.assertIsDisplayed()
.assertIsNotEnabled()
composeTestRule.assertScrollableNodeDoesNotExist("Delete")
composeTestRule.assertScrollableNodeDoesNotExist("Copy TOTP")
composeTestRule.assertScrollableNodeDoesNotExist("Camera")
}
@Test
fun `in ItemType_Login state changing URI text field should trigger UriValueChange`() {
mutableStateFlow.update { currentState ->