diff --git a/Cargo.lock b/Cargo.lock index 175e32c..b7125c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,12 +26,27 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "libc" version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + [[package]] name = "memoffset" version = "0.6.4" @@ -54,9 +69,94 @@ dependencies = [ "memoffset", ] +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "sus" version = "0.0.0" dependencies = [ "nix", + "serde", + "serde_json", + "users", +] + +[[package]] +name = "syn" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", ] diff --git a/Cargo.toml b/Cargo.toml index 37579d9..aa2643e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,6 @@ name = "sus-kernel" [dependencies] nix = "0.23.0" +serde = { version="1.0.130", features = ["derive"] } +serde_json = "1.0.67" +users = "0.11.0" diff --git a/Makefile.toml b/Makefile.toml index 053feda..c1c12f7 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -92,6 +92,13 @@ script = ''' --target-directory="${SUS_INSTALL_PREFIX}/${SUS_INSTALL_DIRECTORY}" \ "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/release/sus-kernel" + # Install the configuration file + install \ + --owner=0 --group=0 \ + --mode=660 \ + --no-target-directory \ + "config/sample/sudoers.json" "${SUS_SUDOERS_PATH}" + ''' diff --git a/config/install.env b/config/install.env index 6cc9afc..09a55f8 100644 --- a/config/install.env +++ b/config/install.env @@ -4,6 +4,11 @@ # variables for where the final binaries are installed. In particular, the # binaries are installed to: # "${SUS_INSTALL_PREFIX}/${SUS_INSTALL_DIRECTORY}" +# +# It also defines where some configuration files are stored. These values must +# agree with the `.rs` configuration files. SUS_INSTALL_PREFIX = /usr/local/ SUS_INSTALL_DIRECTORY = /bin/ + +SUS_SUDOERS_PATH = /etc/sudoers.json diff --git a/config/sample/sudoers.json b/config/sample/sudoers.json new file mode 100644 index 0000000..7269ef0 --- /dev/null +++ b/config/sample/sudoers.json @@ -0,0 +1,158 @@ +{ + "Defaults": [ + { + "Options": [ + { "env_reset": true } + ] + }, + { + "Options": [ + { "mail_badpass": true } + ] + }, + { + "Options": [ + { "secure_path": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" } + ] + } + ], + "User_Aliases": { + "SYSADMINS": [ + { "username": "john" }, + { "username": "tim" }, + { "username": "tom" } + ] + }, + "User_Specs": [ + { + "User_List": [ + { "username": "root" } + ], + "Host_List": [ + { "hostname": "ALL" } + ], + "Cmnd_Specs": [ + { + "runasusers": [ + { "username": "ALL" } + ], + "runasgroups": [ + { "usergroup": "ALL" } + ], + "Options": [ + { "setenv": true } + ], + "Commands": [ + { "command": "ALL" } + ] + } + ] + }, + { + "User_List": [ + { "useralias": "SYSADMINS" } + ], + "Host_List": [ + { "hostname": "ALL" } + ], + "Cmnd_Specs": [ + { + "runasusers": [ + { "username": "ALL" } + ], + "runasgroups": [ + { "usergroup": "ALL" } + ], + "Options": [ + { "setenv": true } + ], + "Commands": [ + { "command": "ALL" } + ] + } + ] + }, + { + "User_List": [ + { "useralias": "SYSADMINS" } + ], + "Host_List": [ + { "hostname": "ALL" } + ], + "Cmnd_Specs": [ + { + "runasusers": [ + { "username": "john" } + ], + "runasgroups": [ + { "usergroup": "sudo" } + ], + "Options": [ + { "authenticate": false }, + { "setenv": true } + ], + "Commands": [ + { "command": "/usr/bin/cat" } + ] + }, + { + "runasusers": [ + { "username": "tom" } + ], + "Options": [ + { "authenticate": false }, + { "setenv": true } + ], + "Commands": [ + { "command": "/etc/shadow" } + ] + } + ] + }, + { + "User_List": [ + { "usergroup": "admin" } + ], + "Host_List": [ + { "hostname": "ALL" } + ], + "Cmnd_Specs": [ + { + "runasusers": [ + { "username": "ALL" } + ], + "Options": [ + { "setenv": true } + ], + "Commands": [ + { "command": "ALL" } + ] + } + ] + }, + { + "User_List": [ + { "usergroup": "sudo" } + ], + "Host_List": [ + { "hostname": "ALL" } + ], + "Cmnd_Specs": [ + { + "runasusers": [ + { "username": "ALL" } + ], + "runasgroups": [ + { "usergroup": "ALL" } + ], + "Options": [ + { "setenv": true } + ], + "Commands": [ + { "command": "ALL" } + ] + } + ] + } + ] +} diff --git a/config/sus-kernel.rs b/config/sus-kernel.rs index afdb922..1460365 100644 --- a/config/sus-kernel.rs +++ b/config/sus-kernel.rs @@ -14,7 +14,6 @@ use crate::executable::factory::AutoExecutableFactory; use crate::executable::run::Runner; use crate::permission; use crate::permission::factory::AutoPermissionFactory; -use crate::permission::verify::Verifier; #[cfg(feature = "log")] use crate::log; @@ -46,23 +45,17 @@ pub const CURRENT_PERMISSION_FACTORY: AutoPermissionFactory = permission::factor pub const REQUESTED_PERMISSION_FACTORY: AutoPermissionFactory = permission::factory::from_commandline; -/// An array of all the [Verifier]s to invoke -/// -/// We might want multiple checks to pass before running [Executable][eb]. This -/// is a list of all the checks that have to pass. -/// -/// Note that *all* the checks have to pass for the [Executable][eb] to be run. -/// Effectively, these checks are `AND`ed together. As a corollary, if this list -/// is empty, the [Executable][eb] will be run unconditionally. -/// -/// [eb]: executable::Executable -pub const VERIFIERS: &[Verifier] = &[]; - /// The method to run the [Executable][eb] created /// /// [eb]: executable::Executable pub const RUNNER: Runner = executable::run::exec; +/// The path to log to +/// +/// The path to sudoers file. For readability purpose, this is represented as JSON +/// and not traditional sudoers file syntax +pub const SUDOER_PATH: &str = "/etc/sudoers.json"; + /// How to log incoming [Request][rq]s /// /// For administrative purposes, it might be useful to log what [Request][rq]s diff --git a/src/bin/sus-kernel/main.rs b/src/bin/sus-kernel/main.rs index cfe5b5a..e704bdc 100644 --- a/src/bin/sus-kernel/main.rs +++ b/src/bin/sus-kernel/main.rs @@ -11,12 +11,14 @@ mod log; mod permission; mod request; -use permission::verify::AbstractVerifier; -use request::Request; +use crate::permission::verify::Verifier; +use permission::verify::from_sudoers; #[cfg(feature = "log")] use log::AbstractLogger; +use request::Request; + /// Method to get the [Logger][lg] to use /// /// Logging is an optional feature for this binary. As such, we need to use @@ -57,14 +59,13 @@ fn main() { // We need to clone them from the slice reference let verifiers = { // Do the clone - let mut vfers = Vec::new(); - vfers.extend_from_slice(config::VERIFIERS); + let vfers = from_sudoers(); // Create and return // Box everything up as well // See: https://newbedev.com/how-to-create-a-vector-of-boxed-closures-in-rust vfers .into_iter() - .map(|f| Box::new(f) as Box) + .map(|f| Box::new(f) as Box) .collect() }; @@ -80,6 +81,7 @@ fn main() { #[cfg(feature = "log")] logger: get_logger(), }; + // Service the request req.service().unwrap(); } diff --git a/src/bin/sus-kernel/permission/verify/mod.rs b/src/bin/sus-kernel/permission/verify/mod.rs index af3522a..7b5ecce 100644 --- a/src/bin/sus-kernel/permission/verify/mod.rs +++ b/src/bin/sus-kernel/permission/verify/mod.rs @@ -5,8 +5,12 @@ //! that might need to be performed. This module holds the methods for doing //! that. It also defines common types for verification. +pub mod parsed_sudoers_type; +pub mod sudoers; +pub mod sudoers_type; use super::Permission; use crate::executable::Executable; +pub use sudoers::from_sudoers; use std::error::Error; use std::fmt; @@ -18,14 +22,7 @@ use std::fmt::{Display, Formatter}; /// the [Permission] they request and the [Executable] the user wishes to run. /// They should then return a [VerifyResult] signalling whether the user is /// allowed to run it. -pub type Verifier = fn(&Permission, &Permission, &Executable) -> VerifyResult; -/// Abstract supertype of [Verifier] -/// -/// For testing purposes, we might want to have [Verifier]s signal other parts -/// of the code. This trait allows for that. Since it's a `dyn` type, we can't -/// create variables with it. However, it will work for automatically generated -/// closures. -pub type AbstractVerifier = dyn FnMut(&Permission, &Permission, &Executable) -> VerifyResult; +pub type Verifier = dyn FnMut(&Permission, &Permission, &Executable) -> VerifyResult; /// Convinience type for the result of a [Verifier] /// @@ -33,6 +30,9 @@ pub type AbstractVerifier = dyn FnMut(&Permission, &Permission, &Executable) -> /// [Result]. For convinience, this type aliases to the expected return type. pub type VerifyResult = Result<(), VerifyError>; +/// String to match on ALL keyword in sudoers +pub const ALL: &str = "ALL"; + /// Error for [Verifier]s /// /// The user may or may not be allowed to run the [Executable] with the diff --git a/src/bin/sus-kernel/permission/verify/parsed_sudoers_type.rs b/src/bin/sus-kernel/permission/verify/parsed_sudoers_type.rs new file mode 100644 index 0000000..a5835ed --- /dev/null +++ b/src/bin/sus-kernel/permission/verify/parsed_sudoers_type.rs @@ -0,0 +1,158 @@ +// Since sudoers store users by username and not uid, +// we use special types in this file to easily query sudoers using ids +use super::sudoers_type; +use super::sudoers_type::Command; +use super::sudoers_type::User::{Useralias, Usergroup, Username}; +use super::Permission; +use super::ALL; +use nix::unistd::{Gid, Uid}; +use std::collections::HashMap; +use std::collections::HashSet; +use std::ffi::CString; +use users::{get_group_by_name, get_user_by_name}; + +#[derive(Debug)] +pub struct AllowedCmd { + pub users: HashSet, + pub allow_all_users: bool, + pub groups: HashSet, + pub allow_all_groups: bool, + pub paths: HashSet, + pub allow_all_cmds: bool, + pub options: Vec, +} + +impl AllowedCmd { + pub fn new() -> Self { + AllowedCmd { + users: HashSet::new(), + groups: HashSet::new(), + paths: HashSet::new(), + options: Vec::new(), + allow_all_cmds: false, + allow_all_users: false, + allow_all_groups: false, + } + } + pub fn is_relevant(&self, req_perm: &Permission) -> bool { + self.users.contains(&req_perm.uid) + || self.groups.contains(&req_perm.primary_gid) + || !self.groups.is_disjoint(&req_perm.secondary_gids) + || self.allow_all_users + || self.allow_all_groups + } +} +#[derive(Debug)] +pub struct Rule { + pub users: HashSet, + pub allow_all_users: bool, + pub groups: HashSet, + pub allow_all_groups: bool, + pub allowed_cmds: Vec, + pub allow_all_cmds: bool, +} + +fn get_uid_from_username(username: &str) -> Option { + get_user_by_name(username).map(|user| Uid::from_raw(user.uid())) +} + +fn get_gid_from_groupname(groupname: &str) -> Option { + get_group_by_name(groupname).map(|user| Gid::from_raw(user.gid())) +} + +impl Rule { + pub fn new() -> Self { + Rule { + users: HashSet::new(), + groups: HashSet::new(), + allowed_cmds: Vec::new(), + allow_all_groups: false, + allow_all_users: false, + allow_all_cmds: false, + } + } + + pub fn is_relevant(&self, curr_perm: &Permission) -> bool { + self.users.contains(&curr_perm.uid) + || self.groups.contains(&curr_perm.primary_gid) + || !self.groups.is_disjoint(&curr_perm.secondary_gids) + || self.allow_all_users + || self.allow_all_groups + } + + pub fn from_userspec( + userspec: &sudoers_type::UserSpec, + useraliases: &HashMap>, + ) -> Self { + let mut rule = Rule::new(); + // Populate rule.users and rule.groups with uid and gid + for user in &userspec.user_list { + match user { + Username(username) => { + if username.eq(ALL) { + rule.allow_all_users = true; + } else if let Some(uid) = get_uid_from_username(username) { + rule.users.insert(uid); + } + } + Usergroup(groupname) => { + if groupname.eq(ALL) { + rule.allow_all_groups = true; + } else if let Some(gid) = get_gid_from_groupname(groupname) { + rule.groups.insert(gid); + } + } + Useralias(alias) => { + for user in &useraliases[alias] { + if let Username(username) = user { + if username.eq(ALL) { + rule.allow_all_users = true; + } else if let Some(uid) = get_uid_from_username(username) { + rule.users.insert(uid); + } + } + } + } + } + } + for cmd_spec in &userspec.cmd_specs { + let mut allowed_cmd = AllowedCmd::new(); + for runasuser in &cmd_spec.run_as_users { + if let Username(username) = runasuser { + if username.eq(ALL) { + allowed_cmd.allow_all_users = true; + } else if let Some(uid) = get_uid_from_username(username) { + allowed_cmd.users.insert(uid); + } + } + } + for runasgroup in &cmd_spec.run_as_groups { + if let Usergroup(groupname) = runasgroup { + if groupname.eq(ALL) { + allowed_cmd.allow_all_users = true; + } else if let Some(gid) = get_gid_from_groupname(groupname) { + allowed_cmd.groups.insert(gid); + } + } + } + for option in &cmd_spec.options { + allowed_cmd.options.push(option.clone()); + } + for command in &cmd_spec.commands { + let Command::CmdPath(path) = command; + if path.to_str().unwrap().eq(ALL) { + allowed_cmd.allow_all_cmds = true; + } else { + allowed_cmd.paths.insert(path.clone()); + } + } + rule.allowed_cmds.push(allowed_cmd); + } + rule + } +} + +pub struct ParsedSudoers { + pub rules: Vec, + pub user_aliases: HashMap>, +} diff --git a/src/bin/sus-kernel/permission/verify/sudoers.rs b/src/bin/sus-kernel/permission/verify/sudoers.rs new file mode 100644 index 0000000..f3e3986 --- /dev/null +++ b/src/bin/sus-kernel/permission/verify/sudoers.rs @@ -0,0 +1,51 @@ +use super::sudoers_type::Sudoers; +use super::{Verifier, VerifyError}; +use crate::config; +use crate::permission::verify::VerifyResult; +use nix::unistd::{Gid, Uid}; +use std::ffi::CString; +use std::fs::File; +use std::io::BufReader; + +#[allow(dead_code)] +#[derive(Debug)] +struct Command { + run_as_users: Vec, + run_as_groups: Vec, + commands: Vec, +} + +#[derive(Debug)] +struct Policy { + users: Vec, + groups: Vec, + cmd_specs: Vec, +} + +#[allow(dead_code)] +pub fn from_sudoers() -> Vec> { + // Declare vector of verifiers to return + let mut verifiers = Vec::new(); + // Parse sudoers.json using serde_json + let file = File::open(config::SUDOER_PATH).unwrap(); + let reader = BufReader::new(file); + let sudoer: Sudoers = serde_json::from_reader(reader).unwrap(); + // Parse sudoer further and retrieve uids and gids + let parsed_sudoer = sudoer.retrieve_ids(); + for rule in parsed_sudoer.rules { + let x: Box = Box::new(move |curr_perm, req_perm, exe| -> VerifyResult { + if rule.is_relevant(curr_perm) { + for allowed_cmd in &rule.allowed_cmds { + if (allowed_cmd.is_relevant(req_perm) && allowed_cmd.paths.contains(&exe.path)) + || allowed_cmd.allow_all_cmds + { + return Ok(()); + } + } + } + Err(VerifyError::NotAllowed { err: None }) + }); + verifiers.push(x); + } + verifiers +} diff --git a/src/bin/sus-kernel/permission/verify/sudoers_type.rs b/src/bin/sus-kernel/permission/verify/sudoers_type.rs new file mode 100644 index 0000000..0bca14e --- /dev/null +++ b/src/bin/sus-kernel/permission/verify/sudoers_type.rs @@ -0,0 +1,90 @@ +use super::parsed_sudoers_type::{ParsedSudoers, Rule}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::ffi::CString; + +#[derive(Deserialize, Serialize, Debug)] +pub enum User { + #[serde(rename = "username")] + Username(String), + #[serde(rename = "usergroup")] + Usergroup(String), + #[serde(rename = "useralias")] + Useralias(String), +} +#[derive(Deserialize, Serialize, Debug)] +pub enum Host { + #[serde(rename = "hostname")] + Hostname(String), +} +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum Option { + #[serde(rename = "setenv")] + Setenv(bool), + #[serde(rename = "authenticate")] + Authenticate(bool), +} +#[derive(Deserialize, Serialize, Debug)] +pub enum Command { + #[serde(rename = "command")] + CmdPath(CString), +} +#[derive(Deserialize, Serialize, Debug)] +pub struct CmdSpec { + #[serde(rename = "runasusers")] + pub run_as_users: Vec, + #[serde(rename = "runasgroups")] + #[serde(default)] + pub run_as_groups: Vec, + #[serde(rename = "Options")] + pub options: Vec