@@ -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} ;
@@ -19,12 +21,37 @@ use crate::{
1921 notify:: NotificationSender ,
2022} ;
2123
22- fn ssh_wrap ( cmd : Exec , ssh : Option < & str > ) -> Exec {
24+ static PASSWORD_CACHE : OnceLock < Mutex < HashMap < String , SecretString > > > =
25+ OnceLock :: new ( ) ;
26+
27+ fn get_cached_password ( host : & str ) -> Option < SecretString > {
28+ let cache = PASSWORD_CACHE . get_or_init ( || Mutex :: new ( HashMap :: new ( ) ) ) ;
29+ let guard = cache. lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
30+ guard. get ( host) . cloned ( )
31+ }
32+
33+ fn cache_password ( host : & str , password : SecretString ) {
34+ let cache = PASSWORD_CACHE . get_or_init ( || Mutex :: new ( HashMap :: new ( ) ) ) ;
35+ let mut guard = cache. lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
36+ guard. insert ( host. to_string ( ) , password) ;
37+ }
38+
39+ fn ssh_wrap (
40+ cmd : Exec ,
41+ ssh : Option < & str > ,
42+ password : Option < & SecretString > ,
43+ ) -> Exec {
2344 if let Some ( ssh) = ssh {
24- Exec :: cmd ( "ssh" )
45+ let mut ssh_cmd = Exec :: cmd ( "ssh" )
2546 . arg ( "-T" )
2647 . arg ( ssh)
27- . stdin ( cmd. to_cmdline_lossy ( ) . as_str ( ) )
48+ . arg ( cmd. to_cmdline_lossy ( ) ) ;
49+
50+ if let Some ( pwd) = password {
51+ ssh_cmd = ssh_cmd. stdin ( format ! ( "{}\n " , pwd. expose_secret( ) ) . as_str ( ) ) ;
52+ }
53+
54+ ssh_cmd
2855 } else {
2956 cmd
3057 }
@@ -422,9 +449,73 @@ impl Command {
422449 ///
423450 /// Panics if the command result is unexpectedly None.
424451 pub fn run ( & self ) -> Result < ( ) > {
425- let cmd = if self . elevate . is_some ( ) {
452+ // Prompt for sudo password if needed for remote deployment
453+ // FIXME: this implementation only covers Sudo. I *think* doas and run0 are
454+ // able to read from stdin, but needs to be tested and possibly
455+ // mitigated.
456+ let sudo_password = if self . ssh . is_some ( ) && self . elevate . is_some ( ) {
457+ let host = self . ssh . as_ref ( ) . unwrap ( ) ;
458+ if let Some ( cached_password) = get_cached_password ( host) {
459+ Some ( cached_password)
460+ } else {
461+ let password =
462+ inquire:: Password :: new ( & format ! ( "[sudo] password for {}:" , host) )
463+ . without_confirmation ( )
464+ . prompt ( )
465+ . context ( "Failed to read sudo password" ) ?;
466+ let secret_password = SecretString :: new ( password) ;
467+ cache_password ( host, secret_password. clone ( ) ) ;
468+ Some ( secret_password)
469+ }
470+ } else {
471+ None
472+ } ;
473+
474+ let cmd = if self . elevate . is_some ( ) && self . ssh . is_none ( ) {
475+ // Local elevation
426476 self . build_sudo_cmd ( ) ?. arg ( & self . command ) . args ( & self . args )
477+ } else if self . elevate . is_some ( ) && self . ssh . is_some ( ) {
478+ // Build elevation command
479+ let elevation_program = self
480+ . elevate
481+ . as_ref ( )
482+ . unwrap ( )
483+ . resolve ( )
484+ . context ( "Failed to resolve elevation program" ) ?;
485+
486+ let program_name = elevation_program
487+ . file_name ( )
488+ . and_then ( |name| name. to_str ( ) )
489+ . ok_or_else ( || {
490+ eyre:: eyre!( "Failed to determine elevation program name" )
491+ } ) ?;
492+
493+ let mut elev_cmd = Exec :: cmd ( & elevation_program) ;
494+
495+ // Add program-specific arguments
496+ if program_name == "sudo" {
497+ elev_cmd = elev_cmd. arg ( "--prompt=" ) . arg ( "--stdin" ) ;
498+ }
499+
500+ // Add env command to handle environment variables
501+ elev_cmd = elev_cmd. arg ( "env" ) ;
502+ for ( key, action) in & self . env_vars {
503+ match action {
504+ EnvAction :: Set ( value) => {
505+ elev_cmd = elev_cmd. arg ( format ! ( "{}={}" , key, value) ) ;
506+ } ,
507+ EnvAction :: Preserve => {
508+ if let Ok ( value) = std:: env:: var ( key) {
509+ elev_cmd = elev_cmd. arg ( format ! ( "{}={}" , key, value) ) ;
510+ }
511+ } ,
512+ _ => { } ,
513+ }
514+ }
515+
516+ elev_cmd. arg ( & self . command ) . args ( & self . args )
427517 } else {
518+ // No elevation
428519 self . apply_env_to_exec ( Exec :: cmd ( & self . command ) . args ( & self . args ) )
429520 } ;
430521
@@ -436,6 +527,7 @@ impl Command {
436527 cmd. stderr ( Redirection :: None ) . stdout ( Redirection :: None )
437528 } ,
438529 self . ssh . as_deref ( ) ,
530+ sudo_password. as_ref ( ) ,
439531 ) ;
440532
441533 if let Some ( m) = & self . message {
@@ -1078,7 +1170,7 @@ mod tests {
10781170 #[ test]
10791171 fn test_ssh_wrap_with_ssh ( ) {
10801172 let cmd = subprocess:: Exec :: cmd ( "echo" ) . arg ( "hello" ) ;
1081- let wrapped = ssh_wrap ( cmd, Some ( "user@host" ) ) ;
1173+ let wrapped = ssh_wrap ( cmd, Some ( "user@host" ) , None ) ;
10821174
10831175 let cmdline = wrapped. to_cmdline_lossy ( ) ;
10841176 assert ! ( cmdline. starts_with( "ssh" ) ) ;
@@ -1089,12 +1181,24 @@ mod tests {
10891181 #[ test]
10901182 fn test_ssh_wrap_without_ssh ( ) {
10911183 let cmd = subprocess:: Exec :: cmd ( "echo" ) . arg ( "hello" ) ;
1092- let wrapped = ssh_wrap ( cmd. clone ( ) , None ) ;
1184+ let wrapped = ssh_wrap ( cmd. clone ( ) , None , None ) ;
10931185
10941186 // Should return the original command unchanged
10951187 assert_eq ! ( wrapped. to_cmdline_lossy( ) , cmd. to_cmdline_lossy( ) ) ;
10961188 }
10971189
1190+ #[ test]
1191+ fn test_ssh_wrap_with_password ( ) {
1192+ let cmd = subprocess:: Exec :: cmd ( "echo" ) . arg ( "hello" ) ;
1193+ let password = SecretString :: new ( "testpass" . to_string ( ) ) ;
1194+ let wrapped = ssh_wrap ( cmd, Some ( "user@host" ) , Some ( & password) ) ;
1195+
1196+ let cmdline = wrapped. to_cmdline_lossy ( ) ;
1197+ assert ! ( cmdline. starts_with( "ssh" ) ) ;
1198+ assert ! ( cmdline. contains( "-T" ) ) ;
1199+ assert ! ( cmdline. contains( "user@host" ) ) ;
1200+ }
1201+
10981202 #[ test]
10991203 #[ serial]
11001204 fn test_apply_env_to_exec ( ) {
0 commit comments