[PM-15864] Add copy private key action for SSH keys (#4462)

This commit is contained in:
Patrick Honkonen
2024-12-20 14:44:31 -05:00
committed by GitHub
parent 2ba516f50f
commit f32eecc0d7
6 changed files with 115 additions and 2 deletions

View File

@@ -15,7 +15,7 @@ import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
@@ -84,12 +84,20 @@ fun VaultItemSshKeyContent(
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
BitwardenPasswordFieldWithActions(
label = stringResource(id = R.string.private_key),
value = sshKeyItemState.privateKey,
onValueChange = { },
singleLine = false,
readOnly = true,
actions = {
BitwardenTonalIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_private_key),
onClick = vaultSshKeyItemTypeHandlers.onCopyPrivateKeyClick,
modifier = Modifier.testTag(tag = "SshKeyCopyPrivateKeyButton"),
)
},
showPassword = sshKeyItemState.showPrivateKey,
showPasswordTestTag = "ViewPrivateKeyButton",
showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick,

View File

@@ -790,6 +790,8 @@ class VaultItemViewModel @Inject constructor(
handlePrivateKeyVisibilityClicked(action)
}
is VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick -> handleCopyPrivateKeyClick()
VaultItemAction.ItemType.SshKey.CopyFingerprintClick -> handleCopyFingerprintClick()
}
}
@@ -824,6 +826,20 @@ class VaultItemViewModel @Inject constructor(
}
}
private fun handleCopyPrivateKeyClick() {
onSshKeyContent { content, sshKey ->
if (content.common.requiresReprompt) {
updateDialogState(
VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.CopyClick(value = sshKey.privateKey),
),
)
return@onSshKeyContent
}
clipboardManager.setText(text = sshKey.privateKey)
}
}
private fun handleCopyFingerprintClick() {
onSshKeyContent { _, sshKey ->
clipboardManager.setText(text = sshKey.fingerprint)
@@ -1960,6 +1976,11 @@ sealed class VaultItemAction {
*/
data class PrivateKeyVisibilityClicked(val isVisible: Boolean) : SshKey()
/**
* The user has clicked the copy button for the private key.
*/
data object CopyPrivateKeyClick : SshKey()
/**
* The user has clicked the copy button for the fingerprint.
*/

View File

@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
data class VaultSshKeyItemTypeHandlers(
val onCopyPublicKeyClick: () -> Unit,
val onShowPrivateKeyClick: (isVisible: Boolean) -> Unit,
val onCopyPrivateKeyClick: () -> Unit,
val onCopyFingerprintClick: () -> Unit,
) {
@@ -34,6 +35,11 @@ data class VaultSshKeyItemTypeHandlers(
),
)
},
onCopyPrivateKeyClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick,
)
},
onCopyFingerprintClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.SshKey.CopyFingerprintClick,

View File

@@ -1124,4 +1124,5 @@ Do you want to switch to this account?</string>
<string name="copied_to_clipboard">Copied to clipboard.</string>
<string name="we_couldnt_verify_the_servers_certificate">We couldnt verify the servers certificate. The certificate chain or proxy settings on your device or your Bitwarden server may not be set up correctly.</string>
<string name="review_flow_launched">Review flow launched!</string>
<string name="copy_private_key">Copy private key</string>
</resources>

View File

@@ -2545,6 +2545,18 @@ class VaultItemScreenTest : BaseComposeTest() {
}
}
@Test
fun `in ssh key state, on copy private key click should send CopyPrivateKeyClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) }
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Copy private key")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick)
}
}
@Test
fun `in ssh key state, fingerprint should be displayed according to state`() {
val fingerprint = "the fingerprint"

View File

@@ -2583,6 +2583,71 @@ class VaultItemViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `onPrivateKeyCopyClick should copy private key to clipboard when re-prompt is not required`() =
runTest {
every { clipboardManager.setText("mockPrivateKey") } just runs
every {
mockCipherView.toViewState(
previousState = null,
isPremiumUser = true,
hasMasterPassword = true,
totpCodeItemData = null,
canDelete = true,
canAssignToCollections = true,
)
} returns createViewState(
common = DEFAULT_COMMON.copy(requiresReprompt = false),
type = DEFAULT_SSH_KEY_TYPE,
)
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
mutableCollectionsStateFlow.value = DataState.Loaded(emptyList())
viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick)
verify(exactly = 1) {
clipboardManager.setText(text = DEFAULT_SSH_KEY_TYPE.privateKey)
}
}
@Test
fun `onPrivateKeyCopyClick should show password dialog when re-prompt is required`() =
runTest {
val sshKeyState = DEFAULT_STATE.copy(viewState = SSH_KEY_VIEW_STATE)
every { clipboardManager.setText("mockPrivateKey") } just runs
every {
mockCipherView.toViewState(
previousState = null,
isPremiumUser = true,
hasMasterPassword = true,
totpCodeItemData = null,
canDelete = true,
canAssignToCollections = true,
)
} returns SSH_KEY_VIEW_STATE
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
mutableCollectionsStateFlow.value = DataState.Loaded(emptyList())
viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPrivateKeyClick)
assertEquals(
sshKeyState.copy(
dialog = VaultItemState.DialogState.MasterPasswordDialog(
action = PasswordRepromptAction.CopyClick(
value = DEFAULT_SSH_KEY_TYPE.privateKey,
),
),
),
viewModel.stateFlow.value,
)
verify(exactly = 0) {
clipboardManager.setText(text = DEFAULT_SSH_KEY_TYPE.privateKey)
}
}
@Test
fun `on CopyFingerprintClick should copy fingerprint to clipboard`() = runTest {
every { clipboardManager.setText("mockFingerprint") } just runs