@@ -4,17 +4,19 @@ mod cli;
44mod help;
55
66use std:: {
7+ env, ffi,
78 fs:: { File , Permissions } ,
89 io:: { self , Read , Seek , Write } ,
910 os:: unix:: prelude:: { MetadataExt , PermissionsExt } ,
1011 path:: { Path , PathBuf } ,
1112 process:: Command ,
13+ str,
1214} ;
1315
1416use crate :: {
1517 common:: resolve:: CurrentUser ,
1618 sudo:: { candidate_sudoers_file, diagnostic} ,
17- sudoers:: Sudoers ,
19+ sudoers:: { self , Sudoers } ,
1820 system:: {
1921 can_execute,
2022 file:: { create_temporary_dir, Chown , FileLock } ,
@@ -265,7 +267,7 @@ fn edit_sudoers_file(
265267 . spawn ( ) ?
266268 . wait_with_output ( ) ?;
267269
268- let ( _sudoers , errors) = File :: open ( tmp_path)
270+ let ( sudoers , errors) = File :: open ( tmp_path)
269271 . and_then ( |reader| Sudoers :: read ( reader, tmp_path) )
270272 . map_err ( |err| {
271273 io_msg ! (
@@ -276,46 +278,42 @@ fn edit_sudoers_file(
276278 )
277279 } ) ?;
278280
279- if errors. is_empty ( ) {
280- break ;
281- }
282-
283- writeln ! ( stderr, "The provided sudoers file format is not recognized or contains syntax errors. Please review:\n " ) ?;
284-
285- for crate :: sudoers:: Error {
286- message,
287- source,
288- location,
289- } in errors
290- {
291- let path = source. as_deref ( ) . unwrap_or ( sudoers_path) ;
292- diagnostic:: diagnostic!( "syntax error: {message}" , path @ location) ;
293- }
294-
295- writeln ! ( stderr) ?;
296-
297- let stdin = io:: stdin ( ) ;
298- let stdout = io:: stdout ( ) ;
281+ if !errors. is_empty ( ) {
282+ writeln ! ( stderr, "The provided sudoers file format is not recognized or contains syntax errors. Please review:\n " ) ?;
283+
284+ for crate :: sudoers:: Error {
285+ message,
286+ source,
287+ location,
288+ } in errors
289+ {
290+ let path = source. as_deref ( ) . unwrap_or ( sudoers_path) ;
291+ diagnostic:: diagnostic!( "syntax error: {message}" , path @ location) ;
292+ }
299293
300- let mut stdin_handle = stdin. lock ( ) ;
301- let mut stdout_handle = stdout. lock ( ) ;
294+ writeln ! ( stderr) ?;
302295
303- loop {
304- stdout_handle
305- . write_all ( "What now? e(x)it without saving / (e)dit again: " . as_bytes ( ) ) ?;
306- stdout_handle. flush ( ) ?;
307-
308- let mut input = [ 0u8 ] ;
309- if let Err ( err) = stdin_handle. read_exact ( & mut input) {
310- writeln ! ( stderr, "visudo: cannot read user input: {err}" ) ?;
311- return Ok ( ( ) ) ;
296+ match ask_response ( b"What now? e(x)it without saving / (e)dit again: " , b"xe" ) ? {
297+ b'x' => return Ok ( ( ) ) ,
298+ _ => continue ,
312299 }
313-
314- match & input {
315- b"e" => break ,
316- b"x" => return Ok ( ( ) ) ,
317- input => writeln ! ( stderr, "Invalid option: {:?}\n " , std:: str :: from_utf8( input) ) ?,
300+ } else {
301+ if sudo_visudo_is_allowed ( sudoers, & host_name) == Some ( false ) {
302+ writeln ! (
303+ stderr,
304+ "It looks like you have removed your ability to run 'sudo visudo' again.\n "
305+ ) ?;
306+ match ask_response (
307+ b"What now? e(x)it without saving / (e)dit again / lock me out and (S)ave: " ,
308+ b"xeS" ,
309+ ) ? {
310+ b'x' => return Ok ( ( ) ) ,
311+ b'S' => { }
312+ _ => continue ,
313+ }
318314 }
315+
316+ break ;
319317 }
320318 }
321319
@@ -354,3 +352,70 @@ fn editor_path_fallback() -> io::Result<PathBuf> {
354352 "cannot find text editor" ,
355353 ) )
356354}
355+
356+ // To detect potential lock-outs if the user called "sudo visudo".
357+ // Note that SUDO_USER will normally be set by sudo.
358+ //
359+ // This returns Some(false) if visudo is forbidden under the given config;
360+ // Some(true) if it is allowed; and None if it cannot be determined, which
361+ // will be the case if e.g. visudo was simply run as root.
362+ fn sudo_visudo_is_allowed ( mut sudoers : Sudoers , host_name : & Hostname ) -> Option < bool > {
363+ let sudo_user =
364+ User :: from_name ( & ffi:: CString :: new ( env:: var ( "SUDO_USER" ) . ok ( ) ?) . ok ( ) ?) . ok ( ) ??;
365+
366+ let super_user = User :: from_uid ( UserId :: ROOT ) . ok ( ) ??;
367+
368+ let request = sudoers:: Request {
369+ user : & super_user,
370+ group : & super_user. primary_group ( ) . ok ( ) ?,
371+ command : & env:: current_exe ( ) . ok ( ) ?,
372+ arguments : & [ ] ,
373+ } ;
374+
375+ Some ( matches ! (
376+ sudoers
377+ . check( & sudo_user, host_name, request)
378+ . authorization( ) ,
379+ sudoers:: Authorization :: Allowed { .. }
380+ ) )
381+ }
382+
383+ // Make sure that the first valid response is the "safest" choice
384+ fn ask_response ( prompt : & [ u8 ] , valid_responses : & [ u8 ] ) -> io:: Result < u8 > {
385+ let stdin = io:: stdin ( ) ;
386+ let stdout = io:: stdout ( ) ;
387+ let mut stderr = io:: stderr ( ) ;
388+
389+ let mut stdin_handle = stdin. lock ( ) ;
390+ let mut stdout_handle = stdout. lock ( ) ;
391+
392+ loop {
393+ stdout_handle. write_all ( prompt) ?;
394+ stdout_handle. flush ( ) ?;
395+
396+ let mut input = [ 0u8 ] ;
397+ if let Err ( err) = stdin_handle. read_exact ( & mut input) {
398+ writeln ! ( stderr, "visudo: cannot read user input: {err}" ) ?;
399+ return Ok ( valid_responses[ 0 ] ) ;
400+ }
401+
402+ // read the trailing newline
403+ loop {
404+ let mut skipped = [ 0u8 ] ;
405+ match stdin_handle. read_exact ( & mut skipped) {
406+ Ok ( ( ) ) if & skipped != b"\n " => continue ,
407+ _ => break ,
408+ }
409+ }
410+
411+ if valid_responses. contains ( & input[ 0 ] ) {
412+ return Ok ( input[ 0 ] ) ;
413+ } else {
414+ writeln ! (
415+ stderr,
416+ "Invalid option: '{}'\n " ,
417+ str :: from_utf8( & input) . unwrap_or( "<INVALID>" )
418+ ) ?;
419+ }
420+ }
421+ }
0 commit comments