Skip to content

Add sudoedit to the parser #1152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 4 additions & 29 deletions src/sudoers/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,17 +395,17 @@ impl Parse for MetaOrTag {
"INTERCEPT is not supported by sudo-rs"
),
// this is less fatal
"LOG_INPUT" | "NOLOG_INPUT" | "LOG_OUTPUT" | "NOLOG_OUTPUT" | "MAIL" | "NOMAIL" => {
"LOG_INPUT" | "NOLOG_INPUT" | "LOG_OUTPUT" | "NOLOG_OUTPUT" | "MAIL" | "NOMAIL"
| "FOLLOW" => {
eprintln_ignore_io_error!(
"warning: {} tags are ignored by sudo-rs",
keyword.as_str()
);
switch(|_| {})?
}

// 'FOLLOW' and 'NOFOLLOW' are only usable in a sudoedit context, which will result in
// a parse error elsewhere. 'NOINTERCEPT' is the default behaviour.
"FOLLOW" | "NOFOLLOW" | "NOINTERCEPT" => switch(|_| {})?,
// 'NOFOLLOW' and 'NOINTERCEPT' are the default behaviour.
"NOFOLLOW" | "NOINTERCEPT" => switch(|_| {})?,

"EXEC" => switch(|tag| tag.noexec = ExecControl::Exec)?,
"NOEXEC" => switch(|tag| tag.noexec = ExecControl::Noexec)?,
Expand Down Expand Up @@ -471,31 +471,6 @@ impl Parse for CommandSpec {
}
}

let start_pos = stream.get_pos();
if let Some(Username(keyword)) = try_nonterminal(stream)? {
if keyword == "sudoedit" {
// note: special behaviour of forward slashes in wildcards, tread carefully
unrecoverable!(pos = start_pos, stream, "sudoedit is not yet supported");
} else if keyword == "list" {
return make(CommandSpec(
tags,
Allow(Meta::Only((glob::Pattern::new("list").unwrap(), None))),
));
} else if keyword.starts_with("sha") {
unrecoverable!(
pos = start_pos,
stream,
"digest specifications are not supported"
)
} else {
unrecoverable!(
pos = start_pos,
stream,
"expected command but found {keyword}"
)
};
}

let cmd: Spec<Command> = expect_nonterminal(stream)?;

make(CommandSpec(tags, cmd))
Expand Down
21 changes: 21 additions & 0 deletions src/sudoers/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ fn permission_test() {
// apparmor
#[cfg(feature = "apparmor")]
pass!(["ALL ALL=(ALL:ALL) APPARMOR_PROFILE=unconfined ALL"], "user" => root(), "server"; "/bin/bar" => [apparmor_profile: Some("unconfined".to_string())]);

// list
pass!(["ALL ALL=(ALL:ALL) /bin/ls, list"], "user" => root(), "server"; "list");
FAIL!(["ALL ALL=(ALL:ALL) ALL, !list"], "user" => root(), "server"; "list");
}

#[test]
Expand Down Expand Up @@ -374,6 +378,23 @@ fn inclusive_username() {
assert_eq!(sirin, "şirin");
}

#[test]
fn sudoedit_recognized() {
let CommandSpec(_, Qualified::Allow(Meta::Only((cmd, args)))) =
parse_eval::<ast::CommandSpec>("sudoedit /etc/tmux.conf")
else {
panic!();
};
assert_eq!(cmd.as_str(), "sudoedit");
assert_eq!(args.unwrap().as_ref(), &["/etc/tmux.conf"][..]);
}

#[test]
#[should_panic = "list does not take arguments"]
fn list_does_not_take_args() {
parse_eval::<ast::CommandSpec>("list /etc/tmux.conf");
}

#[test]
fn directive_test() {
let y = parse_eval::<Spec<UserSpecifier>>;
Expand Down
19 changes: 16 additions & 3 deletions src/sudoers/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,14 @@ impl Token for Command {
Some(args.into_boxed_slice())
};

if command.as_str() == "list" && argpat.is_some() {
return Err("list does not take arguments".to_string());
}

Ok((command, argpat))
}

// all commands start with "/" except "sudoedit"
// all commands start with "/" except "sudoedit" or "list"
fn accept_1st(c: char) -> bool {
SimpleCommand::accept_1st(c)
}
Expand All @@ -218,6 +222,15 @@ impl Token for SimpleCommand {
pat.map_err(|err| format!("wildcard pattern error {err}"))
};

// detect the two edges cases
if cmd == "list" || cmd == "sudoedit" {
return cvt_err(glob::Pattern::new(&cmd));
} else if cmd.starts_with("sha") {
return Err("digest specifications are not supported".to_string());
} else if !cmd.starts_with('/') {
return Err("fully qualified path needed".to_string());
}

// record if the cmd ends in a slash and remove it if it does
let is_dir = cmd.ends_with('/') && {
cmd.pop();
Expand All @@ -240,9 +253,9 @@ impl Token for SimpleCommand {
cvt_err(glob::Pattern::new(&cmd))
}

// all commands start with "/" except "sudoedit"
// all commands start with "/" except "sudoedit" or "list"
fn accept_1st(c: char) -> bool {
c == '/' || c == 's'
c == '/' || c == 's' || c == 'l'
}

fn accept(c: char) -> bool {
Expand Down
Loading