use crate::jump::parser::JumpHost;
use crate::security::Password;
use crate::ssh::tokio_client::{AuthMethod, ClientHandler};
use anyhow::{Context, Result};
use std::path::Path;
use std::sync::Arc;
pub(super) async fn determine_auth_method(
jump_host: &JumpHost,
key_path: Option<&Path>,
use_agent: bool,
use_password: bool,
pre_collected_password: Option<Arc<Password>>,
ssh_connection_config: &crate::ssh::tokio_client::SshConnectionConfig,
) -> Result<AuthMethod> {
let effective_key_path = if let Some(ref jump_key) = jump_host.ssh_key {
use crate::config::{expand_env_vars, expand_tilde};
let expanded_path = expand_env_vars(jump_key);
let path = Path::new(&expanded_path);
Some(if expanded_path.starts_with('~') {
expand_tilde(path)
} else {
path.to_path_buf()
})
} else {
key_path.map(Path::to_path_buf)
};
let mut context =
crate::ssh::AuthContext::new(jump_host.effective_user(), jump_host.host.clone())?
.with_agent(use_agent)
.with_password(use_password)
.with_pre_collected_password(pre_collected_password)
.with_policy(ssh_connection_config.auth_policy.clone());
if let Some(path) = effective_key_path {
context = context.with_key_path(Some(path))?;
}
context.determine_method().await
}
pub(super) async fn authenticate_connection(
handle: &mut russh::client::Handle<ClientHandler>,
username: &str,
auth_method: AuthMethod,
host_description: &str,
) -> Result<()> {
crate::ssh::tokio_client::authentication::authenticate(
handle,
&username.to_string(),
auth_method,
)
.await
.with_context(|| format!("Authentication failed for {host_description} (user: {username})"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::EnvGuard;
use russh::keys::ssh_key::LineEnding;
use tempfile::TempDir;
fn create_test_jump_host() -> JumpHost {
JumpHost::new(
"test.example.com".to_string(),
Some("testuser".to_string()),
Some(22),
)
}
fn create_test_ssh_key(dir: &TempDir, name: &str) -> std::path::PathBuf {
let key_path = dir.path().join(name);
let key = russh::keys::PrivateKey::random(
&mut rand::rng(),
russh::keys::ssh_key::Algorithm::Ed25519,
)
.expect("generate test key");
std::fs::write(
&key_path,
key.to_openssh(LineEnding::LF)
.expect("encode test key")
.as_bytes(),
)
.expect("write test key");
key_path
}
#[tokio::test]
#[serial_test::serial]
async fn test_determine_auth_method_fallback_to_key_file() {
let _sock = EnvGuard::remove("SSH_AUTH_SOCK");
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let key_path = create_test_ssh_key(&temp_dir, "id_test");
let jump_host = create_test_jump_host();
let result = determine_auth_method(
&jump_host,
Some(key_path.as_path()),
true, false, None, &crate::ssh::tokio_client::SshConnectionConfig::default(),
)
.await;
assert!(result.is_ok(), "Should succeed with key file fallback");
let auth_method = result.unwrap();
match auth_method {
AuthMethod::Methods(methods) => {
assert!(matches!(
methods.first(),
Some(AuthMethod::PrivateKeyFileWithPolicy { .. })
));
assert!(matches!(
methods.get(1),
Some(AuthMethod::AgentWithPolicy { .. })
));
}
other => panic!("Expected ordered key and agent plan, got {other:?}"),
}
}
#[tokio::test]
#[serial_test::serial]
async fn test_determine_auth_method_tries_default_keys() {
let _sock = EnvGuard::remove("SSH_AUTH_SOCK");
let temp_home = TempDir::new().expect("Failed to create temp home");
let ssh_dir = temp_home.path().join(".ssh");
std::fs::create_dir_all(&ssh_dir).expect("Failed to create .ssh dir");
create_test_ssh_key(&temp_home, ".ssh/id_ed25519");
let _home = EnvGuard::set("HOME", temp_home.path());
let jump_host = create_test_jump_host();
let result = determine_auth_method(
&jump_host,
None, false, false, None, &crate::ssh::tokio_client::SshConnectionConfig::default(),
)
.await;
assert!(
result.is_ok(),
"Should find default key at ~/.ssh/id_ed25519"
);
let auth_method = result.unwrap();
match auth_method {
AuthMethod::PrivateKeyFileWithPolicy { key_file_path, .. } => {
let path_str = key_file_path.to_string_lossy();
assert!(
path_str.ends_with("id_ed25519") || path_str.contains("id_ed25519"),
"Should use id_ed25519 from default location, got: {path_str}"
);
}
other => {
panic!("Expected PrivateKeyFile, got {:?}", other);
}
}
}
#[tokio::test]
#[serial_test::serial]
async fn test_determine_auth_method_fails_when_no_method_available() {
let _sock = EnvGuard::remove("SSH_AUTH_SOCK");
let temp_home = TempDir::new().expect("Failed to create temp home");
let ssh_dir = temp_home.path().join(".ssh");
std::fs::create_dir_all(&ssh_dir).expect("Failed to create .ssh dir");
let _home = EnvGuard::set("HOME", temp_home.path());
let jump_host = create_test_jump_host();
let result = determine_auth_method(
&jump_host,
None, false, false, None, &crate::ssh::tokio_client::SshConnectionConfig::default(),
)
.await;
let error = result.expect_err("isolated HOME and invalid agent must have no method");
assert!(error.to_string().contains("Permission denied"));
}
#[tokio::test]
#[serial_test::serial]
async fn test_jump_host_ssh_key_priority() {
let _sock = EnvGuard::remove("SSH_AUTH_SOCK");
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let jump_key_path = create_test_ssh_key(&temp_dir, "jump_host_key");
let jump_key_str = jump_key_path.to_string_lossy().to_string();
let cluster_key_path = create_test_ssh_key(&temp_dir, "cluster_key");
let jump_host = JumpHost::with_ssh_key(
"test.example.com".to_string(),
Some("testuser".to_string()),
Some(22),
Some(jump_key_str.clone()),
);
let result = determine_auth_method(
&jump_host,
Some(cluster_key_path.as_path()), false, false, None, &crate::ssh::tokio_client::SshConnectionConfig::default(),
)
.await;
assert!(result.is_ok(), "Should succeed with jump host's key");
let auth_method = result.unwrap();
match auth_method {
AuthMethod::PrivateKeyFileWithPolicy { key_file_path, .. } => {
let path_str = key_file_path.to_string_lossy();
assert!(
path_str.contains("jump_host_key"),
"Should use jump host's key (jump_host_key), got: {path_str}"
);
assert!(
!path_str.contains("cluster_key"),
"Should NOT use cluster key, got: {path_str}"
);
}
other => {
panic!("Expected PrivateKeyFile, got {:?}", other);
}
}
}
#[tokio::test]
#[serial_test::serial]
async fn test_fallback_to_cluster_key() {
let _sock = EnvGuard::remove("SSH_AUTH_SOCK");
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let cluster_key_path = create_test_ssh_key(&temp_dir, "cluster_key");
let jump_host = JumpHost::new(
"test.example.com".to_string(),
Some("testuser".to_string()),
Some(22),
);
let result = determine_auth_method(
&jump_host,
Some(cluster_key_path.as_path()),
false,
false,
None, &crate::ssh::tokio_client::SshConnectionConfig::default(),
)
.await;
assert!(result.is_ok(), "Should succeed with cluster key");
let auth_method = result.unwrap();
match auth_method {
AuthMethod::PrivateKeyFileWithPolicy { key_file_path, .. } => {
let path_str = key_file_path.to_string_lossy();
assert!(
path_str.contains("cluster_key"),
"Should use cluster key, got: {path_str}"
);
}
other => {
panic!("Expected PrivateKeyFile, got {:?}", other);
}
}
}
}