diff --git a/Cargo.lock b/Cargo.lock index 9e0f17e..d3b4761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,54 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "anstream" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" - -[[package]] -name = "anstyle-parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" -dependencies = [ - "anstyle", - "windows-sys", -] - [[package]] name = "bincode" version = "1.3.3" @@ -61,55 +13,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - -[[package]] -name = "clap" -version = "4.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "either" @@ -125,9 +31,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys", @@ -156,9 +62,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ "windows-sys", ] @@ -175,15 +81,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.149" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" @@ -199,9 +105,9 @@ checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "pam" @@ -225,9 +131,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -264,15 +170,16 @@ dependencies = [ [[package]] name = "rudo" -version = "0.1.0" +version = "1.0.0-alpha" dependencies = [ "bincode", - "clap", "fork", "libc", "pam", "rpassword", "serde", + "strum", + "strum_macros", "toml", "unix_mode", "users 0.11.0", @@ -281,9 +188,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.19" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags", "errno", @@ -292,6 +199,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "serde" version = "1.0.189" @@ -322,16 +235,29 @@ dependencies = [ ] [[package]] -name = "strsim" -version = "0.10.0" +name = "strum" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" + +[[package]] +name = "strum_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] [[package]] name = "syn" -version = "2.0.38" +version = "2.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" dependencies = [ "proc-macro2", "quote", @@ -403,22 +329,17 @@ dependencies = [ "log", ] -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - [[package]] name = "which" -version = "4.4.2" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" dependencies = [ "either", "home", "once_cell", "rustix", + "windows-sys", ] [[package]] @@ -445,18 +366,18 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.5" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -469,45 +390,45 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" diff --git a/Cargo.toml b/Cargo.toml index fd37b7e..bc37f3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,20 @@ [package] name = "rudo" -version = "0.1.0" +version = "1.0.0-alpha" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] bincode = "1.3.3" -clap = { version = "4.4.6", features = ["derive"] } fork = "0.1.22" libc = "0.2.149" pam = "0.7.0" rpassword = "7.2.0" serde = { version = "1.0.189", features = ["derive"] } +strum = "0.26.1" +strum_macros = "0.26.1" toml = "0.8.2" unix_mode = "0.1.4" users = "0.11.0" -which = "4.4.2" +which = "6.0.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..76f12f5 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ + +## Installation +``` +cargo build --release +mv target/release/rudo /usr/local/bin/rudo +chown root:root /usr/local/bin/rudo +chmod +xs /usr/local/bin/rudo +``` diff --git a/src/app.rs b/src/app.rs index 093d9c6..dd9dae7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,53 +1,308 @@ -use clap::Parser; -use std::path::PathBuf; +use std::{env::args, path::PathBuf}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None, disable_help_flag = true, disable_version_flag = true)] -pub struct App { - /// Run the given command in background mode. Caution: backgrounded processes are not subject to shell job control. Interactive commands may misbehave. - #[arg(short, long)] - pub background: bool, - /// run as the specified user - #[arg(short, long)] - pub user: Option, - #[arg(short, long)] - pub validate: bool, - pub cmd: Option>, - // /// Run the command from the specified directory. The security policy may return an error if the user does not have permission to specify the working directory. - // #[arg(short = 'D', long)] - // pub chdir: Option, - // /// Indicates to the security policy that the user wishes to preserve existing environment variables. Subject to security policy. - // #[arg(short, long)] - // pub preserve_env: bool, - // #[arg(long)] - // help: bool, - // /// Specify group via name or `#` - // #[arg(short, long)] - // pub group: Option, - // #[arg(short = 'H', long)] - // pub set_home: bool, - // #[arg(short = 'h', long)] - // pub host: Option, - // #[arg(short = 'i', long)] - // pub login: bool, - // /// Remove all cached credentials for user. - // #[arg(short = 'K', long)] - // pub remove_timestamp: bool, - // /// Remove current shell's cached credentials. - // #[arg(short, long)] - // pub reset_timestamp: bool, - // /// Don't update cached credentials. - // #[arg(short = 'N', long)] - // pub no_update: bool, - // /// List privileges for user. - // #[arg(short, long)] - // pub list: Option, - // #[arg(short = 'R', long)] - // pub chroot: Option, - // /// Write prompt to stderr and read from stdin instead of terminal input. - // #[arg(short = 'S', long)] - // pub stdin: bool, - // /// Run shell specified in `SHELL`. - // #[arg(short, long)] - // pub shell: bool, +#[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 { + 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, 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-config", "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::>() + .chunks(80) + .map(|chunk| chunk.into_iter().collect::()) + .collect::>(); + let f = chunks.join("\n"); + output.push_str(&f); + } + output + } +} + +#[derive(Debug, Default)] +pub struct App { + pub background: bool, + pub chdir: Option, + pub chroot: Option, + pub close_from: Option, + pub cmd: Vec, + pub gen_config: bool, + pub group: Option, + pub help: bool, + pub host: Option, + pub list: (bool, Option), + pub login: bool, + pub no_update: bool, + pub preserve_env: Vec, + 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, + pub validate: bool, + pub version: bool, +} + +impl App { + pub fn parse() -> Result { + let args: Vec = 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() + } } diff --git a/src/authentication.rs b/src/authentication.rs index 8a5b9a0..f851b5f 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -1,20 +1,33 @@ -use crate::{error::RudoError, user_info::get_username, AUTH_SERVICE, RETRIES}; +use std::io::stdin; -pub fn check_auth() -> Result<(), RudoError> { +use crate::user_info::get_username; + +const AUTH_SERVICE: &'static str = "system-auth"; +const RETRIES: u32 = 3; + +pub fn check_auth(from_stdin: bool) -> Result<(), String> { let login = get_username().unwrap(); - let mut retries = RETRIES; - for i in 0..retries { - let password = rpassword::prompt_password(format!["password for {login}: "]).unwrap(); + for i in 0..RETRIES { + let password = if from_stdin { + eprintln!["password for {login}: "]; + let mut buf = String::new(); + if let Err(e) = stdin().read_line(&mut buf) { + return Err(format!["Error: Rudo: Failed to read stdin: {e}"]); + } + buf + } else { + rpassword::prompt_password(format!["password for {login}: "]).unwrap() + }; let mut auth = pam::Authenticator::with_password(AUTH_SERVICE).unwrap(); auth.get_handler().set_credentials(&login, password); match auth.authenticate() { Ok(()) => return Ok(()), Err(_) => { - if i < retries - 1 { + if i < RETRIES - 1 { eprintln!["incorrect password, try again"] } } } } - Err(RudoError::AuthenticationError) + Err(String::from("Error: Authentication error.")) } diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..76e41c1 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,93 @@ +use std::{ + fs::{DirBuilder, File, OpenOptions}, + io::{Read, Write}, + path::PathBuf, + time::{Duration, SystemTime}, +}; + +use fork::{daemon, Fork}; +use libc::kill; + +const DEFAULT_AUTH_CACHE_DURATION: u64 = 5 * 60; +const AUTH_CACHE_PATH: &str = "/tmp/rudo/cache/auth/"; +const SESSION_CACHE_PATH: &str = "/tmp/rudo/cache/session/"; + +pub fn clear_auth_cache(alias: &str, ppid: u32) -> Result<(), std::io::Error> { + let ppid_str = format!["{ppid}"]; + let mut path = PathBuf::from(SESSION_CACHE_PATH); + path.push(&ppid_str); + if let Err(e) = std::fs::remove_file(&path) { + println!["Failed to remove session file: {e}"]; + std::process::exit(1); + } + let mut path = PathBuf::from(AUTH_CACHE_PATH); + path.push(ppid_str); + path.push(alias); + if let Err(e) = std::fs::remove_dir_all(&path) { + println!["Failed to remove session dir: {e}"]; + std::process::exit(1); + } + Ok(()) +} + +pub fn update_auth_cache(alias: &str, ppid: u32) -> Result<(), std::io::Error> { + let ppid_str = format!["{ppid}"]; + let mut session_path = PathBuf::from(SESSION_CACHE_PATH); + DirBuilder::new().recursive(true).create(&session_path)?; + session_path.push(&ppid_str); + let mut timestamp_path = PathBuf::from(AUTH_CACHE_PATH); + timestamp_path.push(ppid_str); + DirBuilder::new().recursive(true).create(×tamp_path)?; + timestamp_path.push(alias); + let mut opts = OpenOptions::new(); + opts.write(true).create(true); + let mut timestamp_file = opts.open(×tamp_path)?; + let time = SystemTime::now() + Duration::from_secs(DEFAULT_AUTH_CACHE_DURATION); + timestamp_file.write_all(&bincode::serialize(&time).unwrap_or_default())?; + match OpenOptions::new() + .create_new(true) + .write(true) + .open(&session_path) + { + Ok(_) => { + if let Ok(Fork::Child) = daemon(false, false) { + std::thread::sleep(Duration::from_secs(DEFAULT_AUTH_CACHE_DURATION)); + if unsafe { kill(ppid as _, 0) } == -1 { + clear_auth_cache(alias, ppid)?; + } + } + } + Err(_) => (), + }; + Ok(()) +} + +pub fn check_auth_cache(alias: &str, ppid: u32) -> bool { + let mut path = PathBuf::from(AUTH_CACHE_PATH); + path.push(format!["{ppid}"]); + path.push(alias); + let mut file = if let Ok(file) = File::open(&path) { + file + } else { + return false; + }; + let mut buf = Vec::new(); + if let Err(_) = file.read_to_end(&mut buf) { + return false; + }; + match bincode::deserialize::(&buf) { + Ok(old_time) => { + if old_time.elapsed().unwrap_or_default() + < Duration::from_secs(DEFAULT_AUTH_CACHE_DURATION as u64) + { + true + } else { + false + } + } + Err(e) => { + dbg!["{}", e]; + false + } + } +} diff --git a/src/main.rs b/src/main.rs index 556f93d..e45c720 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,162 +1,165 @@ -use std::{ - fs::{DirBuilder, File, OpenOptions}, - io::{Error, Read, Write}, - os::unix::process::CommandExt, - path::PathBuf, - process::Command, - thread, - time::{Duration, Instant}, -}; - +use std::{env, os::unix::process::CommandExt, process::Command}; mod app; use app::App; -use clap::Parser; -use error::RudoError; -mod error; +use cache::*; +mod cache; mod rudoers; mod user_info; -use fork::{daemon, Fork}; -use libc::kill; +use libc::{close, getpwuid}; use rudoers::check_rudoers; +use users::{get_group_by_name, get_user_by_name}; mod authentication; use authentication::check_auth; +use std::ffi::CStr; use user_info::get_command_path; +use crate::rudoers::make_example_rudoers_file; + /// RUDOLPH THE RED NOSE REINDEER /// HAD A VERY SHINY NOSE /// AND IF YOU EVER SAW IT /// YOU MIGHT EVEN SAY IT GLOWS -const DEFAULT_RUDOERS: &'static str = "/etc/rudoers.toml"; -const AUTH_SERVICE: &'static str = "system-auth"; -const RETRIES: u32 = 3; -const DEFAULT_AUTH_CACHE_DURATION: u32 = 15 * 60; -const AUTH_CACHE_PATH: &str = "/var/db/rudo/cache/auth/"; -const SESSION_CACHE_PATH: &str = "/var/db/rudo/cache/session/"; +const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub fn update_auth_cache(alias: &str, ppid: &str) -> Result<(), std::io::Error> { - let mut path = PathBuf::from(AUTH_CACHE_PATH); - path.push(ppid); - DirBuilder::new().recursive(true).create(&path)?; - path.push(alias); - let mut opts = OpenOptions::new(); - opts.write(true).create(true); - let mut file = opts.open(&path)?; - let time = Instant::now(); - let time = unsafe { std::mem::transmute::(time) }; - file.write(&bincode::serialize(&time).unwrap())?; - Ok(()) -} - -pub fn check_auth_cache(alias: &str, ppid: &str) -> bool { - let mut path = PathBuf::from(AUTH_CACHE_PATH); - path.push(ppid); - path.push(alias); - let mut file = if let Ok(file) = File::open(&path) { - file - } else { - return false; - }; - let mut buf = Vec::new(); - if let Err(_) = file.read_to_end(&mut buf) { - return false; - }; - match bincode::deserialize::(&buf) { - Ok(n) => { - let old_time = unsafe { std::mem::transmute::(n) }; - if old_time.elapsed() < Duration::from_secs(DEFAULT_AUTH_CACHE_DURATION as u64) { - true - } else { - false +pub fn execute(app: App) -> Result<(), String> { + if !app.cmd.is_empty() && !app.shell { + if let Some(close_from) = app.close_from { + for i in close_from.. { + let res = unsafe { close(i) }; + if res == -1 { + break; + } } } - Err(e) => { - dbg!["{}", e]; - false - } - } -} - -pub fn execute(app: &App) -> Result<(), RudoError> { - if let Some(ref cmds) = app.cmd { - let run_as_name = match &app.user { - Some(x) => &x, - None => "root", + let run_as_name = if let Some(nym) = app.user { + nym + } else { + String::from("root") }; - let run_as = users::get_user_by_name(run_as_name).unwrap(); - - let mut child = Command::new(get_command_path(cmds.first().unwrap())?) - .uid(run_as.uid()) - .args(&cmds[1..]) - .spawn() - .unwrap(); + let run_as = get_user_by_name(&run_as_name).unwrap(); + let command_path = get_command_path(app.cmd.first().unwrap())?; + let args = app.cmd; + let args = &args[1..]; + let mut cmd = Command::new(command_path); + cmd.uid(run_as.uid()).args(args); + if let Some(chdir) = app.chdir { + cmd.current_dir(chdir); + } + if let Some(group) = app.group { + let group_id: u32 = if group.starts_with("#") { + if let Ok(id) = group[1..].parse() { + id + } else { + return Err(format!["Error: Failed to parse group ID '{}'", &group[1..]]); + } + } else { + if let Some(group) = get_group_by_name(&group) { + group.gid() + } else { + return Err(format!["Error: Group '{}' not found", group]); + } + }; + cmd.gid(group_id); + } + for var in app.preserve_env { + let value = env::var(&var).unwrap_or_default(); + cmd.env(var, value); + } + // if app.preserve_groups { + // if let Some(user) = get_user_by_name(&get_username()?) {} + // } + if app.set_home { + let ptr = unsafe { (*getpwuid(run_as.uid())).pw_dir }; + let home_dir = match unsafe { CStr::from_ptr(ptr).to_str() } { + Ok(s) => s, + Err(e) => { + return Err(format![ + "Error: Failed to read home dir for {run_as_name}: {e}" + ]); + } + }; + cmd.env("HOME", home_dir); + } + // execute the child + let mut child = cmd.spawn().unwrap(); if app.background { std::process::exit(0); } else { child.wait().unwrap(); } } else { - return Err(RudoError::NoCommandSpecified); + return Err(String::from("Error: No command specified.")); } Ok(()) } -pub fn run() -> Result<(), RudoError> { - let app = App::parse(); +pub fn run() -> Result<(), String> { + let app = App::parse()?; + if app.help { + println!["{}", App::usage()]; + return Ok(()); + } + if app.version { + println!["Rudo v{VERSION}"]; + return Ok(()); + } + if app.gen_config { + println!["{}", make_example_rudoers_file()]; + } let alias = match &app.user { Some(x) => &x, None => "root", }; - let parent_id = std::os::unix::process::parent_id(); - let parent_id_str = format!["{parent_id}"]; - - let needs_password = check_rudoers(&app)? && !check_auth_cache(alias, &parent_id_str) || app.validate; + let needs_password = + check_rudoers(&app)? && !check_auth_cache(alias, parent_id) || app.validate; if needs_password { - check_auth()?; + check_auth(app.stdin)?; } - - if let Err(e) = update_auth_cache(alias, &parent_id_str) { - eprintln!["Failed to update cache: {e}"]; - std::process::exit(1); - } - - let mut path = PathBuf::from(SESSION_CACHE_PATH); - path.push(&parent_id_str); - match OpenOptions::new().create_new(true).write(true).open(&path) { - Ok(_) => { - if let Ok(Fork::Child) = daemon(false, false) { - loop { - if unsafe { kill(parent_id as _, 0) } == -1 { - if let Err(e) = std::fs::remove_file(&path) { - println!["Failed to remove session file: {e}"]; - std::process::exit(1); - } - let mut path = PathBuf::from(AUTH_CACHE_PATH); - path.push(parent_id_str); - if let Err(e) = std::fs::remove_dir_all(&path) { - println!["Failed to remove session dir: {e}"]; - std::process::exit(1); - } - break; - } - thread::sleep(Duration::from_millis(1000)) - } - } + let failed_to_update = String::from("Error: Rudo: Failed to update auth cache: "); + if !app.no_update && !app.remove_timestamp { + if let Err(e) = update_auth_cache(alias, parent_id) { + eprintln!["{failed_to_update}{e}"]; + std::process::exit(1); } - Err(_) => (), - }; - + } + if app.validate { + return Ok(()); + } + if app.remove_timestamp { + if let Err(e) = clear_auth_cache(alias, parent_id) { + eprintln!["{failed_to_update}{e}"]; + std::process::exit(1); + } + if app.cmd.is_empty() { + return Ok(()); + } + } + if app.reset_timestamp { + if let Err(e) = clear_auth_cache(alias, parent_id) { + eprintln!["{failed_to_update}{e}"]; + std::process::exit(1); + } + if let Err(e) = update_auth_cache(alias, parent_id) { + eprintln!["{failed_to_update}{e}"]; + std::process::exit(1); + } + if app.cmd.is_empty() { + return Ok(()); + } + } // Run the command - execute(&app)?; - + execute(app)?; Ok(()) } -fn main() -> Result<(), Error> { +fn main() { match run() { Ok(()) => (), - Err(e) => eprintln!["{e}"], + Err(e) => { + eprintln!["{e}"]; + std::process::exit(1); + } } - Ok(()) } diff --git a/src/rudoers.rs b/src/rudoers.rs index 540335c..d99893b 100644 --- a/src/rudoers.rs +++ b/src/rudoers.rs @@ -1,16 +1,15 @@ +use crate::{app::App, user_info::*}; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, BTreeSet}, fs::File, - io::{Read, Write}, + io::Read, path::PathBuf, }; use users::Group; -use crate::{app::App, user_info::*}; -use crate::{error::RudoError, DEFAULT_RUDOERS}; - const ALL: &'static str = "ALL"; +const DEFAULT_RUDOERS: &'static str = "/etc/rudoers.toml"; fn default_password_required() -> bool { true @@ -69,13 +68,15 @@ fn check_entry( hostname: &str, alias: &str, command_path: &str, -) -> Result { +) -> Result { + let permission_denied = String::from("Error: Permission denied."); + let operation_not_permitted = String::from("Error: Operation not permitted."); match entry { Entry::CommandPaths(cmds) => { if cmds.contains(command_path) || cmds.contains(ALL) { Ok(true) } else { - Err(RudoError::PermissionDenied) + Err(permission_denied) } } Entry::Hosts(hosts) => { @@ -84,27 +85,27 @@ fn check_entry( if host.commands.contains(command_path) || host.commands.contains(ALL) { Ok(host.password_required) } else { - Err(RudoError::PermissionDenied) + Err(permission_denied) } } else { Err(if alias == "root" { - RudoError::PermissionDenied + permission_denied } else { - RudoError::NoSwitchEntry + operation_not_permitted }) } } else { - Err(RudoError::PermissionDenied) + Err(permission_denied) } } } } -pub fn check_rudoers(args: &App) -> Result { - let command_path: PathBuf = if let Some(ref cmd) = args.cmd { - get_command_path(cmd.first().unwrap())? +pub fn check_rudoers(args: &App) -> Result { + let command_path: PathBuf = if let Some(first) = args.cmd.first() { + get_command_path(first)? } else { - return Err(RudoError::NoCommandSpecified); + return Err(String::from("Error: No command specified.")); }; let alias: String = args.user.clone().unwrap_or("root".into()); let hostname: String = get_hostname()?; @@ -114,7 +115,7 @@ pub fn check_rudoers(args: &App) -> Result { Ok(if let Some(entry) = rudoers.users.get(&username) { check_entry(&entry, &hostname, &alias, &command_path.to_string_lossy())? } else { - let mut result = Err(RudoError::NotInRudoers); + let mut result = Err(String::from("Error: User is not in the rudoers file.")); for group in groups { if let Some(entry) = rudoers.groups.get(group.name().to_str().unwrap()) { let output = @@ -129,8 +130,7 @@ pub fn check_rudoers(args: &App) -> Result { }) } -#[test] -fn rudoers_toml() { +pub fn make_example_rudoers_file() -> String { let mut rudoers = Rudoers::default(); rudoers.users.insert( "alice".to_string(), @@ -194,9 +194,5 @@ fn rudoers_toml() { text.push_str(&s); } } - text.push('\n'); - File::create("test_config") - .unwrap() - .write_all(&text.into_bytes()) - .unwrap(); + text } diff --git a/src/user_info.rs b/src/user_info.rs index 4663128..ae540ad 100644 --- a/src/user_info.rs +++ b/src/user_info.rs @@ -4,9 +4,9 @@ use libc::gethostname; use users::{get_current_gid, get_current_username, Group}; use which::which; -use crate::error::RudoError; +// use crate::error::RudoError; -pub fn get_command_path(command: &str) -> Result { +pub fn get_command_path(command: &str) -> Result { if let Ok(path) = which(command) { Ok(path) } else { @@ -16,15 +16,16 @@ pub fn get_command_path(command: &str) -> Result { } } -pub fn get_username() -> Result { +pub fn get_username() -> Result { + let bad_username = String::from("Error: only UTF-8 usernames are supported."); if let Some(username) = get_current_username() { if let Ok(username) = username.into_string() { Ok(username) } else { - Err(RudoError::BadUsername) + Err(bad_username) } } else { - Err(RudoError::BadUsername) + Err(bad_username) } } @@ -38,17 +39,18 @@ pub fn get_groups() -> Vec { } } -pub fn get_hostname() -> Result { +pub fn get_hostname() -> Result { + let bad_hostname = String::from("Error: only UTF-8 hostnames are supported."); let mut hostname = [0u8; 255]; unsafe { if gethostname(hostname.as_mut_ptr() as *mut i8, hostname.len()) != 0 { - return Err(RudoError::BadHostname); + return Err(bad_hostname); } }; if let Ok(s) = String::from_utf8(hostname.to_vec()) { let s = s.trim_matches(char::from_u32(0).unwrap()).to_owned(); Ok(s) } else { - Err(RudoError::BadHostname) + Err(bad_hostname) } }