Skip to content

Commit bf0d019

Browse files
authored
Add support for editor (#1198)
2 parents a00d4db + d6a3dde commit bf0d019

File tree

9 files changed

+126
-87
lines changed

9 files changed

+126
-87
lines changed

src/common/resolve.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ pub(crate) fn resolve_path(command: &Path, path: &str) -> Option<PathBuf> {
210210
// replaced, or passed unchanged to the program that sudo executes.
211211

212212
path.split(':')
213-
.map(PathBuf::from)
213+
.map(Path::new)
214214
// ignore all relative paths ("", "." or "./")
215215
.filter(|path| path.is_absolute())
216216
// construct a possible executable absolute path candidate

src/defaults/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ use settings_dsl::{
2121
result_of, storage_of,
2222
};
2323

24+
pub const SYSTEM_EDITOR: &str = if cfg!(target_os = "linux") {
25+
"/usr/bin/editor"
26+
} else {
27+
"/usr/bin/vi"
28+
};
29+
2430
defaults! {
2531
always_query_group_plugin = false #ignored
2632
always_set_home = false #ignored
@@ -34,7 +40,6 @@ defaults! {
3440
use_pty = true
3541
visiblepw = false #ignored
3642
pwfeedback = false
37-
env_editor = true
3843
rootpw = false
3944
targetpw = false
4045
noexec = false
@@ -55,6 +60,9 @@ defaults! {
5560
passwd_timeout = (5*60) (!= 0) {fractional_minutes}
5661
timestamp_timeout = (15*60) (!= 0) {fractional_minutes}
5762

63+
editor = SYSTEM_EDITOR
64+
env_editor = true
65+
5866
env_keep = ["COLORS", "DISPLAY", "HOSTNAME", "KRB5CCNAME", "LS_COLORS", "PATH",
5967
"PS1", "PS2", "XAUTHORITY", "XAUTHORIZATION", "XDG_CURRENT_DESKTOP"]
6068

src/defaults/settings_dsl.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ macro_rules! storage_of {
44
($id:ident, [ $($value: expr),* ]) => { std::collections::HashSet<String> };
55
($id:ident, $(=int $check: expr;)+ $_: expr) => { i64 };
66
($id:ident, $(=enum $k: ident;)+ $_: ident) => { $crate::defaults::enums::$id };
7-
($id:ident, $_: expr) => { Option<Box<str>> };
7+
($id:ident, None) => { Option<Box<str>> };
8+
($id:ident, $_: expr) => { Box<str> };
89
}
910

1011
macro_rules! referent_of {
@@ -13,7 +14,8 @@ macro_rules! referent_of {
1314
($id:ident, [ $($value: expr),* ]) => { &std::collections::HashSet<String> };
1415
($id:ident, $(=int $check: expr;)+ $_: expr) => { i64 };
1516
($id:ident, $(=enum $k: ident;)+ $_: ident) => { $crate::defaults::enums::$id };
16-
($id:ident, $_: expr) => { Option<&str> };
17+
($id:ident, None) => { Option<&str> };
18+
($id:ident, $_: expr) => { &str };
1719
}
1820

1921
macro_rules! initializer_of {
@@ -23,7 +25,7 @@ macro_rules! initializer_of {
2325
($id:ident, $(=int $check: expr;)+ $value: expr) => { $value };
2426
($id:ident, $(=enum $k: ident;)+ $value: ident) => { $crate::defaults::enums::$id::$value };
2527
($id:ident, None) => { None };
26-
($id:ident, $value: expr) => { Some($value.into()) };
28+
($id:ident, $value: expr) => { $value.into() };
2729
($id:ident, $($_: tt)*) => { return None };
2830
}
2931

@@ -40,9 +42,12 @@ macro_rules! result_of {
4042
($id:expr, $(=value $k: expr;)+ $_: expr) => {
4143
$id
4244
};
43-
($id:expr, $_: expr) => {
45+
($id:expr, None) => {
4446
$id.as_deref()
4547
};
48+
($id:expr, $_: expr) => {
49+
$id.as_ref()
50+
};
4651
}
4752

4853
macro_rules! modifier_of {
@@ -103,7 +108,7 @@ macro_rules! modifier_of {
103108
($id:ident, $value: expr) => {
104109
$crate::defaults::SettingKind::Text(|text| {
105110
let text = text.into();
106-
Some(Box::new(move |obj: &mut Settings| obj.$id = Some(text)))
111+
Some(Box::new(move |obj: &mut Settings| obj.$id = text))
107112
})
108113
};
109114
}

src/sudo/pipeline/edit.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,16 @@ pub fn run_edit(edit_opts: SudoEditOptions) -> Result<(), Error> {
3333
let command_exit_reason = {
3434
super::log_command_execution(&context);
3535

36+
let editor = policy.preferred_editor();
37+
3638
eprintln_ignore_io_error!(
37-
"this would launch sudoedit as requested, to edit the files: {:?}",
38-
context.files_to_edit.as_slice()
39+
"this would launch sudoedit as requested, to edit the files: {:?} using editor {}",
40+
context
41+
.files_to_edit
42+
.into_iter()
43+
.flatten()
44+
.collect::<Vec<_>>(),
45+
editor.display(),
3946
);
4047

4148
Ok::<_, std::io::Error>(ExitReason::Code(42))

src/sudoers/mod.rs

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ use std::collections::{HashMap, HashSet};
1313
use std::io;
1414
use std::path::{Path, PathBuf};
1515

16-
use crate::common::resolve::resolve_path;
16+
use crate::common::resolve::{is_valid_executable, resolve_path};
1717
use crate::defaults;
1818
use crate::log::auth_warn;
19+
use crate::system;
1920
use crate::system::interface::{GroupId, UnixGroup, UnixUser, UserId};
20-
use crate::system::{self, can_execute};
2121
use ast::*;
2222
use tokens::*;
2323

@@ -290,33 +290,62 @@ impl Sudoers {
290290
user_specs.flat_map(|cmd_specs| group_cmd_specs_per_runas(cmd_specs, &self.aliases.cmnd))
291291
}
292292

293-
pub(crate) fn solve_editor_path<User: UnixUser + PartialEq<User>>(
293+
pub(crate) fn visudo_editor_path<User: UnixUser + PartialEq<User>>(
294294
mut self,
295295
on_host: &system::Hostname,
296296
am_user: &User,
297297
target_user: &User,
298-
) -> Option<PathBuf> {
298+
) -> PathBuf {
299299
self.specify_host_user_runas(on_host, am_user, Some(target_user));
300-
if self.settings.env_editor() {
301-
for key in ["SUDO_EDITOR", "VISUAL", "EDITOR"] {
302-
if let Some(var) = std::env::var_os(key) {
303-
let path = Path::new(&var);
304-
if can_execute(path) {
305-
return Some(path.to_owned());
306-
}
307-
let path = resolve_path(
308-
path,
309-
&std::env::var("PATH").unwrap_or(env!("SUDO_PATH_DEFAULT").to_string()),
310-
);
311-
if let Some(path) = path {
312-
return Some(path);
313-
}
314-
}
300+
301+
select_editor(&self.settings, self.settings.env_editor())
302+
}
303+
}
304+
305+
/// Retrieve the chosen editor from a settings object, filtering based on whether the
306+
/// environment is trusted (sudoedit) or maybe less so (visudo)
307+
fn select_editor(settings: &Settings, trusted_env: bool) -> PathBuf {
308+
let blessed_editors = settings.editor();
309+
310+
let is_whitelisted = |path: &Path| -> bool {
311+
trusted_env || blessed_editors.split(':').any(|x| Path::new(x) == path)
312+
};
313+
314+
// find editor in environment, if possible
315+
316+
for key in ["SUDO_EDITOR", "VISUAL", "EDITOR"] {
317+
if let Some(editor) = std::env::var_os(key) {
318+
let editor = PathBuf::from(editor);
319+
320+
let editor = if is_valid_executable(&editor) {
321+
editor
322+
} else if let Some(editor) = resolve_path(
323+
&editor,
324+
&std::env::var("PATH").unwrap_or(env!("SUDO_PATH_DEFAULT").to_string()),
325+
) {
326+
editor
327+
} else {
328+
continue;
329+
};
330+
331+
if is_whitelisted(&editor) {
332+
return editor;
315333
}
316334
}
335+
}
317336

318-
None
337+
// no acceptable editor found in environment, fallback on config
338+
339+
for editor in blessed_editors.split(':') {
340+
let editor = PathBuf::from(editor);
341+
if is_valid_executable(&editor) {
342+
return editor;
343+
}
319344
}
345+
346+
// fallback on hardcoded path -- always provide something to the caller
347+
348+
PathBuf::from(defaults::SYSTEM_EDITOR)
320349
}
321350

322351
// a `take_while` variant that does not consume the first non-matching item

src/sudoers/policy.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ impl Judgement {
142142
Authorization::Forbidden
143143
}
144144
}
145+
146+
#[cfg_attr(not(feature = "sudoedit"), allow(unused))]
147+
pub(crate) fn preferred_editor(&self) -> std::path::PathBuf {
148+
super::select_editor(&self.settings, true)
149+
}
145150
}
146151

147152
impl Sudoers {

src/system/mod.rs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
#[cfg(target_os = "linux")]
33
use std::str::FromStr;
44
use std::{
5-
ffi::{c_int, c_uint, CStr, CString},
5+
ffi::{c_int, c_uint, CStr},
66
fmt, fs, io,
77
mem::MaybeUninit,
88
ops,
9-
os::unix::{self, prelude::OsStrExt},
10-
path::{Path, PathBuf},
9+
os::unix,
10+
path::PathBuf,
1111
};
1212

1313
use crate::{
@@ -41,15 +41,6 @@ pub mod wait;
4141
#[cfg(not(any(target_os = "freebsd", target_os = "linux")))]
4242
compile_error!("sudo-rs only works on Linux and FreeBSD");
4343

44-
pub(crate) fn can_execute<P: AsRef<Path>>(path: P) -> bool {
45-
let Ok(path) = CString::new(path.as_ref().as_os_str().as_bytes()) else {
46-
return false;
47-
};
48-
49-
// SAFETY: we are passing a proper pointer to access
50-
unsafe { libc::access(path.as_ptr(), libc::X_OK) == 0 }
51-
}
52-
5344
pub(crate) fn _exit(status: libc::c_int) -> ! {
5445
// SAFETY: this function is safe to call
5546
unsafe { libc::_exit(status) }

src/visudo/mod.rs

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ use crate::{
1818
sudo::{candidate_sudoers_file, diagnostic},
1919
sudoers::{self, Sudoers},
2020
system::{
21-
can_execute,
2221
file::{create_temporary_dir, Chown, FileLock},
2322
interface::{GroupId, UserId},
2423
signal::{consts::*, register_handlers, SignalStream},
@@ -248,7 +247,6 @@ fn edit_sudoers_file(
248247
) -> io::Result<()> {
249248
let mut stderr = io::stderr();
250249

251-
let mut editor_path = None;
252250
let mut sudoers_contents = Vec::new();
253251

254252
// Since visudo is meant to run as root, resolve shouldn't fail
@@ -262,31 +260,36 @@ fn edit_sudoers_file(
262260

263261
let host_name = Hostname::resolve();
264262

265-
if existed {
263+
let editor_path = if existed {
266264
// If the sudoers file existed, read its contents and write them into the temporary file.
267265
sudoers_file.read_to_end(&mut sudoers_contents)?;
268266
// Rewind the sudoers file so it can be written later.
269267
sudoers_file.rewind()?;
270268
// Write to the temporary file.
271269
tmp_file.write_all(&sudoers_contents)?;
272270

273-
let (sudoers, errors) = Sudoers::read(sudoers_contents.as_slice(), sudoers_path)?;
271+
let (sudoers, _errors) = Sudoers::read(sudoers_contents.as_slice(), sudoers_path)?;
274272

275-
if errors.is_empty() {
276-
editor_path = sudoers.solve_editor_path(&host_name, &current_user, &current_user);
277-
}
278-
}
279-
280-
let editor_path = match editor_path {
281-
Some(path) => path,
282-
None => editor_path_fallback()?,
273+
sudoers.visudo_editor_path(&host_name, &current_user, &current_user)
274+
} else {
275+
// there is no /etc/sudoers config yet, so use a system default
276+
PathBuf::from(crate::defaults::SYSTEM_EDITOR)
283277
};
284278

285279
loop {
286280
Command::new(&editor_path)
287281
.arg("--")
288282
.arg(tmp_path)
289-
.spawn()?
283+
.spawn()
284+
.map_err(|_| {
285+
io::Error::new(
286+
io::ErrorKind::NotFound,
287+
format!(
288+
"specified editor ({}) could not be used",
289+
editor_path.display()
290+
),
291+
)
292+
})?
290293
.wait_with_output()?;
291294

292295
let (sudoers, errors) = File::open(tmp_path)
@@ -354,27 +357,6 @@ fn edit_sudoers_file(
354357
Ok(())
355358
}
356359

357-
fn editor_path_fallback() -> io::Result<PathBuf> {
358-
let editors = if cfg!(target_os = "linux") {
359-
&["/usr/bin/editor"][..]
360-
} else if cfg!(target_os = "freebsd") {
361-
&["/usr/bin/vi"]
362-
} else {
363-
unimplemented!("unknown default editor for target")
364-
};
365-
for &editor in editors {
366-
let path = Path::new(editor);
367-
if can_execute(path) {
368-
return Ok(path.to_owned());
369-
}
370-
}
371-
372-
Err(io::Error::new(
373-
io::ErrorKind::NotFound,
374-
"cannot find text editor",
375-
))
376-
}
377-
378360
// To detect potential lock-outs if the user called "sudo visudo".
379361
// Note that SUDO_USER will normally be set by sudo.
380362
//

0 commit comments

Comments
 (0)