alpha hehehehehe woohooo

main
elfeiin 2024-02-08 04:17:07 -08:00
parent bf66312e8a
commit 2a4d6c9f30
Signed by: elfein
GPG Key ID: A53FDD4FD091A276
9 changed files with 644 additions and 352 deletions

207
Cargo.lock generated
View File

@ -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"

View File

@ -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"

8
README.md Normal file
View File

@ -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
```

View File

@ -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<String>,
#[arg(short, long)]
pub validate: bool,
pub cmd: Option<Vec<String>>,
// /// 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<PathBuf>,
// /// 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 `#<group number>`
// #[arg(short, long)]
// pub group: Option<String>,
// #[arg(short = 'H', long)]
// pub set_home: bool,
// #[arg(short = 'h', long)]
// pub host: Option<String>,
// #[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<String>,
// #[arg(short = 'R', long)]
// pub chroot: Option<PathBuf>,
// /// 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<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-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::<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()
}
}

View File

@ -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."))
}

93
src/cache.rs Normal file
View File

@ -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(&timestamp_path)?;
timestamp_path.push(alias);
let mut opts = OpenOptions::new();
opts.write(true).create(true);
let mut timestamp_file = opts.open(&timestamp_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::<SystemTime>(&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
}
}
}

View File

@ -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::<Instant, u128>(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::<u128>(&buf) {
Ok(n) => {
let old_time = unsafe { std::mem::transmute::<u128, Instant>(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(())
}

View File

@ -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<bool, RudoError> {
) -> Result<bool, String> {
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<bool, RudoError> {
let command_path: PathBuf = if let Some(ref cmd) = args.cmd {
get_command_path(cmd.first().unwrap())?
pub fn check_rudoers(args: &App) -> Result<bool, String> {
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<bool, RudoError> {
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<bool, RudoError> {
})
}
#[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
}

View File

@ -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<PathBuf, RudoError> {
pub fn get_command_path(command: &str) -> Result<PathBuf, String> {
if let Ok(path) = which(command) {
Ok(path)
} else {
@ -16,15 +16,16 @@ pub fn get_command_path(command: &str) -> Result<PathBuf, RudoError> {
}
}
pub fn get_username() -> Result<String, RudoError> {
pub fn get_username() -> Result<String, String> {
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<Group> {
}
}
pub fn get_hostname() -> Result<String, RudoError> {
pub fn get_hostname() -> Result<String, String> {
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)
}
}