309 lines
13 KiB
Rust
309 lines
13 KiB
Rust
use std::{env::args, path::PathBuf};
|
|
use strum::IntoEnumIterator;
|
|
use strum_macros::EnumIter;
|
|
|
|
#[derive(Clone, Copy, Debug, EnumIter)]
|
|
pub enum AppOption {
|
|
Background,
|
|
Chdir,
|
|
CloseFrom,
|
|
GenerateConfig,
|
|
Group,
|
|
Help,
|
|
// Host,
|
|
// List,
|
|
// Login,
|
|
NoUpdate,
|
|
// OtherUser,
|
|
PreserveEnv,
|
|
// PreserveGroups,
|
|
RemoveTimestamp,
|
|
ResetTimestamp,
|
|
// SetHome,
|
|
// Shell,
|
|
Stdin,
|
|
User,
|
|
Validate,
|
|
Version,
|
|
Z,
|
|
}
|
|
|
|
impl AppOption {
|
|
pub fn from_str(s: &str, _has_arg: bool) -> Option<Self> {
|
|
match s {
|
|
"--background" | "-b" => Self::Background,
|
|
"--close-from" | "-C" => Self::CloseFrom,
|
|
"--chdir" | "-D" => Self::Chdir,
|
|
"--group" | "-g" => Self::Group,
|
|
"--gen-conf" => Self::GenerateConfig,
|
|
"--help" | "-h" => {
|
|
// if has_arg {
|
|
Self::Help
|
|
// } else {
|
|
// Self::Host
|
|
// }
|
|
}
|
|
// "host" => Self::Host,
|
|
// "list" | "l" => Self::List,
|
|
// "--login" | "-i" => Self::Login,
|
|
"--no-update" | "-N" => Self::NoUpdate,
|
|
// "other-user" | "U" => Self::OtherUser,
|
|
"--preserve-env" | "-E" => Self::PreserveEnv,
|
|
// "--preserve-groups" | "-P" => Self::PreserveGroups,
|
|
"--remove-timestamp" | "-K" => Self::RemoveTimestamp,
|
|
"--reset_timestamp" | "-k" => Self::ResetTimestamp,
|
|
// "--set-home" | "-H" => Self::SetHome,
|
|
// "--shell" | "-s" => Self::Shell,
|
|
"--stdin" | "-S" => Self::Stdin,
|
|
"--user" | "-u" => Self::User,
|
|
"--validate" | "-v" => Self::Validate,
|
|
"--version" | "-V" => Self::Version,
|
|
"--" => Self::Z,
|
|
_ => {
|
|
return None;
|
|
}
|
|
}
|
|
.into()
|
|
}
|
|
pub fn from_flags(s: &str) -> Result<Vec<Self>, String> {
|
|
let mut output = vec![];
|
|
for ch in s.chars().skip(1) {
|
|
if let Some(opt) = Self::from_str(&format!["-{ch}"], false) {
|
|
if opt.is_flag() {
|
|
output.push(opt)
|
|
} else {
|
|
return Err(format!["{ch}"]);
|
|
}
|
|
} else {
|
|
return Err(format!["{ch}"]);
|
|
}
|
|
}
|
|
Ok(output)
|
|
}
|
|
pub fn usage(self) -> (&'static str, &'static str) {
|
|
match self {
|
|
AppOption::Background => (
|
|
"--background | -b",
|
|
"Run the given command in the background. Caution: backgrounded processes are not subject to shell job control. Interactive commands may misbehave.",
|
|
),
|
|
AppOption::Chdir => (
|
|
"--chdir dir | -D dir",
|
|
"Change to the specified directory before running the command."
|
|
),
|
|
AppOption::CloseFrom => (
|
|
"--close-from n | -C n",
|
|
"Close all file descriptors greater than or equal to n.",
|
|
),
|
|
AppOption::GenerateConfig => ("--gen-conf", "Print example config to standard output."),
|
|
AppOption::Group => (
|
|
"--group | -g",
|
|
"Run the command with the primary group set to group.",
|
|
),
|
|
AppOption::Help => ("--help | -h", "Display program help."),
|
|
// AppOption::Host => ("", ""),
|
|
// AppOption::List => ("--list (command) | -l (command)", "If command is specified, display the fully-qualified path as dictated by the security policy including command-line arguments. Otherwise, display allowed commands for the invoking user."),
|
|
// AppOption::Login => (
|
|
// "--login | -i",
|
|
// "Runs the target user's login shell and passes command to the shell, if any.",
|
|
// ),
|
|
AppOption::NoUpdate => ("--no-update | -N", "Do not update user's cache credentials."),
|
|
// AppOption::OtherUser => ("--other-user user | -U user", ""),
|
|
AppOption::PreserveEnv => (
|
|
"--preserve-env var, ... | -E var, ...",
|
|
"Preserve environment variables.",
|
|
),
|
|
// AppOption::PreserveGroups => ("--preserve-groups | -P", "Preserve invoking user's groups."),
|
|
AppOption::RemoveTimestamp => ("--remove-timestamp | -K", "Remove all cached credentials for invoking user."),
|
|
AppOption::ResetTimestamp => ("--reset-timestamp | -k", "Without command, clears session credentials. With a command, runs the command without updating cached credentials."),
|
|
// AppOption::SetHome => ("--set-home | -H", "Set HOME environment variable to target user's home."),
|
|
// AppOption::Shell => ("--shell | -s", "Run the shell specified by the SHELL environment variable or as specific to invoking user. Passes command to shell."),
|
|
AppOption::Stdin => ("--stdin | -S", "Write the password prompt to standard error and read the password from standard input."),
|
|
AppOption::User => ("--user user| -u user", "Run command as user instead of default."),
|
|
AppOption::Validate => ("--validate | -v", "Re-authenticate the user and update cached credentials."),
|
|
AppOption::Version => ("--version | -V", "Print the version."),
|
|
AppOption::Z => ("--", "Indicates end of options. Subsequent options are passed to the command."),
|
|
}
|
|
}
|
|
pub fn is_flag(self) -> bool {
|
|
match self {
|
|
Self::Background
|
|
| Self::Help
|
|
// | AppOption::Login
|
|
// | AppOption::PreserveGroups
|
|
| Self::GenerateConfig
|
|
| Self::NoUpdate
|
|
| Self::RemoveTimestamp
|
|
| Self::ResetTimestamp
|
|
// | AppOption::SetHome
|
|
// | AppOption::Shell
|
|
| Self::Stdin
|
|
| Self::Validate
|
|
| Self::Version => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
pub fn format_all() -> String {
|
|
let mut output = String::from("Usage: rudo (options) (--) (command)\n");
|
|
let max_len = Self::iter().fold(0, |n, v| n.max(v.usage().0.len()));
|
|
for (dashing, details) in Self::iter().map(|v| v.usage()) {
|
|
let mut spacing = String::from(" ");
|
|
for _ in dashing.len()..max_len {
|
|
spacing.push(' ');
|
|
}
|
|
let s = format!["\n {dashing}{spacing}{details}"];
|
|
let chunks = s
|
|
.chars()
|
|
.collect::<Vec<char>>()
|
|
.chunks(80)
|
|
.map(|chunk| chunk.into_iter().collect::<String>())
|
|
.collect::<Vec<String>>();
|
|
let f = chunks.join("\n");
|
|
output.push_str(&f);
|
|
}
|
|
output
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct App {
|
|
pub background: bool,
|
|
pub chdir: Option<PathBuf>,
|
|
pub chroot: Option<PathBuf>,
|
|
pub close_from: Option<i32>,
|
|
pub cmd: Vec<String>,
|
|
pub gen_config: bool,
|
|
pub group: Option<String>,
|
|
pub help: bool,
|
|
pub host: Option<String>,
|
|
pub list: (bool, Option<String>),
|
|
pub login: bool,
|
|
pub no_update: bool,
|
|
pub preserve_env: Vec<String>,
|
|
pub preserve_groups: bool,
|
|
pub remove_timestamp: bool,
|
|
pub reset_timestamp: bool,
|
|
pub set_home: bool,
|
|
pub shell: bool,
|
|
pub stdin: bool,
|
|
pub user: Option<String>,
|
|
pub validate: bool,
|
|
pub version: bool,
|
|
}
|
|
|
|
impl App {
|
|
pub fn parse() -> Result<Self, String> {
|
|
let args: Vec<String> = args().collect();
|
|
let mut app = App::default();
|
|
let mut args_iter = args.iter().skip(1).peekable();
|
|
'sm: loop {
|
|
if let Some(arg) = args_iter.next() {
|
|
let app_opts = if arg.starts_with("--") {
|
|
if let Some(o) = AppOption::from_str(arg, args_iter.peek().is_some()) {
|
|
vec![o]
|
|
} else {
|
|
return Err(format!["Error: Unknown option {arg}"]);
|
|
}
|
|
} else if arg.starts_with("-") {
|
|
match AppOption::from_flags(arg) {
|
|
Ok(v) => v,
|
|
Err(s) => return Err(format!["Unknown flag {s}"]),
|
|
}
|
|
} else {
|
|
app.cmd.push(arg.to_owned());
|
|
break;
|
|
};
|
|
for app_opt in app_opts {
|
|
match app_opt {
|
|
AppOption::Background => app.background = true,
|
|
AppOption::Chdir => {
|
|
let next = args_iter.next();
|
|
if let Some(s) = next {
|
|
app.chdir = Some(s.into());
|
|
} else {
|
|
return Err(format![
|
|
"Error: Missing argument for {arg}\nUsage: {}\n{}",
|
|
AppOption::Chdir.usage().0,
|
|
AppOption::Chdir.usage().1,
|
|
]);
|
|
}
|
|
}
|
|
AppOption::CloseFrom => {
|
|
let next = args_iter.next();
|
|
if let Some(s) = next {
|
|
if let Ok(n) = s.parse() {
|
|
app.close_from = Some(n);
|
|
} else {
|
|
return Err(format![
|
|
"Error: Invalid argument for {arg}: Expected number, got {s}\nUsage: {}\n{}",
|
|
AppOption::CloseFrom.usage().0,
|
|
AppOption::CloseFrom.usage().1,
|
|
]);
|
|
}
|
|
} else {
|
|
return Err(format![
|
|
"Error: Missing argument for {arg}\nUsage: {}\n{}",
|
|
AppOption::CloseFrom.usage().0,
|
|
AppOption::CloseFrom.usage().1,
|
|
]);
|
|
}
|
|
}
|
|
AppOption::GenerateConfig => app.gen_config = true,
|
|
AppOption::Group => {
|
|
let next = args_iter.next();
|
|
if let Some(s) = next {
|
|
app.group = Some(s.to_owned());
|
|
} else {
|
|
return Err(format![
|
|
"Error: Missing argument for {arg}\nUsage: {}\n{}",
|
|
AppOption::Group.usage().0,
|
|
AppOption::Group.usage().1,
|
|
]);
|
|
}
|
|
}
|
|
AppOption::Help => app.help = true,
|
|
// AppOption::Login => app.login = true,
|
|
AppOption::NoUpdate => app.no_update = true,
|
|
AppOption::PreserveEnv => loop {
|
|
if let Some(arg) = args_iter.next() {
|
|
app.preserve_env.push(arg.to_owned());
|
|
if !arg.ends_with(",") {
|
|
break;
|
|
}
|
|
} else {
|
|
break 'sm;
|
|
}
|
|
},
|
|
// AppOption::PreserveGroups => app.preserve_groups = true,
|
|
AppOption::RemoveTimestamp => app.remove_timestamp = true,
|
|
AppOption::ResetTimestamp => app.reset_timestamp = true,
|
|
// AppOption::SetHome => app.set_home = true,
|
|
// AppOption::Shell => app.shell = true,
|
|
AppOption::Stdin => app.stdin = true,
|
|
AppOption::User => {
|
|
if let Some(arg) = args_iter.next() {
|
|
app.user = Some(arg.to_owned());
|
|
} else {
|
|
return Err(format![
|
|
"Error: Missing argument for {arg}\nUsage: {}\n{}",
|
|
AppOption::User.usage().0,
|
|
AppOption::User.usage().1,
|
|
]);
|
|
}
|
|
}
|
|
AppOption::Validate => app.validate = true,
|
|
AppOption::Version => app.version = true,
|
|
AppOption::Z => break 'sm,
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
app.cmd.extend(args_iter.map(|s| s.to_owned()));
|
|
Ok(app)
|
|
}
|
|
pub fn usage() -> String {
|
|
AppOption::format_all()
|
|
}
|
|
}
|