@@ -2,12 +2,14 @@ use std::{
22 collections:: HashMap ,
33 ffi:: { OsStr , OsString } ,
44 path:: PathBuf ,
5+ sync:: { Mutex , OnceLock } ,
56} ;
67
78use color_eyre:: {
89 Result ,
910 eyre:: { self , Context , bail} ,
1011} ;
12+ use secrecy:: { ExposeSecret , SecretString } ;
1113use subprocess:: { Exec , ExitStatus , Redirection } ;
1214use thiserror:: Error ;
1315use tracing:: { debug, info, warn} ;
@@ -18,12 +20,37 @@ use crate::{
1820 interface:: NixBuildPassthroughArgs ,
1921} ;
2022
21- fn ssh_wrap ( cmd : Exec , ssh : Option < & str > ) -> Exec {
23+ static PASSWORD_CACHE : OnceLock < Mutex < HashMap < String , SecretString > > > =
24+ OnceLock :: new ( ) ;
25+
26+ fn get_cached_password ( host : & str ) -> Option < SecretString > {
27+ let cache = PASSWORD_CACHE . get_or_init ( || Mutex :: new ( HashMap :: new ( ) ) ) ;
28+ let guard = cache. lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
29+ guard. get ( host) . cloned ( )
30+ }
31+
32+ fn cache_password ( host : & str , password : SecretString ) {
33+ let cache = PASSWORD_CACHE . get_or_init ( || Mutex :: new ( HashMap :: new ( ) ) ) ;
34+ let mut guard = cache. lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
35+ guard. insert ( host. to_string ( ) , password) ;
36+ }
37+
38+ fn ssh_wrap (
39+ cmd : Exec ,
40+ ssh : Option < & str > ,
41+ password : Option < & SecretString > ,
42+ ) -> Exec {
2243 if let Some ( ssh) = ssh {
23- Exec :: cmd ( "ssh" )
44+ let mut ssh_cmd = Exec :: cmd ( "ssh" )
2445 . arg ( "-T" )
2546 . arg ( ssh)
26- . stdin ( cmd. to_cmdline_lossy ( ) . as_str ( ) )
47+ . arg ( cmd. to_cmdline_lossy ( ) ) ;
48+
49+ if let Some ( pwd) = password {
50+ ssh_cmd = ssh_cmd. stdin ( format ! ( "{}\n " , pwd. expose_secret( ) ) . as_str ( ) ) ;
51+ }
52+
53+ ssh_cmd
2754 } else {
2855 cmd
2956 }
@@ -421,9 +448,73 @@ impl Command {
421448 ///
422449 /// Panics if the command result is unexpectedly None.
423450 pub fn run ( & self ) -> Result < ( ) > {
424- let cmd = if self . elevate . is_some ( ) {
451+ // Prompt for sudo password if needed for remote deployment
452+ // FIXME: this implementation only covers Sudo. I *think* doas and run0 are
453+ // able to read from stdin, but needs to be tested and possibly
454+ // mitigated.
455+ let sudo_password = if self . ssh . is_some ( ) && self . elevate . is_some ( ) {
456+ let host = self . ssh . as_ref ( ) . unwrap ( ) ;
457+ if let Some ( cached_password) = get_cached_password ( host) {
458+ Some ( cached_password)
459+ } else {
460+ let password =
461+ inquire:: Password :: new ( & format ! ( "[sudo] password for {}:" , host) )
462+ . without_confirmation ( )
463+ . prompt ( )
464+ . context ( "Failed to read sudo password" ) ?;
465+ let secret_password = SecretString :: new ( password) ;
466+ cache_password ( host, secret_password. clone ( ) ) ;
467+ Some ( secret_password)
468+ }
469+ } else {
470+ None
471+ } ;
472+
473+ let cmd = if self . elevate . is_some ( ) && self . ssh . is_none ( ) {
474+ // Local elevation
425475 self . build_sudo_cmd ( ) ?. arg ( & self . command ) . args ( & self . args )
476+ } else if self . elevate . is_some ( ) && self . ssh . is_some ( ) {
477+ // Build elevation command
478+ let elevation_program = self
479+ . elevate
480+ . as_ref ( )
481+ . unwrap ( )
482+ . resolve ( )
483+ . context ( "Failed to resolve elevation program" ) ?;
484+
485+ let program_name = elevation_program
486+ . file_name ( )
487+ . and_then ( |name| name. to_str ( ) )
488+ . ok_or_else ( || {
489+ eyre:: eyre!( "Failed to determine elevation program name" )
490+ } ) ?;
491+
492+ let mut elev_cmd = Exec :: cmd ( & elevation_program) ;
493+
494+ // Add program-specific arguments
495+ if program_name == "sudo" {
496+ elev_cmd = elev_cmd. arg ( "--prompt=" ) . arg ( "--stdin" ) ;
497+ }
498+
499+ // Add env command to handle environment variables
500+ elev_cmd = elev_cmd. arg ( "env" ) ;
501+ for ( key, action) in & self . env_vars {
502+ match action {
503+ EnvAction :: Set ( value) => {
504+ elev_cmd = elev_cmd. arg ( format ! ( "{}={}" , key, value) ) ;
505+ } ,
506+ EnvAction :: Preserve => {
507+ if let Ok ( value) = std:: env:: var ( key) {
508+ elev_cmd = elev_cmd. arg ( format ! ( "{}={}" , key, value) ) ;
509+ }
510+ } ,
511+ _ => { } ,
512+ }
513+ }
514+
515+ elev_cmd. arg ( & self . command ) . args ( & self . args )
426516 } else {
517+ // No elevation
427518 self . apply_env_to_exec ( Exec :: cmd ( & self . command ) . args ( & self . args ) )
428519 } ;
429520
@@ -435,6 +526,7 @@ impl Command {
435526 cmd. stderr ( Redirection :: None ) . stdout ( Redirection :: None )
436527 } ,
437528 self . ssh . as_deref ( ) ,
529+ sudo_password. as_ref ( ) ,
438530 ) ;
439531
440532 if let Some ( m) = & self . message {
@@ -1063,7 +1155,7 @@ mod tests {
10631155 #[ test]
10641156 fn test_ssh_wrap_with_ssh ( ) {
10651157 let cmd = subprocess:: Exec :: cmd ( "echo" ) . arg ( "hello" ) ;
1066- let wrapped = ssh_wrap ( cmd, Some ( "user@host" ) ) ;
1158+ let wrapped = ssh_wrap ( cmd, Some ( "user@host" ) , None ) ;
10671159
10681160 let cmdline = wrapped. to_cmdline_lossy ( ) ;
10691161 assert ! ( cmdline. starts_with( "ssh" ) ) ;
@@ -1074,12 +1166,24 @@ mod tests {
10741166 #[ test]
10751167 fn test_ssh_wrap_without_ssh ( ) {
10761168 let cmd = subprocess:: Exec :: cmd ( "echo" ) . arg ( "hello" ) ;
1077- let wrapped = ssh_wrap ( cmd. clone ( ) , None ) ;
1169+ let wrapped = ssh_wrap ( cmd. clone ( ) , None , None ) ;
10781170
10791171 // Should return the original command unchanged
10801172 assert_eq ! ( wrapped. to_cmdline_lossy( ) , cmd. to_cmdline_lossy( ) ) ;
10811173 }
10821174
1175+ #[ test]
1176+ fn test_ssh_wrap_with_password ( ) {
1177+ let cmd = subprocess:: Exec :: cmd ( "echo" ) . arg ( "hello" ) ;
1178+ let password = SecretString :: new ( "testpass" . to_string ( ) ) ;
1179+ let wrapped = ssh_wrap ( cmd, Some ( "user@host" ) , Some ( & password) ) ;
1180+
1181+ let cmdline = wrapped. to_cmdline_lossy ( ) ;
1182+ assert ! ( cmdline. starts_with( "ssh" ) ) ;
1183+ assert ! ( cmdline. contains( "-T" ) ) ;
1184+ assert ! ( cmdline. contains( "user@host" ) ) ;
1185+ }
1186+
10831187 #[ test]
10841188 #[ serial]
10851189 fn test_apply_env_to_exec ( ) {
0 commit comments