From 83d045a58cd67286ae9d9fa2bee9103fa0cc931a Mon Sep 17 00:00:00 2001 From: Tyler Murphy Date: Thu, 10 Nov 2022 14:17:16 -0500 Subject: [PATCH] documentation and group support --- src/flags.rs | 32 ++++++++++++-- src/main.rs | 115 +++++++++++++++++++++++++++++++++++++++++++------ src/persist.rs | 43 ++++++++++++++---- src/secure.rs | 38 ++++++++++++++++ 4 files changed, 204 insertions(+), 24 deletions(-) diff --git a/src/flags.rs b/src/flags.rs index 9432b8f..c93f3ef 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -1,3 +1,9 @@ + +const HELP_FLAG: &str = "help h"; +const VERSION_FLAG: &str = "version v"; +const DONT_PERSIST: &str = "d"; + + pub struct Flags { pub help: bool, pub version: bool, @@ -5,6 +11,13 @@ pub struct Flags { pub arg_count: usize } + +/// Parses an list of String arguments into a set of flags that can be used by crab +/// #### Arguments +/// * `args` - The list of String arguments +/// #### Returns +/// * `None` - If there is an invalid argument in the list +/// * `Some(Flags)` - If the arguments were secussfully parsed, returning the flags pub fn parse(args: &[String]) -> Option { let mut flags = Flags { help: false, @@ -34,14 +47,20 @@ pub fn parse(args: &[String]) -> Option { Some(flags) } + +/// Checks if a given string is a given argument fn is_arg(arg: &str) -> bool { return arg.starts_with("-"); } -const HELP_FLAG: &str = "help h"; -const VERSION_FLAG: &str = "version v"; -const DONT_PERSIST: &str = "d"; +/// Sets a flag in a `Flags` struct +/// #### Arguments +/// * `arg` - The argument to check +/// * `flags` - The `Flags` stuct to update +/// #### Returns +/// * `true` - If the argument passed is a valid flag +/// * `false` - If the argument passed is not a valid flag fn set_flag(arg: &str, flags: &mut Flags) -> bool { if has_flag_set(&arg, HELP_FLAG) { flags.help = true; @@ -56,6 +75,13 @@ fn set_flag(arg: &str, flags: &mut Flags) -> bool { false } + +/// Returns if a given argument is a certain flag +/// #### Arguments +/// * `arg` - The arch to check +/// #### Returns +/// * `true` - If the argument matches the flag +/// * `false` - If the argument doesn't match the flag fn has_flag_set(arg: &str, check: &str) -> bool { for check_arg in check.split(" ") { if check_arg == arg { diff --git a/src/main.rs b/src/main.rs index 51ca0f2..c6e8201 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,13 @@ use std::env; use std::process::ExitCode; use pwd::Passwd; -use nix::unistd; +use nix::unistd::{self, Uid, Group}; + + +mod persist; +mod flags; +mod secure; -extern crate time; const ERROR_ARGS: u8 = 1; const ERROR_CONFIG: u8 = 2; @@ -12,35 +16,47 @@ const ERROR_NOT_AUTHORIZED: u8 = 4; const ERROR_AUTH_FAILED: u8 = 5; const ERROR_RUN_ROOT: u8 = 6; -mod persist; -mod flags; -mod secure; fn main() -> ExitCode { + + // gets the arguments from the env let args: Vec = env::args().collect(); + + // pase the arguments into valid flags that are usuable by crab let flags = match flags::parse(&args[1..]) { Some(data) => data, + // if there is an invalid flag, print the help message and exit None => { help(); return ExitCode::from(ERROR_ARGS); } }; + + // If the version arg flag is set, print the crab version if flags.version { println!("crab version 0.0.5"); return ExitCode::SUCCESS; } + + // If the help arg flag is set, print the crab help message if flags.help { help(); return ExitCode::SUCCESS; } + + // If the amount of acutal command arguments is less than two, a.k.a just `crab`, print the command usage if args.len() - flags.arg_count < 2 { println!("usage: crab [-d] command [args]"); return ExitCode::SUCCESS; } + + // Load the command config from /etc let config = match config("/etc/crab.conf") { Some(data) => data, None => return ExitCode::from(ERROR_CONFIG) }; + + // get the current user login let user = match Passwd::current_user() { Some(data) => data, None => { @@ -48,6 +64,8 @@ fn main() -> ExitCode { return ExitCode::from(ERROR_NO_USER); } }; + + // check if the user is authorized let persist = match allowed(&config, &user.name) { Some(data) => data && !flags.dont_persist, None => { @@ -56,23 +74,31 @@ fn main() -> ExitCode { } }; + + // authenticate the user if !validate(&user.name, persist) { eprintln!("Authentication failed."); return ExitCode::from(ERROR_AUTH_FAILED); } + // set the uid and gid of the process to root to run the command as root if !unistd::setuid(unistd::geteuid()).is_ok() || !unistd::setgid(unistd::getegid()).is_ok() { eprintln!("Failed to set root permissions"); return ExitCode::from(ERROR_RUN_ROOT); }; + + // execute the passed command let start = 1 + flags.arg_count; let err = exec::execvp(&args[start], &args[start..]); + // print an error if an error was returned eprintln!("{}", err); ExitCode::SUCCESS } + +/// Prints the help message to the standard output fn help() { let help = "Usage: @@ -84,10 +110,22 @@ Options: println!("{}", help); } + struct Config { - users: Vec<(String, bool)> + identitys: Vec<(String, bool)>, } + +/// Validates the authorized user, and authenticates them for +/// the current session. If the user is already persisted, it +/// will attempt to read the persist file, and then skip +/// authentication if the is still persisted. +/// #### Arguments +/// * `user` - The login name of the linux user +/// * `persist` - If the user's login should persist +/// #### Returns +/// * `true` - If the user authenticated sucessfully, or the user is persisted +/// * `false` - If the user failed to authenticate fn validate(user: &str, persist: bool) -> bool { if persist && persist::get_persist(user) { return true; @@ -110,15 +148,68 @@ fn validate(user: &str, persist: bool) -> bool { return true; } + +/// Returns if the user is authorized given a sepcific config and user name. +/// #### Arguments +/// * `config` - A config struct contaning the authorization settings for crab +/// * `user` - The name of the user to check is authorized +/// #### Returns +/// * `None` - If the user is not authorized +/// * `Some(bool)` - If the user is authorized, returning the boolean if the +/// user is set to persist fn allowed(config: &Config, user: &str) -> Option { - for (name, persist) in &config.users { - if name == user { + // get user groups + let groups = get_groups(Uid::current()); + // check each config identiy + for (identity, persist) in &config.identitys { + // if it starts with a :, its a group + if identity.starts_with(":") { + let group = &identity[1..]; + if groups.contains(&group.to_string()) { + return Some(persist.clone()); + }; + // else its a user + } else if identity == user { return Some(persist.clone()); } } None } + +/// Returns a vector of group names of the groups a specific linux user is in +/// #### Arguments +/// * 'uid' - The user id od the linux user to get groups from +/// #### Returns +/// A vector of strings of the groups the user is in. If the vector is empty, +/// either the function coudn't retrieve the users groups, or the user is not in +/// any groups. +fn get_groups(uid: Uid) -> Vec { + let no_change = Uid::from_raw(u32::MAX); + if unistd::setresuid(no_change, uid, no_change).is_err() { + return vec![] + }; + let groups = match nix::unistd::getgroups() { + Ok(data) => data, + Err(_) => return vec![] + }; + let names = groups.iter() + .map(|gid| Group::from_gid(*gid)) + .flatten().flatten() + .map(|group| group.name) + .collect(); + names +} + + +/// Returns a option containing the config ad the specific pile path. +/// This function will print out config syntax errors to the standard +/// output if it failes to parse certain lines. +/// #### Arguments +/// * `path` - The path to the config file +/// #### Returns +/// * `None` - If the config failed to load +/// * `Some(Config)` - If the config was sucessfully parsed. fn config(path: &str) -> Option { let file = match std::fs::read_to_string(path) { Err(e) => { @@ -128,7 +219,7 @@ fn config(path: &str) -> Option { Ok(data) => data }; - let mut users = vec![]; + let mut identitys = vec![]; for (line_num, line) in file.split("\n").enumerate() { let args: Vec<&str> = line.split(" ").collect(); if line.trim() == "" { @@ -138,7 +229,7 @@ fn config(path: &str) -> Option { eprintln!("Error in config at line {}: Not enough arguments", line_num); continue; } - let user: String = args[0].to_string(); + let identity: String = args[0].to_string(); let persist: bool = match args[1].parse() { Err(e) => { eprintln!("Error in config at line {}: {}", line_num, e); @@ -146,7 +237,7 @@ fn config(path: &str) -> Option { }, Ok(data) => data }; - users.push((user, persist)); + identitys.push((identity, persist)); } - Some(Config{users}) + Some(Config{identitys}) } diff --git a/src/persist.rs b/src/persist.rs index 667b12a..fe4ce69 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -1,13 +1,22 @@ use std::time::SystemTime; use serde_json::Value; - use crate::secure; + const PERSIST_TIME: u64 = 60 * 3; const PERSIST_PATH: &str = "/var/run/crab"; + +/// Returns true or false if a user is currently persisted from +/// a prior authentication. If the persist file had been tampered +/// with, or not trusted, it will be ignored, and return false. +/// #### Arguments +/// * `user` - The user to check if is persisted +/// #### Returns +/// * `true` - If the user is persisted +/// * `false` - If the user is not persisted, or if the persist file is not trusted pub fn get_persist(user: &str) -> bool { - let json = match get_terminal_config() { + let json = match get_persist_config() { Some(data) => data, None => return false }; @@ -18,17 +27,21 @@ pub fn get_persist(user: &str) -> bool { return now() - timestamp < PERSIST_TIME && timestamp - 1 < now(); } + +/// Updates the current sessions persist file +/// #### Arguments +/// * `user` - The user to set persisted pub fn set_persist(user: &str) { - let mut json = match get_terminal_config() { + let mut json = match get_persist_config() { Some(data) => data, None => serde_json::from_str("{}").unwrap() }; json[user] = Value::from(now()); - let id = match get_terminal_process() { + let session = match get_current_session() { Some(data) => data, None => return }; - match secure::write_file(PERSIST_PATH, &format!("{}", id), &json.to_string()) { + match secure::write_file(PERSIST_PATH, &format!("{}", session), &json.to_string()) { Ok(_) => {}, Err(_) => { eprintln!("crab: An Internal Has Error") @@ -36,7 +49,12 @@ pub fn set_persist(user: &str) { }; } -fn get_terminal_process() -> Option { + +/// Gets the current session that crab is running in +/// #### Returns +/// * `None` - If crab failed to get the current session +/// * `Some(i32)` - If the session is retrieved, returns the i32/pid_t session id +fn get_current_session() -> Option { let pid: i32 = match std::process::id().try_into() { Ok(data) => data, Err(_) => return None @@ -48,12 +66,17 @@ fn get_terminal_process() -> Option { Some(pid_stat.session) } -fn get_terminal_config() -> Option { - let id = match get_terminal_process() { + +/// Gets the current persist file for the current session +/// #### Returns +/// * `None` - If the persist file is untrusted or doesnt exist +/// * `Some(Value)` - If the persist file is retrieved, returns the serde_json Value of the file +fn get_persist_config() -> Option { + let session = match get_current_session() { Some(data) => data, None => return None }; - let data = match secure::read_file(PERSIST_PATH, &format!("{}", id)) { + let data = match secure::read_file(PERSIST_PATH, &format!("{}", session)) { Some(data) => data, None => "{}".to_string() }; @@ -64,6 +87,8 @@ fn get_terminal_config() -> Option { Some(json) } + +// Gets the current time in seconds since the Unix Epoch fn now() -> u64 { return SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); } diff --git a/src/secure.rs b/src/secure.rs index 5fb0cc8..39339a0 100644 --- a/src/secure.rs +++ b/src/secure.rs @@ -1,6 +1,14 @@ use std::{os::{unix::prelude::PermissionsExt, linux::fs::MetadataExt}, fs, io}; use nix::unistd; + +/// Writes a file securly to a specified path with given data +/// #### Arguments +/// * `dir` - The directory of the secure folder +/// * `file` - The file name to write +/// * `data` - The data to write +/// #### Returns +/// A ``io::Result<()>`` if the write succeded or failed pub fn write_file(dir: &str, file: &str, data: &str) -> Result<(), io::Error> { fs::create_dir_all(dir)?; set_file_permissions(0, 0, 0o100600, dir)?; @@ -11,6 +19,16 @@ pub fn write_file(dir: &str, file: &str, data: &str) -> Result<(), io::Error> { Ok(()) } + +/// Reads the file secutly to a specified path. If the file has +/// been tampered (permissions have been changed), it ignores the +/// file. +/// #### Arguments +/// * `dir` - The directory of the secure folder +/// * `file` - The file name to write +/// #### Returns +/// * `None` - If the files doesnt exist or isnt trusted +/// * `Some(String) - If the file is trusted, it returns the file's contents pub fn read_file(dir: &str, file: &str) -> Option { let path = path(dir,file); if !check_file_permissions(0, 0, 0o100600, &path) { @@ -22,6 +40,15 @@ pub fn read_file(dir: &str, file: &str) -> Option { }; } + +/// Sets the permission for a secure file +/// #### Arguments +/// * `uid` - The user to own the file +/// * `gid` - The group to own the file +/// * `mode` - The mode permissions of the file +/// * `path` - The path of the secure file +/// #### Returns +/// A ``io::Result<()>`` if the write succeded or failed fn set_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> Result<(), io::Error> { unistd::chown(std::path::Path::new(path), Some(unistd::Uid::from(uid)), Some(unistd::Gid::from(gid)))?; let metadata = fs::metadata(path)?; @@ -31,6 +58,15 @@ fn set_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> Result<(), Ok(()) } + +/// Checks if the files permissions equals the given parameters +/// #### Arguments +/// * `uid` - The user to own the file +/// * `gid` - The group to own the file +/// * `mode` - The mode permissions of the file +/// * `path` - The path of the secure file +/// #### Returns +/// True or false if the files permissions match fn check_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> bool { let metadata = match fs::metadata(path) { Ok(data) => data, @@ -40,6 +76,8 @@ fn check_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> bool { return perms.mode() == mode && metadata.st_uid() == uid && metadata.st_gid() == gid; } + +/// Get the path of a file given a directory and file name fn path(dir: &str, file: &str) -> String { return format!("{}/{}", dir, file); } \ No newline at end of file