diff options
author | Roman Melnikov | 2024-02-16 14:49:44 +0100 |
---|---|---|
committer | GitHub | 2024-02-16 14:49:44 +0100 |
commit | 0a0187794ac7f7a1e62cda3dabf8dc041f868790 (patch) | |
tree | 57e0fed6d50a149c6d5c808f671495191977d80d /src | |
parent | 1776009f1f3fb2b5d236b84d9815f2edee463a9b (diff) | |
parent | 5f694ef481610e8c4c77bb963b49e2d3b0d4db3c (diff) |
Merge pull request #257 from n-hass/feature/interactive-sudo
Add support for entering sudo password interactively
Diffstat (limited to 'src')
-rw-r--r-- | src/cli.rs | 24 | ||||
-rw-r--r-- | src/data.rs | 2 | ||||
-rw-r--r-- | src/deploy.rs | 113 | ||||
-rw-r--r-- | src/lib.rs | 6 |
4 files changed, 126 insertions, 19 deletions
@@ -103,6 +103,9 @@ pub struct Opts { /// Which sudo command to use. Must accept at least two arguments: user name to execute commands as and the rest is the command to execute #[clap(long)] sudo: Option<String>, + /// Prompt for sudo password during activation. + #[clap(long)] + interactive_sudo: Option<bool>, } /// Returns if the available Nix installation supports flakes @@ -538,7 +541,25 @@ async fn run_deploy( log_dir.as_deref(), ); - let deploy_defs = deploy_data.defs()?; + let mut deploy_defs = deploy_data.defs()?; + + if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + warn!("Interactive sudo is enabled! Using a sudo password is less secure than correctly configured SSH keys.\nPlease use keys in production environments."); + + if deploy_data.merged_settings.sudo.is_some() { + warn!("Custom sudo commands should be configured to accept password input from stdin when using the 'interactive sudo' option. Deployment may fail if the custom command ignores stdin."); + } else { + // this configures sudo to hide the password prompt and accept input from stdin + // at the time of writing, deploy_defs.sudo defaults to 'sudo -u root' when using user=root and sshUser as non-root + let original = deploy_defs.sudo.unwrap_or("sudo".to_string()); + deploy_defs.sudo = Some(format!("{} -S -p \"\"", original)); + } + + info!("You will now be prompted for the sudo password for {}.", node.node_settings.hostname); + let sudo_password = rpassword::prompt_password(format!("(sudo for {}) Password: ", node.node_settings.hostname)).unwrap_or("".to_string()); + + deploy_defs.sudo_password = Some(sudo_password); + } parts.push((deploy_flake, deploy_data, deploy_defs)); } @@ -665,6 +686,7 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { dry_activate: opts.dry_activate, remote_build: opts.remote_build, sudo: opts.sudo, + interactive_sudo: opts.interactive_sudo }; let supports_flakes = test_flake_support().await.map_err(RunError::FlakeTest)?; diff --git a/src/data.rs b/src/data.rs index c507a31..12b0f01 100644 --- a/src/data.rs +++ b/src/data.rs @@ -35,6 +35,8 @@ pub struct GenericSettings { pub sudo: Option<String>, #[serde(default,rename(deserialize = "remoteBuild"))] pub remote_build: Option<bool>, + #[serde(rename(deserialize = "interactiveSudo"))] + pub interactive_sudo: Option<bool>, } #[derive(Deserialize, Debug, Clone)] diff --git a/src/deploy.rs b/src/deploy.rs index a371c18..9f79d64 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -4,12 +4,12 @@ // // SPDX-License-Identifier: MPL-2.0 -use log::{debug, info}; +use log::{debug, info, trace}; use std::path::Path; use thiserror::Error; -use tokio::process::Command; +use tokio::{io::AsyncWriteExt, process::Command}; -use crate::{DeployDataDefsError, ProfileInfo}; +use crate::{DeployDataDefsError, DeployDefs, ProfileInfo}; struct ActivateCommandData<'a> { sudo: &'a Option<String>, @@ -242,6 +242,23 @@ fn test_revoke_command_builder() { ); } +async fn handle_sudo_stdin(ssh_activate_child: &mut tokio::process::Child, deploy_defs: &DeployDefs) -> Result<(), std::io::Error> { + match ssh_activate_child.stdin.as_mut() { + Some(stdin) => { + let _ = stdin.write_all(format!("{}\n",deploy_defs.sudo_password.clone().unwrap_or("".to_string())).as_bytes()).await; + Ok(()) + } + None => { + Err( + std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to open stdin for sudo command", + ) + ) + } + } +} + #[derive(Error, Debug)] pub enum ConfirmProfileError { #[error("Failed to run confirmation command over SSH (the server should roll back): {0}")] @@ -259,7 +276,9 @@ pub async fn confirm_profile( ssh_addr: &str, ) -> Result<(), ConfirmProfileError> { let mut ssh_confirm_command = Command::new("ssh"); - ssh_confirm_command.arg(ssh_addr); + ssh_confirm_command + .arg(ssh_addr) + .stdin(std::process::Stdio::piped()); for ssh_opt in &deploy_data.merged_settings.ssh_opts { ssh_confirm_command.arg(ssh_opt); @@ -277,11 +296,22 @@ pub async fn confirm_profile( confirm_command ); - let ssh_confirm_exit_status = ssh_confirm_command + let mut ssh_confirm_child = ssh_confirm_command .arg(confirm_command) - .status() - .await + .spawn() .map_err(ConfirmProfileError::SSHConfirm)?; + + if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + trace!("[confirm] Piping in sudo password"); + handle_sudo_stdin(&mut ssh_confirm_child, deploy_defs) + .await + .map_err(ConfirmProfileError::SSHConfirm)?; + } + + let ssh_confirm_exit_status = ssh_confirm_child + .wait() + .await + .map_err(ConfirmProfileError::SSHConfirm)?; match ssh_confirm_exit_status.code() { Some(0) => (), @@ -308,6 +338,9 @@ pub enum DeployProfileError { #[error("Waiting over SSH resulted in a bad exit code: {0:?}")] SSHWaitExit(Option<i32>), + #[error("Failed to pipe to child stdin: {0}")] + SSHActivatePipe(std::io::Error), + #[error("Error confirming deployment: {0}")] Confirm(#[from] ConfirmProfileError), #[error("Deployment data invalid: {0}")] @@ -364,16 +397,29 @@ pub async fn deploy_profile( let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); let mut ssh_activate_command = Command::new("ssh"); - ssh_activate_command.arg(&ssh_addr); + ssh_activate_command + .arg(&ssh_addr) + .stdin(std::process::Stdio::piped()); for ssh_opt in &deploy_data.merged_settings.ssh_opts { ssh_activate_command.arg(&ssh_opt); } if !magic_rollback || dry_activate || boot { - let ssh_activate_exit_status = ssh_activate_command + let mut ssh_activate_child = ssh_activate_command .arg(self_activate_command) - .status() + .spawn() + .map_err(DeployProfileError::SSHSpawnActivate)?; + + if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + trace!("[activate] Piping in sudo password"); + handle_sudo_stdin(&mut ssh_activate_child, deploy_defs) + .await + .map_err(DeployProfileError::SSHActivatePipe)?; + } + + let ssh_activate_exit_status = ssh_activate_child + .wait() .await .map_err(DeployProfileError::SSHActivate)?; @@ -401,16 +447,25 @@ pub async fn deploy_profile( debug!("Constructed wait command: {}", self_wait_command); - let ssh_activate = ssh_activate_command + let mut ssh_activate_child = ssh_activate_command .arg(self_activate_command) .spawn() .map_err(DeployProfileError::SSHSpawnActivate)?; + if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + trace!("[activate] Piping in sudo password"); + handle_sudo_stdin(&mut ssh_activate_child, deploy_defs) + .await + .map_err(DeployProfileError::SSHActivatePipe)?; + } + info!("Creating activation waiter"); let mut ssh_wait_command = Command::new("ssh"); - ssh_wait_command.arg(&ssh_addr); - + ssh_wait_command + .arg(&ssh_addr) + .stdin(std::process::Stdio::piped()); + for ssh_opt in &deploy_data.merged_settings.ssh_opts { ssh_wait_command.arg(ssh_opt); } @@ -419,7 +474,7 @@ pub async fn deploy_profile( let (send_activated, recv_activated) = tokio::sync::oneshot::channel(); let thread = tokio::spawn(async move { - let o = ssh_activate.wait_with_output().await; + let o = ssh_activate_child.wait_with_output().await; let maybe_err = match o { Err(x) => Some(DeployProfileError::SSHActivate(x)), @@ -435,8 +490,21 @@ pub async fn deploy_profile( send_activated.send(()).unwrap(); }); + + let mut ssh_wait_child = ssh_wait_command + .arg(self_wait_command) + .spawn() + .map_err(DeployProfileError::SSHWait)?; + + if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + trace!("[wait] Piping in sudo password"); + handle_sudo_stdin(&mut ssh_wait_child, deploy_defs) + .await + .map_err(DeployProfileError::SSHActivatePipe)?; + } + tokio::select! { - x = ssh_wait_command.arg(self_wait_command).status() => { + x = ssh_wait_child.wait() => { debug!("Wait command ended"); match x.map_err(DeployProfileError::SSHWait)?.code() { Some(0) => (), @@ -498,18 +566,27 @@ pub async fn revoke( let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); let mut ssh_activate_command = Command::new("ssh"); - ssh_activate_command.arg(&ssh_addr); + ssh_activate_command + .arg(&ssh_addr) + .stdin(std::process::Stdio::piped()); for ssh_opt in &deploy_data.merged_settings.ssh_opts { ssh_activate_command.arg(&ssh_opt); } - let ssh_revoke = ssh_activate_command + let mut ssh_revoke_child = ssh_activate_command .arg(self_revoke_command) .spawn() .map_err(RevokeProfileError::SSHSpawnRevoke)?; - let result = ssh_revoke.wait_with_output().await; + if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + trace!("[revoke] Piping in sudo password"); + handle_sudo_stdin(&mut ssh_revoke_child, deploy_defs) + .await + .map_err(RevokeProfileError::SSHRevoke)?; + } + + let result = ssh_revoke_child.wait_with_output().await; match result { Err(x) => Err(RevokeProfileError::SSHRevoke(x)), @@ -165,6 +165,7 @@ pub struct CmdOverrides { pub confirm_timeout: Option<u16>, pub activation_timeout: Option<u16>, pub sudo: Option<String>, + pub interactive_sudo: Option<bool>, pub dry_activate: bool, pub remote_build: bool, } @@ -334,6 +335,7 @@ pub struct DeployDefs { pub ssh_user: String, pub profile_user: String, pub sudo: Option<String>, + pub sudo_password: Option<String>, } enum ProfileInfo { ProfilePath { @@ -369,6 +371,7 @@ impl<'a> DeployData<'a> { ssh_user, profile_user, sudo, + sudo_password: None, }) } @@ -448,6 +451,9 @@ pub fn make_deploy_data<'a, 's>( if let Some(activation_timeout) = cmd_overrides.activation_timeout { merged_settings.activation_timeout = Some(activation_timeout); } + if let Some(interactive_sudo) = cmd_overrides.interactive_sudo { + merged_settings.interactive_sudo = Some(interactive_sudo); + } DeployData { node_name, |