Enable SSH remotes in native frontends (#117)
This commit is contained in:
@@ -4,13 +4,14 @@ use std::{collections::BTreeSet, time::Instant};
|
||||
|
||||
use ironstorage::{
|
||||
command::{
|
||||
CommandRequest, OtpCodeRequest, OtpRequest, OtpUriPresentation, OtpUriRequest,
|
||||
CommandRequest, GitRequest, OtpCodeRequest, OtpRequest, OtpUriPresentation, OtpUriRequest,
|
||||
Presentation, help_text, otp_version_text, version_text,
|
||||
},
|
||||
config::Config,
|
||||
config::SshFingerprint,
|
||||
crypto::KeyInfo,
|
||||
document::{DocumentError, EntryDocument, EntryFieldId},
|
||||
git::{GitConflict, GitProgressPhase, GitSnapshot},
|
||||
git::{GitConflict, GitProgressPhase, GitSnapshot, SshHostKey},
|
||||
otp::OtpCodeValidity,
|
||||
presentation::{ClipboardDisposition, ClipboardError, QrMatrix},
|
||||
read::{FindResults, GrepResults, TreeModel},
|
||||
@@ -223,6 +224,14 @@ pub enum AsyncPayload {
|
||||
conflicts: Vec<GitConflict>,
|
||||
details: Option<SecretBytes>,
|
||||
},
|
||||
GitHostConfirmation {
|
||||
request: GitRequest,
|
||||
host_key: SshHostKey,
|
||||
},
|
||||
GitPassphraseRequired {
|
||||
request: GitRequest,
|
||||
fingerprint: SshFingerprint,
|
||||
},
|
||||
OtpCodeFinished {
|
||||
entry: String,
|
||||
field: Option<EntryFieldId>,
|
||||
@@ -301,6 +310,15 @@ pub enum AppEffect {
|
||||
},
|
||||
AuthenticateWorkflow(Box<WorkflowSubmission>),
|
||||
AuthenticateGit(ironstorage::command::GitRequest),
|
||||
RetryGitAfterHostConfirmation {
|
||||
request: GitRequest,
|
||||
host_key: SshHostKey,
|
||||
},
|
||||
RetryGitWithPassphrase {
|
||||
request: GitRequest,
|
||||
fingerprint: SshFingerprint,
|
||||
passphrase: SecretBytes,
|
||||
},
|
||||
CancelGit,
|
||||
ResolveGit(Vec<ironstorage::git::GitConflictResolution>),
|
||||
AuthenticateOtp(OtpUiRequest),
|
||||
@@ -536,6 +554,14 @@ impl App {
|
||||
self.status = format!("{label} queued for secure-storage authentication…");
|
||||
}
|
||||
|
||||
fn begin_git_retry(&mut self) {
|
||||
self.git_pending = true;
|
||||
self.workflow_pending = false;
|
||||
self.workflow = None;
|
||||
self.transition(Transition::Dismiss);
|
||||
self.status = "Retrying Git operation with confirmed SSH input…".to_owned();
|
||||
}
|
||||
|
||||
pub fn remaining_lease(&self) -> Option<std::time::Duration> {
|
||||
self.remaining_lease
|
||||
}
|
||||
@@ -861,6 +887,26 @@ impl App {
|
||||
self.focus = PaneFocus::Main;
|
||||
self.status = message;
|
||||
}
|
||||
Ok(AsyncPayload::GitHostConfirmation { request, host_key }) => {
|
||||
self.git_pending = false;
|
||||
self.workflow = Some(WorkflowForm::ssh_host(request, host_key));
|
||||
self.workflow_pending = false;
|
||||
self.status =
|
||||
"Verify the SSH host fingerprint through a trusted channel before confirming."
|
||||
.to_owned();
|
||||
self.transition(Transition::OpenDialog);
|
||||
}
|
||||
Ok(AsyncPayload::GitPassphraseRequired {
|
||||
request,
|
||||
fingerprint,
|
||||
}) => {
|
||||
self.git_pending = false;
|
||||
self.workflow = Some(WorkflowForm::ssh_passphrase(request, fingerprint));
|
||||
self.workflow_pending = false;
|
||||
self.status =
|
||||
"Enter the encrypted SSH key passphrase; input stays masked.".to_owned();
|
||||
self.transition(Transition::OpenDialog);
|
||||
}
|
||||
Ok(AsyncPayload::OtpCodeFinished {
|
||||
entry,
|
||||
field,
|
||||
@@ -1337,6 +1383,22 @@ impl App {
|
||||
}
|
||||
WorkflowInput::Lock => Some(self.dispatch(Action::Lock)),
|
||||
WorkflowInput::Submit => match self.workflow.as_ref()?.submission() {
|
||||
Ok(WorkflowSubmission::SshHost { request, host_key }) => {
|
||||
self.begin_git_retry();
|
||||
Some(AppEffect::RetryGitAfterHostConfirmation { request, host_key })
|
||||
}
|
||||
Ok(WorkflowSubmission::SshPassphrase {
|
||||
request,
|
||||
fingerprint,
|
||||
passphrase,
|
||||
}) => {
|
||||
self.begin_git_retry();
|
||||
Some(AppEffect::RetryGitWithPassphrase {
|
||||
request,
|
||||
fingerprint,
|
||||
passphrase,
|
||||
})
|
||||
}
|
||||
Ok(submission) => {
|
||||
self.workflow_pending = true;
|
||||
self.status = "Authenticating before applying the workflow…".to_owned();
|
||||
@@ -2126,7 +2188,7 @@ impl App {
|
||||
fn git_phase_name(phase: GitProgressPhase) -> &'static str {
|
||||
match phase {
|
||||
GitProgressPhase::Validating => "validating repository",
|
||||
GitProgressPhase::Authenticating => "requesting credentials",
|
||||
GitProgressPhase::Authenticating => "verifying remote and credentials",
|
||||
GitProgressPhase::Receiving => "receiving remote objects",
|
||||
GitProgressPhase::Integrating => "integrating fetched changes",
|
||||
GitProgressPhase::Sending => "sending local objects",
|
||||
@@ -2744,4 +2806,40 @@ mod tests {
|
||||
assert!(rows.contains("••••••••"));
|
||||
assert!(!rows.contains("NEVER-RENDER"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_ssh_passphrase_request_opens_a_masked_cancellable_dialog() {
|
||||
let mut app = App::new();
|
||||
app.git_pending = true;
|
||||
let token = app.begin_request();
|
||||
let fingerprint =
|
||||
SshFingerprint::parse("SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||
.expect("fingerprint");
|
||||
assert_eq!(
|
||||
app.apply_result(AsyncResult {
|
||||
token,
|
||||
payload: Ok(AsyncPayload::GitPassphraseRequired {
|
||||
request: GitRequest::Sync {
|
||||
remote: Some("origin".to_owned()),
|
||||
},
|
||||
fingerprint: fingerprint.clone(),
|
||||
}),
|
||||
}),
|
||||
ResultDisposition::Applied
|
||||
);
|
||||
assert_eq!(app.mode(), Mode::Dialog);
|
||||
assert!(!app.git_pending());
|
||||
let rows = app.workflow().expect("SSH prompt").rows().join("\n");
|
||||
assert!(rows.contains(fingerprint.as_str()));
|
||||
assert!(!rows.contains("secret"));
|
||||
app.handle_workflow_input(
|
||||
crossterm::event::KeyCode::Char('Q'),
|
||||
crossterm::event::KeyModifiers::NONE,
|
||||
);
|
||||
let rows = app.workflow().expect("SSH prompt").rows().join("\n");
|
||||
assert!(rows.contains("••••••••"));
|
||||
assert!(!rows.contains('Q'));
|
||||
assert!(matches!(app.dispatch(Action::Cancel), AppEffect::None));
|
||||
assert_eq!(app.mode(), Mode::Browser);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user