diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3657f2da..72fe9f0f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - 'main' + - 'dev' jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 5e7ac21d..abf7e534 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -20,6 +20,7 @@ on: pull_request: branches: - 'main' + - 'dev' jobs: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9215ccf2..e05245aa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - 'main' + - 'dev' jobs: rust-coverage: diff --git a/Cargo.toml b/Cargo.toml index 40b783d1..54b9b128 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["xtask", "rar-common"] [package] name = "rootasrole" # The project version is managed on json file in resources/rootasrole.json -version = "3.0.4" +version = "3.0.5" rust-version = "1.76.0" authors = ["Eddie Billoir "] edition = "2021" diff --git a/README.md b/README.md index ef64e37d..01e7851b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ -# RootAsRole (V3.0.4) : A memory-safe and security-oriented alternative to sudo/su commands +# RootAsRole (V3.0.5) : A memory-safe and security-oriented alternative to sudo/su commands **RootAsRole** is a project to allow Linux/Unix administrators to delegate their administrative tasks access rights to users. Its main features are : diff --git a/book/src/chsr/file-config.md b/book/src/chsr/file-config.md index 232ce821..b85d3682 100644 --- a/book/src/chsr/file-config.md +++ b/book/src/chsr/file-config.md @@ -91,7 +91,12 @@ The following example shows a RootAsRole config without plugins when almost ever "name": "t_complete", // Task name, must be unique in the role "purpose": "complete", // Task purpose, just a description "cred": { - "setuid": "user1", // User to setuid before executing the command + "setuid": { + "fallback": "thefallbackuser", // Fallback user if the -u option is not set + "default": "none", // The sr user cannot use -u option in general + "add": ["theuser"], // the sr user can use "-u theuser" option + "sub": ["anotheruser"] // the sr user cannot use "-u anotheruser" option (overrides add, applies only if default is all) + }, // User to setuid before executing the command "setgid": [ // Groups to setgid before executing the command, The first one is the primary group "group1", "group2" diff --git a/book/src/sr/README.md b/book/src/sr/README.md index 0779c10f..977a37b1 100644 --- a/book/src/sr/README.md +++ b/book/src/sr/README.md @@ -13,6 +13,8 @@ Options: -r, --role <ROLE> Role to select -t, --task <TASK> Task to select (--role required) + -u, --user <USER> Specify the user to execute the command as + -E, --preserve-env Preserve environment variables -p, --prompt <PROMPT> Prompt to display -i, --info Display rights of executor -h, --help Print help (see more with '--help') diff --git a/rar-common/Cargo.toml b/rar-common/Cargo.toml index 56ad562f..d7cf9d29 100644 --- a/rar-common/Cargo.toml +++ b/rar-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rootasrole-core" -version = "3.0.4" +version = "3.0.5" edition = "2021" description = "This core crate contains the RBAC and main features for the RootAsRole project." license = "GPL-3.0-or-later" @@ -26,6 +26,7 @@ hex = "0.4" log = "0.4" syslog = "7.0" env_logger = "0.11" +bon = { version = "3.3.2", features = ["experimental-overwritable"] } [dev-dependencies] log = "0.4" diff --git a/rar-common/src/api.rs b/rar-common/src/api.rs index d2c14c6e..c25a6ddd 100644 --- a/rar-common/src/api.rs +++ b/rar-common/src/api.rs @@ -9,9 +9,13 @@ use serde_json::Value; use strum::EnumIs; #[cfg(feature = "finder")] -use crate::database::finder::{Cred, ExecSettings, FilterMatcher, TaskMatch, UserMin}; +use crate::database::finder::{ActorMatchMin, Cred, ExecSettings, TaskMatch}; +use crate::database::FilterMatcher; -use crate::database::structs::{SActor, SConfig, SRole, STask}; +use crate::database::{ + actor::SActor, + structs::{SConfig, SRole, STask}, +}; use once_cell::sync::Lazy; static API: Lazy> = Lazy::new(|| Mutex::new(PluginManager::new())); @@ -52,7 +56,7 @@ pub type TaskMatcher = fn( matcher: &mut TaskMatch, ) -> PluginResultAction; #[cfg(feature = "finder")] -pub type UserMatcher = fn(role: &SRole, user: &Cred, user_struct: &Value) -> UserMin; +pub type UserMatcher = fn(role: &SRole, user: &Cred, user_struct: &Value) -> ActorMatchMin; pub type RoleInformation = fn(role: &SRole) -> Option; pub type ActorInformation = fn(actor: &SActor) -> Option; @@ -200,7 +204,7 @@ impl PluginManager { } #[cfg(feature = "finder")] - pub fn notify_user_matcher(role: &SRole, user: &Cred, user_struct: &Value) -> UserMin { + pub fn notify_user_matcher(role: &SRole, user: &Cred, user_struct: &Value) -> ActorMatchMin { let api = API.lock().unwrap(); for plugin in api.user_matcher_plugins.iter() { let res = plugin(role, user, user_struct); @@ -208,7 +212,7 @@ impl PluginManager { return res; } } - UserMin::NoMatch + ActorMatchMin::NoMatch } #[cfg(feature = "finder")] diff --git a/rar-common/src/database/actor.rs b/rar-common/src/database/actor.rs new file mode 100644 index 00000000..d4f194c6 --- /dev/null +++ b/rar-common/src/database/actor.rs @@ -0,0 +1,484 @@ +use std::fmt::{self, Formatter}; + +use bon::bon; +use nix::unistd::{Group, User}; +use serde::{ + de::{self, Visitor}, + Deserialize, Deserializer, Serialize, +}; +use serde_json::{Map, Value}; +use strum::EnumIs; + +#[derive(Serialize, Debug, EnumIs, Clone, PartialEq, Eq)] +#[serde(untagged, rename_all = "lowercase")] +pub enum SGenericActorType { + Id(u32), + Name(String), +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +pub struct SUserType(SGenericActorType); + +impl SUserType { + pub(super) fn fetch_id(&self) -> Option { + match &self.0 { + SGenericActorType::Id(id) => Some(*id), + SGenericActorType::Name(name) => match User::from_name(name) { + Ok(Some(user)) => Some(user.uid.as_raw()), + _ => None, + }, + } + } + pub fn fetch_user(&self) -> Option { + match &self.0 { + SGenericActorType::Id(id) => User::from_uid((*id).into()).ok().flatten(), + SGenericActorType::Name(name) => User::from_name(name).ok().flatten(), + } + } + pub fn fetch_eq(&self, other: &Self) -> bool { + let uid = self.fetch_id(); + let ouid = other.fetch_id(); + match (uid, ouid) { + (Some(uid), Some(ouid)) => uid == ouid, + _ => false, + } + } +} + +impl fmt::Display for SUserType { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match &self.0 { + SGenericActorType::Id(id) => write!(f, "{}", id), + SGenericActorType::Name(name) => write!(f, "{}", name), + } + } +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +pub struct SGroupType(SGenericActorType); + +impl fmt::Display for SGroupType { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match &self.0 { + SGenericActorType::Id(id) => write!(f, "{}", id), + SGenericActorType::Name(name) => write!(f, "{}", name), + } + } +} + +impl SGroupType { + pub(super) fn fetch_id(&self) -> Option { + match &self.0 { + SGenericActorType::Id(id) => Some(*id), + SGenericActorType::Name(name) => match Group::from_name(name) { + Ok(Some(group)) => Some(group.gid.as_raw()), + _ => None, + }, + } + } + pub fn fetch_group(&self) -> Option { + match &self.0 { + SGenericActorType::Id(id) => Group::from_gid((*id).into()).ok().flatten(), + SGenericActorType::Name(name) => Group::from_name(name).ok().flatten(), + } + } +} + +impl std::fmt::Display for SGenericActorType { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + SGenericActorType::Id(id) => write!(f, "{}", id), + SGenericActorType::Name(name) => write!(f, "{}", name), + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, EnumIs)] +#[serde(untagged)] +pub enum SGroups { + Single(SGroupType), + Multiple(Vec), +} + +impl SGroups { + pub fn len(&self) -> usize { + match self { + SGroups::Single(_) => 1, + SGroups::Multiple(groups) => groups.len(), + } + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl<'de> Deserialize<'de> for SGenericActorType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct IdVisitor; + + impl<'de> Visitor<'de> for IdVisitor { + type Value = SGenericActorType; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("user ID as a number or string") + } + + fn visit_u32(self, id: u32) -> Result + where + E: de::Error, + { + Ok(SGenericActorType::Id(id)) + } + + fn visit_str(self, id: &str) -> Result + where + E: de::Error, + { + let rid: Result = id.parse(); + match rid { + Ok(id) => Ok(SGenericActorType::Id(id)), + Err(_) => Ok(SGenericActorType::Name(id.to_string())), + } + } + } + + deserializer.deserialize_any(IdVisitor) + } +} + +impl From for SUserType { + fn from(id: u32) -> Self { + SUserType(id.into()) + } +} + +impl From for SGroupType { + fn from(id: u32) -> Self { + SGroupType(id.into()) + } +} + +impl From<&str> for SUserType { + fn from(name: &str) -> Self { + SUserType(name.into()) + } +} + +impl From<&str> for SGroupType { + fn from(name: &str) -> Self { + SGroupType(name.into()) + } +} + +impl From for SGroupType { + fn from(group: Group) -> Self { + SGroupType(SGenericActorType::Id(group.gid.as_raw())) + } +} + +impl From<&str> for SGenericActorType { + fn from(name: &str) -> Self { + SGenericActorType::Name(name.into()) + } +} + +impl From for SGenericActorType { + fn from(id: u32) -> Self { + SGenericActorType::Id(id) + } +} + +impl PartialEq for SUserType { + fn eq(&self, other: &User) -> bool { + let uid = self.fetch_id(); + match uid { + Some(uid) => uid == other.uid.as_raw(), + None => false, + } + } +} + +impl PartialEq for SUserType { + fn eq(&self, other: &str) -> bool { + self.eq(&SUserType::from(other)) + } +} + +impl PartialEq for SGroupType { + fn eq(&self, other: &str) -> bool { + self.eq(&SGroupType::from(other)) + } +} + +impl PartialEq for SUserType { + fn eq(&self, other: &u32) -> bool { + self.eq(&SUserType::from(*other)) + } +} + +impl PartialEq for SGroupType { + fn eq(&self, other: &u32) -> bool { + self.eq(&SGroupType::from(*other)) + } +} + +impl PartialEq for SGroupType { + fn eq(&self, other: &Group) -> bool { + let gid = self.fetch_id(); + match gid { + Some(gid) => gid == other.gid.as_raw(), + None => false, + } + } +} + +impl PartialEq<[SGroupType; N]> for SGroups { + fn eq(&self, other: &[SGroupType; N]) -> bool { + match self { + SGroups::Single(group) => { + if N == 1 { + group == &other[0] + } else { + false + } + } + SGroups::Multiple(groups) => { + if groups.len() == N { + groups.iter().zip(other.iter()).all(|(a, b)| a == b) + } else { + false + } + } + } + } +} + +impl From<[SGroupType; N]> for SGroups { + fn from(groups: [SGroupType; N]) -> Self { + if N == 1 { + SGroups::Single(groups[0].to_owned()) + } else { + SGroups::Multiple(groups.iter().map(|x| x.to_owned()).collect()) + } + } +} + +impl FromIterator for SGroups { + fn from_iter>(iter: I) -> Self { + let mut iter = iter.into_iter(); + let first = iter.next().unwrap(); + let mut groups: Vec = vec![first.as_str().into()]; + for group in iter { + groups.push(group.as_str().into()); + } + if groups.len() == 1 { + SGroups::Single(groups[0].to_owned()) + } else { + SGroups::Multiple(groups) + } + } +} + +impl From<[&str; N]> for SGroups { + fn from(groups: [&str; N]) -> Self { + if N == 1 { + SGroups::Single(groups[0].into()) + } else { + SGroups::Multiple(groups.iter().map(|&x| x.into()).collect()) + } + } +} + +impl From> for SGroups { + fn from(groups: Vec) -> Self { + if groups.len() == 1 { + SGroups::Single(groups[0].into()) + } else { + SGroups::Multiple(groups.into_iter().map(|x| x.into()).collect()) + } + } +} + +impl From> for SGroups { + fn from(groups: Vec) -> Self { + if groups.len() == 1 { + SGroups::Single(groups[0].clone()) + } else { + SGroups::Multiple(groups) + } + } +} + +impl From for SGroups { + fn from(group: u32) -> Self { + SGroups::Single(group.into()) + } +} + +impl From<&str> for SGroups { + fn from(group: &str) -> Self { + SGroups::Single(group.into()) + } +} + +impl PartialEq> for SGroups { + fn eq(&self, other: &Vec) -> bool { + match self { + SGroups::Single(actor) => { + if other.len() == 1 { + return actor == &other[0]; + } + } + SGroups::Multiple(actors) => { + if actors.len() == other.len() { + return actors.iter().all(|actor| other.iter().any(|x| actor == x)); + } + } + } + false + } +} + +impl core::fmt::Display for SGroups { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SGroups::Single(group) => { + write!(f, "{}", group) + } + SGroups::Multiple(groups) => { + write!(f, "{:?}", groups) + } + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum SActor { + #[serde(rename = "user")] + User { + #[serde(alias = "name", skip_serializing_if = "Option::is_none")] + id: Option, + #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] + _extra_fields: Map, + }, + #[serde(rename = "group")] + Group { + #[serde(alias = "names", skip_serializing_if = "Option::is_none")] + groups: Option, + #[serde(default, flatten)] + _extra_fields: Map, + }, + #[serde(untagged)] + Unknown(Value), +} + +#[bon] +impl SActor { + #[builder(finish_fn = build)] + pub fn user( + #[builder(start_fn, into)] id: SUserType, + #[builder(default, with = <_>::from_iter)] _extra_fields: Map, + ) -> Self { + SActor::User { + id: Some(id), + _extra_fields, + } + } + #[builder(finish_fn = build)] + pub fn group( + #[builder(start_fn, into)] groups: SGroups, + #[builder(default, with = <_>::from_iter)] _extra_fields: Map, + ) -> Self { + SActor::Group { + groups: Some(groups), + _extra_fields, + } + } +} + +impl core::fmt::Display for SActor { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SActor::User { id, _extra_fields } => { + write!(f, "User: {}", id.as_ref().unwrap()) + } + SActor::Group { + groups, + _extra_fields, + } => { + write!(f, "Group: {}", groups.as_ref().unwrap()) + } + SActor::Unknown(unknown) => { + write!(f, "Unknown: {}", unknown) + } + } + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_suser_type_creation() { + let user_by_id = SUserType::from(0); + let user_by_name = SUserType::from("testuser"); + + assert_eq!(user_by_id.to_string(), "0"); + assert_eq!(user_by_name.to_string(), "testuser"); + } + #[test] + fn test_fetch_id() { + let user = SUserType::from(0); + assert_eq!(user.fetch_id(), Some(0)); + + let group = SGroupType::from(0); + assert_eq!(group.fetch_id(), Some(0)); + } + #[test] + fn test_fetch_user() { + let user = SUserType::from("testuser"); + assert!(user.fetch_user().is_none()); + let user_by_id = SUserType::from(0); + assert!(user_by_id.fetch_user().is_some()); + } + + #[test] + fn test_sgroups_multiple() { + let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from(200)]); + + assert_eq!(groups.len(), 2); + assert!(!groups.is_empty()); + + if let SGroups::Multiple(group_list) = groups { + assert_eq!(group_list[0].to_string(), "0"); + assert_eq!(group_list[1].to_string(), "200"); + } else { + panic!("Expected SGroups::Multiple"); + } + } + + #[test] + fn test_fech_group() { + let group = SGroupType::from(0); + assert_eq!( + group.fetch_group(), + Some(Group::from_gid(0.into()).unwrap().unwrap()) + ); + + let group = SGroupType::from("root"); + assert_eq!( + group.fetch_group(), + Some(Group::from_name("root").unwrap().unwrap()) + ); + } + + #[test] + fn test_is_empty() { + let groups = SGroups::Multiple(vec![]); + assert!(groups.is_empty()); + } +} diff --git a/rar-common/src/database/finder.rs b/rar-common/src/database/finder.rs index caabd567..9c197f75 100644 --- a/rar-common/src/database/finder.rs +++ b/rar-common/src/database/finder.rs @@ -7,22 +7,23 @@ use std::{ rc::{Rc, Weak}, }; +use bon::Builder; use capctl::CapSet; use glob::Pattern; use log::{debug, warn}; use nix::{ libc::dev_t, - unistd::{Group, Pid, User}, + unistd::{Gid, Group, Pid, Uid, User}, }; #[cfg(feature = "pcre2")] use pcre2::bytes::RegexBuilder; + use strum::EnumIs; use crate::database::{ + actor::SActor, options::{Opt, OptStack}, - structs::{ - SActor, SActorType, SCommand, SCommands, SConfig, SGroups, SRole, STask, SetBehavior, - }, + structs::{SCommand, SCommands, SConfig, SRole, STask, SUserChooser, SetBehavior}, }; use crate::util::{capabilities_are_exploitable, final_path, parse_conf_command}; use crate::{ @@ -31,17 +32,22 @@ use crate::{ }; use bitflags::bitflags; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +use super::{ + actor::{SGroupType, SGroups, SUserType}, + FilterMatcher, +}; + +#[derive(Debug, PartialEq, Eq, Clone, EnumIs)] pub enum MatchError { - NoMatch, - Conflict, + NoMatch(String), + Conflict(String), } impl Display for MatchError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - MatchError::NoMatch => write!(f, "No match"), - MatchError::Conflict => write!(f, "Conflict"), + MatchError::NoMatch(reason) => write!(f, "No match because : {}", reason), + MatchError::Conflict(reason) => write!(f, "Conflict because : {}", reason), } } } @@ -49,8 +55,8 @@ impl Display for MatchError { impl Error for MatchError { fn description(&self) -> &str { match self { - MatchError::NoMatch => "No match", - MatchError::Conflict => "Conflict", + MatchError::NoMatch(_) => "No match", + MatchError::Conflict(_) => "Conflict", } } } @@ -60,7 +66,7 @@ pub struct ExecSettings { pub exec_path: PathBuf, pub exec_args: Vec, pub opt: OptStack, - pub setuid: Option, + pub setuid: Option, pub setgroups: Option, pub caps: Option, pub task: Weak>, @@ -121,25 +127,53 @@ impl PartialEq for ExecSettings { #[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, EnumIs)] #[repr(u32)] -pub enum UserMin { +// Matching user groups for the role +pub enum ActorMatchMin { UserMatch, GroupMatch(usize), NoMatch, } #[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] -#[repr(u32)] -pub enum SetuidMin { - Undefined, - NoSetuidNoSetgid, - Setgid(usize), - Setuid, - SetuidSetgid(usize), - SetgidRoot(usize), - SetuidNotrootSetgidRoot(usize), - SetuidRoot, - SetuidRootSetgid(usize), - SetuidSetgidRoot(usize), + +// Matching setuid and setgid for the role +struct SetuidMin { + is_root: bool, +} +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +struct SetgidMin { + is_root: bool, + nb_groups: usize, +} +impl PartialOrd for SetgidMin { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for SetgidMin { + fn cmp(&self, other: &Self) -> Ordering { + self.is_root + .cmp(&other.is_root) + .then_with(|| self.nb_groups.cmp(&other.nb_groups)) + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)] +pub struct SetUserMin { + uid: Option, + gid: Option, +} +impl PartialOrd for SetUserMin { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for SetUserMin { + fn cmp(&self, other: &Self) -> Ordering { + self.uid + .cmp(&other.uid) + .then_with(|| self.gid.cmp(&other.gid)) + } } #[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] @@ -171,21 +205,21 @@ pub struct SecurityMin(u32); bitflags! { impl SecurityMin: u32 { - const DisableBounding = 0b00001; - const EnableRoot = 0b00010; - const KeepPath = 0b00100; - const KeepUnsafePath = 0b01000; - const KeepEnv = 0b10000; - const SkipAuth = 0b100000; + const DisableBounding = 0b000001; + const EnableRoot = 0b000010; + const KeepEnv = 0b000100; + const KeepPath = 0b001000; + const KeepUnsafePath = 0b010000; + const SkipAuth = 0b100000; } } #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub struct Score { - pub user_min: UserMin, + pub user_min: ActorMatchMin, pub cmd_min: CmdMin, pub caps_min: CapsMin, - pub setuid_min: SetuidMin, + pub setuser_min: SetUserMin, pub security_min: SecurityMin, } @@ -193,7 +227,7 @@ impl Score { pub fn prettyprint(&self) -> String { format!( "{:?}, {:?}, {:?}, {:?}, {:?}", - self.user_min, self.cmd_min, self.caps_min, self.setuid_min, self.security_min + self.user_min, self.cmd_min, self.caps_min, self.setuser_min, self.security_min ) } @@ -206,7 +240,7 @@ impl Score { self.cmd_min .cmp(&other.cmd_min) .then(self.caps_min.cmp(&other.caps_min)) - .then(self.setuid_min.cmp(&other.setuid_min)) + .then(self.setuser_min.cmp(&other.setuser_min)) .then(self.security_min.cmp(&other.security_min)) } } @@ -235,14 +269,45 @@ impl Ord for Score { } } -#[derive(Debug)] +#[derive(Debug, Builder)] pub struct Cred { - pub user: User, + #[builder(field)] pub groups: Vec, + #[builder(field = User::from_uid(Uid::current()).unwrap().unwrap())] + pub user: User, pub tty: Option, + #[builder(default = nix::unistd::getppid(), into)] pub ppid: Pid, } +impl CredBuilder { + pub fn user_id(mut self, uid: impl Into) -> Self { + self.user = User::from_uid(uid.into()).unwrap().unwrap(); + self + } + pub fn user_name(mut self, name: impl Into) -> Self { + self.user = User::from_name(&name.into()).unwrap().unwrap(); + self + } + pub fn group_id(mut self, gid: impl Into) -> Self { + self.groups + .push(Group::from_gid(gid.into()).unwrap().unwrap()); + self + } + pub fn group_name(mut self, name: impl Into) -> Self { + self.groups + .push(Group::from_name(&name.into()).unwrap().unwrap()); + self + } + pub fn groups(mut self, groups: Vec) -> Self { + self.groups = groups + .iter() + .map(|gid| Group::from_gid(*gid).unwrap().unwrap()) + .collect(); + self + } +} + #[derive(Clone, Debug)] pub struct TaskMatch { pub score: Score, @@ -255,7 +320,7 @@ impl TaskMatch { } pub fn user_matching(&self) -> bool { - self.score.user_min != UserMin::NoMatch + self.score.user_min != ActorMatchMin::NoMatch } pub fn command_matching(&self) -> bool { @@ -279,10 +344,10 @@ impl Default for TaskMatch { fn default() -> Self { TaskMatch { score: Score { - user_min: UserMin::NoMatch, + user_min: ActorMatchMin::NoMatch, cmd_min: CmdMin::empty(), caps_min: CapsMin::Undefined, - setuid_min: SetuidMin::Undefined, + setuser_min: SetUserMin::default(), security_min: SecurityMin::empty(), }, settings: ExecSettings::new(), @@ -290,12 +355,6 @@ impl Default for TaskMatch { } } -#[derive(Debug, Default)] -pub struct FilterMatcher { - pub role: Option, - pub task: Option, -} - pub trait TaskMatcher { fn matches( &self, @@ -306,7 +365,7 @@ pub trait TaskMatcher { } pub trait CredMatcher { - fn user_matches(&self, user: &Cred) -> UserMin; + fn user_matches(&self, user: &Cred) -> ActorMatchMin; } fn find_from_envpath(needle: &PathBuf) -> Option { @@ -323,7 +382,7 @@ fn find_from_envpath(needle: &PathBuf) -> Option { None } -fn match_path(input_path: &String, role_path: &String) -> CmdMin { +fn match_path(input_path: &str, role_path: &String) -> CmdMin { if role_path == "**" { return CmdMin::FullWildcardPath; } @@ -373,7 +432,9 @@ fn evaluate_regex_cmd(role_args: String, commandline: String) -> Result>>) -> SecurityMin { } } -fn is_root(actortype: &SActorType) -> bool { - match actortype { - SActorType::Id(id) => *id == 0, - SActorType::Name(name) => name == "root", - } +fn group_is_root(actortype: &SGroupType) -> bool { + (*actortype).fetch_id().map_or(false, |id| id == 0) +} + +fn user_is_root(actortype: &SUserType) -> bool { + (*actortype).fetch_id().map_or(false, |id| id == 0) } fn groups_contains_root(list: Option<&SGroups>) -> bool { if let Some(list) = list { match list { - SGroups::Single(group) => is_root(&group), - SGroups::Multiple(groups) => groups.iter().any(is_root), + SGroups::Single(group) => group_is_root(group), + SGroups::Multiple(groups) => groups.iter().any(group_is_root), } } else { false @@ -498,52 +560,100 @@ fn groups_contains_root(list: Option<&SGroups>) -> bool { fn groups_len(groups: Option<&SGroups>) -> usize { match groups { - Some(groups) => match groups { - SGroups::Single(_) => 1, - SGroups::Multiple(groups) => groups.len(), - }, + Some(groups) => groups.len(), None => 0, } } fn get_setuid_min( - setuid: Option<&SActorType>, + setuid: Option<&SUserType>, setgid: Option<&SGroups>, security_min: &SecurityMin, -) -> SetuidMin { +) -> SetUserMin { match (setuid, setgid) { (Some(setuid), setgid) => { if security_min.contains(SecurityMin::EnableRoot) { // root is privileged - if is_root(setuid) { + if user_is_root(setuid) { if groups_contains_root(setgid) { - SetuidMin::SetuidSetgidRoot(groups_len(setgid)) + SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: Some(SetgidMin { + is_root: true, + nb_groups: (groups_len(setgid)), + }), + } } else if setgid.is_none() || groups_len(setgid) == 0 { - SetuidMin::SetuidRoot + SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: None, + } } else { - SetuidMin::SetuidRootSetgid(groups_len(setgid)) + SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: Some(SetgidMin { + is_root: false, + nb_groups: (groups_len(setgid)), + }), + } } } else if groups_contains_root(setgid) { - SetuidMin::SetuidNotrootSetgidRoot(groups_len(setgid)) + SetUserMin { + uid: Some(SetuidMin { is_root: false }), + gid: Some(SetgidMin { + is_root: true, + nb_groups: (groups_len(setgid)), + }), + } } else if setgid.is_none() || groups_len(setgid) == 0 { - SetuidMin::Setuid + SetUserMin { + uid: Some(SetuidMin { is_root: false }), + gid: None, + } } else { - SetuidMin::SetuidSetgid(groups_len(setgid)) + SetUserMin { + uid: Some(SetuidMin { is_root: false }), + gid: Some(SetgidMin { + is_root: false, + nb_groups: (groups_len(setgid)), + }), + } } } else { // root is a user - SetuidMin::SetuidSetgid(groups_len(setgid)) + SetUserMin { + uid: Some(SetuidMin { is_root: false }), + gid: Some(SetgidMin { + is_root: false, + nb_groups: (groups_len(setgid)), + }), + } } } (None, setgid) => { let len = groups_len(setgid); if len == 0 { - SetuidMin::NoSetuidNoSetgid + SetUserMin { + uid: None, + gid: None, + } } else if security_min.contains(SecurityMin::EnableRoot) && groups_contains_root(setgid) { - SetuidMin::SetgidRoot(len) + SetUserMin { + uid: None, + gid: Some(SetgidMin { + is_root: true, + nb_groups: len, + }), + } } else { - SetuidMin::Setgid(len) + SetUserMin { + uid: None, + gid: Some(SetgidMin { + is_root: false, + nb_groups: len, + }), + } } } } @@ -560,11 +670,13 @@ impl TaskMatcher for Rc> { if let Some(task) = &cmd_opt.task { if task != &self.as_ref().borrow().name.to_string() { debug!("Task {} does not match", self.as_ref().borrow().name); - return Err(MatchError::NoMatch); + return Err(MatchError::NoMatch("Task name does not match".to_string())); } } } debug!("Matching task {}", self.as_ref().borrow().name); + + // Match initial task commands let TaskMatch { mut score, mut settings, @@ -573,6 +685,8 @@ impl TaskMatcher for Rc> { .borrow() .commands .matches(user, cmd_opt, command)?; + + // Process capabilities and security let capset = self .as_ref() .borrow() @@ -582,15 +696,99 @@ impl TaskMatcher for Rc> { .map(|caps| caps.to_capset()); score.caps_min = get_caps_min(&capset); score.security_min = get_security_min(&self.as_ref().borrow().options); - let setuid = &self.as_ref().borrow().cred.setuid; + if cmd_opt + .as_ref() + .and_then(|filter| filter.env_behavior) // if the command wants to override the behavior + .as_ref() + .is_some_and(|behavior| { + settings + .opt + .to_opt() // at this point we own the opt structure + .as_ref() + .borrow() + .env + .as_ref() + .is_some_and(|env| { + !env.override_behavior.is_some_and(|b| b) + || env.default_behavior == *behavior + }) + // but the polcy deny it and the behavior is not the same as the default one + // we return NoMatch + // (explaination: if the behavior is the same as the default one, we don't override it) + }) + { + return Err(MatchError::NoMatch( + "The user wants to override the behavior but the policy deny it".to_string(), + )); + } + // Processing setuid + let setuid: Option = self.as_ref().borrow().cred.setuid.clone(); + let setuid_result = match setuid { + Some(SUserChooser::Actor(s)) => Some(s), + Some(SUserChooser::ChooserStruct(t)) => { + match cmd_opt.as_ref().and_then(|cmd| cmd.user.as_ref()) { + None => { + debug!( + "Aucun utilisateur spécifié dans la commande, fallback utilisé : {:?}", + t.fallback + ); + Some(t.fallback.clone()) // Retourne le fallback si aucun utilisateur n'est spécifié + } + Some(user) => { + debug!("Utilisateur spécifié dans la commande : {}", user); + + // Comparer l'utilisateur spécifié avec le fallback + if user.fetch_eq(&t.fallback) { + debug!( + "L'utilisateur spécifié dans la commande correspond au fallback !" + ); + Some(t.fallback.clone()) // Si l'utilisateur correspond au fallback, utiliser le fallback + } else if t.sub.iter().any(|s| s.fetch_eq(user)) { + // Si l'utilisateur est explicitement interdit dans `sub` + return Err(MatchError::NoMatch( + "L'utilisateur est interdit dans sub.".into(), + )); + } else if t.add.iter().any(|s| s.fetch_eq(user)) { + // Si l'utilisateur est explicitement autorisé dans `add` + + Some(user.clone()) // Retourner une erreur immédiate + } else { + // Aucun match explicite, appliquer le comportement par défaut + match t.default { + SetBehavior::None => { + return Err(MatchError::NoMatch( + "Aucun comportement par défaut applicable.".into(), + )); // Aucun utilisateur par défaut + } + SetBehavior::All => { + debug!("Tous les utilisateurs sont acceptés."); + Some(user.clone()) // Tout utilisateur accepté + } + } + } + } + } + } + None => None, + }; + + // Set gid processing let setgid = &self.as_ref().borrow().cred.setgid; - score.setuid_min = get_setuid_min(setuid.as_ref(), setgid.as_ref(), &score.security_min); - settings.setuid = setuid.clone(); + // Calculate setuid and setgid minimum + score.setuser_min = + get_setuid_min(setuid_result.as_ref(), setgid.as_ref(), &score.security_min); + + // Update task settings + settings.setuid = setuid_result.clone(); settings.setgroups = setgid.clone(); settings.caps = capset; + + // Get options stack from the task let stack = OptStack::from_task(self.clone()); settings.opt = stack; + + // Return the final TaskMatch Ok(TaskMatch { score, settings }) } } @@ -616,7 +814,7 @@ impl TaskMatcher for SCommands { let is_forbidden = get_cmd_min(input_command, &self.sub); if !is_forbidden.is_empty() { debug!("Command is forbidden"); - return Err(MatchError::NoMatch); + return Err(MatchError::NoMatch("Command is forbidden".to_string())); } // otherwise, we check if behavior is No command allowed by default if get_default_behavior(&self.default_behavior).is_none() { @@ -624,7 +822,7 @@ impl TaskMatcher for SCommands { // if the behavior is No command by default, we check if the command is allowed explicitly. min_score = get_cmd_min(input_command, &self.add); if min_score.is_empty() { - return Err(MatchError::NoMatch); + return Err(MatchError::NoMatch("Command is not allowed".to_string())); } } else { min_score = CmdMin::all(); @@ -644,10 +842,10 @@ impl TaskMatcher for SCommands { Ok(TaskMatch { score: Score { - user_min: UserMin::NoMatch, + user_min: ActorMatchMin::NoMatch, cmd_min: min_score, caps_min: CapsMin::Undefined, - setuid_min: SetuidMin::Undefined, + setuser_min: SetUserMin::default(), security_min: SecurityMin::empty(), }, settings, @@ -680,30 +878,30 @@ fn match_groups(groups: &[Group], role_groups: &[SGroups]) -> bool { } impl CredMatcher for Rc> { - fn user_matches(&self, user: &Cred) -> UserMin { + fn user_matches(&self, user: &Cred) -> ActorMatchMin { let borrow = self.as_ref().borrow(); if PluginManager::notify_duty_separation(&self.as_ref().borrow(), user).is_deny() { warn!("You are forbidden to use a role due to a conflict of interest, please contact your administrator"); - return UserMin::NoMatch; + return ActorMatchMin::NoMatch; } let matches = borrow.actors.iter().filter_map(|actor| { match actor { SActor::User { id, .. } => { if let Some(id) = id { if *id == user.user { - return Some(UserMin::UserMatch); + return Some(ActorMatchMin::UserMatch); } } } SActor::Group { groups, .. } => { if let Some(groups) = groups.as_ref() { if match_groups(&user.groups, &[groups.clone()]) { - return Some(UserMin::GroupMatch(groups.len())); + return Some(ActorMatchMin::GroupMatch(groups.len())); } } } SActor::Unknown(element) => { - let min = PluginManager::notify_user_matcher(&as_borrow!(self), user, &element); + let min = PluginManager::notify_user_matcher(&as_borrow!(self), user, element); if !min.is_no_match() { return Some(min); } @@ -711,7 +909,7 @@ impl CredMatcher for Rc> { } None }); - let min = matches.min().unwrap_or(UserMin::NoMatch); + let min = matches.min().unwrap_or(ActorMatchMin::NoMatch); debug!( "Role {} : User {} matches with {:?}", borrow.name, user.user.name, min @@ -746,10 +944,10 @@ impl TaskMatcher for Vec>> { } } Err(err) => match err { - MatchError::NoMatch => { + MatchError::NoMatch(_) => { continue; } - MatchError::Conflict => { + MatchError::Conflict(_) => { return Err(err); } }, @@ -757,11 +955,11 @@ impl TaskMatcher for Vec>> { } debug!("nmatch = {}", nmatch); if nmatch == 0 { - Err(MatchError::NoMatch) + Err(MatchError::NoMatch("No tasks matched".into())) } else if nmatch == 1 { Ok(min_task) } else { - Err(MatchError::Conflict) + Err(MatchError::Conflict("Multiple tasks matched".into())) } } } @@ -792,7 +990,7 @@ impl TaskMatcher for Vec>> { } } Err(err) => { - if err == MatchError::NoMatch { + if err.is_no_match() { continue; } else { return Err(err); @@ -801,11 +999,11 @@ impl TaskMatcher for Vec>> { } } if nmatch == 0 { - Err(MatchError::NoMatch) + Err(MatchError::NoMatch("No roles matched".into())) } else if nmatch == 1 { Ok(min_role) } else { - Err(MatchError::Conflict) + Err(MatchError::Conflict("Multiple roles matched".into())) } } } @@ -820,7 +1018,7 @@ impl TaskMatcher for Rc> { if let Some(cmd_opt) = cmd_opt { if let Some(role) = &cmd_opt.role { if role != &self.as_ref().borrow().name { - return Err(MatchError::NoMatch); + return Err(MatchError::NoMatch("Role name does not match".to_string())); } } } @@ -840,11 +1038,11 @@ impl TaskMatcher for Rc> { nmatch = 1; } } - Err(MatchError::NoMatch) => { + Err(MatchError::NoMatch(_)) => { nmatch = 0; } - Err(MatchError::Conflict) => { - return Err(MatchError::Conflict); + Err(MatchError::Conflict(msg)) => { + return Err(MatchError::Conflict(msg)); } } min_role.score.user_min = user_min; @@ -863,7 +1061,7 @@ impl TaskMatcher for Rc> { min_role.score.prettyprint() ); if nmatch == 0 { - Err(MatchError::NoMatch) + Err(MatchError::NoMatch("No tasks matched".into())) } else if nmatch == 1 { debug!( "=== Role {} === : Match for task {}\nScore : {}", @@ -873,13 +1071,13 @@ impl TaskMatcher for Rc> { ); Ok(min_role) } else { - Err(MatchError::Conflict) + Err(MatchError::Conflict("Multiple tasks matched".into())) } } } fn plugin_role_match( - user_min: UserMin, + user_min: ActorMatchMin, borrow: std::cell::Ref<'_, SRole>, user: &Cred, cmd_opt: &Option, @@ -943,9 +1141,9 @@ impl TaskMatcher for Rc> { } // we ignore error, because it's not a match } if tasks.is_empty() { - Err(MatchError::NoMatch) + Err(MatchError::NoMatch("No roles matched".into())) } else if tasks.len() > 1 { - Err(MatchError::Conflict) + Err(MatchError::Conflict("Multiple roles matched".into())) } else { debug!( "Config : Matched user {}\n - command {:?}\n - with task {}\n - with role {}\n - with score {:?}", @@ -972,7 +1170,7 @@ mod tests { database::{ make_weak_config, options::{EnvBehavior, PathBehavior, SAuthentication, SBounding, SPrivileged}, - structs::IdTask, + structs::{IdTask, RoleGetter, SCredentials, SSetuidSet}, versionning::Versioning, }, rc_refcell, @@ -980,6 +1178,46 @@ mod tests { use super::*; + fn get_non_root_uid() -> u32 { + // list all users + let passwd = fs::read_to_string("/etc/passwd").unwrap(); + let passwd: Vec<&str> = passwd.split('\n').collect(); + return passwd + .iter() + .map(|line| { + let line: Vec<&str> = line.split(':').collect(); + line[2].parse::().unwrap() + }) + .find(|uid| *uid != 0) + .unwrap(); + } + + #[test] + fn test_find_from_envpath() { + let needle = PathBuf::from("ls"); + let result = find_from_envpath(&needle); + println!("{:?}", result); + assert_eq!(result, Some("/usr/bin/ls".into())); + } + + #[test] + fn test_find_from_envpath_absolute_path() { + // Avec un chemin absolu + let needle = PathBuf::from("/bin/ls"); + let result = find_from_envpath(&needle); + println!("{:?}", result); + assert_eq!(result, None); + } + + #[test] + fn test_find_from_envpath_not_found() { + // Avec un fichier qui n'existe pas dans le PATH. + let needle = PathBuf::from("no_path"); + let result = find_from_envpath(&needle); + println!("{:?}", result); + assert_eq!(result, None); + } + #[test] fn test_match_path() { let result = match_path(&"/bin/ls".to_string(), &"/bin/ls".to_string()); @@ -1082,9 +1320,12 @@ mod tests { #[test] fn test_is_root() { - assert!(is_root(&"root".into())); - assert!(is_root(&0.into())); - assert!(!is_root(&1.into())); + assert!(user_is_root(&"root".into())); + assert!(user_is_root(&0.into())); + assert!(!user_is_root(&1.into())); + assert!(group_is_root(&"root".into())); + assert!(group_is_root(&0.into())); + assert!(!group_is_root(&1.into())); } #[test] @@ -1099,51 +1340,132 @@ mod tests { #[test] fn test_get_setuid_min() { - let mut setuid: Option = Some("root".into()); + let mut setuid: Option = Some("root".into()); let mut setgid = Some(SGroups::Single("root".into())); let security_min = SecurityMin::EnableRoot; assert_eq!( get_setuid_min(setuid.as_ref(), setgid.as_ref(), &security_min), - SetuidMin::SetuidSetgidRoot(1) + SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: Some(SetgidMin { + is_root: true, + nb_groups: 1 + }) + } ); setuid = Some("1".into()); assert_eq!( get_setuid_min(setuid.as_ref(), setgid.as_ref(), &security_min), - SetuidMin::SetuidNotrootSetgidRoot(1) + SetUserMin { + uid: Some(SetuidMin { is_root: false }), + gid: Some(SetgidMin { + is_root: true, + nb_groups: 1 + }) + } ); setgid = Some(SGroups::Multiple(vec![1.into(), 2.into()])); assert_eq!( get_setuid_min(setuid.as_ref(), setgid.as_ref(), &security_min), - SetuidMin::SetuidSetgid(2) + SetUserMin { + uid: Some(SetuidMin { is_root: false }), + gid: Some(SetgidMin { + is_root: false, + nb_groups: 2 + }) + } ); assert_eq!( get_setuid_min(None, setgid.as_ref(), &security_min), - SetuidMin::Setgid(2) + SetUserMin { + uid: None, + gid: Some(SetgidMin { + is_root: false, + nb_groups: 2 + }) + } ); assert_eq!( get_setuid_min(None, None, &security_min), - SetuidMin::NoSetuidNoSetgid + SetUserMin { + uid: None, + gid: None + } ); assert_eq!( get_setuid_min(setuid.as_ref(), None, &security_min), - SetuidMin::Setuid - ) + SetUserMin { + uid: Some(SetuidMin { is_root: false }), + gid: None + } + ); + let setuid: Option = Some("root".into()); + assert_eq!( + get_setuid_min(setuid.as_ref(), None, &security_min), + SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: None, + } + ); + setgid = Some(SGroups::Multiple(vec![1.into(), 2.into()])); + assert_eq!( + get_setuid_min(setuid.as_ref(), setgid.as_ref(), &security_min), + SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: Some(SetgidMin { + is_root: false, + nb_groups: 2, + }), + } + ); + setgid = Some(SGroups::Multiple(vec![])); + assert_eq!( + get_setuid_min(setuid.as_ref(), setgid.as_ref(), &security_min), + SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: None, + } + ); + + setgid = Some(SGroups::Multiple(vec![0.into()])); + assert_eq!( + get_setuid_min(None, setgid.as_ref(), &security_min), + SetUserMin { + uid: None, + gid: Some(SetgidMin { + is_root: true, + nb_groups: 1, + }), + } + ); } #[test] fn test_score_cmp() { let score1 = Score { - user_min: UserMin::UserMatch, + user_min: ActorMatchMin::UserMatch, cmd_min: CmdMin::Match, caps_min: CapsMin::CapsAll, - setuid_min: SetuidMin::SetuidSetgidRoot(1), + setuser_min: SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: Some(SetgidMin { + is_root: true, + nb_groups: 1, + }), + }, security_min: SecurityMin::DisableBounding | SecurityMin::EnableRoot, }; let mut score2 = Score { - user_min: UserMin::UserMatch, + user_min: ActorMatchMin::UserMatch, cmd_min: CmdMin::Match, caps_min: CapsMin::CapsAll, - setuid_min: SetuidMin::SetuidSetgidRoot(1), + setuser_min: SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: Some(SetgidMin { + is_root: true, + nb_groups: 1, + }), + }, security_min: SecurityMin::DisableBounding, }; assert_eq!(score1.cmp(&score2), Ordering::Greater); @@ -1154,15 +1476,46 @@ mod tests { assert_eq!(score1.clamp(score2, score2), score2); score2.security_min = SecurityMin::DisableBounding | SecurityMin::EnableRoot; assert_eq!(score1.cmp(&score2), Ordering::Equal); - score2.setuid_min = SetuidMin::SetuidSetgidRoot(2); + score2.setuser_min = SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: Some(SetgidMin { + is_root: true, + nb_groups: 2, + }), + }; assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.setuid_min = SetuidMin::SetuidNotrootSetgidRoot(2); + score2.setuser_min = SetUserMin { + uid: Some(SetuidMin { is_root: false }), + gid: Some(SetgidMin { + is_root: true, + nb_groups: 2, + }), + }; assert_eq!(score1.cmp(&score2), Ordering::Greater); - score2.setuid_min = SetuidMin::SetuidRootSetgid(2); + score2.setuser_min = SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: Some(SetgidMin { + is_root: false, + nb_groups: 2, + }), + }; assert_eq!(score1.cmp(&score2), Ordering::Greater); - score2.setuid_min = SetuidMin::SetuidSetgid(2); + score2.setuser_min = SetUserMin { + uid: Some(SetuidMin { is_root: false }), + gid: Some(SetgidMin { + is_root: false, + nb_groups: 2, + }), + }; assert_eq!(score1.cmp(&score2), Ordering::Greater); - score2.setuid_min = SetuidMin::SetuidSetgidRoot(1); + score2.setuser_min = SetUserMin { + uid: Some(SetuidMin { is_root: true }), + gid: Some(SetgidMin { + is_root: true, + nb_groups: 1, + }), + }; + assert_eq!(score1.cmp(&score2), Ordering::Equal); score2.caps_min = CapsMin::CapsAdmin(1); assert_eq!(score1.cmp(&score2), Ordering::Greater); score2.caps_min = CapsMin::CapsNoAdmin(1); @@ -1181,23 +1534,18 @@ mod tests { assert_eq!(score1.cmp(&score2), Ordering::Less); score2.cmd_min = CmdMin::Match; assert_eq!(score1.cmp(&score2), Ordering::Equal); - score2.user_min = UserMin::GroupMatch(1); + score2.user_min = ActorMatchMin::GroupMatch(1); assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.user_min = UserMin::NoMatch; + score2.user_min = ActorMatchMin::NoMatch; assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.user_min = UserMin::UserMatch; + score2.user_min = ActorMatchMin::UserMatch; assert_eq!(score1.cmp(&score2), Ordering::Equal); } fn setup_test_config(num_roles: usize) -> Rc> { - let config = Rc::new(SConfig::default().into()); - for i in 0..num_roles { - let mut role = SRole::default(); - role.name = format!("role{}", i); - role._config = Some(Rc::downgrade(&config)); - config.as_ref().borrow_mut().roles.push(rc_refcell!(role)); - } - config + SConfig::builder() + .roles((0..num_roles).map(|i| SRole::builder(format!("role{}", i)).build())) + .build() } fn setup_test_role( @@ -1235,12 +1583,12 @@ mod tests { .as_ref() .borrow_mut() .actors - .push(SActor::from_user_string("root")); + .push(SActor::user("root").build()); role1 .as_ref() .borrow_mut() .actors - .push(SActor::from_user_string("root")); + .push(SActor::user("root").build()); r0_task0 .as_ref() @@ -1274,14 +1622,13 @@ mod tests { r1_task1.as_ref().borrow_mut().cred.capabilities = Some(capset.into()); let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), + user: User::from_uid(Uid::from_raw(0)).unwrap().unwrap(), groups: vec![Group::from_name("root").unwrap().unwrap()], ppid: Pid::from_raw(0), tty: None, }; let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - let result = config.matches(&cred, &None, &command); debug!("Result : {:?}", result); assert!(result.is_ok()); @@ -1293,6 +1640,451 @@ mod tests { assert_eq!(result.role().as_ref().borrow().name, "role0"); } + #[test] + + fn test_setuid_fallback_valid() { + // Configuration de test + let config = setup_test_config(1); // Un seul rôle pour simplifier + let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); + let task = role.as_ref().borrow().tasks[0].clone(); + + // Ajout d'un acteur autorisé + role.as_ref() + .borrow_mut() + .actors + .push(SActor::user("root").build()); + + task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); + + // Définition du `setuid` avec un `fallback` + let fallback_user = SUserType::from(get_non_root_uid()); + let chooser_struct = SSetuidSet { + fallback: fallback_user.clone(), + default: SetBehavior::None, + add: vec![], // Pas d'ajout explicite + sub: vec![], // Pas de restriction explicite + }; + task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); + + // Création des credentials avec l'utilisateur correspondant au `fallback` + let cred = Cred::builder().user_name("root").group_name("root").build(); + + // Commande de test + let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; + // Exécution du match + let filter_matcher = FilterMatcher::builder().user(fallback_user.clone()).build(); + + let result = config.matches(&cred, &Some(filter_matcher), &command); + println!("Résultat matches: {:?}", result); + + // Vérification que le match est réussi + assert!(result.is_ok()); + let result = result.unwrap(); + + // Vérification que l'utilisateur assigné est bien celui du fallback + assert_eq!(result.settings.setuid, Some(fallback_user.clone())); + + println!("Test réussi : L'utilisateur spécifié correspond bien au fallback."); + } + + #[test] + + fn test_setuid_fallback_nonarg_valid() { + // Configuration de test + let config = setup_test_config(1); + let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); + let task = role.as_ref().borrow().tasks[0].clone(); + + // Ajout d'un acteur autorisé + role.as_ref() + .borrow_mut() + .actors + .push(SActor::user("root").build()); + + task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); + + // Définition du `setuid` avec un `fallback` + let fallback_user = SUserType::from(get_non_root_uid()); + let chooser_struct = SSetuidSet { + fallback: fallback_user.clone(), + default: SetBehavior::None, + add: vec![], + sub: vec![], + }; + task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); + + // Création des credentials sans spécifier d'utilisateur + let cred = Cred { + user: User::from_name("root").unwrap().unwrap(), // Utilisateur non spécifié + groups: vec![Group::from_name("root").unwrap().unwrap()], + ppid: Pid::from_raw(0), + tty: None, + }; + + // Commande de test + let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; + + // Exécution du match + let result = config.matches(&cred, &None, &command); + + // Vérification que le match est réussi + assert!(result.is_ok()); + let result = result.unwrap(); + + // Vérification que l'utilisateur assigné est bien celui du fallback + assert_eq!(result.settings.setuid, Some(fallback_user.clone())); + + println!("Test réussi : L'utilisateur spécifié correspond bien au fallback lorsqu'aucun utilisateur valide n'est fourni."); + } + + #[test] + + fn test_setuid_add_valid() { + // Configuration de test + let config = setup_test_config(1); // Un seul rôle pour simplifier + let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); + let task = role.as_ref().borrow().tasks[0].clone(); + + // Ajout d'un acteur autorisé + role.as_ref() + .borrow_mut() + .actors + .push(SActor::user("root").build()); + + task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); + + // Définition du `setuid` avec un `fallback` + let fallback_user = SUserType::from(get_non_root_uid()); + let chooser_struct = SSetuidSet { + fallback: fallback_user.clone(), + default: SetBehavior::None, + add: vec![SUserType::from("root")], // Ajout d'un utilisateur + sub: vec![], // Pas de restriction explicite + }; + task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); + + // Création des credentials avec l'utilisateur correspondant à l'ajout + let cred = Cred { + user: User::from_name("root").unwrap().unwrap(), // Même nom que l'ajout + groups: vec![Group::from_name("root").unwrap().unwrap()], + ppid: Pid::from_raw(0), + tty: None, + }; + + // Commande de test + let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; + + // Exécution du match + let filter_matcher = FilterMatcher::builder().user("root").build(); + + let result = config.matches(&cred, &Some(filter_matcher), &command); + + // Vérification que le match est réussi + assert!(result.is_ok()); + let result = result.unwrap(); + + // Vérification que l'utilisateur assigné est bien celui de l'ajout + assert_eq!(result.settings.setuid, Some(SUserType::from("root"))); + + println!("Test réussi : L'utilisateur spécifié correspond bien à l'ajout."); + } + + #[test] + fn test_setuid_add_sub_invalid() { + // Configuration de test + let config = setup_test_config(1); // Un seul rôle pour simplifier + let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); + let task = role.as_ref().borrow().tasks[0].clone(); + + // Ajout d'un acteur autorisé + role.as_ref() + .borrow_mut() + .actors + .push(SActor::user("root").build()); + + task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); + + // Définition du `setuid` avec un `fallback` + let fallback_user = SUserType::from(1); + let chooser_struct = SSetuidSet { + fallback: fallback_user.clone(), + default: SetBehavior::None, + add: vec![SUserType::from("root")], // Ajout d'un utilisateur + sub: vec![SUserType::from("root")], // Restriction d'un utilisateur + }; + task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); + + // Création des credentials avec l'utilisateur correspondant à l'ajout + let cred = Cred { + user: User::from_name("root").unwrap().unwrap(), // Même nom que l'ajout + groups: vec![Group::from_name("root").unwrap().unwrap()], + ppid: Pid::from_raw(0), + tty: None, + }; + + // Commande de test + let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; + // Exécution du match + let filter_matcher = FilterMatcher::builder().user("root").build(); + + let result = config.matches(&cred, &Some(filter_matcher), &command); + println!("Résultat matches: {:?}", result); + // Vérification que le match est réussi + assert!(result.is_err()); + let result = result.unwrap_err(); + + // Vérification que l'erreur est bien de type `NoMatch` + assert!(result.is_no_match()); + + println!("Test réussi : L'utilisateur spécifié ne correspond pas à la restriction."); + } + + #[test] + fn test_setuid_all_sub_invalid() { + // Configuration de test + let config = setup_test_config(1); // Un seul rôle pour simplifier + let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); + let task = role.as_ref().borrow().tasks[0].clone(); + + // Ajout d'un acteur autorisé + role.as_ref() + .borrow_mut() + .actors + .push(SActor::user("root").build()); + task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); + + // Définition du `setuid` avec un `fallback` + let fallback_user = SUserType::from(get_non_root_uid()); + let chooser_struct = SSetuidSet { + fallback: fallback_user.clone(), + default: SetBehavior::All, + add: vec![], + sub: vec![SUserType::from("root")], // Restriction d'un utilisateur + }; + task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); + + // Création des credentials avec l'utilisateur correspondant à l'ajout + let cred = Cred { + user: User::from_name("root").unwrap().unwrap(), // Même nom que l'ajout + groups: vec![Group::from_name("root").unwrap().unwrap()], + ppid: Pid::from_raw(0), + tty: None, + }; + // Commande de test + let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; + // Exécution du match + let filter_matcher = FilterMatcher::builder().user("root").build(); + + let result = config.matches(&cred, &Some(filter_matcher), &command); + + // Vérification que le match est réussi + assert!(result.is_err()); + let result = result.unwrap_err(); + + // Vérification que l'erreur est bien de type `NoMatch` + assert!(result.is_no_match()); + + println!("Test réussi : L'utilisateur spécifié ne correspond pas "); + } + + #[test] + //echec + fn test_setuid_all_valid() { + // Configuration de test + let config = setup_test_config(1); // Un seul rôle pour simplifier + let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); + let task = role.as_ref().borrow().tasks[0].clone(); + + // Ajout d'un acteur autorisé + role.as_ref() + .borrow_mut() + .actors + .push(SActor::user("root").build()); + + task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); + + // Définition du `setuid` avec un `fallback` + let fallback_user = SUserType::from(get_non_root_uid()); + let chooser_struct = SSetuidSet { + fallback: fallback_user.clone(), + default: SetBehavior::All, + add: vec![], + sub: vec![], + }; + task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); + + // Création des credentials avec l'utilisateur correspondant à l'ajout + let cred = Cred { + user: User::from_name("root").unwrap().unwrap(), // Même nom que l'ajout + groups: vec![Group::from_name("root").unwrap().unwrap()], + ppid: Pid::from_raw(0), + tty: None, + }; + + // Commande de test + let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; + // Exécution du match + let filter_matcher = FilterMatcher::builder().user("root").build(); + + let result = config.matches(&cred, &Some(filter_matcher), &command); + + // Vérification que le match est réussi + assert!(result.is_ok()); + let result = result.unwrap(); + + // Vérification que l'utilisateur assigné est bien celui de l'ajout + assert_eq!(result.settings.setuid, Some(SUserType::from("root"))); + + println!("Test réussi : L'utilisateur spécifié correspond bien à l'ajout."); + } + + #[test] + fn test_setuid_none_invalid() { + // Configuration de test + let config = setup_test_config(1); // Un seul rôle pour simplifier + let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); + let task = role.as_ref().borrow().tasks[0].clone(); + + // Ajout d'un acteur autorisé + role.as_ref() + .borrow_mut() + .actors + .push(SActor::user("root").build()); + + // Définition du `setuid` avec un `fallback` + let fallback_user = SUserType::from(get_non_root_uid()); + let chooser_struct = SSetuidSet { + fallback: fallback_user.clone(), + default: SetBehavior::None, + add: vec![], + sub: vec![], + }; + task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); + + // Création des credentials avec l'utilisateur correspondant à l'ajout + let cred = Cred { + user: User::from_name("root").unwrap().unwrap(), // Même nom que l'ajout + groups: vec![Group::from_name("root").unwrap().unwrap()], + ppid: Pid::from_raw(0), + tty: None, + }; + + // Commande de test + let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; + // Exécution du match + let filter_matcher = FilterMatcher::builder().user("root").build(); + let result = config.matches(&cred, &Some(filter_matcher), &command); + + // Vérification que le match est réussi + assert!(result.is_err()); + let result = result.unwrap_err(); + + // Vérification que l'erreur est bien de type `NoMatch` + assert!(result.is_no_match()); + + println!("Test réussi : L'utilisateur spécifié ne correspond pas "); + } + + #[test] + fn test_setuid_all_add_valid() { + // Configuration de test + let config = setup_test_config(1); // Un seul rôle pour simplifier + let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); + let task = role.as_ref().borrow().tasks[0].clone(); + + task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); + + // Ajout d'un acteur autorisé + role.as_ref() + .borrow_mut() + .actors + .push(SActor::user("root").build()); + + task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); + + // Définition du `setuid` avec un `fallback` + let fallback_user = SUserType::from(get_non_root_uid()); + let chooser_struct = SSetuidSet { + fallback: fallback_user.clone(), + default: SetBehavior::All, + add: vec![SUserType::from("root")], // Ajout d'un utilisateur + sub: vec![], + }; + task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); + + // Création des credentials avec l'utilisateur correspondant à l'ajout + let cred = Cred { + user: User::from_name("root").unwrap().unwrap(), // Même nom que l'ajout + groups: vec![Group::from_name("root").unwrap().unwrap()], + ppid: Pid::from_raw(0), + tty: None, + }; + + // Commande de test + let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; + // Exécution du match + let filter_matcher = FilterMatcher::builder().user("root").build(); + + let result = config.matches(&cred, &Some(filter_matcher), &command); + + // Vérification que le match est réussi + assert!(result.is_ok()); + let result = result.unwrap(); + + // Vérification que l'utilisateur assigné est bien celui de l'ajout + assert_eq!(result.settings.setuid, Some(SUserType::from("root"))); + + println!("Test réussi : L'utilisateur spécifié correspond bien à l'ajout."); + } + + #[test] + fn test_setuid_none_add_invalid() { + // Configuration de test + let config = setup_test_config(1); // Un seul rôle pour simplifier + let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); + let task = role.as_ref().borrow().tasks[0].clone(); + // Ajout d'un acteur autorisé + role.as_ref() + .borrow_mut() + .actors + .push(SActor::user("root").build()); + + task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); + // Définition du `setuid` avec un `fallback` + let fallback_user = SUserType::from(get_non_root_uid()); + let chooser_struct = SSetuidSet { + fallback: fallback_user.clone(), + default: SetBehavior::None, + add: vec![SUserType::from("root")], // Ajout d'un utilisateur + sub: vec![], + }; + task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); + + // Création des credentials avec l'utilisateur correspondant à l'ajout + let cred = Cred { + user: User::from_name("root").unwrap().unwrap(), // Même nom que l'ajout + groups: vec![Group::from_name("root").unwrap().unwrap()], + ppid: Pid::from_raw(0), + tty: None, + }; + + // Commande de test + let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; + // Exécution du match + let filter_matcher = FilterMatcher::builder().user("nouser").build(); + + let result = config.matches(&cred, &Some(filter_matcher), &command); + + // Vérification que le match est réussi + assert!(result.is_err()); + let result = result.unwrap_err(); + + // Vérification que l'erreur est bien de type `NoMatch` + assert!(result.is_no_match()); + + println!("Test réussi : L'utilisateur spécifié ne correspond pas "); + } + #[test] fn test_equal_settings() { let mut settings1 = ExecSettings::new(); @@ -1329,7 +2121,7 @@ mod tests { role.as_ref() .borrow_mut() .actors - .push(SActor::from_user_string("root")); + .push(SActor::user("root").build()); let mut task1 = STask::default(); let mut task2 = STask::default(); task1.name = IdTask::Name("task1".to_string()); @@ -1353,7 +2145,7 @@ mod tests { let command = vec!["/bin/ls".to_string()]; let result = role.matches(&cred, &None, &command); assert!(result.is_ok()); - assert!(role.as_ref().borrow_mut()[0] + role.as_ref().borrow_mut()[0] .as_ref() .borrow_mut() .options @@ -1365,7 +2157,7 @@ mod tests { .as_mut() .unwrap() .add - .insert("/test".to_string())); + .replace(["/test".to_string()].iter().cloned().collect()); let result = role.matches(&cred, &None, &command); assert!(result.is_err()); } @@ -1378,7 +2170,7 @@ mod tests { let config = config.data; make_weak_config(&config); config.as_ref().borrow_mut()[0].as_ref().borrow_mut().actors[0] = - SActor::from_user_string("root"); + SActor::user("root").build(); let cred = Cred { user: User::from_name("root").unwrap().unwrap(), groups: vec![Group::from_name("root").unwrap().unwrap()], @@ -1406,4 +2198,110 @@ mod tests { IdTask::Name("t_chsr".to_string()) ); } + #[test] + + fn test_schooseruser_setuid_types() { + let config = SConfig::builder() + .role( + SRole::builder("test") + .actor(SActor::user("root").build()) + .task( + STask::builder(1) + .cred( + SCredentials::builder() + .setuid(SUserChooser::ChooserStruct( + SSetuidSet::builder(SUserType::from(0), SetBehavior::None) + .build(), + )) + .build(), + ) + .commands( + SCommands::builder(SetBehavior::None) + .add(["/bin/ls".into()]) + .build(), + ) + .build(), + ) + .task( + STask::builder(2) + .cred(SCredentials::builder().setuid("root").build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(["/bin/pwd".into()]) + .build(), + ) + .build(), + ) + .task( + STask::builder(3) + .cred(SCredentials::builder().setuid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(["/bin/cat".into()]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + + // Vérifier si les tâches existent avant d’appeler unwrap() + + let t1 = config + .task("test", 1) + .expect(" Erreur : La tâche 1 n'existe pas !"); + let t2 = config + .task("test", 2) + .expect(" Erreur : La tâche 2 n'existe pas !"); + let t3 = config + .task("test", 3) + .expect(" Erreur : La tâche 3 n'existe pas !"); + + // Affichage pour debug + println!("Tâche 1 : {:?}", t1); + println!("Tâche 2 : {:?}", t2); + println!("Tâche 3 : {:?}", t3); + + let cred = Cred::builder().user_name("root").group_name("root").build(); + + let chooser_struct2 = SUserType::from("root"); + let chooser_struct3 = SUserType::from(0); + + let command1 = vec!["/bin/ls".to_string()]; + let command2 = vec!["/bin/pwd".to_string()]; + let command3 = vec!["/bin/cat".to_string()]; + + let filter_matcher = FilterMatcher::builder().user("root").build(); + let result = config.matches(&cred, &Some(filter_matcher), &command1); + assert!(result.is_ok(), "Erreur : L !"); + let result1 = config.matches(&cred, &None, &command1); + assert!(result1.is_ok(), "Erreur : La tâche 1 ne correspond pas !"); + let result2 = config.matches(&cred, &None, &command2); + assert!(result2.is_ok(), "Erreur : La tâche 2 ne correspond pas !"); + let result3 = config.matches(&cred, &None, &command3); + assert!(result3.is_ok(), "Erreur : La tâche 3 ne correspond pas !"); + + let result1 = result1.unwrap(); + let result2 = result2.unwrap(); + let result3 = result3.unwrap(); + + assert_eq!(result1.settings.setuid, Some(SUserType::from(0))); + assert_eq!(result1.settings.task.upgrade(), Some(t1.clone())); + println!( + " Test réussi : L'utilisateur spécifié correspond bien à l'ajout pour la tâche 1." + ); + + assert_eq!(result2.settings.setuid, Some(chooser_struct2)); + assert_eq!(result2.settings.task.upgrade(), Some(t2.clone())); + println!( + " Test réussi : L'utilisateur spécifié correspond bien à l'ajout pour la tâche 2." + ); + + assert_eq!(result3.settings.setuid, Some(chooser_struct3)); + assert_eq!(result3.settings.task.upgrade(), Some(t3.clone())); + println!( + " Test réussi : L'utilisateur spécifié correspond bien à l'ajout pour la tâche 3." + ); + } } diff --git a/rar-common/src/database/mod.rs b/rar-common/src/database/mod.rs index 489b4be0..9b993474 100644 --- a/rar-common/src/database/mod.rs +++ b/rar-common/src/database/mod.rs @@ -5,9 +5,12 @@ use crate::save_settings; use crate::util::{toggle_lock_config, ImmutableLock}; use crate::version::PACKAGE_VERSION; +use actor::SUserType; +use bon::{builder, Builder}; use chrono::Duration; use linked_hash_set::LinkedHashSet; use log::debug; +use options::EnvBehavior; use serde::{de, Deserialize, Serialize}; use self::{migration::Migration, options::EnvKey, structs::SConfig, versionning::Versioning}; @@ -15,18 +18,25 @@ use self::{migration::Migration, options::EnvKey, structs::SConfig, versionning: use crate::util::warn_if_mutable; use crate::SettingsFile; use crate::{open_with_privileges, write_json_config}; -use crate::{ - util::{immutable_effective, parse_capset_iter}, - RemoteStorageSettings, ROOTASROLE, -}; +use crate::{util::immutable_effective, RemoteStorageSettings, ROOTASROLE}; +pub mod actor; #[cfg(feature = "finder")] pub mod finder; pub mod migration; pub mod options; pub mod structs; pub mod versionning; -pub mod wrapper; + +#[derive(Debug, Default, Builder)] +#[builder(on(_, overwritable))] +pub struct FilterMatcher { + pub role: Option, + pub task: Option, + pub env_behavior: Option, + #[builder(into)] + pub user: Option, +} pub fn make_weak_config(config: &Rc>) { for role in &config.as_ref().borrow().roles { @@ -145,39 +155,58 @@ fn write_sconfig( } // deserialize the linked hash set -fn lhs_deserialize_envkey<'de, D>(deserializer: D) -> Result, D::Error> +fn lhs_deserialize_envkey<'de, D>( + deserializer: D, +) -> Result>, D::Error> where D: de::Deserializer<'de>, { - let v: Vec = Vec::deserialize(deserializer)?; - Ok(v.into_iter().collect()) + if let Ok(v) = Vec::::deserialize(deserializer) { + Ok(Some(v.into_iter().collect())) + } else { + Ok(None) + } } // serialize the linked hash set -fn lhs_serialize_envkey(value: &LinkedHashSet, serializer: S) -> Result +fn lhs_serialize_envkey( + value: &Option>, + serializer: S, +) -> Result where S: serde::Serializer, { - let v: Vec = value.iter().cloned().collect(); - v.serialize(serializer) + if let Some(v) = value { + let v: Vec = v.iter().cloned().collect(); + v.serialize(serializer) + } else { + serializer.serialize_none() + } } // deserialize the linked hash set -fn lhs_deserialize<'de, D>(deserializer: D) -> Result, D::Error> +fn lhs_deserialize<'de, D>(deserializer: D) -> Result>, D::Error> where D: de::Deserializer<'de>, { - let v: Vec = Vec::deserialize(deserializer)?; - Ok(v.into_iter().collect()) + if let Ok(v) = Vec::::deserialize(deserializer) { + Ok(Some(v.into_iter().collect())) + } else { + Ok(None) + } } // serialize the linked hash set -fn lhs_serialize(value: &LinkedHashSet, serializer: S) -> Result +fn lhs_serialize(value: &Option>, serializer: S) -> Result where S: serde::Serializer, { - let v: Vec = value.iter().cloned().collect(); - v.serialize(serializer) + if let Some(v) = value { + let v: Vec = v.iter().cloned().collect(); + v.serialize(serializer) + } else { + serializer.serialize_none() + } } pub fn is_default(t: &T) -> bool { @@ -191,7 +220,7 @@ where // hh:mm:ss format match value { Some(value) => serializer.serialize_str(&format!( - "{}:{}:{}", + "{:#02}:{:#02}:{:#02}", value.num_hours(), value.num_minutes() % 60, value.num_seconds() % 60 @@ -219,18 +248,6 @@ where Err(de::Error::custom("Invalid duration format")) } -fn deserialize_capset<'de, D>(deserializer: D) -> Result -where - D: de::Deserializer<'de>, -{ - let s: Vec = Vec::deserialize(deserializer)?; - let res = parse_capset_iter(s.iter().map(|s| s.as_ref())); - match res { - Ok(capset) => Ok(capset), - Err(_) => Err(de::Error::custom("Invalid capset format")), - } -} - fn serialize_capset(value: &capctl::CapSet, serializer: S) -> Result where S: serde::Serializer, @@ -238,3 +255,138 @@ where let v: Vec = value.iter().map(|cap| cap.to_string()).collect(); v.serialize(serializer) } + +#[cfg(test)] +mod tests { + use super::*; + + struct LinkedHashSetTester(LinkedHashSet); + + impl<'de> Deserialize<'de> for LinkedHashSetTester { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Self( + lhs_deserialize_envkey(deserializer).map(|v| v.unwrap())?, + )) + } + } + + impl Serialize for LinkedHashSetTester { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + lhs_serialize_envkey(&Some(self.0.clone()), serializer) + } + } + + impl<'de> Deserialize<'de> for LinkedHashSetTester { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Self(lhs_deserialize(deserializer).map(|v| v.unwrap())?)) + } + } + + impl Serialize for LinkedHashSetTester { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + lhs_serialize(&Some(self.0.clone()), serializer) + } + } + + struct DurationTester(Duration); + + impl<'de> Deserialize<'de> for DurationTester { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Self( + deserialize_duration(deserializer).map(|v| v.unwrap())?, + )) + } + } + + impl Serialize for DurationTester { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serialize_duration(&Some(self.0.clone()), serializer) + } + } + + #[test] + fn test_lhs_deserialize_envkey() { + let json = r#"["key1", "key2", "key3"]"#; + let deserialized: Option> = serde_json::from_str(json).unwrap(); + assert!(deserialized.is_some()); + let set = deserialized.unwrap(); + assert_eq!(set.0.len(), 3); + assert!(set.0.contains(&EnvKey::from("key1"))); + assert!(set.0.contains(&EnvKey::from("key2"))); + assert!(set.0.contains(&EnvKey::from("key3"))); + } + + #[test] + fn test_lhs_serialize_envkey() { + let mut set = LinkedHashSetTester(LinkedHashSet::new()); + set.0.insert(EnvKey::from("key1")); + set.0.insert(EnvKey::from("key2")); + set.0.insert(EnvKey::from("key3")); + let serialized = serde_json::to_string(&Some(set)).unwrap(); + assert_eq!(serialized, r#"["key1","key2","key3"]"#); + } + + #[test] + fn test_lhs_deserialize() { + let json = r#"["value1", "value2", "value3"]"#; + let deserialized: Option> = serde_json::from_str(json).unwrap(); + assert!(deserialized.is_some()); + let set = deserialized.unwrap(); + assert_eq!(set.0.len(), 3); + assert!(set.0.contains("value1")); + assert!(set.0.contains("value2")); + assert!(set.0.contains("value3")); + } + + #[test] + fn test_lhs_serialize() { + let mut set = LinkedHashSetTester(LinkedHashSet::new()); + set.0.insert("value1".to_string()); + set.0.insert("value2".to_string()); + set.0.insert("value3".to_string()); + let serialized = serde_json::to_string(&Some(set)).unwrap(); + assert_eq!(serialized, r#"["value1","value2","value3"]"#); + } + + #[test] + fn test_serialize_duration() { + let duration = Some(DurationTester(Duration::seconds(3661))); + let serialized = serde_json::to_string(&duration).unwrap(); + assert_eq!(serialized, r#""01:01:01""#); + } + + #[test] + fn test_deserialize_duration() { + let json = r#""01:01:01""#; + let deserialized: Option = serde_json::from_str(json).unwrap(); + assert!(deserialized.is_some()); + let duration = deserialized.unwrap(); + assert_eq!(duration.0.num_seconds(), 3661); + } + + #[test] + fn test_is_default() { + assert!(is_default(&0)); + assert!(is_default(&String::new())); + assert!(!is_default(&1)); + assert!(!is_default(&"non-default".to_string())); + } +} diff --git a/rar-common/src/database/options.rs b/rar-common/src/database/options.rs index 2cf04792..0dce33c4 100644 --- a/rar-common/src/database/options.rs +++ b/rar-common/src/database/options.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::{borrow::Borrow, cell::RefCell, rc::Rc}; +use bon::{bon, builder, Builder}; use chrono::Duration; #[cfg(feature = "finder")] @@ -23,7 +24,7 @@ use crate::rc_refcell; #[cfg(feature = "finder")] use super::finder::Cred; -use super::{deserialize_duration, is_default, serialize_duration}; +use super::{deserialize_duration, is_default, serialize_duration, FilterMatcher}; use super::{ lhs_deserialize, lhs_deserialize_envkey, lhs_serialize, lhs_serialize_envkey, @@ -32,8 +33,8 @@ use super::{ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum Level { - None, #[default] + None, Default, Global, Role, @@ -71,7 +72,7 @@ pub enum TimestampType { UID, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default, Builder)] pub struct STimeout { #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")] pub type_field: Option, @@ -85,29 +86,35 @@ pub struct STimeout { pub max_usage: Option, #[serde(default)] #[serde(flatten, skip_serializing_if = "Map::is_empty")] + #[builder(default)] pub _extra_fields: Map, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Builder)] pub struct SPathOptions { #[serde(rename = "default", default, skip_serializing_if = "is_default")] + #[builder(start_fn)] pub default_behavior: PathBehavior, #[serde( default, - skip_serializing_if = "LinkedHashSet::is_empty", + skip_serializing_if = "Option::is_none", deserialize_with = "lhs_deserialize", serialize_with = "lhs_serialize" )] - pub add: LinkedHashSet, + #[builder(with = |v : impl IntoIterator| { v.into_iter().map(|s| s.to_string()).collect() })] + pub add: Option>, #[serde( default, - skip_serializing_if = "LinkedHashSet::is_empty", + skip_serializing_if = "Option::is_none", deserialize_with = "lhs_deserialize", - serialize_with = "lhs_serialize" + serialize_with = "lhs_serialize", + alias = "del" )] - pub sub: LinkedHashSet, + #[builder(with = |v : impl IntoIterator| { v.into_iter().map(|s| s.to_string()).collect() })] + pub sub: Option>, #[serde(default)] #[serde(flatten)] + #[builder(default)] pub _extra_fields: Map, } @@ -127,7 +134,7 @@ enum EnvKeyType { Normal, } -#[derive(Eq, Hash, PartialEq, Serialize, Debug, Clone)] +#[derive(Eq, Hash, PartialEq, Serialize, Debug, Clone, Builder)] #[serde(transparent)] pub struct EnvKey { #[serde(skip)] @@ -141,34 +148,46 @@ impl std::fmt::Display for EnvKey { } } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default, Builder)] pub struct SEnvOptions { #[serde(rename = "default", default, skip_serializing_if = "is_default")] + #[builder(start_fn)] pub default_behavior: EnvBehavior, + #[serde(alias = "override", default, skip_serializing_if = "Option::is_none")] + pub override_behavior: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] + #[builder(default, with = |iter: impl IntoIterator| { + let mut map = HashMap::with_hasher(Default::default()); + map.extend(iter.into_iter().map(|(k, v)| (k.to_string(), v.to_string()))); + map + })] pub set: HashMap, #[serde( default, - skip_serializing_if = "LinkedHashSet::is_empty", + skip_serializing_if = "Option::is_none", deserialize_with = "lhs_deserialize_envkey", serialize_with = "lhs_serialize_envkey" )] - pub keep: LinkedHashSet, + #[builder(with = |v : impl IntoIterator| -> Result<_,String> { let mut res = LinkedHashSet::new(); for s in v { res.insert(EnvKey::new(s.to_string())?); } Ok(res)})] + pub keep: Option>, #[serde( default, - skip_serializing_if = "LinkedHashSet::is_empty", + skip_serializing_if = "Option::is_none", deserialize_with = "lhs_deserialize_envkey", serialize_with = "lhs_serialize_envkey" )] - pub check: LinkedHashSet, + #[builder(with = |v : impl IntoIterator| -> Result<_,String> { let mut res = LinkedHashSet::new(); for s in v { res.insert(EnvKey::new(s.to_string())?); } Ok(res)})] + pub check: Option>, #[serde( default, - skip_serializing_if = "LinkedHashSet::is_empty", + skip_serializing_if = "Option::is_none", deserialize_with = "lhs_deserialize_envkey", serialize_with = "lhs_serialize_envkey" )] - pub delete: LinkedHashSet, + #[builder(with = |v : impl IntoIterator| -> Result<_,String> { let mut res = LinkedHashSet::new(); for s in v { res.insert(EnvKey::new(s.to_string())?); } Ok(res)})] + pub delete: Option>, #[serde(default, flatten)] + #[builder(default)] pub _extra_fields: Map, } @@ -205,6 +224,8 @@ pub enum SAuthentication { #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct Opt { + #[serde(skip)] + pub level: Level, #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -219,76 +240,108 @@ pub struct Opt { pub wildcard_denied: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout: Option, - #[serde(default)] - #[serde(flatten)] + #[serde(default, flatten)] pub _extra_fields: Map, - #[serde(skip)] - pub level: Level, } +#[bon] impl Opt { - pub fn new(level: Level) -> Self { + #[builder] + pub fn new( + #[builder(start_fn)] level: Level, + path: Option, + env: Option, + root: Option, + bounding: Option, + authentication: Option, + #[builder(into)] wildcard_denied: Option, + timeout: Option, + #[builder(default)] _extra_fields: Map, + ) -> Rc> { + rc_refcell!(Opt { + level, + path, + env, + root, + bounding, + authentication, + wildcard_denied, + timeout, + _extra_fields, + }) + } + + pub fn raw_new(level: Level) -> Self { Opt { level, ..Default::default() } } - pub fn level_default() -> Self { - let mut opt = Self::new(Level::Default); - opt.root = Some(SPrivileged::User); - opt.bounding = Some(SBounding::Strict); - opt.path.as_mut().unwrap().default_behavior = PathBehavior::Delete; - opt.path.as_mut().unwrap().add = vec![ - "/usr/local/sbin".to_string(), - "/usr/local/bin".to_string(), - "/usr/sbin".to_string(), - "/usr/bin".to_string(), - "/sbin".to_string(), - "/bin".to_string(), - "/snap/bin".to_string(), - ] - .into_iter() - .collect(); - opt.authentication = SAuthentication::Perform.into(); - let mut env = SEnvOptions::new(EnvBehavior::Delete); - env.keep = vec![ - "HOME".into(), - "USER".into(), - "LOGNAME".into(), - "COLORS".into(), - "DISPLAY".into(), - "HOSTNAME".into(), - "KRB5CCNAME".into(), - "LS_COLORS".into(), - "PS1".into(), - "PS2".into(), - "XAUTHORY".into(), - "XAUTHORIZATION".into(), - "XDG_CURRENT_DESKTOP".into(), - ] - .into_iter() - .collect(); - env.check = vec![ - "COLORTERM".into(), - "LANG".into(), - "LANGUAGE".into(), - "LC_*".into(), - "LINGUAS".into(), - "TERM".into(), - "TZ".into(), - ] - .into_iter() - .collect(); - opt.env = Some(env); - let timeout = STimeout { - type_field: Some(TimestampType::PPID), - duration: Some(Duration::minutes(5)), - ..Default::default() - }; - opt.timeout = Some(timeout); - opt.wildcard_denied = Some(";&|".to_string()); - opt + pub fn level_default() -> Rc> { + Self::builder(Level::Default) + .root(SPrivileged::User) + .bounding(SBounding::Strict) + .path( + SPathOptions::builder(PathBehavior::Delete) + .add([ + "/usr/local/sbin", + "/usr/local/bin", + "/usr/sbin", + "/usr/bin", + "/sbin", + "/snap/bin", + ]) + .build(), + ) + .authentication(SAuthentication::Perform) + .env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep([ + "HOME", + "USER", + "LOGNAME", + "COLORS", + "DISPLAY", + "HOSTNAME", + "KRB5CCNAME", + "LS_COLORS", + "PS1", + "PS2", + "XAUTHORY", + "XAUTHORIZATION", + "XDG_CURRENT_DESKTOP", + ]) + .unwrap() + .check([ + "COLORTERM", + "LANG", + "LANGUAGE", + "LC_*", + "LINGUAS", + "TERM", + "TZ", + ]) + .unwrap() + .delete([ + "PS4", + "SHELLOPTS", + "PERLLIB", + "PERL5LIB", + "PERL5OPT", + "PYTHONINSPECT", + ]) + .unwrap() + .build(), + ) + .timeout( + STimeout::builder() + .type_field(TimestampType::PPID) + .duration(Duration::minutes(5)) + .build(), + ) + .wildcard_denied(";&|") + .build() } } @@ -311,13 +364,7 @@ impl Default for Opt { impl Default for OptStack { fn default() -> Self { OptStack { - stack: [ - None, - Some(Rc::new(Opt::level_default().into())), - None, - None, - None, - ], + stack: [None, Some(Opt::level_default()), None, None, None], roles: None, role: None, task: None, @@ -329,8 +376,8 @@ impl Default for SPathOptions { fn default() -> Self { SPathOptions { default_behavior: PathBehavior::Inherit, - add: LinkedHashSet::new(), - sub: LinkedHashSet::new(), + add: None, + sub: None, _extra_fields: Map::default(), } } @@ -418,28 +465,6 @@ impl<'de> Deserialize<'de> for EnvKey { } } -#[cfg(test)] -impl SPathOptions { - fn new(behavior: PathBehavior) -> Self { - let mut res = SPathOptions::default(); - res.default_behavior = behavior; - res - } -} - -impl Default for SEnvOptions { - fn default() -> Self { - SEnvOptions { - default_behavior: EnvBehavior::default(), - set: HashMap::new(), - keep: LinkedHashSet::new(), - check: LinkedHashSet::new(), - delete: LinkedHashSet::new(), - _extra_fields: Map::default(), - } - } -} - impl SEnvOptions { pub fn new(behavior: EnvBehavior) -> Self { SEnvOptions { @@ -463,11 +488,17 @@ impl EnvSet for HashMap { } impl EnvSet for LinkedHashSet { - fn env_matches(&self, wildcarded: &EnvKey) -> bool { - match wildcarded.env_type { - EnvKeyType::Normal => self.contains(wildcarded), - EnvKeyType::Wildcarded => self.iter().any(|s| check_wildcarded(wildcarded, &s.value)), - } + fn env_matches(&self, needle: &EnvKey) -> bool { + self.iter().any(|s| match s.env_type { + EnvKeyType::Normal => s == needle, + EnvKeyType::Wildcarded => check_wildcarded(s, &needle.value), + }) + } +} + +impl EnvSet for Option> { + fn env_matches(&self, needle: &EnvKey) -> bool { + self.as_ref().map_or(false, |set| set.env_matches(needle)) } } @@ -550,23 +581,26 @@ pub struct OptStack { task: Option>>, } -type FinalPath = ( - PathBehavior, - Rc>>, - Rc>>, -); - -type FinalEnv = ( - EnvBehavior, - HashMap, - LinkedHashSet, - LinkedHashSet, - LinkedHashSet, -); - -impl OptStack { - pub fn from_task(task: Rc>) -> Self { - let mut stack = OptStack::from_role( +#[cfg(not(tarpaulin_include))] +impl OptStackBuilder { + fn opt(mut self, opt: Option>>) -> Self { + if let Some(opt) = opt { + self.stack[opt.as_ref().borrow().level as usize] = Some(opt.clone()); + } + self + } + fn with_task( + self, + task: Rc>, + ) -> OptStackBuilder< + opt_stack_builder::SetTask>>, + > + where + ::Roles: opt_stack_builder::IsUnset, + ::Role: opt_stack_builder::IsUnset, + ::Task: opt_stack_builder::IsUnset, + { + self.with_role( task.as_ref() .borrow() ._role @@ -574,13 +608,19 @@ impl OptStack { .unwrap() .upgrade() .unwrap(), - ); - stack.task = Some(task.to_owned()); - stack.set_opt(Level::Task, task.as_ref().borrow().options.to_owned()); - stack + ) + .task(task.to_owned()) + .opt(task.as_ref().borrow().options.to_owned()) } - pub fn from_role(role: Rc>) -> Self { - let mut stack = OptStack::from_roles( + fn with_role( + self, + role: Rc>, + ) -> OptStackBuilder>> + where + ::Roles: opt_stack_builder::IsUnset, + ::Role: opt_stack_builder::IsUnset, + { + self.with_roles( role.as_ref() .borrow() ._config @@ -588,57 +628,118 @@ impl OptStack { .unwrap() .upgrade() .unwrap(), - ); - stack.role = Some(role.to_owned()); - stack.set_opt(Level::Role, role.as_ref().borrow().options.to_owned()); - stack - } - pub fn from_roles(roles: Rc>) -> Self { - let mut stack = OptStack::new(roles); - stack.set_opt( - Level::Global, - stack - .get_roles() - .unwrap() - .as_ref() - .borrow() - .options - .to_owned(), - ); - stack + ) + .role(role.to_owned()) + .opt(role.as_ref().borrow().options.to_owned()) } - fn new(roles: Rc>) -> OptStack { - let mut res = OptStack::default(); - let mut opt = Opt { - level: Level::Global, - root: Some(SPrivileged::User), - bounding: Some(SBounding::Strict), - ..Default::default() - }; - let mut env = SEnvOptions::new(EnvBehavior::Delete); - env.check = ["TZ".into(), "LOGNAME".into(), "LOGIN".into(), "USER".into()] - .iter() - .cloned() - .collect(); - opt.env = Some(env); - opt.path.as_mut().unwrap().default_behavior = PathBehavior::Delete; - res.set_opt(Level::Global, Some(Rc::new(RefCell::new(opt)))); - res.roles = Some(roles); - res + fn with_roles( + self, + roles: Rc>, + ) -> OptStackBuilder> + where + ::Roles: opt_stack_builder::IsUnset, + { + self.with_default() + .roles(roles.to_owned()) + .opt(roles.as_ref().borrow().options.to_owned()) } - fn get_roles(&self) -> Option>> { - self.roles.to_owned() + fn with_default(self) -> Self { + self.opt(Some( + Opt::builder(Level::Default) + .root(SPrivileged::User) + .bounding(SBounding::Strict) + .path( + SPathOptions::builder(PathBehavior::Delete) + .add([ + "/usr/local/sbin", + "/usr/local/bin", + "/usr/sbin", + "/usr/bin", + "/sbin", + "/bin", + "/snap/bin", + ]) + .build(), + ) + .authentication(SAuthentication::Perform) + .env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep([ + "HOME", + "USER", + "LOGNAME", + "COLORS", + "DISPLAY", + "HOSTNAME", + "KRB5CCNAME", + "LS_COLORS", + "PS1", + "PS2", + "XAUTHORY", + "XAUTHORIZATION", + "XDG_CURRENT_DESKTOP", + ]) + .unwrap() + .check([ + "COLORTERM", + "LANG", + "LANGUAGE", + "LC_*", + "LINGUAS", + "TERM", + "TZ", + ]) + .unwrap() + .delete([ + "PS4", + "SHELLOPTS", + "PERLLIB", + "PERL5LIB", + "PERL5OPT", + "PYTHONINSPECT", + ]) + .unwrap() + .build(), + ) + .timeout( + STimeout::builder() + .type_field(TimestampType::TTY) + .duration(Duration::minutes(5)) + .build(), + ) + .wildcard_denied(";&|") + .build(), + )) } +} - fn set_opt(&mut self, level: Level, opt: Option>>) { - if let Some(opt) = opt { - self.stack[level as usize] = Some(opt); - } else { - self.stack[level as usize] = Some(Rc::new(Opt::new(level).into())); +#[bon] +impl OptStack { + #[builder] + pub fn new( + #[builder(field)] stack: [Option>>; 5], + roles: Option>>, + role: Option>>, + task: Option>>, + ) -> Self { + OptStack { + stack, + roles, + role, + task, } } + pub fn from_task(task: Rc>) -> Self { + OptStack::builder().with_task(task).build() + } + pub fn from_role(role: Rc>) -> Self { + OptStack::builder().with_role(role).build() + } + pub fn from_roles(roles: Rc>) -> Self { + OptStack::builder().with_roles(roles).build() + } fn find_in_options Option<(Level, V)>, V>(&self, f: F) -> Option<(Level, V)> { for opt in self.stack.iter().rev() { @@ -663,41 +764,46 @@ impl OptStack { #[cfg(feature = "finder")] fn calculate_path(&self) -> String { - let (final_behavior, final_add, final_sub) = self.get_final_path(); - let final_add = final_add - .clone() - .as_ref() - .borrow() - .difference(&final_sub.as_ref().borrow()) - .fold("".to_string(), |mut acc, s| { - if !acc.is_empty() { - acc.insert(0, ':'); - } - acc.insert_str(0, s); - acc - }); - match final_behavior { - PathBehavior::Inherit | PathBehavior::Delete => final_add, - is_safe => std::env::vars() - .find_map(|(key, value)| if key == "PATH" { Some(value) } else { None }) - .unwrap_or(String::new()) - .split(':') - .filter(|s| { - !final_sub.as_ref().borrow().contains(*s) - && (!is_safe.is_keep_safe() || PathBuf::from(s).exists()) - }) - .fold(final_add, |mut acc, s| { + let path = self.get_final_path(); + let default = LinkedHashSet::new(); + println!("path: {:?}", path); + if let Some(add) = path.add { + let final_add = add.difference(path.sub.as_ref().unwrap_or(&default)).fold( + "".to_string(), + |mut acc, s| { if !acc.is_empty() { - acc.push(':'); + acc.insert(0, ':'); } - acc.push_str(s); + acc.insert_str(0, s); acc - }), + }, + ); + match path.default_behavior { + PathBehavior::Inherit | PathBehavior::Delete => final_add, + is_safe => std::env::vars() + .find_map(|(key, value)| if key == "PATH" { Some(value) } else { None }) + .unwrap_or(String::new()) + .split(':') + .filter(|s| { + !path.sub.as_ref().unwrap_or(&default).contains(*s) + && (!is_safe.is_keep_safe() || PathBuf::from(s).exists()) + }) + .fold(final_add, |mut acc, s| { + if !acc.is_empty() { + acc.push(':'); + } + acc.push_str(s); + acc + }), + } + } else { + "".to_string() } } - fn get_final_path(&self) -> FinalPath { + fn get_final_path(&self) -> SPathOptions { let mut final_behavior = PathBehavior::Delete; + let default = LinkedHashSet::new(); let final_add = rc_refcell!(LinkedHashSet::new()); // Cannot use HashSet as we need to keep order let final_sub = rc_refcell!(LinkedHashSet::new()); @@ -706,19 +812,21 @@ impl OptStack { let final_sub_clone = Rc::clone(&final_sub); if let Some(p) = opt.path.borrow().as_ref() { match p.default_behavior { - PathBehavior::Delete => { - final_add_clone.as_ref().replace(p.add.clone()); - } - PathBehavior::KeepSafe | PathBehavior::KeepUnsafe => { - final_sub_clone.as_ref().replace(p.sub.clone()); + PathBehavior::KeepSafe | PathBehavior::KeepUnsafe | PathBehavior::Delete => { + if let Some(add) = p.add.as_ref() { + final_add_clone.as_ref().replace(add.clone()); + } + if let Some(sub) = p.sub.as_ref() { + final_sub_clone.as_ref().replace(sub.clone()); + } } PathBehavior::Inherit => { if final_behavior.is_delete() { let union: LinkedHashSet = final_add_clone .as_ref() .borrow() - .union(&p.add) - .filter(|e| !p.sub.contains(*e)) + .union(p.add.as_ref().unwrap_or(&default)) + .filter(|e| !p.sub.as_ref().unwrap_or(&default).contains(*e)) .cloned() .collect(); final_add_clone.as_ref().borrow_mut().extend(union); @@ -727,8 +835,8 @@ impl OptStack { let union: LinkedHashSet = final_sub_clone .as_ref() .borrow() - .union(&p.sub) - .filter(|e| !p.add.contains(*e)) + .union(p.sub.as_ref().unwrap_or(&default)) + .filter(|e| !p.add.as_ref().unwrap_or(&default).contains(*e)) .cloned() .collect(); final_sub_clone.as_ref().borrow_mut().extend(union); @@ -740,13 +848,33 @@ impl OptStack { } } }); - (final_behavior, final_add, final_sub) + SPathOptions::builder(final_behavior) + .add( + final_add + .clone() + .as_ref() + .borrow() + .iter() + .collect::>() + .as_slice(), + ) + .sub( + final_sub + .clone() + .as_ref() + .borrow() + .iter() + .collect::>() + .as_slice(), + ) + .build() } #[allow(dead_code)] #[cfg(not(tarpaulin_include))] - fn union_all_path(&self) -> FinalPath { + fn union_all_path(&self) -> SPathOptions { let mut final_behavior = PathBehavior::Delete; + let default = LinkedHashSet::new(); let final_add = rc_refcell!(LinkedHashSet::new()); // Cannot use HashSet as we need to keep order let final_sub = rc_refcell!(LinkedHashSet::new()); @@ -759,8 +887,8 @@ impl OptStack { let union = final_add_clone .as_ref() .borrow() - .union(&p.add) - .filter(|e| !p.sub.contains(*e)) + .union(p.add.as_ref().unwrap_or(&default)) + .filter(|e| !p.sub.as_ref().unwrap_or(&default).contains(*e)) .cloned() .collect(); // policy is to delete, so we add whitelist and remove blacklist @@ -771,8 +899,8 @@ impl OptStack { let union = final_sub_clone .as_ref() .borrow() - .union(&p.sub) - .filter(|e| !p.add.contains(*e)) + .union(p.sub.as_ref().unwrap_or(&default)) + .filter(|e| !p.add.as_ref().unwrap_or(&default).contains(*e)) .cloned() .collect(); //policy is to keep, so we remove blacklist and add whitelist @@ -783,8 +911,8 @@ impl OptStack { let union: LinkedHashSet = final_add_clone .as_ref() .borrow() - .union(&p.add) - .filter(|e| !p.sub.contains(*e)) + .union(p.add.as_ref().unwrap_or(&default)) + .filter(|e| !p.sub.as_ref().unwrap_or(&default).contains(*e)) .cloned() .collect(); final_add_clone.as_ref().borrow_mut().extend(union); @@ -793,8 +921,8 @@ impl OptStack { let union: LinkedHashSet = final_sub_clone .as_ref() .borrow() - .union(&p.sub) - .filter(|e| !p.add.contains(*e)) + .union(p.sub.as_ref().unwrap_or(&default)) + .filter(|e| !p.add.as_ref().unwrap_or(&default).contains(*e)) .cloned() .collect(); final_sub_clone.as_ref().borrow_mut().extend(union); @@ -806,34 +934,54 @@ impl OptStack { } } }); - (final_behavior, final_add, final_sub) + SPathOptions::builder(final_behavior) + .add( + final_add + .clone() + .as_ref() + .borrow() + .iter() + .collect::>() + .as_slice(), + ) + .sub( + final_sub + .clone() + .as_ref() + .borrow() + .iter() + .collect::>() + .as_slice(), + ) + .build() } #[cfg(feature = "finder")] pub fn calculate_filtered_env( &self, + opt_filter: Option, target: Cred, final_env: I, ) -> Result, String> where I: Iterator, { - let (final_behavior, final_set, final_keep, final_check, final_delete) = - self.get_final_env(); - if final_behavior.is_keep() { + let env = self.get_final_env(opt_filter); + println!("env: {:?}", env); + if env.default_behavior.is_keep() { warn!("Keeping environment variables is dangerous operation, it can lead to security vulnerabilities. Please consider using delete instead. See https://www.sudo.ws/security/advisories/bash_env/, https://www.sudo.ws/security/advisories/perl_env/ or https://nvd.nist.gov/vuln/detail/CVE-2006-0151"); } - let mut final_env: HashMap = match final_behavior { + let mut final_env: HashMap = match env.default_behavior { EnvBehavior::Inherit => Err("Internal Error with environment behavior".to_string()), EnvBehavior::Delete => Ok(final_env .filter_map(|(key, value)| { let key = EnvKey::new(key).expect("Unexpected environment variable"); - if final_keep.env_matches(&key) - || (final_check.env_matches(&key) && check_env(&key.value, &value)) + if env.keep.env_matches(&key) + || (env.check.env_matches(&key) && check_env(&key.value, &value)) { debug!("Keeping env: {}={}", key.value, value); Some((key.value, value)) @@ -846,8 +994,8 @@ impl OptStack { EnvBehavior::Keep => Ok(final_env .filter_map(|(key, value)| { let key = EnvKey::new(key).expect("Unexpected environment variable"); - if !final_delete.env_matches(&key) - || (final_check.env_matches(&key) && check_env(&key.value, &value)) + if !env.delete.env_matches(&key) + || (env.check.env_matches(&key) && check_env(&key.value, &value)) { debug!("Keeping env: {}={}", key.value, value); Some((key.value, value)) @@ -869,23 +1017,26 @@ impl OptStack { "SHELL".into(), target.user.shell.to_string_lossy().to_string(), ); - final_env.extend(final_set); + final_env.extend(env.set); Ok(final_env) } - fn get_final_env(&self) -> FinalEnv { + fn get_final_env(&self, cmd_filter: Option) -> SEnvOptions { let mut final_behavior = EnvBehavior::default(); let mut final_set = HashMap::new(); let mut final_keep = LinkedHashSet::new(); let mut final_check = LinkedHashSet::new(); let mut final_delete = LinkedHashSet::new(); + let overriden_behavior = cmd_filter.as_ref().and_then(|f| f.env_behavior); self.iter_in_options(|opt| { if let Some(p) = opt.env.borrow().as_ref() { final_behavior = match p.default_behavior { - EnvBehavior::Delete => { + EnvBehavior::Delete | EnvBehavior::Keep => { // policy is to delete, so we add whitelist and remove blacklist final_keep = p .keep + .as_ref() + .unwrap_or(&LinkedHashSet::new()) .iter() .filter(|e| { !p.set.env_matches(e) @@ -896,67 +1047,37 @@ impl OptStack { .collect(); final_check = p .check + .as_ref() + .unwrap_or(&LinkedHashSet::new()) .iter() .filter(|e| !p.set.env_matches(e) || !p.delete.env_matches(e)) .cloned() .collect(); - final_set = p.set.clone(); - debug!("check: {:?}", final_check); - p.default_behavior - } - EnvBehavior::Keep => { - //policy is to keep, so we remove blacklist and add whitelist final_delete = p .delete + .as_ref() + .unwrap_or(&LinkedHashSet::new()) .iter() - .filter(|e| { - !p.set.env_matches(e) - || !p.keep.env_matches(e) - || !p.check.env_matches(e) - }) - .cloned() - .collect(); - final_check = p - .check - .iter() - .filter(|e| !p.set.env_matches(e) || !p.keep.env_matches(e)) + .filter(|e| !p.set.env_matches(e) || !p.check.env_matches(e)) .cloned() .collect(); final_set = p.set.clone(); + debug!("check: {:?}", final_check); p.default_behavior } EnvBehavior::Inherit => { - if final_behavior.is_delete() { - final_keep = final_keep - .union(&p.keep) - .filter(|e| { - !p.set.env_matches(e) - || !p.delete.env_matches(e) - || !p.check.env_matches(e) - }) - .cloned() - .collect(); - final_check = final_check - .union(&p.check) - .filter(|e| !p.set.env_matches(e) || !p.delete.env_matches(e)) - .cloned() - .collect(); - } else { - final_delete = final_delete - .union(&p.delete) - .filter(|e| { - !p.set.env_matches(e) - || !p.keep.env_matches(e) - || !p.check.env_matches(e) - }) - .cloned() - .collect(); - final_check = final_check - .union(&p.check) - .filter(|e| !p.set.env_matches(e) || !p.keep.env_matches(e)) - .cloned() - .collect(); - } + final_keep = final_keep + .union(p.keep.as_ref().unwrap_or(&LinkedHashSet::new())) + .cloned() + .collect(); + final_check = final_check + .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new())) + .cloned() + .collect(); + final_delete = final_delete + .union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new())) + .cloned() + .collect(); final_set.extend(p.set.clone()); debug!("check: {:?}", final_check); final_behavior @@ -964,13 +1085,15 @@ impl OptStack { }; } }); - ( - final_behavior, - final_set, - final_keep, - final_check, - final_delete, - ) + SEnvOptions::builder(overriden_behavior.unwrap_or(final_behavior)) + .set(final_set) + .keep(final_keep) + .unwrap() + .check(final_check) + .unwrap() + .delete(final_delete) + .unwrap() + .build() } #[allow(dead_code)] @@ -993,12 +1116,12 @@ impl OptStack { EnvBehavior::Delete => { // policy is to delete, so we add whitelist and remove blacklist final_keep = final_keep - .union(&p.keep) + .union(p.keep.as_ref().unwrap_or(&LinkedHashSet::new())) .filter(|e| !p.check.env_matches(e) || !p.delete.env_matches(e)) .cloned() .collect(); final_check = final_check - .union(&p.check) + .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new())) .filter(|e| !p.delete.env_matches(e)) .cloned() .collect(); @@ -1007,12 +1130,12 @@ impl OptStack { EnvBehavior::Keep => { //policy is to keep, so we remove blacklist and add whitelist final_delete = final_delete - .union(&p.delete) + .union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new())) .filter(|e| !p.keep.env_matches(e) || !p.check.env_matches(e)) .cloned() .collect(); final_check = final_check - .union(&p.check) + .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new())) .filter(|e| !p.keep.env_matches(e)) .cloned() .collect(); @@ -1021,23 +1144,23 @@ impl OptStack { EnvBehavior::Inherit => { if final_behavior.is_delete() { final_keep = final_keep - .union(&p.keep) + .union(p.keep.as_ref().unwrap_or(&LinkedHashSet::new())) .filter(|e| !p.delete.env_matches(e) || !p.check.env_matches(e)) .cloned() .collect(); final_check = final_check - .union(&p.check) + .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new())) .filter(|e| !p.delete.env_matches(e)) .cloned() .collect(); } else { final_delete = final_delete - .union(&p.delete) + .union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new())) .filter(|e| !p.keep.env_matches(e) || !p.check.env_matches(e)) .cloned() .collect(); final_check = final_check - .union(&p.check) + .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new())) .filter(|e| !p.keep.env_matches(e)) .cloned() .collect(); @@ -1097,53 +1220,68 @@ impl OptStack { .unwrap_or((Level::None, STimeout::default())) } - pub fn to_opt(&self) -> Opt { - let mut res = Opt::default(); - let (final_behavior, final_add, final_sub) = self.get_final_path(); - res.path.as_mut().unwrap().default_behavior = final_behavior; - res.path.as_mut().unwrap().add = final_add.as_ref().borrow().clone(); - res.path.as_mut().unwrap().sub = final_sub.as_ref().borrow().clone(); - let (final_behavior, final_set, final_keep, final_check, final_delete) = - self.get_final_env(); - res.env.as_mut().unwrap().default_behavior = final_behavior; - res.env.as_mut().unwrap().set = final_set; - res.env.as_mut().unwrap().keep = final_keep; - res.env.as_mut().unwrap().check = final_check; - res.env.as_mut().unwrap().delete = final_delete; - self.iter_in_options(|opt| { - if let Some(p) = opt.root.as_ref() { - res.root.as_ref().replace(p); - } - if let Some(p) = opt.bounding.as_ref() { - res.bounding.as_ref().replace(p); - } - if let Some(p) = opt.wildcard_denied.as_ref() { - res.wildcard_denied.as_ref().replace(p); - } - if let Some(p) = opt.timeout.as_ref() { - res.timeout.as_ref().replace(p); - } - }); - res + fn get_level(&self) -> Level { + let (level, _) = self + .find_in_options(|opt| Some((opt.level, ()))) + .unwrap_or((Level::None, ())); + level + } + + pub fn to_opt(&self) -> Rc> { + Opt::builder(self.get_level()) + .path(self.get_final_path()) + .env(self.get_final_env(None)) + .maybe_root( + self.find_in_options(|opt| opt.root.map(|root| (opt.level, root))) + .map(|(_, root)| root), + ) + .maybe_bounding( + self.find_in_options(|opt| opt.bounding.map(|bounding| (opt.level, bounding))) + .map(|(_, bounding)| bounding), + ) + .maybe_authentication( + self.find_in_options(|opt| { + opt.authentication + .map(|authentication| (opt.level, authentication)) + }) + .map(|(_, authentication)| authentication), + ) + .maybe_wildcard_denied( + self.find_in_options(|opt| { + opt.wildcard_denied + .borrow() + .as_ref() + .map(|wildcard| (opt.level, wildcard.clone())) + }) + .map(|(_, wildcard)| wildcard), + ) + .maybe_timeout( + self.find_in_options(|opt| opt.timeout.clone().map(|timeout| (opt.level, timeout))) + .map(|(_, timeout)| timeout), + ) + .build() } } impl PartialEq for OptStack { fn eq(&self, other: &Self) -> bool { // we must assess that every option result in the same final result - let (final_behavior, final_add, final_sub) = self.get_final_path(); - let (other_final_behavior, other_final_add, other_final_sub) = other.get_final_path(); - let res = final_behavior == other_final_behavior - && final_add + let path = self.get_final_path(); + let default = LinkedHashSet::new(); + let other_path = other.get_final_path(); + let res = path.default_behavior == other_path.default_behavior + && path + .add .as_ref() - .borrow() - .symmetric_difference(&other_final_add.as_ref().borrow()) + .unwrap_or(&default) + .symmetric_difference(other_path.add.as_ref().unwrap_or(&default)) .count() == 0 - && final_sub + && path + .sub .as_ref() - .borrow() - .symmetric_difference(&other_final_sub.as_ref().borrow()) + .unwrap_or(&default) + .symmetric_difference(other_path.sub.as_ref().unwrap_or(&default)) .count() == 0 && self.get_root_behavior().1 == other.get_root_behavior().1 @@ -1152,7 +1290,7 @@ impl PartialEq for OptStack { && self.get_authentication().1 == other.get_authentication().1 && self.get_timeout().1 == other.get_timeout().1; debug!( - "final_behavior == other_final_behavior : {} + "final_behavior == other_path.behavior : {} && add {:?} - other_add {:?} == 0 : {} && sub - other_sub == 0 : {} && self.get_root_behavior().1 == other.get_root_behavior().1 : {} @@ -1160,19 +1298,19 @@ impl PartialEq for OptStack { && self.get_wildcard().1 == other.get_wildcard().1 : {} && self.get_authentication().1 == other.get_authentication().1 : {} && self.get_timeout().1 == other.get_timeout().1 : {}", - final_behavior == other_final_behavior, - final_add.as_ref().borrow(), - other_final_add.as_ref().borrow(), - final_add + path.default_behavior == other_path.default_behavior, + path.add, + other_path.add, + path.add .as_ref() - .borrow() - .symmetric_difference(&other_final_add.as_ref().borrow()) + .unwrap_or(&default) + .symmetric_difference(other_path.add.as_ref().unwrap_or(&default)) .count() == 0, - final_sub + path.sub .as_ref() - .borrow() - .symmetric_difference(&other_final_sub.as_ref().borrow()) + .unwrap_or(&default) + .symmetric_difference(other_path.sub.as_ref().unwrap_or(&default)) .count() == 0, self.get_root_behavior().1 == other.get_root_behavior().1, @@ -1189,182 +1327,460 @@ impl PartialEq for OptStack { #[cfg(test)] mod tests { - use nix::unistd::Group; use nix::unistd::Pid; - use nix::unistd::User; - - use crate::as_borrow_mut; - use crate::database::wrapper::SConfigWrapper; - use crate::database::wrapper::SRoleWrapper; - use crate::database::wrapper::STaskWrapper; - use crate::rc_refcell; use super::super::options::*; use super::super::structs::*; + fn env_key_set_equal(a: I, b: J) -> bool + where + I: IntoIterator, + J: IntoIterator, + { + let mut a_vec: Vec<_> = a.into_iter().collect(); + let mut b_vec: Vec<_> = b.into_iter().collect(); + a_vec.sort_by(|a, b| a.value.cmp(&b.value)); + b_vec.sort_by(|a, b| a.value.cmp(&b.value)); + a_vec == b_vec + } + + fn hashset_vec_equal(a: I, b: J) -> bool + where + I: IntoIterator, + I::Item: Into, + J: IntoIterator, + J::Item: Into, + { + let mut a_vec: Vec = a.into_iter().map(Into::into).collect(); + let mut b_vec: Vec = b.into_iter().map(Into::into).collect(); + a_vec.sort(); + b_vec.sort(); + a_vec == b_vec + } + #[test] fn test_find_in_options() { - let config = rc_refcell!(SConfig::default()); - let role = rc_refcell!(SRole::new("test".to_string(), Rc::downgrade(&config))); - let mut global_path = SPathOptions::default(); - global_path.default_behavior = PathBehavior::Delete; - global_path.add.insert("path1".to_string()); - let mut role_path = SPathOptions::default(); - role_path.default_behavior = PathBehavior::Inherit; - role_path.add.insert("path2".to_string()); - let mut config_global = Opt::new(Level::Global); - config_global.path = Some(global_path); - as_borrow_mut!(config).options = Some(rc_refcell!(config_global)); - let mut config_role = Opt::new(Level::Role); - config_role.path = Some(role_path.clone()); - as_borrow_mut!(role).options = Some(rc_refcell!(config_role)); - as_borrow_mut!(config).roles.push(role); + let config = SConfig::builder() + .role( + SRole::builder("test") + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Inherit) + .add(["path2"]) + .build(), + ) + .build() + }) + .build(), + ) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path1"]) + .build(), + ) + .build() + }) + .build(); let options = OptStack::from_role(config.as_ref().borrow().roles[0].clone()); - let res: Option<(Level, SPathOptions)> = options.find_in_options(|opt| opt.path.clone().map(|value| (opt.level, value))); - assert_eq!(res, Some((Level::Role, role_path))); + assert_eq!( + res, + Some(( + Level::Role, + SPathOptions::builder(PathBehavior::Inherit) + .add(["path2"]) + .build() + )) + ); } + #[cfg(feature = "finder")] #[test] fn test_get_path() { - let config = rc_refcell!(SConfig::default()); - let role = rc_refcell!(SRole::new("test".to_string(), Rc::downgrade(&config))); - let mut global_path = SPathOptions::default(); - global_path.default_behavior = PathBehavior::Delete; - global_path.add.insert("path1".to_string()); - let mut role_path = SPathOptions::default(); - role_path.default_behavior = PathBehavior::Inherit; - role_path.add.insert("path2".to_string()); - let mut config_global = Opt::new(Level::Global); - config_global.path = Some(global_path); - as_borrow_mut!(config).options = Some(rc_refcell!(config_global)); - let mut config_role = Opt::new(Level::Role); - config_role.path = Some(role_path); - as_borrow_mut!(role).options = Some(rc_refcell!(config_role)); - as_borrow_mut!(config).roles.push(role); + let config = SConfig::builder() + .role( + SRole::builder("test") + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Inherit) + .add(["path2"]) + .build(), + ) + .build() + }) + .build(), + ) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path1"]) + .build(), + ) + .build() + }) + .build(); let options = OptStack::from_role(config.as_ref().borrow().roles.first().unwrap().clone()); let res = options.calculate_path(); assert_eq!(res, "path2:path1"); } + #[cfg(feature = "finder")] #[test] fn test_get_path_delete() { - let role = SRoleWrapper::default(); - as_borrow_mut!(role).name = "test".to_string(); - let mut path_options = SPathOptions::new(PathBehavior::Delete); - path_options.add.insert("path2".to_string()); - let mut opt_role = Opt::new(Level::Role); - opt_role.path = Some(path_options); - as_borrow_mut!(role).options = Some(rc_refcell!(opt_role)); - let config = SConfigWrapper::default(); - as_borrow_mut!(config).roles.push(role.clone()); - let mut global_options = Opt::new(Level::Global); - global_options.path = Some(SPathOptions::new(PathBehavior::Delete)); - global_options - .path - .as_mut() - .unwrap() - .add - .insert("path1".to_string()); - as_borrow_mut!(role)._config = Some(Rc::downgrade(&config)); - let options = OptStack::from_role(role).calculate_path(); + let config = SConfig::builder() + .role( + SRole::builder("test") + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path2"]) + .build(), + ) + .build() + }) + .build(), + ) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path1"]) + .build(), + ) + .build() + }) + .build(); + let options = OptStack::from_role(config.role("test").unwrap()).calculate_path(); assert!(options.contains("path2")); } + #[cfg(feature = "finder")] #[test] fn test_opt_add_sub() { - let role = SRoleWrapper::default(); - as_borrow_mut!(role).name = "test".to_string(); - let mut path_options = SPathOptions::new(PathBehavior::Delete); - path_options.sub.insert("path1".to_string()); - let mut opt_role = Opt::new(Level::Role); - opt_role.path = Some(path_options); - as_borrow_mut!(role).options = Some(rc_refcell!(opt_role)); - let mut path_options = SPathOptions::new(PathBehavior::Delete); - path_options.add.insert("path1".to_string()); - let mut opt_global = Opt::new(Level::Global); - opt_global.path = Some(path_options); - let config = SConfigWrapper::default(); - as_borrow_mut!(config).roles.push(role.clone()); - as_borrow_mut!(config).options = Some(rc_refcell!(opt_global)); - as_borrow_mut!(role)._config = Some(Rc::downgrade(&config)); - let options = OptStack::from_role(role).calculate_path(); + let config = SConfig::builder() + .role( + SRole::builder("test") + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Delete) + .sub(["path1"]) + .build(), + ) + .build() + }) + .build(), + ) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path1"]) + .build(), + ) + .build() + }) + .build(); + let options = OptStack::from_role(config.role("test").unwrap()).calculate_path(); assert!(!options.contains("path1")); } #[test] fn test_env_global_to_task() { - let mut env_options = SEnvOptions::new(EnvBehavior::Delete); - env_options.keep.insert("env1".into()); - let mut opt = Opt::new(Level::Task); - opt.env = Some(env_options); - let task = STaskWrapper::default(); - as_borrow_mut!(task).name = IdTask::Number(1); - as_borrow_mut!(task).options = Some(rc_refcell!(opt)); - let role = SRoleWrapper::default(); - as_borrow_mut!(role).name = "test".to_string(); - let mut env_options = SEnvOptions::new(EnvBehavior::Delete); - env_options.keep.insert("env2".into()); - let mut opt = Opt::new(Level::Role); - opt.env = Some(env_options); - as_borrow_mut!(role).options = Some(rc_refcell!(opt)); - as_borrow_mut!(task)._role = Some(Rc::downgrade(&role)); - - let mut env_options = SEnvOptions::new(EnvBehavior::Delete); - env_options.keep.insert("env3".into()); - - let mut opt = Opt::new(Level::Global); - opt.env = Some(env_options); - let config = SConfigWrapper::default(); - as_borrow_mut!(config).roles.push(role.clone()); - as_borrow_mut!(config).options = Some(rc_refcell!(opt)); - as_borrow_mut!(role)._config = Some(Rc::downgrade(&config)); - let options = OptStack::from_task(task).to_opt(); - let res = options.env.unwrap().keep; - assert!(res.contains(&EnvKey::from("env1"))); + let config = SConfig::builder() + .role( + SRole::builder("test") + .task( + STask::builder(1) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["env1"]) + .unwrap() + .build(), + ) + .build() + }) + .build(), + ) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["env2"]) + .unwrap() + .build(), + ) + .build() + }) + .build(), + ) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["env3"]) + .unwrap() + .build(), + ) + .build() + }) + .build(); + let binding = OptStack::from_task(config.task("test", 1).unwrap()).to_opt(); + let options = binding.as_ref().borrow(); + let res = &options.env.as_ref().unwrap().keep; + assert!(res + .as_ref() + .unwrap_or(&LinkedHashSet::new()) + .contains(&EnvKey::from("env1"))); } // test to_opt() for OptStack #[test] fn test_to_opt() { - let role = SRoleWrapper::default(); - as_borrow_mut!(role).name = "test".to_string(); - let mut path_options = SPathOptions::new(PathBehavior::Inherit); - path_options.add.insert("path2".to_string()); - let mut opt_role = Opt::new(Level::Role); - opt_role.path = Some(path_options); - as_borrow_mut!(role).options = Some(rc_refcell!(opt_role)); - let mut path_options = SPathOptions::new(PathBehavior::Delete); - path_options.add.insert("path1".to_string()); - let mut opt_global = Opt::new(Level::Global); - opt_global.path = Some(path_options); - let config = SConfigWrapper::default(); - as_borrow_mut!(config).roles.push(role.clone()); - as_borrow_mut!(config).options = Some(rc_refcell!(opt_global)); - as_borrow_mut!(role)._config = Some(Rc::downgrade(&config)); - let options = OptStack::from_role(role).to_opt(); - assert_eq!(options.path.unwrap().add.len(), 2); + let config = SConfig::builder() + .role( + SRole::builder("test") + .task( + STask::builder(1) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Inherit) + .add(["path3"]) + .build(), + ) + .env( + SEnvOptions::builder(EnvBehavior::Inherit) + .keep(["env3"]) + .unwrap() + .build(), + ) + .root(SPrivileged::User) + .bounding(SBounding::Strict) + .authentication(SAuthentication::Perform) + .timeout( + STimeout::builder() + .type_field(TimestampType::TTY) + .duration(Duration::minutes(3)) + .build(), + ) + .wildcard_denied("c") + .build() + }) + .build(), + ) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Inherit) + .add(["path2"]) + .build(), + ) + .env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["env1"]) + .unwrap() + .build(), + ) + .root(SPrivileged::Privileged) + .bounding(SBounding::Strict) + .authentication(SAuthentication::Skip) + .timeout( + STimeout::builder() + .type_field(TimestampType::PPID) + .duration(Duration::minutes(2)) + .build(), + ) + .wildcard_denied("b") + .build() + }) + .build(), + ) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path1"]) + .build(), + ) + .env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["env2"]) + .unwrap() + .build(), + ) + .root(SPrivileged::Privileged) + .bounding(SBounding::Ignore) + .authentication(SAuthentication::Perform) + .timeout( + STimeout::builder() + .type_field(TimestampType::TTY) + .duration(Duration::minutes(1)) + .build(), + ) + .wildcard_denied("a") + .build() + }) + .build(); + let default = LinkedHashSet::new(); + let stack = OptStack::from_roles(config.clone()); + let opt = stack.to_opt(); + let global_options = opt.as_ref().borrow(); + assert_eq!( + global_options.path.as_ref().unwrap().default_behavior, + PathBehavior::Delete + ); + assert!(hashset_vec_equal( + global_options + .path + .as_ref() + .unwrap() + .add + .as_ref() + .unwrap_or(&default) + .clone(), + vec!["path1"] + )); + assert_eq!( + global_options.env.as_ref().unwrap().default_behavior, + EnvBehavior::Delete + ); + assert!(env_key_set_equal( + global_options + .env + .as_ref() + .unwrap() + .keep + .as_ref() + .unwrap_or(&LinkedHashSet::new()) + .clone(), + vec![EnvKey::from("env2")] + )); + assert_eq!(global_options.root.unwrap(), SPrivileged::Privileged); + assert_eq!(global_options.bounding.unwrap(), SBounding::Ignore); + assert_eq!( + global_options.authentication.unwrap(), + SAuthentication::Perform + ); + assert_eq!( + global_options.timeout.as_ref().unwrap().duration.unwrap(), + Duration::minutes(1) + ); + assert_eq!( + global_options.timeout.as_ref().unwrap().type_field.unwrap(), + TimestampType::TTY + ); + assert_eq!(global_options.wildcard_denied.as_ref().unwrap(), "a"); + let opt = OptStack::from_role(config.clone().role("test").unwrap()).to_opt(); + let role_options = opt.as_ref().borrow(); + assert_eq!( + role_options.path.as_ref().unwrap().default_behavior, + PathBehavior::Delete + ); + assert!(hashset_vec_equal( + role_options + .path + .as_ref() + .unwrap() + .add + .as_ref() + .unwrap_or(&default) + .clone(), + vec!["path1", "path2"] + )); + assert_eq!( + role_options.env.as_ref().unwrap().default_behavior, + EnvBehavior::Delete + ); + assert!(env_key_set_equal( + role_options + .env + .as_ref() + .unwrap() + .keep + .as_ref() + .unwrap_or(&LinkedHashSet::new()) + .clone(), + vec![EnvKey::from("env1")] + )); + assert_eq!(role_options.root.unwrap(), SPrivileged::Privileged); + assert_eq!(role_options.bounding.unwrap(), SBounding::Strict); + assert_eq!(role_options.authentication.unwrap(), SAuthentication::Skip); + assert_eq!( + role_options.timeout.as_ref().unwrap().duration.unwrap(), + Duration::minutes(2) + ); + assert_eq!( + role_options.timeout.as_ref().unwrap().type_field.unwrap(), + TimestampType::PPID + ); + assert_eq!(role_options.wildcard_denied.as_ref().unwrap(), "b"); + let opt = OptStack::from_task(config.task("test", 1).unwrap()).to_opt(); + let task_options = opt.as_ref().borrow(); + assert_eq!( + task_options.path.as_ref().unwrap().default_behavior, + PathBehavior::Delete + ); + assert!(hashset_vec_equal( + task_options + .path + .as_ref() + .unwrap() + .add + .as_ref() + .unwrap_or(&default) + .clone(), + vec!["path1", "path2", "path3"] + )); + assert_eq!( + task_options.env.as_ref().unwrap().default_behavior, + EnvBehavior::Delete + ); + assert!(env_key_set_equal( + task_options + .env + .as_ref() + .unwrap() + .keep + .as_ref() + .unwrap_or(&LinkedHashSet::new()) + .clone(), + vec![EnvKey::from("env1"), EnvKey::from("env3")] + )); + assert_eq!(task_options.root.unwrap(), SPrivileged::User); + assert_eq!(task_options.bounding.unwrap(), SBounding::Strict); + assert_eq!( + task_options.authentication.unwrap(), + SAuthentication::Perform + ); + assert_eq!( + task_options.timeout.as_ref().unwrap().duration.unwrap(), + Duration::minutes(3) + ); + assert_eq!( + task_options.timeout.as_ref().unwrap().type_field.unwrap(), + TimestampType::TTY + ); + assert_eq!(task_options.wildcard_denied.as_ref().unwrap(), "c"); } #[test] fn test_get_timeout() { - let role = SRoleWrapper::default(); - as_borrow_mut!(role).name = "test".to_string(); - let mut timeout = STimeout::default(); - timeout.duration = Some(Duration::minutes(5)); - let mut opt_role = Opt::new(Level::Role); - opt_role.timeout = Some(timeout); - as_borrow_mut!(role).options = Some(rc_refcell!(opt_role)); - let mut timeout = STimeout::default(); - timeout.duration = Some(Duration::minutes(10)); - let mut opt_global = Opt::new(Level::Global); - opt_global.timeout = Some(timeout); - let config = SConfigWrapper::default(); - as_borrow_mut!(config).roles.push(role.clone()); - as_borrow_mut!(config).options = Some(rc_refcell!(opt_global)); - as_borrow_mut!(role)._config = Some(Rc::downgrade(&config)); - let options = OptStack::from_role(role).get_timeout(); + let config = SConfig::builder() + .role( + SRole::builder("test") + .options(|opt| { + opt.timeout(STimeout::builder().duration(Duration::minutes(5)).build()) + .build() + }) + .build(), + ) + .options(|opt| { + opt.timeout( + STimeout::builder() + .type_field(TimestampType::PPID) + .duration(Duration::minutes(10)) + .build(), + ) + .build() + }) + .build(); + let options = OptStack::from_role(config.role("test").unwrap()).get_timeout(); assert_eq!(options.1.duration.unwrap(), Duration::minutes(5)); assert_eq!(options.0, Level::Role); assert!(options.1.type_field.is_none()); @@ -1372,66 +1788,52 @@ mod tests { #[test] fn test_get_root_behavior() { - let task = STaskWrapper::default(); - as_borrow_mut!(task).name = IdTask::Number(1); - as_borrow_mut!(task).options = Some(rc_refcell!(Opt::new(Level::Task))); - let role = SRoleWrapper::default(); - as_borrow_mut!(role).name = "test".to_string(); - let root = SPrivileged::User; - let mut opt_role = Opt::new(Level::Role); - opt_role.root = Some(root); - as_borrow_mut!(role).options = Some(rc_refcell!(opt_role)); - let root = SPrivileged::Privileged; - let mut opt_global = Opt::new(Level::Global); - opt_global.root = Some(root); - let config = SConfigWrapper::default(); - as_borrow_mut!(task)._role = Some(Rc::downgrade(&role)); - as_borrow_mut!(role).tasks.push(task.clone()); - as_borrow_mut!(config).roles.push(role.clone()); - as_borrow_mut!(config).options = Some(rc_refcell!(opt_global)); - as_borrow_mut!(role)._config = Some(Rc::downgrade(&config)); - let options = OptStack::from_task(task).get_root_behavior(); - assert_eq!(options.1, SPrivileged::User); + let config = SConfig::builder() + .role( + SRole::builder("test") + .task(STask::builder(1).build()) + .options(|opt| opt.root(SPrivileged::User).build()) + .build(), + ) + .options(|opt| opt.root(SPrivileged::Privileged).build()) + .build(); + let (level, sprivilege) = + OptStack::from_task(config.task("test", 1).unwrap()).get_root_behavior(); + assert_eq!(level, Level::Role); + assert_eq!(sprivilege, SPrivileged::User); } #[test] fn test_get_bounding() { - let role = SRoleWrapper::default(); - as_borrow_mut!(role).name = "test".to_string(); - let bounding = SBounding::Strict; - let mut opt_role = Opt::new(Level::Role); - opt_role.bounding = Some(bounding); - as_borrow_mut!(role).options = Some(rc_refcell!(opt_role)); - let bounding = SBounding::Ignore; - let mut opt_global = Opt::new(Level::Global); - opt_global.bounding = Some(bounding); - let config = SConfigWrapper::default(); - as_borrow_mut!(config).roles.push(role.clone()); - as_borrow_mut!(config).options = Some(rc_refcell!(opt_global)); - as_borrow_mut!(role)._config = Some(Rc::downgrade(&config)); - let options = OptStack::from_role(role).get_bounding(); - assert_eq!(options.1, SBounding::Strict); + let config = SConfig::builder() + .role( + SRole::builder("test") + .options(|opt| opt.bounding(SBounding::Strict).build()) + .build(), + ) + .options(|opt| opt.bounding(SBounding::Ignore).build()) + .build(); + let (level, bounding) = OptStack::from_role(config.role("test").unwrap()).get_bounding(); + assert_eq!(level, Level::Role); + assert_eq!(bounding, SBounding::Strict); } #[test] fn test_get_wildcard() { - let role = SRoleWrapper::default(); - as_borrow_mut!(role).name = "test".to_string(); - let wildcard = ";&|".to_string(); - let mut opt_role = Opt::new(Level::Role); - opt_role.wildcard_denied = Some(wildcard); - as_borrow_mut!(role).options = Some(rc_refcell!(opt_role)); - let wildcard = ";&|".to_string(); - let mut opt_global = Opt::new(Level::Global); - opt_global.wildcard_denied = Some(wildcard); - let config = SConfigWrapper::default(); - as_borrow_mut!(config).roles.push(role.clone()); - as_borrow_mut!(config).options = Some(rc_refcell!(opt_global)); - as_borrow_mut!(role)._config = Some(Rc::downgrade(&config)); - let options = OptStack::from_role(role).get_wildcard(); - assert_eq!(options.1, ";&|"); + let config = SConfig::builder() + .role( + SRole::builder("test") + .options(|opt| opt.wildcard_denied("b").build()) + .build(), + ) + .options(|opt| opt.wildcard_denied("a").build()) + .build(); + let (level, wildcard) = OptStack::from_role(config.role("test").unwrap()).get_wildcard(); + assert_eq!(level, Level::Role); + assert_eq!(wildcard, "b"); } + #[cfg(feature = "finder")] #[test] fn test_tz_is_safe() { assert!(tz_is_safe("America/New_York")); @@ -1448,49 +1850,364 @@ mod tests { #[cfg(feature = "finder")] #[test] fn test_check_env() { - let mut env_options = SEnvOptions::new(EnvBehavior::Inherit); - env_options.keep.insert("env1".into()); - let mut opt = Opt::new(Level::Task); - opt.env = Some(env_options); - let task = STaskWrapper::default(); - as_borrow_mut!(task).name = IdTask::Number(1); - as_borrow_mut!(task).options = Some(rc_refcell!(opt)); - let role = SRoleWrapper::default(); - as_borrow_mut!(role).name = "test".to_string(); - let mut env_options = SEnvOptions::new(EnvBehavior::Inherit); - env_options.check.insert("env2".into()); - let mut opt = Opt::new(Level::Role); - opt.env = Some(env_options); - as_borrow_mut!(role).options = Some(rc_refcell!(opt)); - as_borrow_mut!(task)._role = Some(Rc::downgrade(&role)); - - let mut env_options = SEnvOptions::new(EnvBehavior::Delete); - env_options.check.insert("env3".into()); - env_options.set.insert("env4".into(), "value4".into()); - - let mut opt = Opt::new(Level::Global); - opt.env = Some(env_options); - let config = SConfigWrapper::default(); - as_borrow_mut!(config).roles.push(role.clone()); - as_borrow_mut!(config).options = Some(rc_refcell!(opt)); - as_borrow_mut!(role)._config = Some(Rc::downgrade(&config)); - let options = OptStack::from_task(task); + let config = SConfig::builder() + .role( + SRole::builder("test") + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Inherit) + .check(["env2"]) + .unwrap() + .build(), + ) + .build() + }) + .task( + STask::builder(IdTask::Number(1)) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Inherit) + .keep(["env1"]) + .unwrap() + .build(), + ) + .build() + }) + .build(), + ) + .build(), + ) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Delete) + .check(["env3"]) + .unwrap() + .set([("env4".to_string(), "value4".to_string())]) + .build(), + ) + .build() + }) + .build(); + let options = OptStack::from_task(config.task("test", 1).unwrap()); let mut test_env = HashMap::new(); test_env.insert("env1".to_string(), "value1".to_string()); test_env.insert("env2".into(), "va%lue2".into()); test_env.insert("env3".into(), "value3".into()); - let cred = Cred { - user: User::from_uid(0.into()).unwrap().unwrap(), - groups: vec![Group::from_gid(0.into()).unwrap().unwrap()], - tty: None, - ppid: Pid::from_raw(0), - }; + let cred = Cred::builder() + .user_id(0) + .group_id(0) + .ppid(Pid::from_raw(0)) + .build(); let result = options - .calculate_filtered_env(cred, test_env.into_iter()) + .calculate_filtered_env(None, cred, test_env.into_iter()) .unwrap(); assert_eq!(result.get("env1").unwrap(), "value1"); assert_eq!(result.get("env3").unwrap(), "value3"); assert!(result.get("env2").is_none()); assert_eq!(result.get("env4").unwrap(), "value4"); } + + #[cfg(feature = "finder")] + #[test] + fn test_override_env() { + let config = SConfig::builder() + .role( + SRole::builder("test") + .task( + STask::builder(IdTask::Number(1)) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Inherit) + .keep(["env1"]) + .unwrap() + .build(), + ) + .build() + }) + .build(), + ) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Inherit) + .check(["env2"]) + .unwrap() + .build(), + ) + .build() + }) + .build(), + ) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Delete) + .check(["env3"]) + .unwrap() + .set([("env4".to_string(), "value4".to_string())]) + .build(), + ) + .build() + }) + .build(); + + let options = OptStack::from_task(config.task("test", 1).unwrap()); + let mut test_env = HashMap::new(); + test_env.insert("env1".to_string(), "value1".to_string()); + test_env.insert("env2".into(), "va%lue2".into()); + test_env.insert("env3".into(), "value3".into()); + let cred = Cred::builder().user_id(0).group_id(0).build(); + let result = options + .calculate_filtered_env(None, cred, test_env.into_iter()) + .unwrap(); + assert_eq!(result.get("env1").unwrap(), "value1"); + assert_eq!(result.get("env3").unwrap(), "value3"); + assert!(result.get("env2").is_none()); + assert_eq!(result.get("env4").unwrap(), "value4"); + } + + #[test] + fn is_wildcard_env_key() { + assert!(!is_valid_env_name("TEST_.*")); + assert!(!is_valid_env_name("123")); + assert!(!is_valid_env_name("")); + assert!(is_regex("TEST_.*")); + } + + #[test] + fn test_wildcard_env() { + let config = SConfig::builder() + .role( + SRole::builder("test") + .task( + STask::builder(IdTask::Number(1)) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["TEST_.*"]) + .unwrap() + .build(), + ) + .build() + }) + .build(), + ) + .build(), + ) + .build(); + let options = OptStack::from_task(config.task("test", 1).unwrap()); + let mut test_env = HashMap::new(); + test_env.insert("TEST_A".to_string(), "value1".to_string()); + test_env.insert("TEST_B".into(), "value2".into()); + test_env.insert("TESTaA".into(), "value3".into()); + let cred = Cred::builder().user_id(0).group_id(0).build(); + let result = options + .calculate_filtered_env(None, cred, test_env.into_iter()) + .unwrap(); + assert_eq!(result.get("TEST_A").unwrap(), "value1"); + assert_eq!(result.get("TEST_B").unwrap(), "value2"); + assert!(result.get("TESTaA").is_none()); + } + + #[test] + fn test_safe_path() { + let path = std::env::var("PATH").unwrap(); + std::env::set_var("PATH", "/sys:./proc:/tmp:/bin"); + let config = SConfig::builder() + .role( + SRole::builder("test") + .task( + STask::builder(IdTask::Number(1)) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::KeepSafe) + .add(Vec::::new()) + .sub(["/sys"]) + .build(), + ) + .build() + }) + .build(), + ) + .build(), + ) + .build(); + let options = OptStack::from_task(config.task("test", 1).unwrap()); + let res = options.calculate_path(); + + assert_eq!(res, "/tmp:/bin"); + std::env::set_var("PATH", path); + } + + #[test] + fn test_unsafe_path() { + let path = std::env::var("PATH").unwrap(); + + let config = SConfig::builder() + .role( + SRole::builder("test") + .task( + STask::builder(IdTask::Number(1)) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::KeepUnsafe) + .add(Vec::::new()) + .sub(["/sys"]) + .build(), + ) + .build() + }) + .build(), + ) + .build(), + ) + .build(); + let options = OptStack::from_task(config.task("test", 1).unwrap()); + std::env::set_var("PATH", "/sys:./proc:/tmp:/bin"); + let res = options.calculate_path(); + assert_eq!(res, "./proc:/tmp:/bin"); + std::env::set_var("PATH", path); + } + + #[test] + fn test_inherit_keep_path() { + let path = std::env::var("PATH").unwrap(); + let config = SConfig::builder() + .role( + SRole::builder("test") + .task( + STask::builder(IdTask::Number(1)) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Inherit) + .add(Vec::::new()) + .sub(["/sys"]) + .build(), + ) + .build() + }) + .build(), + ) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::KeepSafe) + .add(Vec::::new()) + .sub(["/tmp"]) + .build(), + ) + .build() + }) + .build(), + ) + .build(); + let options = OptStack::from_task(config.task("test", 1).unwrap()); + std::env::set_var("PATH", "/sys:./proc:/tmp:/bin"); + let res = options.calculate_path(); + + assert_eq!(res, "/bin"); + std::env::set_var("PATH", path); + } + + #[test] + fn test_final_env_keep() { + let config = SConfig::builder() + .role( + SRole::builder("test") + .task( + STask::builder(IdTask::Number(1)) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Inherit) + .delete(["env1"]) + .unwrap() + .build(), + ) + .build() + }) + .build(), + ) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Inherit) + .delete(["env2"]) + .unwrap() + .build(), + ) + .build() + }) + .build(), + ) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Keep) + .delete(["env3"]) + .unwrap() + .build(), + ) + .build() + }) + .build(); + let options = OptStack::from_task(config.task("test", 1).unwrap()); + let test_env = [ + ("env1", "value1"), + ("env2", "value2"), + ("env3", "value3"), + ("env4", "value4"), + ("env5", "value5"), + ] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())); + + let cred = Cred::builder().user_id(0).group_id(0).build(); + let result = options + .calculate_filtered_env(None, cred, test_env.into_iter()) + .unwrap(); + assert!(result.get("env1").is_none()); + assert!(result.get("env2").is_none()); + assert!(result.get("env3").is_none()); + assert_eq!(result.get("env4").unwrap(), "value4"); + assert_eq!(result.get("env5").unwrap(), "value5"); + } + + #[test] + fn test_opt_filter_env() { + let config = SConfig::builder() + .role( + SRole::builder("test") + .task( + STask::builder(IdTask::Number(1)) + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Delete) + .delete(["envA"]) + .unwrap() + .override_behavior(true) + .build(), + ) + .build() + }) + .build(), + ) + .build(), + ) + .build(); + let options = OptStack::from_task(config.task("test", 1).unwrap()); + let test_env = [("envA", "value1"), ("envB", "value2"), ("envC", "value3")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())); + + let cred = Cred::builder().user_id(0).group_id(0).build(); + let result = options + .calculate_filtered_env( + Some( + FilterMatcher::builder() + .env_behavior(EnvBehavior::Keep) + .build(), + ), + cred, + test_env.into_iter(), + ) + .unwrap(); + assert!(result.get("envA").is_none()); + assert_eq!(result.get("envB").unwrap(), "value2"); + assert_eq!(result.get("envC").unwrap(), "value3"); + } } diff --git a/rar-common/src/database/structs.rs b/rar-common/src/database/structs.rs index 04fbf5fb..1456a807 100644 --- a/rar-common/src/database/structs.rs +++ b/rar-common/src/database/structs.rs @@ -1,11 +1,9 @@ -use capctl::CapSet; +use bon::{bon, builder, Builder}; +use capctl::{Cap, CapSet}; use derivative::Derivative; -use nix::{ - errno::Errno, - unistd::{Group, User}, -}; use serde::{ - de::{self, Visitor}, + de::{self, MapAccess, SeqAccess, Visitor}, + ser::SerializeMap, Deserialize, Deserializer, Serialize, }; use serde_json::{Map, Value}; @@ -13,7 +11,6 @@ use strum::{Display, EnumIs}; use std::{ cell::RefCell, - cmp::Ordering, error::Error, fmt, ops::{Index, Not}, @@ -21,15 +18,19 @@ use std::{ }; use super::{ + actor::{SActor, SGroups, SUserType}, is_default, - options::Opt, - wrapper::{OptWrapper, STaskWrapper}, + options::{Level, Opt, OptBuilder}, }; #[derive(Deserialize, Serialize, PartialEq, Eq, Debug)] pub struct SConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub options: OptWrapper, + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "sconfig_opt" + )] + pub options: Option>>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub roles: Vec>>, #[serde(default)] @@ -37,6 +38,15 @@ pub struct SConfig { pub _extra_fields: Map, } +fn sconfig_opt<'de, D>(deserializer: D) -> Result>>, D::Error> +where + D: Deserializer<'de>, +{ + let mut opt = Opt::deserialize(deserializer)?; + opt.level = Level::Global; + Ok(Some(Rc::new(RefCell::new(opt)))) +} + #[derive(Serialize, Deserialize, Debug, Derivative)] #[serde(rename_all = "kebab-case")] #[derivative(PartialEq, Eq)] @@ -45,9 +55,13 @@ pub struct SRole { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub actors: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub tasks: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub options: OptWrapper, + pub tasks: Vec>>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "srole_opt" + )] + pub options: Option>>, #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] pub _extra_fields: Map, #[serde(skip)] @@ -55,60 +69,13 @@ pub struct SRole { pub _config: Option>>, } -#[derive(Serialize, PartialEq, Eq, Debug, EnumIs, Clone)] -#[serde(untagged, rename_all = "lowercase")] -pub enum SActorType { - Id(u32), - Name(String), -} - -impl std::fmt::Display for SActorType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - SActorType::Id(id) => write!(f, "{}", id), - SActorType::Name(name) => write!(f, "{}", name), - } - } -} - -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, EnumIs)] -#[serde(untagged)] -pub enum SGroups { - Single(SActorType), - Multiple(Vec), -} - -impl SGroups { - pub fn len(&self) -> usize { - match self { - SGroups::Single(_) => 1, - SGroups::Multiple(groups) => groups.len(), - } - } - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum SActor { - #[serde(rename = "user")] - User { - #[serde(alias = "name", skip_serializing_if = "Option::is_none")] - id: Option, - #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] - _extra_fields: Map, - }, - #[serde(rename = "group")] - Group { - #[serde(alias = "names", skip_serializing_if = "Option::is_none")] - groups: Option, - #[serde(default, flatten)] - _extra_fields: Map, - }, - #[serde(untagged)] - Unknown(Value), +fn srole_opt<'de, D>(deserializer: D) -> Result>>, D::Error> +where + D: Deserializer<'de>, +{ + let mut opt = Opt::deserialize(deserializer)?; + opt.level = Level::Role; + Ok(Some(Rc::new(RefCell::new(opt)))) } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs)] @@ -138,8 +105,12 @@ pub struct STask { pub cred: SCredentials, #[serde(default, skip_serializing_if = "is_default")] pub commands: SCommands, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub options: OptWrapper, + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "stask_opt" + )] + pub options: Option>>, #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] pub _extra_fields: Map, #[serde(skip)] @@ -147,22 +118,82 @@ pub struct STask { pub _role: Option>>, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +fn stask_opt<'de, D>(deserializer: D) -> Result>>, D::Error> +where + D: Deserializer<'de>, +{ + let mut opt = Opt::deserialize(deserializer)?; + opt.level = Level::Task; + Ok(Some(Rc::new(RefCell::new(opt)))) +} + +#[derive(Serialize, Deserialize, Debug, Builder, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct SCredentials { #[serde(skip_serializing_if = "Option::is_none")] - pub setuid: Option, + #[builder(into)] + pub setuid: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(into)] pub setgid: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub capabilities: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[builder(into)] pub additional_auth: Option, // TODO: to extract as plugin #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] + #[builder(default)] pub _extra_fields: Map, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Display, Debug, EnumIs)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(untagged)] +pub enum SUserChooser { + Actor(SUserType), + ChooserStruct(SSetuidSet), +} + +impl From for SUserChooser { + fn from(actor: SUserType) -> Self { + SUserChooser::Actor(actor) + } +} + +impl From for SUserChooser { + fn from(set: SSetuidSet) -> Self { + SUserChooser::ChooserStruct(set) + } +} + +impl From<&str> for SUserChooser { + fn from(name: &str) -> Self { + SUserChooser::Actor(name.into()) + } +} + +impl From for SUserChooser { + fn from(id: u32) -> Self { + SUserChooser::Actor(id.into()) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq)] + +pub struct SSetuidSet { + #[builder(start_fn, into)] + pub fallback: SUserType, + #[serde(rename = "default", default, skip_serializing_if = "is_default")] + #[builder(start_fn)] + pub default: SetBehavior, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[builder(default, with = FromIterator::from_iter)] + pub add: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[builder(default, with = FromIterator::from_iter)] + pub sub: Vec, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Display, Debug, EnumIs, Clone)] #[serde(rename_all = "lowercase")] #[derive(Default)] pub enum SetBehavior { @@ -171,28 +202,148 @@ pub enum SetBehavior { None, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Builder)] pub struct SCapabilities { - #[serde(rename = "default", default, skip_serializing_if = "is_default")] + #[builder(start_fn)] pub default_behavior: SetBehavior, - #[serde( - default, - skip_serializing_if = "CapSet::is_empty", - deserialize_with = "super::deserialize_capset", - serialize_with = "super::serialize_capset" - )] + #[builder(field)] pub add: CapSet, - #[serde( - default, - skip_serializing_if = "CapSet::is_empty", - deserialize_with = "super::deserialize_capset", - serialize_with = "super::serialize_capset" - )] + #[builder(field)] pub sub: CapSet, - #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] + #[builder(default, with = <_>::from_iter)] pub _extra_fields: Map, } +impl SCapabilitiesBuilder { + pub fn add_cap(mut self, cap: Cap) -> Self { + self.add.add(cap); + self + } + pub fn add_all(mut self, set: CapSet) -> Self { + self.add = set; + self + } + pub fn sub_cap(mut self, cap: Cap) -> Self { + self.sub.add(cap); + self + } + pub fn sub_all(mut self, set: CapSet) -> Self { + self.sub = set; + self + } +} + +impl Serialize for SCapabilities { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.default_behavior.is_none() && self.sub.is_empty() && self._extra_fields.is_empty() { + super::serialize_capset(&self.add, serializer) + } else { + let mut map = serializer.serialize_map(Some(3))?; + if self.default_behavior.is_none() { + map.serialize_entry("default", &self.default_behavior)?; + } + if !self.add.is_empty() { + let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("add", &v)?; + } + if !self.sub.is_empty() { + let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("del", &v)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } + } +} +impl<'de> Deserialize<'de> for SCapabilities { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SCapabilitiesVisitor; + + impl<'de> Visitor<'de> for SCapabilitiesVisitor { + type Value = SCapabilities; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an array of strings or a map with SCapabilities fields") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut add = CapSet::default(); + while let Some(cap) = seq.next_element::()? { + add.add(cap.parse().map_err(de::Error::custom)?); + } + + Ok(SCapabilities { + default_behavior: SetBehavior::None, + add, + sub: CapSet::default(), + _extra_fields: Map::new(), + }) + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut default_behavior = SetBehavior::None; + let mut add = CapSet::default(); + let mut sub = CapSet::default(); + let mut _extra_fields = Map::new(); + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "default" => { + default_behavior = map + .next_value() + .expect("default entry must be either 'all' or 'none'"); + } + "add" => { + let values: Vec = + map.next_value().expect("add entry must be a list"); + for value in values { + add.add(value.parse().map_err(|_| { + de::Error::custom(format!("Invalid capability: {}", value)) + })?); + } + } + "sub" | "del" => { + let values: Vec = + map.next_value().expect("sub entry must be a list"); + for value in values { + sub.add(value.parse().map_err(|_| { + de::Error::custom(format!("Invalid capability: {}", value)) + })?); + } + } + other => { + _extra_fields.insert(other.to_string(), map.next_value()?); + } + } + } + + Ok(SCapabilities { + default_behavior, + add, + sub, + _extra_fields, + }) + } + } + + deserializer.deserialize_any(SCapabilitiesVisitor) + } +} + #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] #[serde(untagged)] pub enum SCommand { @@ -206,7 +357,7 @@ pub struct SCommands { pub default_behavior: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub add: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(default, alias = "del", skip_serializing_if = "Vec::is_empty")] pub sub: Vec, #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] pub _extra_fields: Map, @@ -287,6 +438,12 @@ impl Default for SCapabilities { } } +impl Default for SSetuidSet { + fn default() -> Self { + SSetuidSet::builder(0, SetBehavior::None).build() + } +} + impl Default for IdTask { fn default() -> Self { IdTask::Number(0) @@ -297,21 +454,21 @@ impl Default for IdTask { // From implementations // ------------------------ -impl From for SActorType { - fn from(id: u32) -> Self { - SActorType::Id(id) +impl From for IdTask { + fn from(id: usize) -> Self { + IdTask::Number(id) } } -impl From for SActorType { +impl From for IdTask { fn from(name: String) -> Self { - SActorType::Name(name) + IdTask::Name(name) } } -impl From<&str> for SActorType { +impl From<&str> for IdTask { fn from(name: &str) -> Self { - SActorType::Name(name.to_string()) + IdTask::Name(name.to_string()) } } @@ -335,65 +492,121 @@ impl From for SCapabilities { // ------------------------ // This try to deserialize a number as an ID and a string as a name -impl<'de> Deserialize<'de> for SActorType { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct IdVisitor; - impl<'de> Visitor<'de> for IdVisitor { - type Value = SActorType; +// ======================== +// Implementations for Struct navigation +// ======================== +#[bon] +impl SConfig { + #[builder] + pub fn new( + #[builder(field)] roles: Vec>>, + #[builder(with = |f : fn(OptBuilder) -> Rc> | f(Opt::builder(Level::Global)))] + options: Option>>, + _extra_fields: Option>, + ) -> Rc> { + let c = Rc::new(RefCell::new(SConfig { + roles: roles.clone(), + options: options.clone(), + _extra_fields: _extra_fields.unwrap_or_default().clone(), + })); + for role in &roles { + role.borrow_mut()._config = Some(Rc::downgrade(&c)); + } + c + } +} - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("user ID as a number or string") - } +pub trait RoleGetter { + fn role(&self, name: &str) -> Option>>; + fn task>( + &self, + role: &str, + name: T, + ) -> Result>, Box>; +} - fn visit_u32(self, id: u32) -> Result - where - E: de::Error, - { - Ok(SActorType::Id(id)) - } +pub trait TaskGetter { + fn task(&self, name: &IdTask) -> Option>>; +} - fn visit_str(self, id: &str) -> Result - where - E: de::Error, - { - let rid: Result = id.parse(); - match rid { - Ok(id) => Ok(SActorType::Id(id)), - Err(_) => Ok(SActorType::Name(id.to_string())), - } - } - } +impl RoleGetter for Rc> { + fn role(&self, name: &str) -> Option>> { + self.as_ref() + .borrow() + .roles + .iter() + .find(|role| role.borrow().name == name) + .cloned() + } + fn task>( + &self, + role: &str, + name: T, + ) -> Result>, Box> { + let name = name.into(); + self.role(role) + .and_then(|role| role.as_ref().borrow().task(&name).cloned()) + .ok_or_else(|| format!("Task {} not found in role {}", name, role).into()) + } +} - deserializer.deserialize_any(IdVisitor) +impl TaskGetter for Rc> { + fn task(&self, name: &IdTask) -> Option>> { + self.as_ref() + .borrow() + .tasks + .iter() + .find(|task| task.borrow().name == *name) + .cloned() } } -// ======================== -// Implementations for Struct navigation -// ======================== +impl SConfigBuilder { + pub fn role(mut self, role: Rc>) -> Self { + self.roles.push(role); + self + } + pub fn roles(mut self, roles: impl IntoIterator>>) -> Self { + self.roles.extend(roles); + self + } +} -impl SConfig { - pub fn role(&self, name: &str) -> Option<&Rc>> { - self.roles.iter().find(|role| role.borrow().name == name) +impl SRoleBuilder { + pub fn task(mut self, task: Rc>) -> Self { + self.tasks.push(task); + self } - pub fn task(&self, role: &str, name: &IdTask) -> Result>, Box> { - self.role(role) - .and_then(|role| role.as_ref().borrow().task(name).cloned()) - .ok_or_else(|| format!("Task {} not found in role {}", name, role).into()) + pub fn actor(mut self, actor: SActor) -> Self { + self.actors.push(actor); + self } } +#[bon] impl SRole { - pub fn new(name: String, config: Weak>) -> Self { - SRole { + #[builder] + pub fn new( + #[builder(start_fn, into)] name: String, + #[builder(field)] tasks: Vec>>, + #[builder(field)] actors: Vec, + #[builder(with = |f : fn(OptBuilder) -> Rc> | f(Opt::builder(Level::Role)))] + options: Option>>, + #[builder(default)] _extra_fields: Map, + ) -> Rc> { + let s = Rc::new(RefCell::new(SRole { name, - _config: Some(config), - ..Default::default() + actors, + tasks, + options, + _extra_fields, + _config: None, + })); + for task in s.as_ref().borrow_mut().tasks.iter() { + task.borrow_mut()._role = Some(Rc::downgrade(&s)); } + s } pub fn config(&self) -> Option>> { self._config.as_ref()?.upgrade() @@ -405,13 +618,28 @@ impl SRole { } } +#[bon] impl STask { - pub fn new(name: IdTask, role: Weak>) -> Self { - STask { + #[builder] + pub fn new( + #[builder(start_fn, into)] name: IdTask, + purpose: Option, + #[builder(default)] cred: SCredentials, + #[builder(default)] commands: SCommands, + #[builder(with = |f : fn(OptBuilder) -> Rc> | f(Opt::builder(Level::Task)))] + options: Option>>, + #[builder(default)] _extra_fields: Map, + _role: Option>>, + ) -> Rc> { + Rc::new(RefCell::new(STask { name, - _role: Some(role), - ..Default::default() - } + purpose, + cred, + commands, + options, + _extra_fields, + _role, + })) } pub fn role(&self) -> Option>> { self._role.as_ref()?.upgrade() @@ -434,46 +662,24 @@ impl Index for SRole { } } -// ================= -// Display implementations -// ================= - -impl core::fmt::Display for SActor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - SActor::User { id, _extra_fields } => { - write!(f, "User: {}", id.as_ref().unwrap()) - } - SActor::Group { - groups, - _extra_fields, - } => { - write!(f, "Group: {}", groups.as_ref().unwrap()) - } - SActor::Unknown(unknown) => { - write!(f, "Unknown: {}", unknown) - } - } - } -} - -impl core::fmt::Display for SGroups { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - SGroups::Single(group) => { - write!(f, "{}", group) - } - SGroups::Multiple(groups) => { - write!(f, "{:?}", groups) - } +#[bon] +impl SCommands { + #[builder] + pub fn new( + #[builder(start_fn)] default_behavior: SetBehavior, + #[builder(default, with = FromIterator::from_iter)] add: Vec, + #[builder(default, with = FromIterator::from_iter)] sub: Vec, + #[builder(default, with = <_>::from_iter)] _extra_fields: Map, + ) -> Self { + SCommands { + default_behavior: Some(default_behavior), + add, + sub, + _extra_fields, } } } -// ================= -// Other implementations -// ================= - impl SCapabilities { pub fn to_capset(&self) -> CapSet { let mut capset = match self.default_behavior { @@ -486,196 +692,11 @@ impl SCapabilities { } } -impl PartialEq for SActorType { +impl PartialEq for SUserChooser { fn eq(&self, other: &str) -> bool { match self { - SActorType::Name(name) => name == other, - SActorType::Id(id) => other.parse().map(|oid: u32| oid == *id).unwrap_or(false), - } - } -} - -impl PartialEq for SActorType { - fn eq(&self, other: &User) -> bool { - match self { - SActorType::Name(name) => name == &other.name, - SActorType::Id(id) => other.uid.as_raw() == *id, - } - } -} - -impl PartialEq for SActorType { - fn eq(&self, other: &Group) -> bool { - match self { - SActorType::Name(name) => name == &other.name, - SActorType::Id(id) => other.gid.as_raw() == *id, - } - } -} - -impl PartialEq for SGroups { - fn eq(&self, other: &str) -> bool { - match self { - SGroups::Single(actor) => actor == other, - SGroups::Multiple(actors) => actors.len() == 1 && &actors[0] == other, - } - } -} - -impl PartialEq> for SGroups { - fn eq(&self, other: &Vec) -> bool { - match self { - SGroups::Single(actor) => { - if other.len() == 1 { - return actor == &other[0]; - } - } - SGroups::Multiple(actors) => { - if actors.len() == other.len() { - return actors.iter().all(|actor| other.iter().any(|x| actor == x)); - } - } - } - false - } -} - -impl FromIterator for SGroups { - fn from_iter>(iter: I) -> Self { - let mut iter = iter.into_iter(); - let first = iter.next().unwrap(); - let mut groups = vec![SActorType::Name(first)]; - for group in iter { - groups.push(SActorType::Name(group)); - } - if groups.len() == 1 { - SGroups::Single(groups[0].to_owned()) - } else { - SGroups::Multiple(groups) - } - } -} - -impl From for Vec { - fn from(val: SGroups) -> Self { - match val { - SGroups::Single(group) => vec![group], - SGroups::Multiple(groups) => groups, - } - } -} - -impl SActorType { - pub fn into_group(&self) -> Result, Errno> { - match self { - SActorType::Name(name) => Group::from_name(name), - SActorType::Id(id) => Group::from_gid(id.to_owned().into()), - } - } - pub fn into_user(&self) -> Result, Errno> { - match self { - SActorType::Name(name) => User::from_name(name), - SActorType::Id(id) => User::from_uid(id.to_owned().into()), - } - } -} - -impl PartialOrd for SGroups { - fn partial_cmp(&self, other: &SGroups) -> Option { - let other = Into::>::into(other.clone()); - self.partial_cmp(&other) - } -} - -impl PartialOrd> for SGroups { - fn partial_cmp(&self, other: &Vec) -> Option { - match self { - SGroups::Single(group) => { - if other.len() == 1 { - if group == &other[0] { - return Some(Ordering::Equal); - } - } else if other.iter().any(|x| group == x) { - return Some(Ordering::Less); - } - } - SGroups::Multiple(groups) => { - if groups.is_empty() && other.is_empty() { - return Some(Ordering::Equal); - } else if groups.len() == other.len() { - if groups.iter().all(|x| other.iter().any(|y| x == y)) { - return Some(Ordering::Equal); - } - } else if groups.len() < other.len() { - if groups.iter().all(|x| other.iter().any(|y| x == y)) { - return Some(Ordering::Less); - } - } else if other.iter().all(|x| groups.iter().any(|y| y == x)) { - return Some(Ordering::Greater); - } - } - } - None - } -} - -impl From for Vec { - fn from(val: SGroups) -> Self { - match val { - SGroups::Single(group) => vec![group.into_group().unwrap().unwrap()], - SGroups::Multiple(groups) => groups - .into_iter() - .map(|x| x.into_group().unwrap().unwrap()) - .collect(), - } - } -} - -impl SActor { - pub fn from_user_string(user: &str) -> Self { - SActor::User { - id: Some(user.into()), - _extra_fields: Map::default(), - } - } - pub fn from_user_id(id: u32) -> Self { - SActor::User { - id: Some(id.into()), - _extra_fields: Map::default(), - } - } - pub fn from_group_string(group: &str) -> Self { - SActor::Group { - groups: Some(SGroups::Single(group.into())), - _extra_fields: Map::default(), - } - } - pub fn from_group_id(id: u32) -> Self { - SActor::Group { - groups: Some(SGroups::Single(id.into())), - _extra_fields: Map::default(), - } - } - pub fn from_group_vec_string(group: Vec<&str>) -> Self { - Self::from_group_vec_actors( - group - .into_iter() - .map(|str| str.into()) - .collect::>(), - ) - } - pub fn from_group_vec_id(groups: Vec) -> Self { - Self::from_group_vec_actors( - groups - .into_iter() - .map(|id| id.into()) - .collect::>(), - ) - } - pub fn from_group_vec_actors(groups: Vec) -> Self { - SActor::Group { - groups: Some(SGroups::Multiple(groups)), - _extra_fields: Map::default(), + SUserChooser::Actor(actor) => actor == &SUserType::from(other), + SUserChooser::ChooserStruct(chooser) => chooser.fallback == *other, } } } @@ -685,10 +706,17 @@ mod tests { use capctl::Cap; use chrono::Duration; + use linked_hash_set::LinkedHashSet; use crate::{ as_borrow, - database::options::{EnvBehavior, PathBehavior, SAuthentication, TimestampType}, + database::{ + actor::SGroupType, + options::{ + EnvBehavior, PathBehavior, SAuthentication, SBounding, SEnvOptions, SPathOptions, + SPrivileged, STimeout, TimestampType, + }, + }, }; use super::*; @@ -698,7 +726,6 @@ mod tests { println!("START"); let config = r#" { - "version": "1.0.0", "options": { "path": { "default": "delete", @@ -707,6 +734,7 @@ mod tests { }, "env": { "default": "delete", + "override_behavior": true, "keep": ["keep_env"], "check": ["check_env"] }, @@ -737,7 +765,12 @@ mod tests { "name": "task1", "purpose": "purpose1", "cred": { - "setuid": "setuid1", + "setuid": { + "fallback": "user1", + "default": "all", + "add": ["user2"], + "sub": ["user3"] + }, "setgid": "setgid1", "capabilities": { "default": "all", @@ -761,11 +794,28 @@ mod tests { let options = config.options.as_ref().unwrap().as_ref().borrow(); let path = options.path.as_ref().unwrap(); assert_eq!(path.default_behavior, PathBehavior::Delete); - assert!(path.add.front().is_some_and(|s| s == "path_add")); + let default = LinkedHashSet::new(); + assert!(path + .add + .as_ref() + .unwrap_or(&default) + .front() + .is_some_and(|s| s == "path_add")); let env = options.env.as_ref().unwrap(); assert_eq!(env.default_behavior, EnvBehavior::Delete); - assert!(env.keep.front().is_some_and(|s| s == "keep_env")); - assert!(env.check.front().is_some_and(|s| s == "check_env")); + assert!(env.override_behavior.is_some_and(|b| b)); + assert!(env + .keep + .as_ref() + .unwrap_or(&LinkedHashSet::new()) + .front() + .is_some_and(|s| s == "keep_env")); + assert!(env + .check + .as_ref() + .unwrap_or(&LinkedHashSet::new()) + .front() + .is_some_and(|s| s == "check_env")); assert!(options.root.as_ref().unwrap().is_privileged()); assert!(options.bounding.as_ref().unwrap().is_ignore()); assert_eq!(options.authentication, Some(SAuthentication::Skip)); @@ -776,18 +826,19 @@ mod tests { assert_eq!(timeout.duration, Some(Duration::minutes(5))); assert_eq!(config.roles[0].as_ref().borrow().name, "role1"); let actor0 = &config.roles[0].as_ref().borrow().actors[0]; - match actor0 { - SActor::User { id, .. } => { - assert_eq!(id.as_ref().unwrap(), "user1"); + assert_eq!( + actor0, + &SActor::User { + id: Some("user1".into()), + _extra_fields: Map::default() } - _ => panic!("unexpected actor type"), - } + ); let actor1 = &config.roles[0].as_ref().borrow().actors[1]; match actor1 { SActor::Group { groups, .. } => match groups.as_ref().unwrap() { SGroups::Multiple(groups) => { - assert_eq!(groups[0], SActorType::Name("group1".into())); - assert_eq!(groups[1], SActorType::Id(1000)); + assert_eq!(&groups[0], "group1"); + assert_eq!(groups[1], 1000); } _ => panic!("unexpected actor group type"), }, @@ -796,8 +847,16 @@ mod tests { let role = config.roles[0].as_ref().borrow(); assert_eq!(as_borrow!(role[0]).purpose.as_ref().unwrap(), "purpose1"); let cred = &as_borrow!(&role[0]).cred; - assert_eq!(cred.setuid.as_ref().unwrap(), "setuid1"); - assert_eq!(cred.setgid.as_ref().unwrap(), "setgid1"); + let setuidstruct = SSetuidSet { + fallback: "user1".into(), + default: SetBehavior::All, + add: ["user2".into()].into(), + sub: ["user3".into()].into(), + }; + assert!( + matches!(cred.setuid.as_ref().unwrap(), SUserChooser::ChooserStruct(set) if set == &setuidstruct) + ); + assert_eq!(*cred.setgid.as_ref().unwrap(), ["setgid1".into()]); let capabilities = cred.capabilities.as_ref().unwrap(); assert_eq!(capabilities.default_behavior, SetBehavior::All); assert!(capabilities.add.has(Cap::NET_BIND_SERVICE)); @@ -814,7 +873,6 @@ mod tests { fn test_unknown_fields() { let config = r#" { - "version": "1.0.0", "options": { "path": { "default": "delete", @@ -937,20 +995,223 @@ mod tests { } #[test] - fn test_sgroups_compare() { - let single = SGroups::Single(SActorType::Name("single".into())); - let multiple = SGroups::Multiple(vec![ - SActorType::Name("single".into()), - SActorType::Id(1000), - ]); - assert!(single == single); - assert!(single <= multiple); - assert!(multiple >= single); - assert!(multiple == multiple); - let multiple2 = SGroups::Multiple(vec![ - SActorType::Name("single".into()), - SActorType::Id(1001), - ]); - assert!(multiple != multiple2); + fn test_deserialize_alias() { + let config = r#" + { + "options": { + "path": { + "default": "delete", + "add": ["path_add"], + "del": ["path_sub"] + }, + "env": { + "default": "delete", + "keep": ["keep_env"], + "check": ["check_env"] + }, + "root": "privileged", + "bounding": "ignore", + "authentication": "skip", + "wildcard-denied": "wildcards", + "timeout": { + "type": "ppid", + "duration": "00:05:00" + } + }, + "roles": [ + { + "name": "role1", + "actors": [ + { + "type": "user", + "name": "user1" + }, + { + "type":"group", + "groups": ["group1","1000"] + } + ], + "tasks": [ + { + "name": "task1", + "purpose": "purpose1", + "cred": { + "setuid": "setuid1", + "setgid": "setgid1", + "capabilities": ["cap_net_bind_service"] + }, + "commands": { + "default": "all", + "add": ["cmd1"], + "del": ["cmd2"] + } + } + ] + } + ] + } + "#; + let config: SConfig = serde_json::from_str(config).unwrap(); + let options = config.options.as_ref().unwrap().as_ref().borrow(); + let path = options.path.as_ref().unwrap(); + assert_eq!(path.default_behavior, PathBehavior::Delete); + let default = LinkedHashSet::new(); + assert!(path + .add + .as_ref() + .unwrap_or(&default) + .front() + .is_some_and(|s| s == "path_add")); + let env = options.env.as_ref().unwrap(); + assert_eq!(env.default_behavior, EnvBehavior::Delete); + assert!(env + .keep + .as_ref() + .unwrap() + .front() + .is_some_and(|s| s == "keep_env")); + assert!(env + .check + .as_ref() + .unwrap() + .front() + .is_some_and(|s| s == "check_env")); + assert!(options.root.as_ref().unwrap().is_privileged()); + assert!(options.bounding.as_ref().unwrap().is_ignore()); + assert_eq!(options.authentication, Some(SAuthentication::Skip)); + assert_eq!(options.wildcard_denied.as_ref().unwrap(), "wildcards"); + + let timeout = options.timeout.as_ref().unwrap(); + assert_eq!(timeout.type_field, Some(TimestampType::PPID)); + assert_eq!(timeout.duration, Some(Duration::minutes(5))); + assert_eq!(config.roles[0].as_ref().borrow().name, "role1"); + let actor0 = &config.roles[0].as_ref().borrow().actors[0]; + match actor0 { + SActor::User { id, .. } => { + assert_eq!(id.as_ref().unwrap(), "user1"); + } + _ => panic!("unexpected actor type"), + } + let actor1 = &config.roles[0].as_ref().borrow().actors[1]; + match actor1 { + SActor::Group { groups, .. } => match groups.as_ref().unwrap() { + SGroups::Multiple(groups) => { + assert_eq!(groups[0], SGroupType::from("group1")); + assert_eq!(groups[1], SGroupType::from(1000)); + } + _ => panic!("unexpected actor group type"), + }, + _ => panic!("unexpected actor {:?}", actor1), + } + let role = config.roles[0].as_ref().borrow(); + assert_eq!(as_borrow!(role[0]).purpose.as_ref().unwrap(), "purpose1"); + let cred = &as_borrow!(&role[0]).cred; + assert_eq!( + cred.setuid.as_ref().unwrap(), + &SUserChooser::from(SUserType::from("setuid1")) + ); + assert_eq!(cred.setgid.as_ref().unwrap(), &SGroups::from(["setgid1"])); + let capabilities = cred.capabilities.as_ref().unwrap(); + assert_eq!(capabilities.default_behavior, SetBehavior::None); + assert!(capabilities.add.has(Cap::NET_BIND_SERVICE)); + assert!(capabilities.sub.is_empty()); + let commands = &as_borrow!(&role[0]).commands; + assert_eq!( + *commands.default_behavior.as_ref().unwrap(), + SetBehavior::All + ); + assert_eq!(commands.add[0], SCommand::Simple("cmd1".into())); + assert_eq!(commands.sub[0], SCommand::Simple("cmd2".into())); + } + + #[test] + fn test_serialize() { + let config = SConfig::builder() + .role( + SRole::builder("role1") + .actor(SActor::user("user1").build()) + .actor( + SActor::group([SGroupType::from("group1"), SGroupType::from(1000)]).build(), + ) + .task( + STask::builder("task1") + .purpose("purpose1".into()) + .cred( + SCredentials::builder() + .setuid(SUserChooser::ChooserStruct( + SSetuidSet::builder("user1", SetBehavior::All) + .add(["user2".into()]) + .sub(["user3".into()]) + .build(), + )) + .setgid(["setgid1"]) + .capabilities( + SCapabilities::builder(SetBehavior::All) + .add_cap(Cap::NET_BIND_SERVICE) + .sub_cap(Cap::SYS_ADMIN) + .build(), + ) + .build(), + ) + .commands( + SCommands::builder(SetBehavior::All) + .add(["cmd1".into()]) + .sub(["cmd2".into()]) + .build(), + ) + .build(), + ) + .build(), + ) + .options(|opt| { + opt.path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path_add"]) + .sub(["path_sub"]) + .build(), + ) + .env( + SEnvOptions::builder(EnvBehavior::Delete) + .override_behavior(true) + .keep(["keep_env"]) + .unwrap() + .check(["check_env"]) + .unwrap() + .build(), + ) + .root(SPrivileged::Privileged) + .bounding(SBounding::Ignore) + .authentication(SAuthentication::Skip) + .wildcard_denied("wildcards") + .timeout( + STimeout::builder() + .type_field(TimestampType::PPID) + .duration(Duration::minutes(5)) + .build(), + ) + .build() + }) + .build(); + let config = serde_json::to_string_pretty(&config).unwrap(); + println!("{}", config); + } + + #[test] + fn test_serialize_operride_behavior_option() { + let config = SConfig::builder() + .options(|opt| { + opt.env( + SEnvOptions::builder(EnvBehavior::Inherit) + .override_behavior(true) + .build(), + ) + .build() + }) + .build(); + let config = serde_json::to_string(&config).unwrap(); + assert_eq!( + config, + "{\"options\":{\"env\":{\"override_behavior\":true}}}" + ); } } diff --git a/rar-common/src/database/wrapper.rs b/rar-common/src/database/wrapper.rs deleted file mode 100644 index 78b0a865..00000000 --- a/rar-common/src/database/wrapper.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use super::{ - options::Opt, - structs::{SConfig, SRole, STask}, -}; -pub type RcRefCell = Rc>; - -pub type SConfigWrapper = RcRefCell; -pub type SRoleWrapper = RcRefCell; -pub type STaskWrapper = RcRefCell; -pub type OptWrapper = Option>; - -pub trait DefaultWrapper { - fn default() -> Self; -} - -impl DefaultWrapper for SConfigWrapper { - fn default() -> Self { - Rc::new(RefCell::new(SConfig::default())) - } -} - -impl DefaultWrapper for SRoleWrapper { - fn default() -> Self { - Rc::new(RefCell::new(SRole::default())) - } -} - -impl DefaultWrapper for STaskWrapper { - fn default() -> Self { - Rc::new(RefCell::new(STask::default())) - } -} - -impl DefaultWrapper for OptWrapper { - fn default() -> Self { - None - } -} diff --git a/rar-common/src/lib.rs b/rar-common/src/lib.rs index f586b35b..68fe82c7 100644 --- a/rar-common/src/lib.rs +++ b/rar-common/src/lib.rs @@ -54,6 +54,7 @@ const ROOTASROLE: &str = "target/rootasrole.json"; use std::{cell::RefCell, error::Error, ffi::OsStr, path::PathBuf, rc::Rc}; +use bon::Builder; use log::debug; use serde::{Deserialize, Serialize}; @@ -90,15 +91,16 @@ pub enum Storage { JSON(Rc>), } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Builder)] pub struct SettingsFile { pub storage: Settings, #[serde(flatten)] pub config: Rc>, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Builder)] pub struct Settings { + #[builder(default = StorageMethod::JSON)] pub method: StorageMethod, #[serde(skip_serializing_if = "Option::is_none")] pub settings: Option, @@ -106,11 +108,13 @@ pub struct Settings { pub ldap: Option, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default)] pub struct RemoteStorageSettings { #[serde(skip_serializing_if = "Option::is_none")] + #[builder(name = not_immutable,with = || false)] pub immutable: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(into)] pub path: Option, #[serde(skip_serializing_if = "Option::is_none")] pub host: Option, @@ -189,22 +193,6 @@ impl Default for Settings { } } -impl Default for RemoteStorageSettings { - fn default() -> Self { - Self { - immutable: None, - path: None, - host: None, - port: None, - auth: None, - database: None, - schema: None, - table_prefix: None, - properties: None, - } - } -} - pub fn save_settings(settings: Rc>) -> Result<(), Box> { let default_remote: RemoteStorageSettings = RemoteStorageSettings::default(); // remove immutable flag diff --git a/rar-common/src/plugin/hashchecker.rs b/rar-common/src/plugin/hashchecker.rs index 05ae1887..3babc952 100644 --- a/rar-common/src/plugin/hashchecker.rs +++ b/rar-common/src/plugin/hashchecker.rs @@ -127,9 +127,10 @@ mod tests { use super::*; + use crate::database::actor::SActor; use crate::database::finder::{Cred, TaskMatcher}; use crate::{ - database::structs::{IdTask, SActor, SCommand, SCommands, SConfig, SRole, STask}, + database::structs::{IdTask, SCommand, SCommands, SConfig, SRole, STask}, rc_refcell, }; @@ -160,7 +161,7 @@ mod tests { .as_ref() .borrow_mut() .actors - .push(SActor::from_user_id(0)); + .push(SActor::user(0).build()); config.as_ref().borrow_mut().roles.push(role1); diff --git a/rar-common/src/plugin/hierarchy.rs b/rar-common/src/plugin/hierarchy.rs index 1eb3314a..91ffc4cc 100644 --- a/rar-common/src/plugin/hierarchy.rs +++ b/rar-common/src/plugin/hierarchy.rs @@ -3,8 +3,9 @@ use std::cmp::Ordering; use crate::{ api::{PluginManager, PluginResultAction}, database::{ - finder::{Cred, FilterMatcher, TaskMatch, TaskMatcher}, - structs::SRole, + finder::{Cred, TaskMatch, TaskMatcher}, + structs::{RoleGetter, SRole}, + FilterMatcher, }, }; @@ -37,7 +38,7 @@ fn find_in_parents( Some(Ok(parents)) => { debug!("Found parents {:?}", parents.0); for parent in parents.0.iter() { - if let Some(role) = config.as_ref().borrow().role(parent) { + if let Some(role) = config.role(parent) { debug!("Checking parent role {}", parent); match role.as_ref().borrow().tasks.matches(user, filter, command) { Ok(matches) => { @@ -82,8 +83,9 @@ mod tests { use super::*; use crate::{ database::{ - finder::UserMin, - structs::{IdTask, SActor, SCommand, SCommands, SConfig, STask}, + actor::SActor, + finder::ActorMatchMin, + structs::{IdTask, SCommand, SCommands, SConfig, STask}, }, rc_refcell, }; @@ -112,7 +114,7 @@ mod tests { .as_ref() .borrow_mut() .actors - .push(SActor::from_user_id(0)); + .push(SActor::user(0).build()); role1.as_ref().borrow_mut()._extra_fields.insert( "parents".to_string(), serde_json::Value::Array(vec![serde_json::Value::String("role1".to_string())]), @@ -128,7 +130,7 @@ mod tests { tty: None, }; let mut matcher = TaskMatch::default(); - matcher.score.user_min = UserMin::UserMatch; + matcher.score.user_min = ActorMatchMin::UserMatch; let res = find_in_parents( &config.as_ref().borrow().roles[1].as_ref().borrow(), &cred, @@ -164,7 +166,7 @@ mod tests { .as_ref() .borrow_mut() .actors - .push(SActor::from_user_id(0)); + .push(SActor::user(0).build()); role1.as_ref().borrow_mut()._extra_fields.insert( "parents".to_string(), serde_json::Value::Array(vec![serde_json::Value::String("role1".to_string())]), @@ -180,7 +182,7 @@ mod tests { tty: None, }; let mut matcher = TaskMatch::default(); - matcher.score.user_min = UserMin::UserMatch; + matcher.score.user_min = ActorMatchMin::UserMatch; let matches = config.matches(&cred, &None, &["ls".to_string()]).unwrap(); assert_eq!( matches.settings.task.upgrade().unwrap(), diff --git a/rar-common/src/plugin/ssd.rs b/rar-common/src/plugin/ssd.rs index 5321b58b..9a27f266 100644 --- a/rar-common/src/plugin/ssd.rs +++ b/rar-common/src/plugin/ssd.rs @@ -1,4 +1,4 @@ -use std::ffi::CString; +use std::{cell::RefCell, ffi::CString, rc::Rc}; use ::serde::Deserialize; use nix::unistd::{getgrouplist, Group, User}; @@ -8,8 +8,9 @@ use crate::{ api::{PluginManager, PluginResult}, as_borrow, database::{ + actor::{SActor, SGroups}, finder::Cred, - structs::{SActor, SConfig, SGroups, SRole}, + structs::{RoleGetter, SConfig, SRole}, }, }; @@ -67,7 +68,7 @@ fn groups_subset_of(groups: &[Group], actors: &[SActor]) -> bool { } // Check if user and its related groups are forbidden to use the role -fn user_is_forbidden(user: &User, ssd_roles: &[String], sconfig: &SConfig) -> bool { +fn user_is_forbidden(user: &User, ssd_roles: &[String], sconfig: Rc>) -> bool { let mut groups_to_check = Vec::new(); if let Ok(groups) = getgrouplist( CString::new(user.name.as_str()).unwrap().as_c_str(), @@ -92,7 +93,11 @@ fn user_is_forbidden(user: &User, ssd_roles: &[String], sconfig: &SConfig) -> bo false } -fn groups_are_forbidden(groups: &[Group], ssd_roles: &[String], sconfig: &SConfig) -> bool { +fn groups_are_forbidden( + groups: &[Group], + ssd_roles: &[String], + sconfig: Rc>, +) -> bool { for role in ssd_roles.iter() { if let Some(role) = sconfig.role(role) { if groups_subset_of(groups, &as_borrow!(role).actors) { @@ -119,8 +124,8 @@ fn check_separation_of_duty(role: &SRole, actor: &Cred) -> PluginResult { return PluginResult::Neutral; } let roles = roles.unwrap().0; - if user_is_forbidden(&actor.user, &roles, &as_borrow!(sconfig)) - || groups_are_forbidden(&actor.groups, &roles, &as_borrow!(sconfig)) + if user_is_forbidden(&actor.user, &roles, sconfig.clone()) + || groups_are_forbidden(&actor.groups, &roles, sconfig.clone()) { PluginResult::Deny } else { @@ -144,7 +149,7 @@ mod tests { use super::*; use crate::{ - database::structs::{SActor, SConfig, SRole}, + database::structs::{SConfig, SRole}, rc_refcell, }; use nix::unistd::{Group, Pid}; @@ -153,42 +158,44 @@ mod tests { #[test] fn test_user_contained_in() { let user = User::from_uid(0.into()).unwrap().unwrap(); - let actors = vec![SActor::from_user_id(0)]; + let actors = vec![SActor::user(0).build()]; assert!(user_contained_in(&user, &actors)); } #[test] fn test_group_contained_in() { let group = Group::from_gid(0.into()).unwrap().unwrap(); - let actors = vec![SActor::from_group_id(0)]; + let actors = vec![SActor::group(0).build()]; assert!(group_contained_in(&group, &actors)); } #[test] fn test_groups_subset_of() { let groups = vec![Group::from_gid(0.into()).unwrap().unwrap()]; - let actors = vec![SActor::from_group_id(0)]; + let actors = vec![SActor::group(0).build()]; assert!(groups_subset_of(&groups, &actors)); } #[test] fn test_user_is_forbidden() { let user = User::from_uid(0.into()).unwrap().unwrap(); - let sconfig = SConfig::default(); + let sconfig = SConfig::builder().build(); let roles = vec!["role1".to_string()]; - assert!(!user_is_forbidden(&user, &roles, &sconfig)); + assert!(!user_is_forbidden(&user, &roles, sconfig)); } #[test] fn test_groups_are_forbidden() { let groups = vec![Group::from_gid(0.into()).unwrap().unwrap()]; - let mut sconfig = SConfig::default(); - let mut role = SRole::default(); - role.name = "role1".to_string(); - role.actors.push(SActor::from_group_id(0)); - sconfig.roles.push(rc_refcell!(role)); + let sconfig = SConfig::builder() + .role( + SRole::builder("role1".to_string()) + .actor(SActor::group(0).build()) + .build(), + ) + .build(); let roles = vec!["role1".to_string()]; - assert!(groups_are_forbidden(&groups, &roles, &sconfig)); + assert!(groups_are_forbidden(&groups, &roles, sconfig)); } #[test] @@ -200,7 +207,7 @@ mod tests { role.as_ref() .borrow_mut() .actors - .push(SActor::from_group_id(0)); + .push(SActor::group(0).build()); role.as_ref().borrow_mut()._extra_fields.insert( "ssd".to_string(), serde_json::Value::Array(vec![Value::String("role1".to_string())]), diff --git a/rar-common/src/util.rs b/rar-common/src/util.rs index 6c59770e..8e24a667 100644 --- a/rar-common/src/util.rs +++ b/rar-common/src/util.rs @@ -255,8 +255,9 @@ pub fn final_path(path: &str) -> PathBuf { } #[cfg(debug_assertions)] -pub fn subsribe(tool: &str) -> Result<(), Box> { +pub fn subsribe(_: &str) -> Result<(), Box> { env_logger::Builder::from_default_env() + .filter_level(log::LevelFilter::Debug) .format_module_path(true) .init(); Ok(()) diff --git a/rar-common/src/version.rs b/rar-common/src/version.rs index 6c7b08a4..31e9796f 100644 --- a/rar-common/src/version.rs +++ b/rar-common/src/version.rs @@ -1,4 +1,4 @@ // This file is generated by build.rs // Do not edit this file directly // Instead edit build.rs and run cargo build -pub const PACKAGE_VERSION: &str = "3.0.4"; +pub const PACKAGE_VERSION: &str = "3.0.5"; diff --git a/resources/rootasrole.json b/resources/rootasrole.json index 437f244f..646f4c05 100644 --- a/resources/rootasrole.json +++ b/resources/rootasrole.json @@ -1,5 +1,5 @@ { - "version": "3.0.0", + "version": "3.0.5", "storage": { "method": "json", "settings": { @@ -81,7 +81,10 @@ "name": "t_root", "purpose": "access to every commands", "cred": { - "setuid": "root", + "setuid": { + "fallback": "root", + "default": "all" + }, "setgid": "root", "capabilities": { "default": "all", @@ -90,6 +93,11 @@ }, "commands": { "default": "all" + }, + "options": { + "env": { + "override_behavior": true + } } }, { @@ -98,10 +106,7 @@ "cred": { "setuid": "root", "setgid": "root", - "capabilities": { - "default": "none", - "add": ["CAP_LINUX_IMMUTABLE"] - } + "capabilities": ["CAP_LINUX_IMMUTABLE"] }, "commands": { "default": "none", diff --git a/src/chsr/cli/data.rs b/src/chsr/cli/data.rs index cdf92fe0..ab2b6235 100644 --- a/src/chsr/cli/data.rs +++ b/src/chsr/cli/data.rs @@ -6,11 +6,12 @@ use linked_hash_set::LinkedHashSet; use pest_derive::Parser; use rar_common::database::{ + actor::{SActor, SGroups, SUserType}, options::{ EnvBehavior, EnvKey, OptType, PathBehavior, SAuthentication, SBounding, SPrivileged, TimestampType, }, - structs::{IdTask, SActor, SActorType, SGroups, SetBehavior}, + structs::{IdTask, SetBehavior}, }; #[derive(Parser)] @@ -74,7 +75,7 @@ pub struct Inputs { pub cmd_policy: Option, pub cmd_id: Option>, pub cred_caps: Option, - pub cred_setuid: Option, + pub cred_setuid: Option, pub cred_setgid: Option, pub cred_policy: Option, pub options: bool, diff --git a/src/chsr/cli/mod.rs b/src/chsr/cli/mod.rs index b2391aa3..dbc0a0cf 100644 --- a/src/chsr/cli/mod.rs +++ b/src/chsr/cli/mod.rs @@ -39,18 +39,20 @@ where #[cfg(test)] mod tests { - use std::{env::current_dir, io::Write, rc::Rc}; + use std::{env::current_dir, io::Write}; + use linked_hash_set::LinkedHashSet; use rar_common::{ database::{ + actor::SActor, options::*, read_json_config, structs::{SCredentials, *}, versionning::Versioning, }, - get_settings, rc_refcell, + get_settings, util::remove_with_privileges, - RemoteStorageSettings, SettingsFile, Storage, StorageMethod, + RemoteStorageSettings, Settings, SettingsFile, Storage, StorageMethod, }; use crate::ROOTASROLE; @@ -62,123 +64,174 @@ mod tests { use test_log::test; fn setup(name: &str) { - //Write json test json file - let path = format!("{}.{}", ROOTASROLE, name); - let mut file = std::fs::File::create(path.clone()).unwrap_or_else(|_| { + let file_path = format!("{}.{}", ROOTASROLE, name); + let versionned = Versioning::new( + SettingsFile::builder() + .storage( + Settings::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(file_path.clone()) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SConfig::builder() + .options(|opt| { + opt.timeout( + STimeout::builder() + .type_field(TimestampType::PPID) + .duration( + TimeDelta::hours(15) + .checked_add(&TimeDelta::minutes(30)) + .unwrap() + .checked_add(&TimeDelta::seconds(30)) + .unwrap(), + ) + .max_usage(1) + .build(), + ) + .path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path1", "path2"]) + .sub(["path3", "path4"]) + .build(), + ) + .env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["env1", "env2"]) + .unwrap() + .check(["env3", "env4"]) + .unwrap() + .delete(["env5", "env6"]) + .unwrap() + .set([("env7", "val7"), ("env8", "val8")]) + .build(), + ) + .root(SPrivileged::Privileged) + .bounding(SBounding::Ignore) + .wildcard_denied("*") + .build() + }) + .role( + SRole::builder("complete") + .options(|opt| { + opt.timeout( + STimeout::builder() + .type_field(TimestampType::PPID) + .duration( + TimeDelta::hours(15) + .checked_add(&TimeDelta::minutes(30)) + .unwrap() + .checked_add(&TimeDelta::seconds(30)) + .unwrap(), + ) + .max_usage(1) + .build(), + ) + .path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path1", "path2"]) + .sub(["path3", "path4"]) + .build(), + ) + .env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["env1", "env2"]) + .unwrap() + .check(["env3", "env4"]) + .unwrap() + .delete(["env5", "env6"]) + .unwrap() + .set([("env7", "val7"), ("env8", "val8")]) + .build(), + ) + .root(SPrivileged::Privileged) + .bounding(SBounding::Ignore) + .wildcard_denied("*") + .build() + }) + .actor(SActor::user(0).build()) + .actor(SActor::group(0).build()) + .actor(SActor::group(["groupA", "groupB"]).build()) + .task( + STask::builder("t_complete") + .options(|opt| { + opt.timeout( + STimeout::builder() + .type_field(TimestampType::PPID) + .duration( + TimeDelta::hours(15) + .checked_add(&TimeDelta::minutes(30)) + .unwrap() + .checked_add(&TimeDelta::seconds(30)) + .unwrap(), + ) + .max_usage(1) + .build(), + ) + .path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path1", "path2"]) + .sub(["path3", "path4"]) + .build(), + ) + .env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["env1", "env2"]) + .unwrap() + .check(["env3", "env4"]) + .unwrap() + .delete(["env5", "env6"]) + .unwrap() + .set([("env7", "val7"), ("env8", "val8")]) + .build(), + ) + .root(SPrivileged::Privileged) + .bounding(SBounding::Ignore) + .wildcard_denied("*") + .build() + }) + .commands( + SCommands::builder(SetBehavior::All) + .add(["ls".into(), "echo".into()]) + .sub(["cat".into(), "grep".into()]) + .build(), + ) + .cred( + SCredentials::builder() + .setuid("user1") + .setgid(["group1", "group2"]) + .capabilities( + SCapabilities::builder(SetBehavior::All) + .add_cap(Cap::LINUX_IMMUTABLE) + .add_cap(Cap::NET_BIND_SERVICE) + .sub_cap(Cap::SYS_ADMIN) + .sub_cap(Cap::SYS_BOOT) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ); + let mut file = std::fs::File::create(file_path.clone()).unwrap_or_else(|_| { panic!( - "Failed to create {:?}/{} file at", + "Failed to create {:?}/{:?} file at", current_dir().unwrap(), - path + file_path ) }); - let mut settings = SettingsFile::default(); - settings.storage.method = StorageMethod::JSON; - settings.storage.settings = Some(RemoteStorageSettings::default()); - settings.storage.settings.as_mut().unwrap().path = Some(path.into()); - settings.storage.settings.as_mut().unwrap().immutable = Some(false); - - let mut opt = Opt::default(); - - opt.timeout = Some(STimeout::default()); - opt.timeout.as_mut().unwrap().type_field = Some(TimestampType::PPID); - opt.timeout.as_mut().unwrap().duration = Some( - TimeDelta::hours(15) - .checked_add(&TimeDelta::minutes(30)) - .unwrap() - .checked_add(&TimeDelta::seconds(30)) - .unwrap(), - ); - opt.timeout.as_mut().unwrap().max_usage = Some(1); - - opt.path = Some(SPathOptions::default()); - opt.path.as_mut().unwrap().default_behavior = PathBehavior::Delete; - opt.path.as_mut().unwrap().add = vec!["path1".to_string(), "path2".to_string()] - .into_iter() - .collect(); - opt.path.as_mut().unwrap().sub = vec!["path3".to_string(), "path4".to_string()] - .into_iter() - .collect(); - - opt.env = Some(SEnvOptions::default()); - opt.env.as_mut().unwrap().default_behavior = EnvBehavior::Delete; - opt.env.as_mut().unwrap().keep = vec!["env1".into(), "env2".into()].into_iter().collect(); - opt.env.as_mut().unwrap().check = vec!["env3".into(), "env4".into()].into_iter().collect(); - opt.env.as_mut().unwrap().delete = vec!["env5".into(), "env6".into()].into_iter().collect(); - - opt.root = Some(SPrivileged::Privileged); - opt.bounding = Some(SBounding::Ignore); - opt.wildcard_denied = Some("*".to_string()); - - settings.config.as_ref().borrow_mut().options = Some(rc_refcell!(opt.clone())); - - settings.config.as_ref().borrow_mut().roles = vec![]; - - let mut role = SRole::default(); - role.name = "complete".to_string(); - role.actors = vec![ - SActor::from_user_id(0), - SActor::from_group_id(0), - SActor::from_group_vec_string(vec!["groupA", "groupB"]), - ]; - role.options = Some(rc_refcell!(opt.clone())); - let role = rc_refcell!(role); - - let mut task = STask::new(IdTask::Name("t_complete".to_string()), Rc::downgrade(&role)); - task.purpose = Some("complete".to_string()); - task.commands = SCommands::default(); - task.commands.default_behavior = Some(SetBehavior::All); - task.commands.add.push(SCommand::Simple("ls".to_string())); - task.commands.add.push(SCommand::Simple("echo".to_string())); - task.commands.sub.push(SCommand::Simple("cat".to_string())); - task.commands.sub.push(SCommand::Simple("grep".to_string())); - - task.cred = SCredentials::default(); - task.cred.setuid = Some(SActorType::Name("user1".to_string())); - task.cred.setgid = Some(SGroups::Multiple(vec![ - SActorType::Name("group1".to_string()), - SActorType::Name("group2".to_string()), - ])); - task.cred.capabilities = Some(SCapabilities::default()); - task.cred.capabilities.as_mut().unwrap().default_behavior = SetBehavior::All; - task.cred - .capabilities - .as_mut() - .unwrap() - .add - .add(Cap::LINUX_IMMUTABLE); - task.cred - .capabilities - .as_mut() - .unwrap() - .add - .add(Cap::NET_BIND_SERVICE); - task.cred - .capabilities - .as_mut() - .unwrap() - .sub - .add(Cap::SYS_ADMIN); - task.cred - .capabilities - .as_mut() - .unwrap() - .sub - .add(Cap::SYS_BOOT); - - task.options = Some(rc_refcell!(opt.clone())); - - role.as_ref().borrow_mut().tasks.push(rc_refcell!(task)); - settings.config.as_ref().borrow_mut().roles.push(role); - - let versionned = Versioning::new(settings.clone()); - - file.write_all( - serde_json::to_string_pretty(&versionned) - .unwrap() - .as_bytes(), - ) - .unwrap(); - + let jsonstr = serde_json::to_string_pretty(&versionned).unwrap(); + file.write_all(jsonstr.as_bytes()).unwrap(); file.flush().unwrap(); } @@ -373,17 +426,17 @@ mod tests { .as_ref() .borrow() .actors - .contains(&SActor::from_user_string("user1"))); + .contains(&SActor::user("user1").build())); assert!(config.as_ref().borrow()[0] .as_ref() .borrow() .actors - .contains(&SActor::from_group_string("group1"))); + .contains(&SActor::group("group1").build())); assert!(config.as_ref().borrow()[0] .as_ref() .borrow() .actors - .contains(&SActor::from_group_vec_string(vec!["group2", "group3"]))); + .contains(&SActor::group(["group2", "group3"]).build())); assert!(main( &Storage::JSON(config.clone()), "r complete revoke -u user1 -g group1 -g group2&group3".split(" "), @@ -399,17 +452,17 @@ mod tests { .as_ref() .borrow() .actors - .contains(&SActor::from_user_string("user1"))); + .contains(&SActor::user("user1").build())); assert!(!config.as_ref().borrow()[0] .as_ref() .borrow() .actors - .contains(&SActor::from_group_string("group1"))); + .contains(&SActor::group("group1").build())); assert!(!config.as_ref().borrow()[0] .as_ref() .borrow() .actors - .contains(&SActor::from_group_vec_string(vec!["group2", "group3"]))); + .contains(&SActor::group(["group2", "group3"]).build())); teardown("r_complete_grant_u_user1_g_group1_g_group2_group3"); } #[test] @@ -1288,6 +1341,7 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); + let default = LinkedHashSet::new(); assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] .as_ref() .borrow() @@ -1300,6 +1354,8 @@ mod tests { .as_ref() .unwrap() .add + .as_ref() + .unwrap_or(&default) .contains(&"/usr/bin".to_string())); assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] .as_ref() @@ -1313,6 +1369,8 @@ mod tests { .as_ref() .unwrap() .add + .as_ref() + .unwrap_or(&default) .contains(&"/bin".to_string())); assert!(main( &Storage::JSON(config.clone()), @@ -1337,6 +1395,8 @@ mod tests { .as_ref() .unwrap() .add + .as_ref() + .unwrap_or(&default) .contains(&"/usr/bin".to_string())); assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] .as_ref() @@ -1350,6 +1410,8 @@ mod tests { .as_ref() .unwrap() .add + .as_ref() + .unwrap_or(&default) .contains(&"/bin".to_string())); debug!("====="); assert!(main( @@ -1375,6 +1437,8 @@ mod tests { .as_ref() .unwrap() .add + .as_ref() + .unwrap_or(&default) .is_empty()); debug!("====="); assert!(main( @@ -1400,6 +1464,8 @@ mod tests { .as_ref() .unwrap() .add + .as_ref() + .unwrap_or(&default) .contains(&"/usr/bin".to_string())); assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] .as_ref() @@ -1413,6 +1479,8 @@ mod tests { .as_ref() .unwrap() .add + .as_ref() + .unwrap_or(&default) .contains(&"/bin".to_string())); assert_eq!( config.as_ref().borrow()[0].as_ref().borrow().tasks[0] @@ -1427,6 +1495,8 @@ mod tests { .as_ref() .unwrap() .add + .as_ref() + .unwrap_or(&default) .len(), 2 ); @@ -1466,6 +1536,8 @@ mod tests { .as_ref() .unwrap() .sub + .as_ref() + .unwrap_or(&default) .contains(&"/tmp".to_string())); assert!(main( &Storage::JSON(config.clone()), @@ -1506,6 +1578,8 @@ mod tests { .as_ref() .unwrap() .sub + .as_ref() + .unwrap_or(&default) .len(), 1 ); @@ -1521,6 +1595,8 @@ mod tests { .as_ref() .unwrap() .sub + .as_ref() + .unwrap_or(&default) .contains(&"/tmp".to_string())); assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] .as_ref() @@ -1534,6 +1610,8 @@ mod tests { .as_ref() .unwrap() .sub + .as_ref() + .unwrap_or(&default) .contains(&"/usr/bin".to_string())); assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] .as_ref() @@ -1547,6 +1625,8 @@ mod tests { .as_ref() .unwrap() .sub + .as_ref() + .unwrap_or(&default) .contains(&"/bin".to_string())); teardown("r_complete_t_t_complete_o_path_whitelist_add"); } @@ -1617,6 +1697,8 @@ mod tests { .as_ref() .unwrap() .keep + .as_ref() + .unwrap() .contains(&"MYVAR".to_string().into())); assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] .as_ref() @@ -1630,6 +1712,8 @@ mod tests { .as_ref() .unwrap() .keep + .as_ref() + .unwrap() .contains(&"VAR2".to_string().into())); assert_eq!( config.as_ref().borrow()[0].as_ref().borrow().tasks[0] @@ -1644,6 +1728,8 @@ mod tests { .as_ref() .unwrap() .keep + .as_ref() + .unwrap() .len(), 2 ); @@ -1694,6 +1780,8 @@ mod tests { .as_ref() .unwrap() .delete + .as_ref() + .unwrap() .contains(&"MYVAR".to_string().into())); assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] .as_ref() @@ -1707,6 +1795,8 @@ mod tests { .as_ref() .unwrap() .delete + .as_ref() + .unwrap() .contains(&"VAR2".to_string().into())); assert_eq!( config.as_ref().borrow()[0].as_ref().borrow().tasks[0] @@ -1721,6 +1811,8 @@ mod tests { .as_ref() .unwrap() .delete + .as_ref() + .unwrap() .len(), 2 ); @@ -2120,6 +2212,8 @@ mod tests { .as_ref() .unwrap() .keep + .as_ref() + .unwrap() .contains(&"MYVAR".to_string().into())); assert!( config.as_ref().borrow()[0].as_ref().borrow().tasks[0] @@ -2134,6 +2228,8 @@ mod tests { .as_ref() .unwrap() .keep + .as_ref() + .unwrap() .len() > 1 ); @@ -2160,6 +2256,8 @@ mod tests { .as_ref() .unwrap() .keep + .as_ref() + .unwrap() .contains(&"MYVAR".to_string().into())); debug!("====="); assert!(main( @@ -2185,6 +2283,8 @@ mod tests { .as_ref() .unwrap() .keep + .as_ref() + .unwrap() .contains(&"MYVAR".to_string().into())); assert_eq!( config.as_ref().borrow()[0].as_ref().borrow().tasks[0] @@ -2199,6 +2299,8 @@ mod tests { .as_ref() .unwrap() .keep + .as_ref() + .unwrap() .len(), 1 ); @@ -2236,7 +2338,7 @@ mod tests { .as_ref() .unwrap() .keep - .is_empty()); + .is_none()); teardown("r_complete_t_t_complete_o_env_whitelist_purge"); } #[test] @@ -2271,6 +2373,8 @@ mod tests { .as_ref() .unwrap() .delete + .as_ref() + .unwrap() .contains(&"MYVAR".to_string().into())); assert!(main( &Storage::JSON(config.clone()), @@ -2295,6 +2399,8 @@ mod tests { .as_ref() .unwrap() .delete + .as_ref() + .unwrap() .contains(&"MYVAR".to_string().into())); teardown("r_complete_t_t_complete_o_env_blacklist_add_MYVAR"); } @@ -2330,6 +2436,8 @@ mod tests { .as_ref() .unwrap() .delete + .as_ref() + .unwrap() .contains(&"MYVAR".to_string().into())); assert_eq!( config.as_ref().borrow()[0].as_ref().borrow().tasks[0] @@ -2344,6 +2452,8 @@ mod tests { .as_ref() .unwrap() .delete + .as_ref() + .unwrap() .len(), 1 ); @@ -2381,7 +2491,7 @@ mod tests { .as_ref() .unwrap() .delete - .is_empty()); + .is_none()); teardown("r_complete_t_t_complete_o_env_blacklist_purge"); } #[test] @@ -2416,6 +2526,8 @@ mod tests { .as_ref() .unwrap() .check + .as_ref() + .unwrap() .contains(&"MYVAR".to_string().into())); debug!("====="); assert!(main( @@ -2441,6 +2553,8 @@ mod tests { .as_ref() .unwrap() .check + .as_ref() + .unwrap() .contains(&"MYVAR".to_string().into())); debug!("====="); assert!(main( @@ -2466,6 +2580,8 @@ mod tests { .as_ref() .unwrap() .check + .as_ref() + .unwrap() .contains(&"MYVAR".to_string().into())); assert_eq!( config.as_ref().borrow()[0].as_ref().borrow().tasks[0] @@ -2480,6 +2596,8 @@ mod tests { .as_ref() .unwrap() .check + .as_ref() + .unwrap() .len(), 1 ); @@ -2507,7 +2625,7 @@ mod tests { .as_ref() .unwrap() .check - .is_empty()); + .is_none()); teardown("r_complete_t_t_complete_o_env_checklist_add_MYVAR"); } #[test] diff --git a/src/chsr/cli/pair.rs b/src/chsr/cli/pair.rs index ed9a4911..79f1655b 100644 --- a/src/chsr/cli/pair.rs +++ b/src/chsr/cli/pair.rs @@ -8,10 +8,11 @@ use pest::iterators::Pair; use crate::cli::data::{RoleType, TaskType}; use rar_common::database::{ + actor::{SActor, SGroupType}, options::{ EnvBehavior, OptType, PathBehavior, SAuthentication, SBounding, SPrivileged, TimestampType, }, - structs::{IdTask, SActor, SActorType, SGroups, SetBehavior}, + structs::{IdTask, SetBehavior}, }; use super::data::*; @@ -192,7 +193,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) -> Result<(), Box { @@ -221,15 +222,16 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) -> Result<(), Box>(), + ) + .build(), + ); } debug!("actors: {:?}", inputs.actors); } @@ -265,10 +267,17 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) -> Result<(), Box { - inputs.cred_setuid = Some(pair.as_str().chars().skip(9).collect::().into()); + inputs.cred_setuid = Some( + pair.as_str() + .chars() + .skip(9) + .collect::() + .as_str() + .into(), + ); } Rule::cred_g => { - let mut vec: Vec = Vec::new(); + let mut vec: Vec = Vec::new(); for pair in pair.clone().into_inner() { if pair.as_rule() == Rule::actor_name { vec.push(pair.as_str().into()); @@ -277,11 +286,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) -> Result<(), Box { @@ -426,7 +431,7 @@ mod test { }; use rar_common::{ - database::structs::SActor, + database::actor::SActor, util::{BOLD, RED, RST}, }; @@ -470,9 +475,9 @@ mod test { assert_eq!( inputs.actors, Some(vec![ - SActor::from_user_string("u1"), - SActor::from_user_string("u2"), - SActor::from_group_vec_string(vec!["g1", "g2"]) + SActor::user("u1").build(), + SActor::user("u2").build(), + SActor::group(["g1", "g2"]).build() ]) ); } diff --git a/src/chsr/cli/process.rs b/src/chsr/cli/process.rs index 96409aab..4d5cc3c4 100644 --- a/src/chsr/cli/process.rs +++ b/src/chsr/cli/process.rs @@ -9,7 +9,7 @@ use log::debug; use rar_common::{ database::{ options::{Opt, OptType}, - structs::IdTask, + structs::{IdTask, RoleGetter}, }, Storage, }; @@ -398,8 +398,6 @@ pub fn perform_on_target_opt( task_id: Option, exec_on_opt: impl Fn(Rc>) -> Result<(), Box>, ) -> Result<(), Box> { - let mut config = rconfig.as_ref().borrow_mut(); - // Helper function to execute on option or create a new one fn execute_or_create_option( exec_on_opt: impl Fn(Rc>) -> Result<(), Box>, @@ -417,7 +415,7 @@ pub fn perform_on_target_opt( // If role_id is provided, find the role if let Some(role_id) = role_id { - let role = config.role(&role_id).ok_or("Role not found")?; + let role = rconfig.role(&role_id).ok_or("Role not found")?; let mut role_borrowed = role.as_ref().borrow_mut(); // If task_id is provided, find the task @@ -430,5 +428,5 @@ pub fn perform_on_target_opt( } // No role_id, use global config options - execute_or_create_option(exec_on_opt, &mut config.options) + execute_or_create_option(exec_on_opt, &mut rconfig.as_ref().borrow_mut().options) } diff --git a/src/chsr/cli/process/json.rs b/src/chsr/cli/process/json.rs index d8763eaa..0bf9d6d7 100644 --- a/src/chsr/cli/process/json.rs +++ b/src/chsr/cli/process/json.rs @@ -1,25 +1,16 @@ -use std::{ - cell::RefCell, - collections::HashMap, - error::Error, - ops::Deref, - rc::{Rc, Weak}, -}; +use std::{cell::RefCell, collections::HashMap, error::Error, ops::Deref, rc::Rc}; use linked_hash_set::LinkedHashSet; -use log::debug; +use log::{debug, warn}; use crate::cli::data::{InputAction, RoleType, SetListType, TaskType, TimeoutOpt}; -use rar_common::{ - database::{ - options::{ - EnvBehavior, EnvKey, Opt, OptStack, OptType, PathBehavior, SEnvOptions, SPathOptions, - STimeout, - }, - structs::{IdTask, SCapabilities, SCommand, SRole, STask}, +use rar_common::database::{ + options::{ + EnvBehavior, EnvKey, Opt, OptStack, OptType, PathBehavior, SEnvOptions, SPathOptions, + STimeout, }, - rc_refcell, + structs::{IdTask, RoleGetter, SCapabilities, SCommand, SRole, STask, SUserChooser}, }; use super::perform_on_target_opt; @@ -36,8 +27,8 @@ pub fn list_json( let config = rconfig.as_ref().borrow(); debug!("list_json {:?}", config); if let Some(role_id) = role_id { - if let Some(role) = config.role(&role_id) { - list_task(task_id, role, options, options_type, task_type, role_type) + if let Some(role) = rconfig.role(&role_id) { + list_task(task_id, &role, options, options_type, task_type, role_type) } else { Err("Role not found".into()) } @@ -58,7 +49,8 @@ fn list_task( if let Some(task_id) = task_id { if let Some(task) = role.as_ref().borrow().task(&task_id) { if options { - let opt = OptStack::from_task(task.clone()).to_opt(); + let rcopt = OptStack::from_task(task.clone()).to_opt(); + let opt = rcopt.as_ref().borrow(); if let Some(opttype) = options_type { match opttype { OptType::Env => { @@ -84,7 +76,7 @@ fn list_task( } } } else { - println!("{}", serde_json::to_string_pretty(&opt)?); + println!("{}", serde_json::to_string_pretty(&rcopt)?); } } else { print_task(task, task_type.unwrap_or(TaskType::All)); @@ -156,30 +148,35 @@ pub fn role_add_del( role_type: Option, ) -> Result> { debug!("chsr role r1 {:?}", action); - let mut config = rconfig.as_ref().borrow_mut(); match action { InputAction::Add => { //verify if role exists - if config.role(&role_id).is_some() { + if rconfig.role(&role_id).is_some() { return Err("Role already exists".into()); } - config + rconfig + .as_ref() + .borrow_mut() .roles - .push(rc_refcell!(SRole::new(role_id, Weak::new()))); + .push(SRole::builder(role_id).build()); Ok(true) } InputAction::Del => { - if config.role(&role_id).is_none() { + if rconfig.role(&role_id).is_none() { return Err("Role do not exists".into()); } - config.roles.retain(|r| r.as_ref().borrow().name != role_id); + rconfig + .as_ref() + .borrow_mut() + .roles + .retain(|r| r.as_ref().borrow().name != role_id); Ok(true) } InputAction::Purge => { - if config.role(&role_id).is_none() { + if rconfig.role(&role_id).is_none() { return Err("Role do not exists".into()); } - let role = config.role(&role_id).unwrap(); + let role = rconfig.role(&role_id).unwrap(); match role_type { Some(RoleType::Actors) => { role.as_ref().borrow_mut().actors.clear(); @@ -206,8 +203,7 @@ pub fn task_add_del( task_type: Option, ) -> Result> { debug!("chsr role r1 task t1 add|del"); - let config = rconfig.as_ref().borrow_mut(); - let role = config.role(&role_id).ok_or("Role not found")?; + let role = rconfig.role(&role_id).ok_or("Role not found")?; match action { InputAction::Add => { //verify if task exists @@ -223,7 +219,7 @@ pub fn task_add_del( role.as_ref() .borrow_mut() .tasks - .push(rc_refcell!(STask::new(task_id, Weak::new()))); + .push(STask::builder(task_id).build()); Ok(true) } InputAction::Del => { @@ -275,11 +271,10 @@ pub fn grant_revoke( rconfig: &Rc>, role_id: String, action: InputAction, - mut actors: Vec, + mut actors: Vec, ) -> Result> { debug!("chsr role r1 grant|revoke"); - let config = rconfig.as_ref().borrow_mut(); - let role = config.role(&role_id).ok_or("Role not found")?; + let role = rconfig.role(&role_id).ok_or("Role not found")?; match action { InputAction::Add => { //verify if actor is already in role @@ -315,18 +310,17 @@ pub fn cred_set( role_id: String, task_id: IdTask, cred_caps: Option, - cred_setuid: Option, - cred_setgid: Option, + cred_setuid: Option, + cred_setgid: Option, ) -> Result> { debug!("chsr role r1 task t1 cred"); - let config = rconfig.as_ref().borrow_mut(); - match config.task(&role_id, &task_id) { + match rconfig.task(&role_id, task_id) { Ok(task) => { if let Some(caps) = cred_caps { task.as_ref().borrow_mut().cred.capabilities = Some(SCapabilities::from(caps)); } if let Some(setuid) = cred_setuid { - task.as_ref().borrow_mut().cred.setuid = Some(setuid); + task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::Actor(setuid)); } if let Some(setgid) = cred_setgid { task.as_ref().borrow_mut().cred.setgid = Some(setgid); @@ -342,12 +336,11 @@ pub fn cred_unset( role_id: String, task_id: IdTask, cred_caps: Option, - cred_setuid: Option, - cred_setgid: Option, + cred_setuid: Option, + cred_setgid: Option, ) -> Result> { debug!("chsr role r1 task t1 cred unset"); - let config = rconfig.as_ref().borrow_mut(); - match config.task(&role_id, &task_id) { + match rconfig.task(&role_id, task_id) { Ok(task) => { if let Some(caps) = cred_caps { if caps.is_empty() { @@ -379,8 +372,7 @@ pub fn cred_caps( cred_caps: capctl::CapSet, ) -> Result> { debug!("chsr role r1 task t1 cred caps"); - let config = rconfig.as_ref().borrow_mut(); - let task = config.task(&role_id, &task_id)?; + let task = rconfig.task(&role_id, task_id)?; match setlist_type { SetListType::White => match action { InputAction::Add => { @@ -457,8 +449,7 @@ pub fn cred_setpolicy( cred_policy: rar_common::database::structs::SetBehavior, ) -> Result> { debug!("chsr role r1 task t1 cred setpolicy"); - let config = rconfig.as_ref().borrow_mut(); - let task = config.task(&role_id, &task_id)?; + let task = rconfig.task(&role_id, task_id)?; if task.as_ref().borrow_mut().cred.capabilities.is_none() { task.as_ref() .borrow_mut() @@ -530,8 +521,7 @@ pub fn cmd_whitelist_action( action: InputAction, ) -> Result> { debug!("chsr role r1 task t1 command whitelist add c1"); - let config = rconfig.as_ref().borrow_mut(); - let task = config.task(&role_id, &task_id)?; + let task = rconfig.task(&role_id, task_id)?; let cmd = SCommand::Simple(shell_words::join(cmd_id.iter())); match setlist_type { SetListType::White => match action { @@ -587,8 +577,7 @@ pub fn cmd_setpolicy( cmd_policy: rar_common::database::structs::SetBehavior, ) -> Result> { debug!("chsr role r1 task t1 command setpolicy"); - let config = rconfig.as_ref().borrow_mut(); - let task = config.task(&role_id, &task_id)?; + let task = rconfig.task(&role_id, task_id)?; task.as_ref() .borrow_mut() .commands @@ -609,14 +598,14 @@ pub fn env_set_policylist( opt.as_ref().borrow_mut().env = Some(SEnvOptions { default_behavior: options_env_policy, keep: if options_env_policy.is_delete() { - options_env.clone() + Some(options_env.clone()) } else { - LinkedHashSet::new() + None }, delete: if options_env_policy.is_keep() { - options_env.clone() + Some(options_env.clone()) } else { - LinkedHashSet::new() + None }, ..Default::default() }); @@ -676,19 +665,18 @@ pub fn path_set( ) -> Result> { debug!("chsr o path set"); perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { - let mut default_path = SPathOptions::default(); let mut binding = opt.as_ref().borrow_mut(); - let path = binding.path.as_mut().unwrap_or(&mut default_path); + let path = binding.path.get_or_insert(SPathOptions::default()); match setlist_type { Some(SetListType::White) => { - path.add = options_path.split(':').map(|s| s.to_string()).collect(); + path.add = Some(options_path.split(':').map(|s| s.to_string()).collect()); } Some(SetListType::Black) => { - path.sub = options_path.split(':').map(|s| s.to_string()).collect(); + path.sub = Some(options_path.split(':').map(|s| s.to_string()).collect()); } None => { path.default_behavior = PathBehavior::Delete; - path.add = options_path.split(':').map(|s| s.to_string()).collect(); + path.add = Some(options_path.split(':').map(|s| s.to_string()).collect()); } _ => unreachable!("Unknown setlist type"), } @@ -705,15 +693,18 @@ pub fn path_purge( ) -> Result> { debug!("chsr o path purge"); perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { - let mut default_path = SPathOptions::default(); let mut binding = opt.as_ref().borrow_mut(); - let path = binding.path.as_mut().unwrap_or(&mut default_path); + let path = binding.path.get_or_insert(SPathOptions::default()); match setlist_type { Some(SetListType::White) => { - path.add.clear(); + if let Some(add) = &mut path.add { + add.clear(); + } } Some(SetListType::Black) => { - path.sub.clear(); + if let Some(sub) = &mut path.sub { + sub.clear(); + } } _ => unreachable!("Unknown setlist type"), } @@ -731,22 +722,21 @@ pub fn env_whitelist_set( ) -> Result> { debug!("chsr o env whitelist set"); perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { - let mut default_env = SEnvOptions::default(); let mut binding = opt.as_ref().borrow_mut(); - let env = binding.env.as_mut().unwrap_or(&mut default_env); + let env = binding.env.get_or_insert(SEnvOptions::default()); match setlist_type { Some(SetListType::White) => { - env.keep = options_env.clone(); + env.keep = Some(options_env.clone()); } Some(SetListType::Black) => { - env.delete = options_env.clone(); + env.delete = Some(options_env.clone()); } Some(SetListType::Check) => { - env.check = options_env.clone(); + env.check = Some(options_env.clone()); } None => { env.default_behavior = EnvBehavior::Delete; - env.keep = options_env.clone(); + env.keep = Some(options_env.clone()); } _ => unreachable!("Unknown setlist type"), } @@ -827,6 +817,7 @@ pub fn path_setlist2( Some(SetListType::White) => match action { InputAction::Add => { path.add + .get_or_insert(LinkedHashSet::new()) .extend(options_path.split(':').map(|s| s.to_string())); } InputAction::Del => { @@ -835,20 +826,24 @@ pub fn path_setlist2( .split(':') .map(|s| s.to_string()) .collect::>(); - path.add = path - .add - .difference(&hashset) - .cloned() - .collect::>(); + if let Some(path) = &mut path.add { + *path = path + .difference(&hashset) + .cloned() + .collect::>(); + } else { + warn!("No path to remove from del list"); + } } InputAction::Set => { - path.add = options_path.split(':').map(|s| s.to_string()).collect(); + path.add = Some(options_path.split(':').map(|s| s.to_string()).collect()); } _ => unreachable!("Unknown action {:?}", action), }, Some(SetListType::Black) => match action { InputAction::Add => { path.sub + .get_or_insert(LinkedHashSet::new()) .extend(options_path.split(':').map(|s| s.to_string())); } InputAction::Del => { @@ -857,14 +852,17 @@ pub fn path_setlist2( .split(':') .map(|s| s.to_string()) .collect::>(); - path.sub = path - .sub - .difference(&hashset) - .cloned() - .collect::>(); + if let Some(path) = &mut path.sub { + *path = path + .difference(&hashset) + .cloned() + .collect::>(); + } else { + warn!("No path to remove from del list"); + } } InputAction::Set => { - path.sub = options_path.split(':').map(|s| s.to_string()).collect(); + path.sub = Some(options_path.split(':').map(|s| s.to_string()).collect()); } _ => unreachable!("Unknown action {:?}", action), }, @@ -913,36 +911,33 @@ pub fn env_setlist_add( let mut default_env = SEnvOptions::default(); let mut binding = opt.as_ref().borrow_mut(); let env = binding.env.as_mut().unwrap_or(&mut default_env); + match setlist_type { Some(SetListType::White) => match action { InputAction::Add => { if options_key_env.is_none() { return Err("Empty list".into()); } - env.keep.extend(options_key_env.as_ref().unwrap().clone()); + env.keep + .get_or_insert(LinkedHashSet::new()) + .extend(options_key_env.as_ref().unwrap().clone()); } InputAction::Del => { if options_key_env.is_none() { return Err("Empty list".into()); } - env.keep = env - .keep - .difference( - &options_key_env - .as_ref() - .unwrap() - .iter() - .cloned() - .collect::>(), - ) - .cloned() - .collect::>(); + if let Some(keep) = &mut env.keep { + *keep = keep + .difference(options_key_env.as_ref().unwrap()) + .cloned() + .collect::>(); + } } InputAction::Purge => { - env.keep = LinkedHashSet::new(); + env.keep = None; } InputAction::Set => { - env.keep = options_key_env.as_ref().unwrap().clone(); + env.keep = Some(options_key_env.as_ref().unwrap().clone()); } _ => unreachable!("Unknown action {:?}", action), }, @@ -951,23 +946,26 @@ pub fn env_setlist_add( if options_key_env.is_none() { return Err("Empty list".into()); } - env.delete.extend(options_key_env.as_ref().unwrap().clone()); + env.delete + .get_or_insert(LinkedHashSet::new()) + .extend(options_key_env.as_ref().unwrap().clone()); } InputAction::Del => { if options_key_env.is_none() { return Err("Empty list".into()); } - env.delete = env - .delete - .difference(options_key_env.as_ref().unwrap()) - .cloned() - .collect::>(); + if let Some(delete) = &mut env.delete { + *delete = delete + .difference(options_key_env.as_ref().unwrap()) + .cloned() + .collect::>(); + } } InputAction::Purge => { - env.delete = LinkedHashSet::new(); + env.delete = None; } InputAction::Set => { - env.delete = options_key_env.as_ref().unwrap().clone(); + env.delete = Some(options_key_env.as_ref().unwrap().clone()); } _ => unreachable!("Unknown action {:?}", action), }, @@ -976,23 +974,26 @@ pub fn env_setlist_add( if options_key_env.is_none() { return Err("Empty list".into()); } - env.check.extend(options_key_env.as_ref().unwrap().clone()); + env.check + .get_or_insert(LinkedHashSet::new()) + .extend(options_key_env.as_ref().unwrap().clone()); } InputAction::Del => { if options_key_env.is_none() { return Err("Empty list".into()); } - env.check = env - .check - .difference(options_key_env.as_ref().unwrap()) - .cloned() - .collect::>(); + if let Some(check) = &mut env.check { + *check = check + .difference(options_key_env.as_ref().unwrap()) + .cloned() + .collect::>(); + } } InputAction::Set => { - env.check = options_key_env.as_ref().unwrap().clone(); + env.check = Some(options_key_env.as_ref().unwrap().clone()); } InputAction::Purge => { - env.check = LinkedHashSet::new(); + env.check = None; } _ => unreachable!("Unknown action {:?}", action), }, @@ -1019,7 +1020,7 @@ pub fn env_setlist_add( }, None => match action { InputAction::Set => { - env.keep = options_key_env.as_ref().unwrap().clone(); + env.keep = Some(options_key_env.as_ref().unwrap().clone()); } _ => unreachable!("Unknown action {:?}", action), }, diff --git a/src/sr/main.rs b/src/sr/main.rs index 4f42ae61..1a1d684c 100644 --- a/src/sr/main.rs +++ b/src/sr/main.rs @@ -8,7 +8,12 @@ use nix::{ sys::stat, unistd::{getgroups, getuid, isatty, Group, User}, }; -use rar_common::database::finder::{Cred, FilterMatcher, TaskMatch, TaskMatcher}; +use rar_common::database::{ + actor::{SGroups, SUserType}, + finder::{Cred, TaskMatch, TaskMatcher}, + options::EnvBehavior, + FilterMatcher, +}; use rar_common::database::{options::OptStack, structs::SConfig}; use rar_common::util::escape_parser_string; @@ -20,7 +25,7 @@ use std::{cell::RefCell, error::Error, io::stdout, os::fd::AsRawFd, rc::Rc}; use rar_common::plugin::register_plugins; use rar_common::{ self, - database::{read_json_config, structs::SGroups}, + database::read_json_config, util::{ activates_no_new_privs, dac_override_effective, drop_effective, read_effective, setgid_effective, setpcap_effective, setuid_effective, subsribe, BOLD, RST, UNDERLINE, @@ -54,11 +59,17 @@ const USAGE: &str = formatcp!( {BOLD}-t, --task {RST} Task option allows you to select a specific task to use in the selected role. Note: You must specify a role to designate a task + {BOLD}-E, --preserve-env {RST} + Task option allows you to select a specific task to use in the selected role. Note: You must specify a role to designate a task + {BOLD}-p, --prompt {RST} Prompt option allows you to override the default password prompt and use a custom one [default: "Password: "] + {BOLD}-u, --user {RST} + Specify the user to execute the command as + {BOLD}-i, --info{RST} Display rights of executor @@ -129,31 +140,27 @@ where { let mut args = Cli::default(); let mut iter = s.into_iter().skip(1); + let mut role = None; + let mut task = None; + let mut user: Option = None; + let mut env = None; while let Some(arg) = iter.next() { // matches only first options match arg.as_ref() { + "-u" | "--user" => { + user = iter.next().map(|s| escape_parser_string(s).as_str().into()); + } "-S" | "--stdin" => { args.stdin = true; } "-r" | "--role" => { - if let Some(opt_filter) = args.opt_filter.as_mut() { - opt_filter.role = iter.next().map(|s| escape_parser_string(s)); - } else { - args.opt_filter = Some(FilterMatcher { - role: iter.next().map(|s| escape_parser_string(s)), - task: None, - }); - } + role = iter.next().map(|s| escape_parser_string(s)); } "-t" | "--task" => { - if let Some(opt_filter) = args.opt_filter.as_mut() { - opt_filter.task = iter.next().map(|s| escape_parser_string(s)); - } else { - args.opt_filter = Some(FilterMatcher { - task: iter.next().map(|s| escape_parser_string(s)), - role: None, - }); - } + task = iter.next().map(|s| escape_parser_string(s)); + } + "-E" | "--preserve-env" => { + env.replace(EnvBehavior::Keep); } "-p" | "--prompt" => { args.prompt = iter @@ -177,6 +184,14 @@ where } } } + args.opt_filter = Some( + FilterMatcher::builder() + .maybe_role(role) + .maybe_task(task) + .maybe_env_behavior(env) + .maybe_user(user) + .build(), + ); for arg in iter { args.command.push(escape_parser_string(arg)); } @@ -267,7 +282,7 @@ fn main() -> Result<(), Box> { //execute command let envset = optstack - .calculate_filtered_env(cred, std::env::vars()) + .calculate_filtered_env(args.opt_filter, cred, std::env::vars()) .expect("Failed to calculate env"); let pty = Pty::new().expect("Failed to create pty"); @@ -372,7 +387,7 @@ fn set_capabilities(execcfg: &rar_common::database::finder::ExecSettings, optsta fn setuid_setgid(execcfg: &rar_common::database::finder::ExecSettings) { let uid = execcfg.setuid.as_ref().and_then(|u| { - let res = u.into_user().unwrap_or(None); + let res = u.fetch_user(); if let Some(user) = res { Some(user.uid.as_raw()) } else { @@ -381,7 +396,7 @@ fn setuid_setgid(execcfg: &rar_common::database::finder::ExecSettings) { }); let gid = execcfg.setgroups.as_ref().and_then(|g| match g { SGroups::Single(g) => { - let res = g.into_group().unwrap_or(None); + let res = g.fetch_group(); if let Some(group) = res { Some(group.gid.as_raw()) } else { @@ -389,7 +404,7 @@ fn setuid_setgid(execcfg: &rar_common::database::finder::ExecSettings) { } } SGroups::Multiple(g) => { - let res = g.first().unwrap().into_group().unwrap_or(None); + let res = g.first().unwrap().fetch_group(); if let Some(group) = res { Some(group.gid.as_raw()) } else { @@ -399,7 +414,7 @@ fn setuid_setgid(execcfg: &rar_common::database::finder::ExecSettings) { }); let groups = execcfg.setgroups.as_ref().and_then(|g| match g { SGroups::Single(g) => { - let res = g.into_group().unwrap_or(None); + let res = g.fetch_group(); if let Some(group) = res { Some(vec![group.gid.as_raw()]) } else { @@ -407,7 +422,7 @@ fn setuid_setgid(execcfg: &rar_common::database::finder::ExecSettings) { } } SGroups::Multiple(g) => { - let res = g.iter().map(|g| g.into_group().unwrap_or(None)); + let res = g.iter().map(|g| g.fetch_group()); let mut groups = Vec::new(); for group in res.flatten() { groups.push(group.gid.as_raw()); @@ -427,13 +442,12 @@ fn setuid_setgid(execcfg: &rar_common::database::finder::ExecSettings) { mod tests { use libc::getgid; use nix::unistd::Pid; + use rar_common::database::actor::SActor; use rar_common::rc_refcell; use super::*; use rar_common::database::make_weak_config; - use rar_common::database::structs::{ - IdTask, SActor, SCommand, SCommands, SConfig, SRole, STask, - }; + use rar_common::database::structs::{IdTask, SCommand, SCommands, SConfig, SRole, STask}; #[test] fn test_from_json_execution_settings() { @@ -465,7 +479,7 @@ mod tests { role.as_ref() .borrow_mut() .actors - .push(SActor::from_user_id(0)); + .push(SActor::user(0).build()); role.as_ref().borrow_mut().tasks.push(task); let task = rc_refcell!(STask::default()); task.as_ref().borrow_mut().name = IdTask::Name("task2".to_owned()); diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index b674faa1..e70df7ed 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "xtask" # The project version is managed on json file in resources/rootasrole.json -version = "3.0.4" +version = "3.0.5" edition = "2021" publish = false