diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e05245aa..fda0f7d2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest container: image: xd009642/tarpaulin:develop-nightly - options: --security-opt seccomp=unconfined + options: --security-opt seccomp=unconfined --privileged steps: - name: Checkout code uses: actions/checkout@v2 diff --git a/.vscode/launch.json b/.vscode/launch.json index 76ea54be..a3f110d6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,59 +5,14 @@ "version": "0.2.0", "configurations": [ { - "name": "(gdb) Test", - "type": "cppdbg", - "request": "launch", - "preLaunchTask": "make build_unit_test", - "program": "${workspaceFolder}/bin/unit_test", - "args": [], - "stopAtEntry": false, - "cwd": "${fileDirname}", - "environment": [], - "externalConsole": false, - "MIMode": "gdb", - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - }, - { - "description": "Set Disassembly Flavor to Intel", - "text": "-gdb-set disassembly-flavor intel", - "ignoreFailures": true - }, - { "description": "The new process is debugged after a fork. The parent process runs unimpeded.", - "text": "-gdb-set follow-fork-mode child", - "ignoreFailures": true - } - ] - }, - { - "name": "(gdb) Launch", - "type": "cppdbg", + "type": "lldb", "request": "launch", - "preLaunchTask": "setcap", - "program": "/usr/bin/sr", + "name": "Launch", + "program": "${workspaceFolder}/target/debug/sr", "args": ["ls"], - "stopAtEntry": false, - "cwd": "${fileDirname}", - "environment": [], - "externalConsole": false, - "MIMode": "gdb", - "miDebuggerPath": "${workspaceFolder}/.vscode/gdb_root.sh", - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - }, - { - "description": "Set Disassembly Flavor to Intel", - "text": "-gdb-set disassembly-flavor intel", - "ignoreFailures": true - } - ], + "cwd": "${workspaceFolder}" } + + ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ce4d4d7a..c1707c71 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,58 +6,14 @@ } }, "tasks": [ - { - "type": "cppbuild", - "label": "C/C++: gcc build active file", - "command": "/usr/bin/gcc", - "args": [ - "-fdiagnostics-color=always", - "-g", - "${file}", - "-o", - "${fileDirname}/${fileBasenameNoExtension}" - ], - "options": { - "cwd": "${fileDirname}" - }, - "problemMatcher": [ - "$gcc" - ], - "group": { - "kind": "build", - "isDefault": true - }, - "detail": "Task generated by Debugger." - }, - { - "type": "shell", - "label": "make", - "command": "sudo", - "args": [ - "-E", - "/usr/bin/make", - "install" - ], - "options": { - "cwd": "${cwd}" - }, - "problemMatcher": [ - "$gcc" - ], - "group": { - "kind": "build", - "isDefault": true - }, - "detail": "Task generated by Debugger." - }, + { "type": "shell", "label": "setcap", - "dependsOn": "make", "command": "sudo", "args": [ "/usr/bin/setcap", - "=eip", + "=p", "${cwd}/bin/sr" ], "options": { @@ -66,58 +22,6 @@ "group": { "kind": "none" } - }, - { - "type": "shell", - "label": "make build_unit_test", - "command": "sudo", - "args": [ - "/usr/bin/make", - "build_unit_test" - ], - "options": { - "cwd": "${workspaceFolder}", - "env": { - "GDB_DEBUG": "1", - "DEBUG": "1" - } - }, - "problemMatcher": [ - "$gcc" - ], - "group": { - "kind": "build", - "isDefault": true - }, - "detail": "Task generated by Debugger." - }, - { - "type": "shell", - "label": "debug unit_test", - "dependsOn": "make build_unit_test", - "command": "${cwd}/bin/unit_test", - "args": [ - "--debug=gdb" - ], - "options": { - "cwd": "${cwd}" - }, - "isBackground": true, - "problemMatcher": { - "pattern": [ - { - "regexp": ".", - "file": 1, - "location": 2, - "message": 3 - } - ], - "background": { - "activeOnStart": true, - "beginsPattern": ".", - "endsPattern": "Listening on port" - } - } } ], diff --git a/Cargo.toml b/Cargo.toml index 4ae18228..56caa7d1 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.6" +version = "3.1.0" rust-version = "1.76.0" authors = ["Eddie Billoir "] edition = "2021" @@ -25,9 +25,17 @@ maintainance ={ status = "actively-maintained", badge = "https://img.shields.io/ [profile.release] strip = "symbols" lto = true -opt-level = "s" +opt-level = 3 codegen-units = 1 +[profile.profiling] +strip = "none" +lto = false +opt-level = 1 +inherits = "release" +debug = true + + #[features] #cursive_lib = [ "cursive" ] #srlibs = [ "pam-client", "bitflags" ] @@ -45,6 +53,7 @@ path = "src/chsr/main.rs" [features] default = ["finder"] finder = ["dep:pcre2", "rar-common/pcre2", "rar-common/finder"] +pcre2 = ["dep:pcre2", "rar-common/pcre2"] [lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] } @@ -65,7 +74,7 @@ capctl = "0.2" pcre2 = { version = "0.2", optional = true } serde = { version = "1.0", features=["rc", "derive"] } serde_json = "1.0" -ciborium = "0.2" +cbor4ii = { version = "1.0.0", features = ["serde", "serde1", "use_std"] } glob = "0.3" pam-client2 = "0.5" bitflags = { version = "2.6" } @@ -80,6 +89,8 @@ pest = "2.7" pest_derive = "2.7" const_format = "0.2" hex = "0.4" +bon = "3.5.1" +serde_json_borrow = "0.7.1" [dev-dependencies] log = "0.4" diff --git a/README.md b/README.md index 5ad145ef..58e250e3 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ -# RootAsRole (V3.0.6) : A memory-safe and security-oriented alternative to sudo/su commands +# RootAsRole (V3.1.0) : 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/build.rs b/build.rs index 77a3d70b..87d750c2 100644 --- a/build.rs +++ b/build.rs @@ -53,7 +53,7 @@ fn set_readme_version(package_version: &str, file: &str) -> Result<(), Box { + Id(u32), + #[serde(borrow)] + Name(Cow<'a, str>), +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +pub struct DUserType<'a>(#[serde(borrow)] DGenericActorType<'a>); + impl SUserType { - pub(super) fn fetch_id(&self) -> Option { + pub fn fetch_id(&self) -> Option { match &self.0 { SGenericActorType::Id(id) => Some(*id), SGenericActorType::Name(name) => match User::from_name(name) { @@ -45,6 +56,18 @@ impl SUserType { } } +impl DUserType<'_> { + pub fn fetch_id(&self) -> Option { + match &self.0 { + DGenericActorType::Id(id) => Some(*id), + DGenericActorType::Name(name) => match User::from_name(name) { + Ok(Some(user)) => Some(user.uid.as_raw()), + _ => None, + }, + } + } +} + impl fmt::Display for SUserType { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match &self.0 { @@ -57,6 +80,9 @@ impl fmt::Display for SUserType { #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct SGroupType(SGenericActorType); +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +pub struct DGroupType<'a>(#[serde(borrow)] DGenericActorType<'a>); + impl fmt::Display for SGroupType { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match &self.0 { @@ -92,22 +118,66 @@ impl SGroupType { } } -impl std::fmt::Display for SGenericActorType { +impl DGroupType<'_> { + pub fn fetch_id(&self) -> Option { + match &self.0 { + DGenericActorType::Id(id) => Some(*id), + DGenericActorType::Name(name) => match Group::from_name(name) { + Ok(Some(group)) => Some(group.gid.as_raw()), + _ => None, + }, + } + } +} + +impl Display for DGroupType<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - SGenericActorType::Id(id) => write!(f, "{}", id), - SGenericActorType::Name(name) => write!(f, "{}", name), + match &self.0 { + DGenericActorType::Id(id) => write!(f, "{}", id), + DGenericActorType::Name(name) => write!(f, "{}", name), + } + } +} +impl Display for DUserType<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match &self.0 { + DGenericActorType::Id(id) => write!(f, "{}", id), + DGenericActorType::Name(name) => write!(f, "{}", name), } } } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, EnumIs)] +#[derive(Serialize, PartialEq, Eq, Debug, Clone, EnumIs)] #[serde(untagged)] pub enum SGroups { Single(SGroupType), Multiple(Vec), } +impl Display for SGroups { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + SGroups::Single(group) => write!(f, "[{}]", group), + SGroups::Multiple(groups) => { + let mut result = String::new(); + for group in groups { + result.push_str(&format!("{}, ", group)); + } + result.pop(); // Remove last comma + result.pop(); // Remove last space + write!(f, "[{}]", result) + } + } + } +} + +#[derive(Serialize, PartialEq, Eq, Debug, Clone, EnumIs, strum::Display)] +#[serde(untagged)] +pub enum DGroups<'a> { + Single(#[serde(borrow)] DGroupType<'a>), + Multiple(#[serde(borrow)] Cow<'a, [DGroupType<'a>]>), +} + impl SGroups { pub fn len(&self) -> usize { match self { @@ -130,50 +200,133 @@ impl SGroups { } } -impl<'de> Deserialize<'de> for SGenericActorType { +impl DGroups<'_> { + pub fn len(&self) -> usize { + match self { + DGroups::Single(_) => 1, + DGroups::Multiple(groups) => groups.len(), + } + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl<'de: 'a, 'a> Deserialize<'de> for DGroups<'a> { fn deserialize(deserializer: D) -> Result where - D: Deserializer<'de>, + D: serde::Deserializer<'de>, { - struct IdVisitor; - - impl<'de> Visitor<'de> for IdVisitor { - type Value = SGenericActorType; + struct DGroupsVisitor<'a> { + marker: std::marker::PhantomData<&'a ()>, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for DGroupsVisitor<'a> { + type Value = DGroups<'a>; - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("user ID as a number or string") + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or a number") } - fn visit_u32(self, id: u32) -> Result + fn visit_borrowed_str(self, v: &'de str) -> Result where - E: de::Error, + E: serde::de::Error, { - Ok(SGenericActorType::Id(id)) + if let Ok(group) = v.parse() { + return Ok(DGroups::Single(DGroupType(DGenericActorType::Id(group)))); + } else { + return Ok(DGroups::Single(DGroupType(DGenericActorType::Name( + Cow::Borrowed(v), + )))); + } } - fn visit_u64(self, id: u64) -> Result + fn visit_u64(self, value: u64) -> Result where - E: de::Error, + E: serde::de::Error, { - if id > u32::MAX as u64 { - return Err(E::custom("user ID too large")); + if value > u32::MAX as u64 { + return Err(E::custom("value is too large")); } - Ok(SGenericActorType::Id(id as u32)) + Ok(DGroups::Single(DGroupType(DGenericActorType::Id( + value as u32, + )))) } - fn visit_str(self, id: &str) -> Result + fn visit_seq(self, mut seq: A) -> Result where - E: de::Error, + A: serde::de::SeqAccess<'de>, { - let rid: Result = id.parse(); - match rid { - Ok(id) => Ok(SGenericActorType::Id(id)), - Err(_) => Ok(SGenericActorType::Name(id.to_string())), + let mut groups = Vec::new(); + while let Some(group) = seq.next_element::>()? { + groups.push(group); + } + if groups.len() == 1 { + Ok(DGroups::Single(groups.remove(0))) + } else { + Ok(DGroups::Multiple(Cow::Owned(groups))) } } } + deserializer.deserialize_any(DGroupsVisitor { + marker: std::marker::PhantomData, + }) + } +} - deserializer.deserialize_any(IdVisitor) +impl<'de: 'a, 'a> Deserialize<'de> for SGroups { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SGroupsVisitor; + impl<'de: 'a, 'a> serde::de::Visitor<'de> for SGroupsVisitor { + type Value = SGroups; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or a number") + } + + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + if let Ok(group) = v.parse() { + return Ok(SGroups::Single(SGroupType(SGenericActorType::Id(group)))); + } else { + return Ok(SGroups::Single(SGroupType(SGenericActorType::Name( + v.to_string(), + )))); + } + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + if value > u32::MAX as u64 { + return Err(E::custom("value is too large")); + } + Ok(SGroups::Single(SGroupType(SGenericActorType::Id( + value as u32, + )))) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut groups = Vec::new(); + while let Some(group) = seq.next_element::()? { + groups.push(group); + } + if groups.len() == 1 { + Ok(SGroups::Single(groups.remove(0))) + } else { + Ok(SGroups::Multiple(groups)) + } + } + } + deserializer.deserialize_any(SGroupsVisitor) } } @@ -195,12 +348,48 @@ impl From<&str> for SUserType { } } +impl<'a> From<&'a str> for DUserType<'a> { + fn from(name: &'a str) -> Self { + DUserType(name.into()) + } +} + +impl<'a> From for DUserType<'a> { + fn from(name: String) -> Self { + DUserType(DGenericActorType::Name(name.into())) + } +} + +impl<'a> From<&'a str> for DGroupType<'a> { + fn from(name: &'a str) -> Self { + DGroupType(name.into()) + } +} + +impl From for DUserType<'_> { + fn from(id: u32) -> Self { + DUserType(id.into()) + } +} + +impl From for DGroupType<'_> { + fn from(id: u32) -> Self { + DGroupType(id.into()) + } +} + impl From<&str> for SGroupType { fn from(name: &str) -> Self { SGroupType(name.into()) } } +impl<'a> From> for DGroupType<'a> { + fn from(name: Cow<'a, str>) -> Self { + DGroupType(DGenericActorType::Name(name)) + } +} + impl From for SGroupType { fn from(group: Group) -> Self { SGroupType(SGenericActorType::Id(group.gid.as_raw())) @@ -213,6 +402,22 @@ impl From<&str> for SGenericActorType { } } +impl<'a> From<&'a str> for DGenericActorType<'a> { + fn from(name: &'a str) -> Self { + if name.parse::().is_ok() { + DGenericActorType::Id(name.parse().unwrap()) + } else { + DGenericActorType::Name(Cow::Borrowed(name)) + } + } +} + +impl<'a> From for DGenericActorType<'a> { + fn from(name: u32) -> Self { + DGenericActorType::Id(name) + } +} + impl From for SGenericActorType { fn from(id: u32) -> Self { SGenericActorType::Id(id) @@ -229,6 +434,16 @@ impl PartialEq for SUserType { } } +impl PartialEq for DUserType<'_> { + 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)) @@ -247,6 +462,12 @@ impl PartialEq for SUserType { } } +impl PartialEq for DUserType<'_> { + fn eq(&self, other: &u32) -> bool { + self.eq(&DUserType::from(*other)) + } +} + impl PartialEq for SGroupType { fn eq(&self, other: &u32) -> bool { self.eq(&SGroupType::from(*other)) @@ -263,23 +484,12 @@ impl PartialEq for SGroupType { } } -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 PartialEq for DGroupType<'_> { + fn eq(&self, other: &Group) -> bool { + let gid = self.fetch_id(); + match gid { + Some(gid) => gid == other.gid.as_raw(), + None => false, } } } @@ -294,18 +504,48 @@ impl From<[SGroupType; N]> for SGroups { } } -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()); +impl TryInto> for &DGroups<'_> { + type Error = String; + + fn try_into(self) -> Result, Self::Error> { + match self { + DGroups::Single(group) => Ok(vec![group + .fetch_id() + .ok_or(format!("{} group does not exist", group))?]), + DGroups::Multiple(groups) => { + let mut ids = Vec::new(); + for group in groups.iter() { + ids.push( + group + .fetch_id() + .ok_or(format!("{} group does not exist", group))?, + ); + } + Ok(ids) + } } - if groups.len() == 1 { - SGroups::Single(groups[0].to_owned()) - } else { - SGroups::Multiple(groups) + } +} + +impl TryInto> for SGroups { + type Error = String; + + fn try_into(self) -> Result, Self::Error> { + match self { + SGroups::Single(group) => Ok(vec![group + .fetch_id() + .ok_or(format!("{} group does not exist", group))?]), + SGroups::Multiple(groups) => { + let mut ids = Vec::new(); + for group in groups { + ids.push( + group + .fetch_id() + .ok_or(format!("{} group does not exist", group))?, + ); + } + Ok(ids) + } } } } @@ -340,6 +580,22 @@ impl From> for SGroups { } } +impl<'a> From>> for DGroups<'a> { + fn from(groups: Vec>) -> Self { + if groups.len() == 1 { + DGroups::Single(groups[0].clone()) + } else { + DGroups::Multiple(Cow::Owned(groups)) + } + } +} + +impl<'a> From> for DGroups<'a> { + fn from(groups: DGroupType<'a>) -> Self { + DGroups::Single(groups) + } +} + impl From for SGroups { fn from(group: u32) -> Self { SGroups::Single(group.into()) @@ -370,19 +626,6 @@ impl PartialEq> for SGroups { } } -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 { @@ -404,6 +647,25 @@ pub enum SActor { Unknown(Value), } +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, strum::Display)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum DActor<'a> { + #[serde(rename = "user")] + #[strum(to_string = "User {id}")] + User { + #[serde(borrow, alias = "name")] + id: DUserType<'a>, + }, + #[serde(rename = "group")] + #[strum(to_string = "Group {groups}")] + Group { + #[serde(borrow, alias = "names", alias = "id")] + groups: DGroups<'a>, + }, + #[serde(untagged)] + Unknown(Value), +} + #[bon] impl SActor { #[builder(finish_fn = build)] @@ -448,6 +710,8 @@ impl core::fmt::Display for SActor { } #[cfg(test)] mod tests { + use nix::unistd::getuid; + use super::*; #[test] @@ -465,6 +729,14 @@ mod tests { let group = SGroupType::from(0); assert_eq!(group.fetch_id(), Some(0)); + let user = SUserType::from("root"); + assert_eq!(user.fetch_id(), Some(0)); + + let group = SGroupType::from("root"); + assert_eq!(group.fetch_id(), Some(0)); + + let group = SGroupType::from("unkown"); + assert_eq!(group.fetch_id(), None); } #[test] fn test_fetch_user() { @@ -476,6 +748,9 @@ mod tests { #[test] fn test_sgroups_multiple() { + let groups = SGroups::from(0); + + assert_eq!(groups.len(), 1); let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from(200)]); assert_eq!(groups.len(), 2); @@ -517,4 +792,221 @@ mod tests { assert!(!group1.fetch_eq(&group2)); } + + #[test] + fn test_duser_type_creation() { + let user_by_id = DUserType::from(0); + let user_by_name = DUserType::from("testuser"); + + assert_eq!(user_by_id.to_string(), "0"); + assert_eq!(user_by_name.to_string(), "testuser"); + } + #[test] + fn test_fetch_did() { + let user = DUserType::from(0); + assert_eq!(user.fetch_id(), Some(0)); + + let group = DGroupType::from(0); + assert_eq!(group.fetch_id(), Some(0)); + let user = DUserType::from("root"); + assert_eq!(user.fetch_id(), Some(0)); + + let group = DGroupType::from("root"); + assert_eq!(group.fetch_id(), Some(0)); + + let group = DGroupType::from("unkown"); + assert_eq!(group.fetch_id(), None); + } + + #[test] + fn test_dgroups_single() { + let groups = DGroups::from(DGroupType::from(0)); + + assert_eq!(groups.len(), 1); + assert!(!groups.is_empty()); + + if let DGroups::Single(group_list) = groups { + assert_eq!(group_list.to_string(), "0"); + } else { + panic!("Expected SGroups::Single"); + } + } + + #[test] + fn test_is_dempty() { + let groups = DGroups::Multiple(Cow::Borrowed(&[])); + assert!(groups.is_empty()); + } + + #[test] + fn test_sactor_display() { + let user = SActor::User { + id: Some(SUserType::from(0)), + _extra_fields: Map::new(), + }; + let group = SActor::Group { + groups: Some(SGroups::from(vec![SGroupType::from(0)])), + _extra_fields: Map::new(), + }; + assert_eq!(user.to_string(), "User: 0"); + assert_eq!(group.to_string(), "Group: [0]"); + let group = SActor::Group { + groups: Some(SGroups::from(vec![ + SGroupType::from(0), + SGroupType::from("test"), + ])), + _extra_fields: Map::new(), + }; + assert_eq!(group.to_string(), "Group: [0, test]"); + let unknown = SActor::Unknown(Value::String("unknown".to_string())); + assert_eq!(unknown.to_string(), "Unknown: \"unknown\""); + } + + #[test] + fn test_display_dgrouptype() { + let group = DGroupType::from("test"); + assert_eq!(group.to_string(), "test"); + } + + #[test] + fn test_partialeq_sgroups() { + let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from("test")]); + let other_groups = vec![SGroupType::from(0), SGroupType::from("test")]; + assert_eq!(groups, other_groups); + let other_groups = vec![SGroupType::from(0), SGroupType::from("test2")]; + assert_ne!(groups, other_groups); + let other_groups = vec![SGroupType::from(0)]; + assert_ne!(groups, other_groups); + let other_groups = vec![ + SGroupType::from(0), + SGroupType::from("test"), + SGroupType::from("test2"), + ]; + assert_ne!(groups, other_groups); + let groups = SGroups::from(0); + let other_groups = vec![SGroupType::from(0)]; + assert_eq!(groups, other_groups); + let other_groups = vec![SGroupType::from(0), SGroupType::from("test")]; + assert_ne!(groups, other_groups); + } + + #[test] + fn test_sfetcheq_group() { + let group1 = SGroupType::from(0); + let group2 = SGroupType::from(0); + + assert!(group1.fetch_eq(&group2)); + let group2 = SGroupType::from("root"); + assert!(group1.fetch_eq(&group2)); + let group2 = SGroupType::from("unkown"); + assert!(!group1.fetch_eq(&group2)); + + let groups = SGroups::from(vec![ + SGroupType::from(0), + SGroupType::from(getuid().as_raw() + 1), + ]); + let other_groups = SGroups::from(vec![ + SGroupType::from(0), + SGroupType::from(getuid().as_raw() + 1), + ]); + assert!(groups.fetch_eq(&other_groups)); + let other_groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from("test2")]); + assert!(!groups.fetch_eq(&other_groups)); + let other_groups = SGroups::from(0); + assert!(!groups.fetch_eq(&other_groups)); + let groups = SGroups::from(0); + assert!(groups.fetch_eq(&other_groups)); + } + + #[test] + fn test_sfetcheq_user() { + let user1 = SUserType::from(0); + let user2 = SUserType::from(0); + + assert!(user1.fetch_eq(&user2)); + let user2 = SUserType::from("root"); + assert!(user1.fetch_eq(&user2)); + let user2 = SUserType::from("unkown"); + assert!(!user1.fetch_eq(&user2)); + } + + #[test] + fn test_from() { + let cow = Cow::Borrowed("test"); + let group = DGroupType::from(cow); + assert_eq!(group.to_string(), "test"); + let group = Group::from_gid(0.into()).unwrap().unwrap(); + let group = SGroupType::from(group); + assert_eq!(group.fetch_id(), Some(0)); + let group = SGroups::from([SGroupType::from(0)]); + assert!(group.is_single()); + let group = SGroups::from(["test"]); + assert!(group.is_single()); + let group = SGroups::from(["test", "test2"]); + assert!(!group.is_single()); + let group = SGroups::from(vec![0, 1]); + assert!(!group.is_single()); + let group = SGroups::from(vec![0]); + assert!(group.is_single()); + } + + #[test] + fn test_partialeq_user() { + assert!(SUserType::from(0) == 0); + assert!(SUserType::from(0) != 1); + assert!(DUserType::from(0) == 0); + assert!(DUserType::from(0) != 1); + let user = User::from_uid(0.into()).unwrap().unwrap(); + assert!(SUserType::from(0) == user); + assert!(SUserType::from(0) != 1); + assert!(DUserType::from(0) == user); + assert!(DUserType::from(0) != 1); + assert!(SUserType::from("root") == user); + assert!(SUserType::from("test") != user); + assert!(DUserType::from("root") == user); + assert!(DUserType::from("test") != user); + } + + #[test] + fn test_partialeq_group() { + let group = Group::from_gid(0.into()).unwrap().unwrap(); + assert!(SGroupType::from(0) == group); + assert!(SGroupType::from(1) != group); + assert!(SGroupType::from("root") == group); + assert!(SGroupType::from("test") != group); + assert!(DGroupType::from(0) == group); + assert!(DGroupType::from(1) != group); + assert!(DGroupType::from("root") == group); + assert!(DGroupType::from("test") != group); + } + + #[test] + fn test_tryinto_sgroups() { + let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from(1)]); + let ids: Vec = groups.try_into().unwrap(); + assert_eq!(ids, vec![0, 1]); + + let groups = SGroups::from(vec![SGroupType::from(0)]); + let ids: Vec = groups.try_into().unwrap(); + assert_eq!(ids, vec![0]); + + let groups = SGroups::from(vec![SGroupType::from("unkown")]); + let ids: Result, _> = groups.try_into(); + assert!(ids.is_err()); + } + + #[test] + fn test_tryinto_dgroups() { + let groups: DGroups<'_> = DGroups::from(vec![0.into(), 1.into()]); + let ids: Vec = (&groups).try_into().unwrap(); + assert_eq!(ids, vec![0, 1]); + + let groups = DGroups::from(vec![DGroupType::from(0)]); + let ids: Vec = (&groups).try_into().unwrap(); + assert_eq!(ids, vec![0]); + + let groups = DGroups::from(vec![DGroupType::from("unkown")]); + let ids: Result, _> = (&groups).try_into(); + assert!(ids.is_err()); + } } diff --git a/rar-common/src/database/de.rs b/rar-common/src/database/de.rs new file mode 100644 index 00000000..43e3f29f --- /dev/null +++ b/rar-common/src/database/de.rs @@ -0,0 +1,425 @@ +use core::fmt; + +use serde::Deserialize; + +use crate::database::structs::{SCommand, SetBehavior}; + +use super::{ + actor::SGenericActorType, + structs::{SCapabilities, SCommands}, +}; +use capctl::CapSet; +use serde::{ + de::{self, MapAccess, SeqAccess, Visitor}, + Deserializer, +}; +use serde_json::Map; +use strum::Display; + +impl<'de> Deserialize<'de> for SetBehavior { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SetBehaviorVisitor; + + impl<'de> Visitor<'de> for SetBehaviorVisitor { + type Value = SetBehavior; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or a number") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + value.parse().map_err(de::Error::custom) + } + fn visit_i32(self, v: i32) -> Result + where + E: de::Error, + { + if v > 1 || v < 0 { + return Err(de::Error::custom(format!( + "Invalid value for SetBehavior: {}", + v + ))); + } + SetBehavior::from_repr(v as u8).ok_or(de::Error::custom(format!( + "Invalid value for SetBehavior: {}", + v + ))) + } + fn visit_u8(self, v: u8) -> Result + where + E: de::Error, + { + self.visit_i32(v as i32) + } + fn visit_u16(self, v: u16) -> Result + where + E: de::Error, + { + self.visit_i32(v as i32) + } + fn visit_u32(self, v: u32) -> Result + where + E: de::Error, + { + self.visit_i32(v as i32) + } + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + if v > i32::MAX as u64 { + return Err(de::Error::custom(format!( + "Invalid value for SetBehavior: {}", + v + ))); + } + self.visit_i32(v as i32) + } + fn visit_i8(self, v: i8) -> Result + where + E: de::Error, + { + self.visit_i32(v as i32) + } + fn visit_i16(self, v: i16) -> Result + where + E: de::Error, + { + self.visit_i32(v as i32) + } + fn visit_i64(self, v: i64) -> Result + where + E: de::Error, + { + if v > i32::MAX as i64 || v < i32::MIN as i64 { + return Err(de::Error::custom(format!( + "Invalid value for SetBehavior: {}", + v + ))); + } + self.visit_i32(v as i32) + } + } + + deserializer.deserialize_any(SetBehaviorVisitor) + } +} + +impl<'de> Deserialize<'de> for SCapabilities { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SCapabilitiesVisitor; + + #[derive(Deserialize, Display)] + #[serde(rename_all = "kebab-case")] + #[repr(u8)] + enum Field { + #[serde(alias = "d")] + Default, + #[serde(alias = "a")] + Add, + #[serde(alias = "del", alias = "s")] + Sub, + #[serde(untagged)] + Other(String), + } + + 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(), + }) + } + + 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 { + Field::Default => { + default_behavior = map + .next_value() + .expect("default entry must be either 'all' or 'none'"); + } + Field::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)) + })?); + } + } + Field::Sub => { + 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)) + })?); + } + } + Field::Other(other) => { + _extra_fields.insert(other.to_string(), map.next_value()?); + } + } + } + + Ok(SCapabilities { + default_behavior, + add, + sub, + }) + } + } + + deserializer.deserialize_any(SCapabilitiesVisitor) + } +} + +impl<'de> Deserialize<'de> for SGenericActorType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let raw: serde_json::Value = Deserialize::deserialize(deserializer)?; + match raw { + serde_json::Value::Number(num) if num.is_u64() => { + Ok(SGenericActorType::Id(num.as_u64().unwrap() as u32)) + } + serde_json::Value::String(ref s) => { + if let Ok(num) = s.parse() { + Ok(SGenericActorType::Id(num)) + } else { + Ok(SGenericActorType::Name(s.clone())) + } + } + _ => Err(serde::de::Error::custom( + "Invalid input for SGenericActorType", + )), + } + } +} + +impl<'de> Deserialize<'de> for SCommands { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize, Display)] + #[serde(field_identifier, rename_all = "kebab-case")] + enum Fields { + #[serde(alias = "d")] + Default, + #[serde(alias = "a")] + Add, + #[serde(alias = "del", alias = "s")] + Sub, + #[serde(untagged)] + Other(String), + } + struct SCommandsVisitor; + impl<'de> Visitor<'de> for SCommandsVisitor { + type Value = SCommands; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or a number") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut add = Vec::new(); + while let Some(cmd) = seq.next_element::()? { + add.push(cmd); + } + Ok(SCommands { + default_behavior: Some(SetBehavior::None), + add, + sub: Vec::new(), + _extra_fields: Map::new(), + }) + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut default_behavior = None; + let mut add = Vec::new(); + let mut sub = Vec::new(); + let mut _extra_fields = Map::new(); + + while let Some(key) = map.next_key()? { + match key { + Fields::Default => { + default_behavior = Some( + map.next_value() + .expect("default entry must be either 'all' or 'none'"), + ); + } + Fields::Add => { + let values: Vec = + map.next_value().expect("add entry must be a list"); + add.extend(values); + } + Fields::Sub => { + let values: Vec = + map.next_value().expect("sub entry must be a list"); + sub.extend(values); + } + Fields::Other(other) => { + _extra_fields.insert(other.to_string(), map.next_value()?); + } + } + } + + Ok(SCommands { + default_behavior, + add, + sub, + _extra_fields, + }) + } + } + deserializer.deserialize_any(SCommandsVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use capctl::Cap; + use serde_json::json; + + #[test] + fn test_set_behavior_deserialization() { + let json_data = json!("none"); + let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); + assert_eq!(behavior, SetBehavior::None); + + let json_data = json!("all"); + let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); + assert_eq!(behavior, SetBehavior::All); + + let json_data = json!(0); + let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); + assert_eq!(behavior, SetBehavior::from_repr(0).unwrap()); + + let json_data = json!(1); + let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); + assert_eq!(behavior, SetBehavior::from_repr(1).unwrap()); + + let invalid_data = json!(2); + assert!(serde_json::from_value::(invalid_data).is_err()); + } + + #[test] + fn test_s_capabilities_deserialization_seq() { + let json_data = json!(["CAP_SYS_ADMIN", "CAP_NET_BIND_SERVICE", "CAP_CHOWN"]); + let caps: SCapabilities = serde_json::from_value(json_data).unwrap(); + + assert!(caps.add.has(Cap::SYS_ADMIN)); + assert!(caps.add.has(Cap::NET_BIND_SERVICE)); + assert!(caps.add.has(Cap::CHOWN)); + assert_eq!(caps.default_behavior, SetBehavior::None); + } + + #[test] + fn test_s_capabilities_deserialization_map() { + let json_data = json!({ + "default": "none", + "add": ["CAP_SYS_ADMIN", "CAP_CHOWN"], + "sub": ["CAP_NET_RAW"] + }); + + let caps: SCapabilities = serde_json::from_value(json_data).unwrap(); + + assert!(caps.add.has(Cap::SYS_ADMIN)); + assert!(caps.add.has(Cap::CHOWN)); + assert!(caps.sub.has(Cap::NET_RAW)); + assert_eq!(caps.default_behavior, SetBehavior::None); + } + + #[test] + fn test_invalid_capabilities() { + let invalid_data = json!(["INVALID_CAPABILITY", "CAP_FAKE"]); + assert!(serde_json::from_value::(invalid_data).is_err()); + } + + #[test] + fn test_s_generic_actor_type_deserialization() { + let json_data = json!(42); + let actor_type: SGenericActorType = serde_json::from_value(json_data).unwrap(); + assert_eq!(actor_type, SGenericActorType::Id(42)); + + let json_data = json!("actor_name"); + let actor_type: SGenericActorType = serde_json::from_value(json_data).unwrap(); + assert_eq!( + actor_type, + SGenericActorType::Name("actor_name".to_string()) + ); + + let invalid_data = json!(null); + assert!(serde_json::from_value::(invalid_data).is_err()); + } + + #[test] + fn test_s_commands_deserialization_seq() { + let json_data = json!(["/bin/ls", "/bin/cat"]); + + let commands: SCommands = serde_json::from_value(json_data).unwrap(); + + assert_eq!(commands.add.len(), 2); + assert_eq!(commands.add[0], "/bin/ls".into()); + assert_eq!(commands.add[1], "/bin/cat".into()); + } + + #[test] + fn test_s_commands_deserialization_map() { + let json_data = json!({ + "default": "all", + "add": ["/bin/ls"], + "sub": ["/bin/cat"] + }); + + let commands: SCommands = serde_json::from_value(json_data).unwrap(); + + assert_eq!(commands.default_behavior.unwrap(), SetBehavior::All); + assert_eq!(commands.add.len(), 1); + assert_eq!(commands.add[0], "/bin/ls".into()); + assert_eq!(commands.sub.len(), 1); + assert_eq!(commands.sub[0], "/bin/cat".into()); + } +} diff --git a/rar-common/src/database/finder.rs b/rar-common/src/database/finder.rs deleted file mode 100644 index 52310259..00000000 --- a/rar-common/src/database/finder.rs +++ /dev/null @@ -1,3391 +0,0 @@ -use std::{ - cell::RefCell, - cmp::Ordering, - error::Error, - fmt::{Display, Formatter}, - path::PathBuf, - rc::{Rc, Weak}, -}; - -use bon::Builder; -use capctl::CapSet; -use glob::Pattern; -use log::{debug, warn}; -use nix::{ - libc::dev_t, - 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::{ - SCommand, SCommands, SConfig, SGroupschooser, SRole, STask, SUserChooser, SetBehavior, - }, -}; -use crate::util::{capabilities_are_exploitable, final_path, parse_conf_command}; -use crate::{ - api::{PluginManager, PluginResultAction}, - as_borrow, -}; -use bitflags::bitflags; - -use super::{ - actor::{SGroupType, SGroups, SUserType}, - FilterMatcher, -}; - -#[derive(Debug, PartialEq, Eq, Clone, EnumIs)] -pub enum MatchError { - NoMatch(String), - Conflict(String), -} - -impl Display for MatchError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - MatchError::NoMatch(reason) => write!(f, "No match because : {}", reason), - MatchError::Conflict(reason) => write!(f, "Conflict because : {}", reason), - } - } -} - -impl Error for MatchError { - fn description(&self) -> &str { - match self { - MatchError::NoMatch(_) => "No match", - MatchError::Conflict(_) => "Conflict", - } - } -} - -#[derive(Clone, Debug)] -pub struct ExecSettings { - pub exec_path: PathBuf, - pub exec_args: Vec, - pub opt: OptStack, - pub setuid: Option, - pub setgroups: Option, - pub caps: Option, - pub task: Weak>, -} - -impl ExecSettings { - fn new() -> ExecSettings { - ExecSettings { - exec_path: PathBuf::new(), - exec_args: Vec::new(), - opt: OptStack::default(), - setuid: None, - setgroups: None, - caps: None, - task: Weak::new(), - } - } - - pub fn task(&self) -> Rc> { - self.task.upgrade().expect("Internal Error") - } - - pub fn role(&self) -> Rc> { - self.task() - .as_ref() - .borrow() - .role() - .expect("Internal Error") - } -} - -impl PartialEq for ExecSettings { - fn eq(&self, other: &Self) -> bool { - // We ignore the task field - let res = self.exec_path == other.exec_path - && self.exec_args == other.exec_args - && self.opt == other.opt - && self.setuid == other.setuid - && self.setgroups == other.setgroups - && self.caps == other.caps; - debug!( - "Comparing self.exec_path == other.exec_path : {} - && self.exec_args == other.exec_args : {} - && self.opt == other.opt : {} - && self.setuid == other.setuid : {} - && self.setgroups == other.setgroups : {} - && self.caps == other.caps : {}", - self.exec_path == other.exec_path, - self.exec_args == other.exec_args, - self.opt == other.opt, - self.setuid == other.setuid, - self.setgroups == other.setgroups, - self.caps == other.caps - ); - res - } -} - -#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, EnumIs)] -#[repr(u32)] -// Matching user groups for the role -pub enum ActorMatchMin { - UserMatch, - GroupMatch(usize), - NoMatch, -} - -#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] - -// 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)] -pub struct CmdMin(u32); - -bitflags! { - - impl CmdMin: u32 { - const Match = 0b00001; - const WildcardPath = 0b00010; - const RegexArgs = 0b00100; - const FullRegexArgs = 0b01000; - const FullWildcardPath = 0b10000; - } -} - -#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] -pub enum CapsMin { - Undefined, - NoCaps, - CapsNoAdmin(usize), - CapsAdmin(usize), - CapsAll, -} - -#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] -pub struct SecurityMin(u32); - -bitflags! { - - impl SecurityMin: u32 { - 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: ActorMatchMin, - pub cmd_min: CmdMin, - pub caps_min: CapsMin, - pub setuser_min: SetUserMin, - pub security_min: SecurityMin, -} - -impl Score { - pub fn prettyprint(&self) -> String { - format!( - "{:?}, {:?}, {:?}, {:?}, {:?}", - self.user_min, self.cmd_min, self.caps_min, self.setuser_min, self.security_min - ) - } - - pub fn user_cmp(&self, other: &Score) -> Ordering { - self.user_min.cmp(&other.user_min) - } - - /// Compare the score of tasks results - pub fn cmd_cmp(&self, other: &Score) -> Ordering { - self.cmd_min - .cmp(&other.cmd_min) - .then(self.caps_min.cmp(&other.caps_min)) - .then(self.setuser_min.cmp(&other.setuser_min)) - .then(self.security_min.cmp(&other.security_min)) - } -} - -impl PartialOrd for Score { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Score { - fn cmp(&self, other: &Self) -> Ordering { - self.cmd_cmp(other).then(self.user_cmp(other)) - } - - fn max(self, other: Self) -> Self { - std::cmp::max_by(self, other, Ord::cmp) - } - - fn min(self, other: Self) -> Self { - std::cmp::min_by(self, other, Ord::cmp) - } - - fn clamp(self, min: Self, max: Self) -> Self { - self.max(min).min(max) - } -} - -#[derive(Debug, Builder)] -pub struct Cred { - #[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, - pub settings: ExecSettings, -} - -impl TaskMatch { - pub fn fully_matching(&self) -> bool { - self.user_matching() && self.command_matching() - } - - pub fn user_matching(&self) -> bool { - self.score.user_min != ActorMatchMin::NoMatch - } - - pub fn command_matching(&self) -> bool { - !self.score.cmd_min.is_empty() - } - - pub fn task(&self) -> Rc> { - self.settings.task.upgrade().expect("Internal Error") - } - - pub fn role(&self) -> Rc> { - self.task() - .as_ref() - .borrow() - .role() - .expect("Internal Error") - } -} - -impl Default for TaskMatch { - fn default() -> Self { - TaskMatch { - score: Score { - user_min: ActorMatchMin::NoMatch, - cmd_min: CmdMin::empty(), - caps_min: CapsMin::Undefined, - setuser_min: SetUserMin::default(), - security_min: SecurityMin::empty(), - }, - settings: ExecSettings::new(), - } - } -} - -pub trait TaskMatcher { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result; -} - -pub trait CredMatcher { - fn user_matches(&self, user: &Cred) -> ActorMatchMin; -} - -fn find_from_envpath(needle: &PathBuf) -> Option { - if needle.is_absolute() { - return None; - } - let env_path = std::env::var_os("PATH").unwrap(); - for path in std::env::split_paths(&env_path) { - let path = path.join(needle); - if path.exists() { - return Some(path); - } - } - None -} - -fn match_path(input_path: &str, role_path: &String) -> CmdMin { - if role_path == "**" { - return CmdMin::FullWildcardPath; - } - let mut match_status = CmdMin::empty(); - let new_path = final_path(input_path); - let role_path = final_path(role_path); - debug!("Matching path {:?} with {:?}", new_path, role_path); - if new_path == role_path { - match_status |= CmdMin::Match; - } else if let Ok(pattern) = Pattern::new(role_path.to_str().unwrap()) { - if pattern.matches_path(&new_path) { - match_status |= CmdMin::WildcardPath; - } - } - if match_status.is_empty() { - debug!( - "No match for path ``{:?}`` for evaluated path : ``{:?}``", - new_path, role_path - ); - } - match_status -} - -/// Check if input args is matching with role args and return the score -/// role args can contains regex -/// input args is the command line args -fn match_args(input_args: &[String], role_args: &[String]) -> Result> { - if role_args[0] == ".*" { - return Ok(CmdMin::FullRegexArgs); - } - let commandline = input_args.join(" "); - let role_args = role_args.join(" "); - debug!("Matching args {:?} with {:?}", commandline, role_args); - if commandline != role_args { - debug!("test regex"); - evaluate_regex_cmd(role_args, commandline).inspect_err(|e| { - debug!("{:?},No match for args {:?}", e, input_args); - }) - } else { - Ok(CmdMin::Match) - } -} - -#[cfg(feature = "pcre2")] -fn evaluate_regex_cmd(role_args: String, commandline: String) -> Result> { - let regex = RegexBuilder::new().build(&role_args)?; - if regex.is_match(commandline.as_bytes())? { - Ok(CmdMin::RegexArgs) - } else { - Err(Box::new(MatchError::NoMatch( - "Regex for command does not match".to_string(), - ))) - } -} - -#[cfg(not(feature = "pcre2"))] -fn evaluate_regex_cmd(_role_args: String, _commandline: String) -> Result> { - Err(Box::new(MatchError::NoMatch)) -} - -/// Check if input command line is matching with role command line and return the score -fn match_command_line(input_command: &[String], role_command: &[String]) -> CmdMin { - let mut result = CmdMin::empty(); - if !input_command.is_empty() { - result = match_path(&input_command[0], &role_command[0]); - if result.is_empty() || role_command.len() == 1 { - return result; - } - match match_args(&input_command[1..], &role_command[1..]) { - Ok(args_result) => result |= args_result, - Err(err) => { - if err.downcast_ref::().is_none() { - warn!("Error: {}", err); - } - return CmdMin::empty(); - } - } - } - result -} - -/// Find the minimum score for all commands that match the input command line -fn get_cmd_min(input_command: &[String], commands: &[SCommand]) -> CmdMin { - let mut min_score: CmdMin = CmdMin::empty(); - debug!("Input {:?} matches with {:?}", input_command, commands); - for command in commands { - match parse_conf_command(command) { - Ok(command) => { - let new_score = match_command_line(input_command, &command); - debug!("Score for command {:?} is {:?}", command, new_score); - if !new_score.is_empty() && (min_score.is_empty() || (new_score < min_score)) { - debug!("New min score for command {:?} is {:?}", command, new_score); - min_score = new_score; - } - } - Err(err) => { - warn!("Error: {}", err); - } - } - } - min_score -} - -fn get_caps_min(caps: &Option) -> CapsMin { - match caps { - Some(caps) => { - if caps.is_empty() { - CapsMin::NoCaps - } else if *caps == !CapSet::empty() { - CapsMin::CapsAll - } else if capabilities_are_exploitable(caps) { - CapsMin::CapsAdmin(caps.size()) - } else { - CapsMin::CapsNoAdmin(caps.size()) - } - } - None => CapsMin::NoCaps, - } -} - -fn get_security_min(opt: &Option>>) -> SecurityMin { - match opt { - Some(opt) => { - let opt = opt.as_ref().borrow(); - let mut result = SecurityMin::empty(); - if let Some(value) = opt.bounding { - if value.is_strict() { - result |= SecurityMin::DisableBounding; - } - } - if let Some(value) = opt.root { - if value.is_privileged() { - result |= SecurityMin::EnableRoot; - } - } - if let Some(value) = &opt.path { - if value.default_behavior.is_keep_unsafe() { - result |= SecurityMin::KeepUnsafePath; - } else if value.default_behavior.is_keep_safe() { - result |= SecurityMin::KeepPath; - } - } - if let Some(value) = &opt.env { - if value.default_behavior.is_keep() { - result |= SecurityMin::KeepEnv; - } - } - if opt.authentication.is_some_and(|auth| auth.is_skip()) { - result |= SecurityMin::SkipAuth; - } - result - } - None => SecurityMin::empty(), - } -} - -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) => group_is_root(group), - SGroups::Multiple(groups) => groups.iter().any(group_is_root), - } - } else { - false - } -} - -fn groups_len(groups: Option<&SGroups>) -> usize { - match groups { - Some(groups) => groups.len(), - None => 0, - } -} - -fn get_setuid_min( - setuid: Option<&SUserType>, - setgid: Option<&SGroups>, - security_min: &SecurityMin, -) -> SetUserMin { - match (setuid, setgid) { - (Some(setuid), setgid) => { - if security_min.contains(SecurityMin::EnableRoot) { - // root is privileged - if user_is_root(setuid) { - if groups_contains_root(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 { - SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: None, - } - } else { - SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: Some(SetgidMin { - is_root: false, - nb_groups: (groups_len(setgid)), - }), - } - } - } else if groups_contains_root(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 { - SetUserMin { - uid: Some(SetuidMin { is_root: false }), - gid: None, - } - } else { - SetUserMin { - uid: Some(SetuidMin { is_root: false }), - gid: Some(SetgidMin { - is_root: false, - nb_groups: (groups_len(setgid)), - }), - } - } - } else { - // root is a user - 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 { - SetUserMin { - uid: None, - gid: None, - } - } else if security_min.contains(SecurityMin::EnableRoot) && groups_contains_root(setgid) - { - SetUserMin { - uid: None, - gid: Some(SetgidMin { - is_root: true, - nb_groups: len, - }), - } - } else { - SetUserMin { - uid: None, - gid: Some(SetgidMin { - is_root: false, - nb_groups: len, - }), - } - } - } - } -} - -impl TaskMatcher for Rc> { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result { - if let Some(cmd_opt) = cmd_opt { - 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("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, - } = self - .as_ref() - .borrow() - .commands - .matches(user, cmd_opt, command)?; - - // Process capabilities and security - let capset = self - .as_ref() - .borrow() - .cred - .capabilities - .as_ref() - .map(|caps| caps.to_capset()); - score.caps_min = get_caps_min(&capset); - score.security_min = get_security_min(&self.as_ref().borrow().options); - 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; - let setgid_result: Option = match setgid { - Some(SGroupschooser::Group(s)) => Some(s.clone()), - Some(SGroupschooser::StructChooser(m)) => { - match cmd_opt.as_ref().and_then(|cmd| cmd.group.as_ref()) { - None => { - debug!( - "No group specified in the command, fallback used : {:?}", - m.fallback - ); - Some(m.fallback.clone()) - } - Some(ggroup) => { - debug!("Group specified in the command : {:?}", ggroup); - if ggroup.fetch_eq(&m.fallback) { - debug!("The group specified in the command matches the fallback !"); - Some(m.fallback.clone()) - } else if m.sub.iter().any(|s| s.fetch_eq(ggroup)) { - return Err(MatchError::NoMatch( - "The group is forbidden in sub.".into(), - )); - } else if m.add.iter().any(|s| s.fetch_eq(ggroup)) { - Some(ggroup.clone()) - } else { - match m.default { - SetBehavior::None => { - return Err(MatchError::NoMatch( - "No default behavior applicable.".into(), - )); - } - SetBehavior::All => { - debug!("All groups are accepted."); - Some(ggroup.clone()) - } - } - } - } - } - } - None => None, - }; - - // Calculate setuid and setgid minimum - score.setuser_min = get_setuid_min( - setuid_result.as_ref(), - setgid_result.as_ref(), - &score.security_min, - ); - - // Update task settings - settings.setuid = setuid_result.clone(); - settings.setgroups = setgid_result.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 }) - } -} - -fn get_default_behavior(commands: &Option) -> &SetBehavior { - match commands.as_ref() { - Some(commands) => commands, - None => &SetBehavior::None, - } -} - -impl TaskMatcher for SCommands { - fn matches( - &self, - _: &Cred, - _: &Option, - input_command: &[String], - ) -> Result { - let min_score: CmdMin; - let mut settings = ExecSettings::new(); - // if the command is forbidden, we return NoMatch - debug!("Checking if command is forbidden"); - let is_forbidden = get_cmd_min(input_command, &self.sub); - if !is_forbidden.is_empty() { - debug!("Command is forbidden"); - 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() { - debug!("Checking if command is allowed by default"); - // 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("Command is not allowed".to_string())); - } - } else { - min_score = CmdMin::all(); - debug!("Command is allowed by default"); - } - - if let Some(program) = - find_from_envpath(&input_command[0].parse().expect("The path is not valid")) - { - settings.exec_path = program; - settings.exec_args = input_command[1..].to_vec(); - } else { - // encapsulate the command in sh command - settings.exec_path = PathBuf::from("/bin/sh"); - settings.exec_args = vec!["-c".to_string(), shell_words::join(input_command)]; - } - - Ok(TaskMatch { - score: Score { - user_min: ActorMatchMin::NoMatch, - cmd_min: min_score, - caps_min: CapsMin::Undefined, - setuser_min: SetUserMin::default(), - security_min: SecurityMin::empty(), - }, - settings, - }) - } -} - -/// Check if user's groups is matching with any of the role's groups -fn match_groups(groups: &[Group], role_groups: &[SGroups]) -> bool { - for role_group in role_groups { - if match role_group { - SGroups::Single(group) => { - debug!( - "Checking group {}, with {:?}, it must be {}", - group, - groups, - groups.iter().any(|g| group == g) - ); - groups.iter().any(|g| group == g) - } - SGroups::Multiple(multiple_actors) => multiple_actors.iter().all(|actor| { - debug!("Checking group {}, with {:?}", actor, groups); - groups.iter().any(|g| actor == g) - }), - } { - return true; - } - } - false -} - -impl CredMatcher for Rc> { - 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 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(ActorMatchMin::UserMatch); - } - } - } - SActor::Group { groups, .. } => { - if let Some(groups) = groups.as_ref() { - if match_groups(&user.groups, &[groups.clone()]) { - return Some(ActorMatchMin::GroupMatch(groups.len())); - } - } - } - SActor::Unknown(element) => { - let min = PluginManager::notify_user_matcher(&as_borrow!(self), user, element); - if !min.is_no_match() { - return Some(min); - } - } - } - None - }); - let min = matches.min().unwrap_or(ActorMatchMin::NoMatch); - debug!( - "Role {} : User {} matches with {:?}", - borrow.name, user.user.name, min - ); - min - } -} - -impl TaskMatcher for Vec>> { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result { - let mut min_task = TaskMatch::default(); - let mut nmatch = 0; - for task in self.iter() { - match task.matches(user, cmd_opt, command) { - Ok(mut task_match) => { - if !min_task.command_matching() - || task_match.score.cmd_cmp(&min_task.score) == Ordering::Less - { - task_match.score.user_min = min_task.score.user_min; - task_match.settings.task = Rc::downgrade(task); - min_task = task_match; - nmatch = 1; - } else if task_match.score == min_task.score - && task_match.settings != min_task.settings - { - nmatch += 1; - } - } - Err(err) => match err { - MatchError::NoMatch(_) => { - continue; - } - MatchError::Conflict(_) => { - return Err(err); - } - }, - } - } - debug!("nmatch = {}", nmatch); - if nmatch == 0 { - Err(MatchError::NoMatch("No tasks matched".into())) - } else if nmatch == 1 { - Ok(min_task) - } else { - Err(MatchError::Conflict("Multiple tasks matched".into())) - } - } -} - -impl TaskMatcher for Vec>> { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result { - let mut min_role = TaskMatch::default(); - let mut nmatch = 0; - for role in self.iter() { - match role.matches(user, cmd_opt, command) { - Ok(mut role_match) => { - role_match.score.user_min = min_role.score.user_min; - if min_role.score.cmd_min.is_empty() || role_match.score < min_role.score { - min_role = role_match; - nmatch = 1; - } else if role_match.score == min_role.score - && !Rc::ptr_eq( - &role_match.settings.task.upgrade().unwrap(), - &min_role.settings.task.upgrade().unwrap(), - ) - { - nmatch += 1; - } - } - Err(err) => { - if err.is_no_match() { - continue; - } else { - return Err(err); - } - } - } - } - if nmatch == 0 { - Err(MatchError::NoMatch("No roles matched".into())) - } else if nmatch == 1 { - Ok(min_role) - } else { - Err(MatchError::Conflict("Multiple roles matched".into())) - } - } -} - -impl TaskMatcher for Rc> { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result { - 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("Role name does not match".to_string())); - } - } - } - let borrow = self.as_ref().borrow(); - let mut min_role = TaskMatch::default(); - let user_min = self.user_matches(user); - min_role.score.user_min = user_min; - - let mut nmatch = 0; - - match borrow.tasks.matches(user, cmd_opt, command) { - Ok(task_match) => { - if !min_role.fully_matching() - || (task_match.command_matching() && task_match.score < min_role.score) - { - min_role = task_match; - nmatch = 1; - } - } - Err(MatchError::NoMatch(_)) => { - nmatch = 0; - } - Err(MatchError::Conflict(msg)) => { - return Err(MatchError::Conflict(msg)); - } - } - min_role.score.user_min = user_min; - plugin_role_match( - user_min, - borrow, - user, - cmd_opt, - command, - &mut min_role, - &mut nmatch, - ); - debug!( - "==== Role {} ====\n score: {}", - self.as_ref().borrow().name, - min_role.score.prettyprint() - ); - if nmatch == 0 { - Err(MatchError::NoMatch("No tasks matched".into())) - } else if nmatch == 1 { - debug!( - "=== Role {} === : Match for task {}\nScore : {}", - self.as_ref().borrow().name, - min_role.task().as_ref().borrow().name.to_string(), - min_role.score.prettyprint() - ); - Ok(min_role) - } else { - Err(MatchError::Conflict("Multiple tasks matched".into())) - } - } -} - -fn plugin_role_match( - user_min: ActorMatchMin, - borrow: std::cell::Ref<'_, SRole>, - user: &Cred, - cmd_opt: &Option, - command: &[String], - min_role: &mut TaskMatch, - nmatch: &mut i32, -) { - let mut matcher = TaskMatch::default(); - matcher.score.user_min = user_min; - // notify plugins - match PluginManager::notify_role_matcher(&borrow, user, cmd_opt, command, &mut matcher) { - PluginResultAction::Override => { - *min_role = matcher; - *nmatch = if min_role.fully_matching() { 1 } else { 0 }; - } - PluginResultAction::Edit => { - debug!("Plugin edit"); - if !min_role.command_matching() - || (matcher.command_matching() && matcher.score.cmd_min < min_role.score.cmd_min) - { - *min_role = matcher; - *nmatch = 1; - } else if matcher.score == min_role.score { - *nmatch += 1; - } else if !matcher.fully_matching() { - *nmatch = 0; - } - } - PluginResultAction::Ignore => {} - } - debug!("nmatch = {}", nmatch); -} - -impl TaskMatcher for Rc> { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result { - debug!( - "Config : Matching user {} with command {:?}", - user.user.name, command - ); - let mut tasks: Vec = Vec::new(); - for role in self.as_ref().borrow().roles.iter() { - if let Ok(matched) = role.matches(user, cmd_opt, command) { - if matched.fully_matching() { - if tasks.is_empty() || matched.score < tasks[0].score { - tasks.clear(); - tasks.push(matched); - } else if matched.score == tasks[0].score - && !Rc::ptr_eq( - &matched.settings.task.upgrade().unwrap(), - &tasks[0].settings.task.upgrade().unwrap(), - ) - { - tasks.push(matched); - } - } - } // we ignore error, because it's not a match - } - if tasks.is_empty() { - Err(MatchError::NoMatch("No roles matched".into())) - } else if tasks.len() > 1 { - Err(MatchError::Conflict("Multiple roles matched".into())) - } else { - debug!( - "Config : Matched user {}\n - command {:?}\n - with task {}\n - with role {}\n - with score {:?}", - user.user.name, - command, - tasks[0].task().as_ref().borrow().name.to_string(), - tasks[0].role().as_ref().borrow().name, - tasks[0].score.prettyprint() - ); - Ok(tasks[0].clone()) - } - } -} - -#[cfg(test)] -mod tests { - - use std::{fs, vec}; - - use capctl::Cap; - use test_log::test; - - use crate::{ - database::{ - options::{EnvBehavior, PathBehavior, SAuthentication, SBounding, SPrivileged}, - structs::{IdTask, RoleGetter, SCredentials, SSetgidSet, SSetuidSet}, - versionning::Versioning, - }, make_weak_config, rc_refcell - }; - - 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(); - } - - fn get_non_root_gid(nth: usize) -> Option { - // list all users - let passwd = fs::read_to_string("/etc/group").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() - }) - .filter(|uid| *uid != 0) - .nth(nth); - } - - #[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()); - assert_eq!(result, CmdMin::Match); - } - - #[test] - fn test_match_args() { - let result = match_args( - &["-l".to_string(), "-a".to_string()], - &["-l".to_string(), "-a".to_string()], - ); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), CmdMin::Match); - } - - #[test] - fn test_match_command_line() { - let result = match_command_line( - &["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()], - &["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()], - ); - assert_eq!(result, CmdMin::Match); - } - - #[test] - fn test_get_cmd_min() { - let result = get_cmd_min( - &["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()], - &[ - "/bin/l*".into(), - "/bin/ls .*".into(), - "/bin/ls -l -a".into(), - ], - ); - assert_eq!(result, CmdMin::Match); - } - - #[test] - fn test_get_caps_min_all() { - let caps = !CapSet::empty(); - assert_eq!(get_caps_min(&Some(caps)), CapsMin::CapsAll); - } - - #[test] - fn test_get_caps_min_no_admin() { - let mut caps = CapSet::empty(); - caps.add(Cap::NET_BIND_SERVICE); - assert_eq!(get_caps_min(&Some(caps)), CapsMin::CapsNoAdmin(1)); - } - - #[test] - fn test_get_caps_min_admin() { - let mut caps = CapSet::empty(); - caps.add(Cap::SYS_ADMIN); - assert_eq!(get_caps_min(&Some(caps)), CapsMin::CapsAdmin(1)); - } - - #[test] - fn test_get_caps_min_no_caps() { - assert_eq!(get_caps_min(&None), CapsMin::NoCaps); - } - - #[test] - fn test_get_security_min() { - let rcopt = Rc::new(RefCell::new(Opt::default())); - { - let opt = &mut rcopt.as_ref().borrow_mut(); - opt.bounding = Some(SBounding::Strict); - opt.root = Some(SPrivileged::Privileged); - opt.path.as_mut().unwrap().default_behavior = PathBehavior::KeepUnsafe; - opt.env.as_mut().unwrap().default_behavior = EnvBehavior::Keep; - opt.authentication = Some(SAuthentication::Skip); - } - - assert_eq!( - get_security_min(&Some(rcopt.clone())), - SecurityMin::DisableBounding - | SecurityMin::EnableRoot - | SecurityMin::KeepUnsafePath - | SecurityMin::KeepEnv - | SecurityMin::SkipAuth - ); - rcopt - .as_ref() - .borrow_mut() - .path - .as_mut() - .unwrap() - .default_behavior = PathBehavior::KeepSafe; - assert_eq!( - get_security_min(&Some(rcopt.clone())), - SecurityMin::DisableBounding - | SecurityMin::EnableRoot - | SecurityMin::KeepPath - | SecurityMin::KeepEnv - | SecurityMin::SkipAuth - ); - } - - #[test] - fn test_is_root() { - 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] - fn test_list_contains_root() { - let mut list = SGroups::Single("root".into()); - assert!(groups_contains_root(Some(&list))); - list = SGroups::Multiple(vec!["root".into(), 1.into()]); - assert!(groups_contains_root(Some(&list))); - list = SGroups::Multiple(vec![1.into(), 2.into()]); - assert!(!groups_contains_root(Some(&list))); - } - - #[test] - fn test_get_setuid_min() { - 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), - 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), - 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), - 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), - SetUserMin { - uid: None, - gid: Some(SetgidMin { - is_root: false, - nb_groups: 2 - }) - } - ); - assert_eq!( - get_setuid_min(None, None, &security_min), - SetUserMin { - uid: None, - gid: None - } - ); - assert_eq!( - get_setuid_min(setuid.as_ref(), None, &security_min), - 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: ActorMatchMin::UserMatch, - cmd_min: CmdMin::Match, - caps_min: CapsMin::CapsAll, - 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: ActorMatchMin::UserMatch, - cmd_min: CmdMin::Match, - caps_min: CapsMin::CapsAll, - 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); - assert_eq!(score2.cmp(&score1), Ordering::Less); - assert_eq!(score1.max(score2), score1); - assert_eq!(score1.min(score2), score2); - assert_eq!(score1.clamp(score2, score1), score1); - assert_eq!(score1.clamp(score2, score2), score2); - score2.security_min = SecurityMin::DisableBounding | SecurityMin::EnableRoot; - assert_eq!(score1.cmp(&score2), Ordering::Equal); - 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.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.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.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.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); - assert_eq!(score1.cmp(&score2), Ordering::Greater); - score2.caps_min = CapsMin::NoCaps; - assert_eq!(score1.cmp(&score2), Ordering::Greater); - score2.caps_min = CapsMin::CapsAll; - assert_eq!(score1.cmp(&score2), Ordering::Equal); - score2.cmd_min = CmdMin::FullWildcardPath; - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.cmd_min = CmdMin::WildcardPath; - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.cmd_min = CmdMin::RegexArgs; - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.cmd_min = CmdMin::FullRegexArgs; - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.cmd_min = CmdMin::Match; - assert_eq!(score1.cmp(&score2), Ordering::Equal); - score2.user_min = ActorMatchMin::GroupMatch(1); - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.user_min = ActorMatchMin::NoMatch; - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.user_min = ActorMatchMin::UserMatch; - assert_eq!(score1.cmp(&score2), Ordering::Equal); - } - - fn setup_test_config(num_roles: usize) -> Rc> { - SConfig::builder() - .roles((0..num_roles).map(|i| SRole::builder(format!("role{}", i)).build())) - .build() - } - - fn setup_test_role( - num_tasks: usize, - role: Option>>, - with_config: Option>>, - ) -> Rc> { - let role = role.unwrap_or_else(|| { - let mut role = SRole::default(); - role.name = "test".to_string(); - role._config = with_config.map(|config| Rc::downgrade(&config)); - Rc::new(RefCell::new(role)) - }); - for i in 0..num_tasks { - let mut task = STask::default(); - task.name = IdTask::Name(format!("{}_task_{}", role.as_ref().borrow().name, i)); - task._role = Some(Rc::downgrade(&role)); - role.as_ref().borrow_mut().tasks.push(Rc::new(task.into())); - } - role - } - - #[test] - fn test_matcher_matches() { - let config = setup_test_config(2); - let role0 = setup_test_role(2, Some(config.as_ref().borrow().roles[0].clone()), None); - let r0_task0 = role0.as_ref().borrow().tasks[0].clone(); - let r0_task1 = role0.as_ref().borrow().tasks[1].clone(); - let role1 = setup_test_role(2, Some(config.as_ref().borrow().roles[1].clone()), None); - let r1_task0 = role1.as_ref().borrow().tasks[0].clone(); - let r1_task1 = role1.as_ref().borrow().tasks[1].clone(); - - // every tasks matches but not at the same score, so the least one is matched - role0 - .as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - role1 - .as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - r0_task0 - .as_ref() - .borrow_mut() - .commands - .add - .push("/bin/ls -l -a".into()); // candidate - r0_task1 - .as_ref() - .borrow_mut() - .commands - .add - .push("/bin/ls .*".into()); // regex args > r1_task1 - - r1_task0 - .as_ref() - .borrow_mut() - .commands - .add - .push("/bin/ls -l -a".into()); //AllCaps > r1_task1 - r1_task1 - .as_ref() - .borrow_mut() - .commands - .add - .push("/bin/ls -l -a".into()); //One Capability > r1_task1 - - r1_task0.as_ref().borrow_mut().cred.capabilities = Some((!CapSet::empty()).into()); - let mut capset = CapSet::empty(); - capset.add(Cap::SYS_ADMIN); - r1_task1.as_ref().borrow_mut().cred.capabilities = Some(capset.into()); - - let cred = Cred { - 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()); - let result = result.unwrap(); - assert_eq!( - result.task().as_ref().borrow().name, - IdTask::Name("role0_task_0".to_string()) - ); - 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)); - - 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)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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_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)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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 autorisé - 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)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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(); - let mut settings2 = ExecSettings::new(); - assert_eq!(settings1, settings2); - settings1.exec_path = PathBuf::from("/bin/ls"); - assert_ne!(settings1, settings2); - settings2.exec_path = PathBuf::from("/bin/ls"); - assert_eq!(settings1, settings2); - settings1.exec_args = vec!["-l".to_string()]; - assert_ne!(settings1, settings2); - settings2.exec_args = vec!["-l".to_string()]; - assert_eq!(settings1, settings2); - settings1.setuid = Some("root".into()); - assert_ne!(settings1, settings2); - settings2.setuid = Some("root".into()); - assert_eq!(settings1, settings2); - settings1.setgroups = Some(SGroups::Single("root".into())); - assert_ne!(settings1, settings2); - settings2.setgroups = Some(SGroups::Single("root".into())); - assert_eq!(settings1, settings2); - settings1.caps = Some(CapSet::empty()); - assert_ne!(settings1, settings2); - settings2.caps = Some(CapSet::empty()); - assert_eq!(settings1, settings2); - } - - #[test] - fn test_two_task_matches_equals() { - let config = rc_refcell!(SConfig::default()); - let role = rc_refcell!(SRole::default()); - role.as_ref().borrow_mut()._config = Some(Rc::downgrade(&config)); - role.as_ref().borrow_mut().name = "test".to_string(); - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - let mut task1 = STask::default(); - let mut task2 = STask::default(); - task1.name = IdTask::Name("task1".to_string()); - task2.name = IdTask::Name("task2".to_string()); - task1.commands.add.push("/bin/ls".into()); - task2.commands.add.push("/bin/ls".into()); - task1.options = Some(Rc::new(RefCell::new(Opt::default()))); - task2.options = Some(Rc::new(RefCell::new(Opt::default()))); - task1._role = Some(Rc::downgrade(&role)); - task2._role = Some(Rc::downgrade(&role)); - task1.cred.capabilities = Some((!CapSet::empty()).into()); - task2.cred.capabilities = Some((!CapSet::empty()).into()); - role.as_ref().borrow_mut().tasks.push(Rc::new(task1.into())); - role.as_ref().borrow_mut().tasks.push(Rc::new(task2.into())); - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - let command = vec!["/bin/ls".to_string()]; - let result = role.matches(&cred, &None, &command); - assert!(result.is_ok()); - role.as_ref().borrow_mut()[0] - .as_ref() - .borrow_mut() - .options - .as_mut() - .unwrap() - .as_ref() - .borrow_mut() - .path - .as_mut() - .unwrap() - .add - .replace(["/test".to_string()].iter().cloned().collect()); - let result = role.matches(&cred, &None, &command); - assert!(result.is_err()); - } - - #[test] - fn test_two_role_default() { - let config: Versioning>> = - serde_json::from_str(&fs::read_to_string("../resources/rootasrole.json").unwrap()) - .unwrap(); - let config = config.data; - make_weak_config(&config); - config.as_ref().borrow_mut()[0].as_ref().borrow_mut().actors[0] = - SActor::user("root").build(); - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - let command = vec!["/bin/ls".to_string()]; - let result = config.matches(&cred, &None, &command); - assert!(result.is_ok()); - // must match the r_root role and t_root task - let result = result.unwrap(); - assert_eq!(result.role().as_ref().borrow().name, "r_root"); - assert_eq!( - result.task().as_ref().borrow().name, - IdTask::Name("t_root".to_string()) - ); - let command = vec!["/usr/bin/chsr".to_string(), "show".to_string()]; - let result = config.matches(&cred, &None, &command); - assert!(result.is_ok()); - // must match the r_root role and t_chsr task - let result = result.unwrap(); - assert_eq!(result.role().as_ref().borrow().name, "r_root"); - assert_eq!( - result.task().as_ref().borrow().name, - 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." - ); - } - - #[test] - fn test_setgid_fallback_single_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(fallback_group.clone()) - .build(); - - // Exécution du match - 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 le groupe assigné est bien celui de fallback - assert_eq!(result.settings.setgroups, Some(fallback_group.clone())); - - println!("Test successful: The specified user correctly matches the fallback."); - } - - #[test] - fn test_setgid_fallback_multiple_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 `setgid` avec un `fallback` - let fallback_group = SGroups::Multiple(vec![ - SGroupType::from(get_non_root_gid(0).unwrap()), - SGroupType::from(get_non_root_gid(1).unwrap()), - ]); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::Multiple(vec![ - SGroupType::from(get_non_root_gid(0).unwrap()), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - .build(); - - // Exécution du match - 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 le groupe assigné est bien celui de fallaback - assert_eq!(result.settings.setgroups, Some(fallback_group.clone())); - - println!("Test successful: The specified user correctly matches the fallback."); - } - - #[test] - fn test_setgid_fallback_nonarg_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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 le groupe assigné est bien celui de fallback - assert_eq!(result.settings.setgroups, Some(fallback_group.clone())); - - println!("Test successful: The specified group correctly matches the fallback when no valid group is provided."); - } - - #[test] - fn test_setgid_add_single_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::from("root")], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::from("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 le groupe assigné est bien celui de l'ajout - assert_eq!(result.settings.setgroups, Some(SGroups::from("root"))); - println!("Test réussi : Le groupe spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setgid_add_multiple_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::Multiple(vec![ - SGroupType::from(0), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::Multiple(vec![ - SGroupType::from(0), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - .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 le groupe assigné est bien celui de l'ajout - assert_eq!( - result.settings.setgroups, - Some(SGroups::Multiple(vec![ - SGroupType::from(0), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - ); - println!("Test réussi : Le groupe spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setgid_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::from("root")], - sub: vec![SGroups::from("root")], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::from("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 : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_all_sub_single_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::All, - add: vec![], - sub: vec![SGroups::from("root")], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::from("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 : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_all_sub_multiple_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::All, - add: vec![], - sub: vec![SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - .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 : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::All, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::from("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 le groupe assigné est autorisé - assert_eq!(result.settings.setgroups, Some(SGroups::from("root"))); - - println!("Test réussi : Le groupe spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setgid_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()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::from("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 : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_all_add_single_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::All, - add: vec![SGroups::from("root")], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::from("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 le groupe assigné est bien celui de l'ajout - assert_eq!(result.settings.setgroups, Some(SGroups::from("root"))); - - println!("Test réussi : Le groupe spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setgid_all_add_multiple_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::All, - add: vec![SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - .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 le groupe assigné est bien celui de l'ajout - assert_eq!( - result.settings.setgroups, - Some(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - ); - - println!("Test réussi : Le groupe spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setgid_none_add_single_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::None); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::from("root")], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .user(SUserType::from(get_non_root_gid(1).unwrap())) - .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 : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_none_add_multiple_invvalid() { - // 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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::Multiple(vec![ - SGroupType::from(get_non_root_gid(0).unwrap()), - SGroupType::from("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 : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_add_multiple_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::from("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 : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_add_single_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::from("root")], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(0).unwrap()), - ])) - .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 : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_add_diff_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(0).unwrap()), - ])) - .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 : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_add_list_or_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 `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![ - SGroups::from("root"), - SGroups::from(get_non_root_gid(1).unwrap()), - ], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - 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() - .group(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - .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 : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_sgroupschooser_from() { - let sgroup = SGroups::from(get_non_root_gid(0).unwrap()); - let sgroupschooser = SGroupschooser::from(sgroup.clone()); - assert_eq!(sgroupschooser, SGroupschooser::Group(sgroup.clone())); - let chooser_struct = SSetgidSet { - fallback: sgroup.clone(), - default: SetBehavior::None, - add: vec![ - SGroups::from("root"), - SGroups::from(get_non_root_gid(1).unwrap()), - ], - sub: vec![], - }; - let sgroupschooser = SGroupschooser::from(chooser_struct.clone()); - assert_eq!( - sgroupschooser, - SGroupschooser::StructChooser(chooser_struct) - ); - let group = "grp"; - let sgroupschooser = SGroupschooser::from(group); - assert_eq!(sgroupschooser, SGroupschooser::Group(group.into())); - let group = 0; - let sgroupschooser = SGroupschooser::from(group); - assert_eq!(sgroupschooser, SGroupschooser::Group(group.into())); - } -} diff --git a/rar-common/src/database/migration.rs b/rar-common/src/database/migration.rs index 2a00e6db..93f125e8 100644 --- a/rar-common/src/database/migration.rs +++ b/rar-common/src/database/migration.rs @@ -5,7 +5,6 @@ use semver::Version; use crate::PACKAGE_VERSION; - type MigrationFn = fn(&Migration, &mut T) -> Result<(), Box>; pub struct Migration { @@ -38,6 +37,7 @@ impl Migration { to: &Version, ) -> Result> { debug!("Checking migration from {} to {} :", self.from(), self.to()); + #[cfg(not(tarpaulin_include))] debug!( " \tself.from() == *from -> {}\tself.from() == *to -> {} diff --git a/rar-common/src/database/mod.rs b/rar-common/src/database/mod.rs index 58cdeb66..91061751 100644 --- a/rar-common/src/database/mod.rs +++ b/rar-common/src/database/mod.rs @@ -5,16 +5,20 @@ use bon::{builder, Builder}; use chrono::Duration; use linked_hash_set::LinkedHashSet; use options::EnvBehavior; -use serde::{de, Deserialize, Serialize}; +use serde::{de::Deserialize, de::Deserializer, Serialize}; use self::options::EnvKey; +#[cfg(feature = "finder")] +pub mod score; pub mod actor; -#[cfg(feature = "finder")] -pub mod finder; +//#[cfg(feature = "finder")] +//pub mod finder; +pub mod de; pub mod migration; pub mod options; +pub mod ser; pub mod structs; pub mod versionning; @@ -24,9 +28,10 @@ pub struct FilterMatcher { pub role: Option, pub task: Option, pub env_behavior: Option, - #[builder(into)] - pub user: Option, - pub group: Option, + #[builder(with = |s: impl Into| -> Result<_,String> { s.into().fetch_id().ok_or("This user does not exist".into()) })] + pub user: Option, + #[builder(with = |s: impl Into| -> Result<_,String> { s.into().try_into() })] + pub group: Option>, } // deserialize the linked hash set @@ -34,7 +39,7 @@ fn lhs_deserialize_envkey<'de, D>( deserializer: D, ) -> Result>, D::Error> where - D: de::Deserializer<'de>, + D: Deserializer<'de>, { if let Ok(v) = Vec::::deserialize(deserializer) { Ok(Some(v.into_iter().collect())) @@ -62,7 +67,7 @@ where // deserialize the linked hash set fn lhs_deserialize<'de, D>(deserializer: D) -> Result>, D::Error> where - D: de::Deserializer<'de>, + D: Deserializer<'de>, { if let Ok(v) = Vec::::deserialize(deserializer) { Ok(Some(v.into_iter().collect())) @@ -88,7 +93,7 @@ pub fn is_default(t: &T) -> bool { t == &T::default() } -fn serialize_duration(value: &Option, serializer: S) -> Result +pub fn serialize_duration(value: &Option, serializer: S) -> Result where S: serde::Serializer, { @@ -104,19 +109,18 @@ where } } -fn deserialize_duration<'de, D>(deserializer: D) -> Result, D::Error> +pub fn deserialize_duration<'de, D>(deserializer: D) -> Result, D::Error> where - D: de::Deserializer<'de>, + D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; match convert_string_to_duration(&s) { Ok(d) => Ok(d), - Err(e) => Err(de::Error::custom(e)), + Err(e) => Err(serde::de::Error::custom(e)), } } -fn convert_string_to_duration(s: &String) -> Result, Box> -{ +fn convert_string_to_duration(s: &String) -> Result, Box> { let mut parts = s.split(':'); //unwrap or error if let (Some(hours), Some(minutes), Some(seconds)) = (parts.next(), parts.next(), parts.next()) @@ -143,16 +147,14 @@ where mod tests { use super::*; - struct LinkedHashSetTester(LinkedHashSet); + struct LinkedHashSetTester(pub Option>); 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())?, - )) + Ok(Self(lhs_deserialize_envkey(deserializer)?)) } } @@ -161,7 +163,7 @@ mod tests { where S: serde::Serializer, { - lhs_serialize_envkey(&Some(self.0.clone()), serializer) + lhs_serialize_envkey(&self.0, serializer) } } @@ -170,7 +172,7 @@ mod tests { where D: serde::Deserializer<'de>, { - Ok(Self(lhs_deserialize(deserializer).map(|v| v.unwrap())?)) + Ok(Self(lhs_deserialize(deserializer)?)) } } @@ -179,20 +181,18 @@ mod tests { where S: serde::Serializer, { - lhs_serialize(&Some(self.0.clone()), serializer) + lhs_serialize(&self.0, serializer) } } - struct DurationTester(Duration); + struct DurationTester(Option); 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())?, - )) + Ok(Self(deserialize_duration(deserializer)?)) } } @@ -201,7 +201,7 @@ mod tests { where S: serde::Serializer, { - serialize_duration(&Some(self.0.clone()), serializer) + serialize_duration(&self.0, serializer) } } @@ -210,21 +210,11 @@ mod tests { 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"]"#); + let set = deserialized.unwrap().0.unwrap(); + assert_eq!(set.len(), 3); + assert!(set.contains(&EnvKey::from("key1"))); + assert!(set.contains(&EnvKey::from("key2"))); + assert!(set.contains(&EnvKey::from("key3"))); } #[test] @@ -232,26 +222,26 @@ mod tests { 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")); + let set = deserialized.unwrap().0.unwrap(); + assert_eq!(set.len(), 3); + assert!(set.contains("value1")); + assert!(set.contains("value2")); + assert!(set.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 mut set = LinkedHashSetTester(Some(LinkedHashSet::new())); + set.0.as_mut().unwrap().insert("value1".to_string()); + set.0.as_mut().unwrap().insert("value2".to_string()); + set.0.as_mut().unwrap().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 duration = DurationTester(Some(Duration::seconds(3661))); let serialized = serde_json::to_string(&duration).unwrap(); assert_eq!(serialized, r#""01:01:01""#); } @@ -259,10 +249,10 @@ mod tests { #[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); + let deserialized: DurationTester = serde_json::from_str(json).unwrap(); + assert!(deserialized.0.is_some()); + let duration = deserialized.0.unwrap(); + assert_eq!(duration.num_seconds(), 3661); } #[test] @@ -272,4 +262,81 @@ mod tests { assert!(!is_default(&1)); assert!(!is_default(&"non-default".to_string())); } + #[test] + fn test_lhs_serialize_empty() { + let set: LinkedHashSetTester = LinkedHashSetTester(None); + let serialized = serde_json::to_string(&Some(set)).unwrap(); + assert_eq!(serialized, r#"null"#); + let set: LinkedHashSetTester = LinkedHashSetTester(None); + let serialized = serde_json::to_string(&Some(set)).unwrap(); + assert_eq!(serialized, r#"null"#); + let duration = DurationTester(None); + let serialized = serde_json::to_string(&duration).unwrap(); + assert_eq!(serialized, r#"null"#); + } + #[test] + fn test_lhs_deserialize_envkey_null() { + let json = r#"null"#; + let deserialized: Option> = serde_json::from_str(json).unwrap(); + assert!(deserialized.is_none()); + } + + #[test] + fn test_lhs_deserialize_empty_object() { + let json = r#"{}"#; + let deserialized: Result>, _> = + serde_json::from_str(json); + assert!(deserialized.is_err()); + } + + #[test] + fn test_lhs_serialize_empty_set() { + let set = LinkedHashSetTester(Some(LinkedHashSet::::new())); + let serialized = serde_json::to_string(&Some(set)).unwrap(); + assert_eq!(serialized, r#"[]"#); + } + + #[test] + fn test_serialize_duration_large() { + let duration = Some(DurationTester(Some(Duration::seconds(3600 * 25 + 61)))); + let serialized = serde_json::to_string(&duration).unwrap(); + assert_eq!(serialized, r#""25:01:01""#); + } + + #[test] + fn test_deserialize_duration_leading_zeros() { + let json = r#""001:002:003""#; + let deserialized: DurationTester = serde_json::from_str(json).unwrap(); + assert!(deserialized.0.is_some()); + let duration = deserialized.0.unwrap(); + assert_eq!(duration.num_seconds(), 3723); + } + + #[test] + fn test_deserialize_duration_with_spaces() { + let json = r#"" 01:01:01 ""#; + let deserialized: Result = serde_json::from_str(json); + assert!(deserialized.is_err()); + } + + #[test] + fn test_deserialize_duration_non_numeric() { + let json = r#""aa:bb:cc""#; + let deserialized: Result = serde_json::from_str(json); + assert!(deserialized.is_err()); + } + + #[test] + fn test_deserialize_duration_invalid() { + let json = r#""test""#; + let deserialized: Result = serde_json::from_str(json); + assert!(deserialized.is_err()); + } + + #[test] + fn test_lhs_deserialize_envkey_mixed_types() { + let json = r#"["key1", 123, null]"#; + let deserialized: Result, _> = serde_json::from_str(json); + assert!(deserialized.is_err()); + } } diff --git a/rar-common/src/database/options.rs b/rar-common/src/database/options.rs index 672d921c..e021d72e 100644 --- a/rar-common/src/database/options.rs +++ b/rar-common/src/database/options.rs @@ -1,14 +1,10 @@ use std::collections::HashMap; use std::env; -#[cfg(feature = "finder")] -use std::path::PathBuf; use std::{borrow::Borrow, cell::RefCell, rc::Rc}; use bon::{bon, builder, Builder}; use chrono::Duration; -#[cfg(feature = "finder")] -use libc::PATH_MAX; use linked_hash_set::LinkedHashSet; #[cfg(feature = "pcre2")] @@ -18,21 +14,24 @@ use serde_json::{Map, Value}; use strum::{Display, EnumIs, EnumIter, EnumString, FromRepr}; use log::debug; -#[cfg(feature = "finder")] -use log::warn; use crate::rc_refcell; -#[cfg(feature = "finder")] -use super::finder::Cred; -use super::{convert_string_to_duration, deserialize_duration, is_default, serialize_duration, FilterMatcher}; +//#[cfg(feature = "finder")] +//use super::finder::Cred; +//#[cfg(feature = "finder")] +//use super::finder::SecurityMin; +use super::{ + convert_string_to_duration, deserialize_duration, is_default, serialize_duration, FilterMatcher, +}; use super::{ lhs_deserialize, lhs_deserialize_envkey, lhs_serialize, lhs_serialize_envkey, structs::{SConfig, SRole, STask}, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)] +#[repr(u8)] pub enum Level { #[default] None, @@ -52,10 +51,13 @@ pub enum OptType { Timeout, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] +#[repr(u8)] pub enum PathBehavior { Delete, KeepSafe, @@ -64,10 +66,13 @@ pub enum PathBehavior { Inherit, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy, Display, EnumString)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy, Display, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] +#[repr(u8)] pub enum TimestampType { #[default] PPID, @@ -115,16 +120,19 @@ pub struct SPathOptions { )] #[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, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString)] +// ...existing code... +impl SPathOptions {} +// ...existing code... + +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] +#[repr(u8)] pub enum EnvBehavior { Delete, Keep, @@ -159,13 +167,13 @@ pub struct SEnvOptions { 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| { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[builder(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, + pub set: Option>, #[serde( default, skip_serializing_if = "Option::is_none", @@ -195,37 +203,46 @@ pub struct SEnvOptions { pub _extra_fields: Map, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] +#[repr(u8)] pub enum SBounding { Strict, - Ignore, #[default] Inherit, + Ignore, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "kebab-case")] #[derive(Default)] +#[repr(u8)] pub enum SPrivileged { - Privileged, #[default] User, Inherit, + Privileged, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "kebab-case")] #[derive(Default)] +#[repr(u8)] pub enum SAuthentication { - Skip, #[default] Perform, Inherit, + Skip, } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] @@ -264,8 +281,8 @@ impl Opt { #[builder(into)] wildcard_denied: Option, timeout: Option, #[builder(default)] _extra_fields: Map, - ) -> Rc> { - rc_refcell!(Opt { + ) -> Self { + Opt { level, path, env, @@ -275,55 +292,45 @@ impl Opt { wildcard_denied, timeout, _extra_fields, - }) - } - - pub fn raw_new(level: Level) -> Self { - Opt { - level, - ..Default::default() } } - pub fn level_default() -> Rc> { + pub fn level_default() -> Self { Self::builder(Level::Default) .maybe_root(env!("RAR_USER_CONSIDERED").parse().ok()) - .maybe_bounding( - env!("RAR_BOUNDING").parse().ok() - ) - .path( - SPathOptions::builder(env!("RAR_PATH_DEFAULT").parse().unwrap_or(PathBehavior::Delete)) - .add(env!("RAR_PATH_ADD_LIST").split(':').collect::>()) - .sub(env!("RAR_PATH_REMOVE_LIST").split(':').collect::>()) - .build(), - ) - .maybe_authentication( - env!("RAR_AUTHENTICATION").parse().ok() - ) + .maybe_bounding(env!("RAR_BOUNDING").parse().ok()) + .path(SPathOptions::level_default()) + .maybe_authentication(env!("RAR_AUTHENTICATION").parse().ok()) .env( - SEnvOptions::builder(env!("RAR_ENV_DEFAULT").parse().unwrap_or(EnvBehavior::Delete)) - .keep(env!("RAR_ENV_KEEP_LIST").split(',').collect::>()) - .unwrap() - .check(env!("RAR_ENV_CHECK_LIST").split(',').collect::>()) - .unwrap() - .delete( - env!("RAR_ENV_DELETE_LIST") - .split(',') - .collect::>(), - ) - .unwrap() - .set( - serde_json::from_str(env!("RAR_ENV_SET_LIST")) - .unwrap_or_else(|_| Map::default()), - ) - .maybe_override_behavior(env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().ok()) - .build(), + SEnvOptions::builder( + env!("RAR_ENV_DEFAULT") + .parse() + .unwrap_or(EnvBehavior::Delete), + ) + .keep(env!("RAR_ENV_KEEP_LIST").split(',').collect::>()) + .unwrap() + .check(env!("RAR_ENV_CHECK_LIST").split(',').collect::>()) + .unwrap() + .delete( + env!("RAR_ENV_DELETE_LIST") + .split(',') + .collect::>(), + ) + .unwrap() + .set( + serde_json::from_str(env!("RAR_ENV_SET_LIST")) + .unwrap_or_else(|_| Map::default()), + ) + .maybe_override_behavior(env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().ok()) + .build(), ) .timeout( STimeout::builder() .maybe_type_field(env!("RAR_TIMEOUT_TYPE").parse().ok()) .maybe_duration( - convert_string_to_duration(&env!("RAR_TIMEOUT_DURATION").to_string()).ok().flatten(), + convert_string_to_duration(&env!("RAR_TIMEOUT_DURATION").to_string()) + .ok() + .flatten(), ) .build(), ) @@ -348,28 +355,33 @@ impl Default for Opt { } } -impl Default for OptStack { - fn default() -> Self { - OptStack { - stack: [None, Some(Opt::level_default()), None, None, None], - roles: None, - role: None, - task: None, - } - } -} - impl Default for SPathOptions { fn default() -> Self { SPathOptions { default_behavior: PathBehavior::Inherit, add: None, sub: None, - _extra_fields: Map::default(), } } } +impl SPathOptions { + pub fn level_default() -> Self { + SPathOptions::builder( + env!("RAR_PATH_DEFAULT") + .parse() + .unwrap_or(PathBehavior::Delete), + ) + .add(env!("RAR_PATH_ADD_LIST").split(':').collect::>()) + .sub( + env!("RAR_PATH_REMOVE_LIST") + .split(':') + .collect::>(), + ) + .build() + } +} + fn is_valid_env_name(s: &str) -> bool { let mut chars = s.chars(); @@ -388,12 +400,12 @@ fn is_valid_env_name(s: &str) -> bool { #[cfg(feature = "pcre2")] fn is_regex(s: &str) -> bool { - Regex::new(s).is_ok() + Regex::new(&format!("^{}$", s)).is_ok() } #[cfg(not(feature = "pcre2"))] fn is_regex(_s: &str) -> bool { - true // Always return true if regex feature is disabled + false // Always return true if regex feature is disabled } impl EnvKey { @@ -452,28 +464,10 @@ impl<'de> Deserialize<'de> for EnvKey { } } -impl SEnvOptions { - pub fn new(behavior: EnvBehavior) -> Self { - SEnvOptions { - default_behavior: behavior, - ..Default::default() - } - } -} - trait EnvSet { fn env_matches(&self, wildcarded: &EnvKey) -> bool; } -impl EnvSet for HashMap { - fn env_matches(&self, wildcarded: &EnvKey) -> bool { - match wildcarded.env_type { - EnvKeyType::Normal => self.contains_key(&wildcarded.value), - EnvKeyType::Wildcarded => self.keys().any(|s| check_wildcarded(wildcarded, s)), - } - } -} - impl EnvSet for LinkedHashSet { fn env_matches(&self, needle: &EnvKey) -> bool { self.iter().any(|s| match s.env_type { @@ -502,64 +496,6 @@ fn check_wildcarded(_wildcarded: &EnvKey, _s: &String) -> bool { true } -#[cfg(feature = "finder")] -fn tz_is_safe(tzval: &str) -> bool { - // tzcode treats a value beginning with a ':' as a path. - let tzval = if let Some(val) = tzval.strip_prefix(':') { - val - } else { - tzval - }; - - // Reject fully-qualified TZ that doesn't begin with the zoneinfo dir. - if tzval.starts_with('/') { - return false; - } - - // Make sure TZ only contains printable non-space characters - // and does not contain a '..' path element. - let mut lastch = '/'; - for cp in tzval.chars() { - if cp.is_ascii_whitespace() || !cp.is_ascii_graphic() { - return false; - } - if lastch == '/' - && cp == '.' - && tzval - .chars() - .nth(tzval.chars().position(|c| c == '.').unwrap() + 1) - == Some('.') - && (tzval - .chars() - .nth(tzval.chars().position(|c| c == '.').unwrap() + 2) - == Some('/') - || tzval - .chars() - .nth(tzval.chars().position(|c| c == '.').unwrap() + 2) - .is_none()) - { - return false; - } - lastch = cp; - } - - // Reject extra long TZ values (even if not a path). - if tzval.len() >= >::try_into(PATH_MAX).unwrap() { - return false; - } - - true -} - -#[cfg(feature = "finder")] -fn check_env(key: &str, value: &str) -> bool { - debug!("Checking env: {}={}", key, value); - match key { - "TZ" => tz_is_safe(value), - _ => !value.chars().any(|c| c == '/' || c == '%'), - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OptStack { pub(crate) stack: [Option>>; 5], @@ -633,9 +569,7 @@ impl OptStackBuilder { } fn with_default(self) -> Self { - self.opt(Some( - Opt::level_default() - )) + self.opt(Some(rc_refcell!(Opt::level_default()))) } } @@ -686,44 +620,6 @@ impl OptStack { } } - #[cfg(feature = "finder")] - fn calculate_path(&self) -> String { - let path = self.get_final_path(); - let default = LinkedHashSet::new(); - 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.insert(0, ':'); - } - 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) -> SPathOptions { let mut final_behavior = PathBehavior::Delete; let default = LinkedHashSet::new(); @@ -793,156 +689,6 @@ impl OptStack { .build() } - #[allow(dead_code)] - #[cfg(not(tarpaulin_include))] - 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()); - self.iter_in_options(|opt| { - let final_add_clone = Rc::clone(&final_add); - let final_sub_clone = Rc::clone(&final_sub); - if let Some(p) = opt.path.borrow().as_ref() { - match p.default_behavior { - PathBehavior::Delete => { - let union = final_add_clone - .as_ref() - .borrow() - .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 - final_add_clone.as_ref().replace(union); - debug!("delete final_add: {:?}", final_add_clone.as_ref().borrow()); - } - PathBehavior::KeepSafe | PathBehavior::KeepUnsafe => { - let union = final_sub_clone - .as_ref() - .borrow() - .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 - final_sub_clone.as_ref().replace(union); - } - PathBehavior::Inherit => { - if final_behavior.is_delete() { - let union: LinkedHashSet = final_add_clone - .as_ref() - .borrow() - .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); - debug!("inherit final_add: {:?}", final_add_clone.as_ref().borrow()); - } else { - let union: LinkedHashSet = final_sub_clone - .as_ref() - .borrow() - .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); - } - } - } - if !p.default_behavior.is_inherit() { - final_behavior = p.default_behavior; - } - } - }); - 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 env = self.get_final_env(opt_filter); - 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 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 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)) - } else { - debug!("Dropping env: {}", key.value); - None - } - }) - .collect()), - EnvBehavior::Keep => Ok(final_env - .filter_map(|(key, value)| { - let key = EnvKey::new(key).expect("Unexpected environment variable"); - 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)) - } else { - debug!("Dropping env: {}", key.value); - None - } - }) - .collect()), - }?; - final_env.insert("PATH".into(), self.calculate_path()); - final_env.insert("LOGNAME".into(), target.user.name.clone()); - final_env.insert("USER".into(), target.user.name); - final_env.insert("HOME".into(), target.user.dir.to_string_lossy().to_string()); - final_env - .entry("TERM".into()) - .or_insert_with(|| "unknown".into()); - final_env.insert( - "SHELL".into(), - target.user.shell.to_string_lossy().to_string(), - ); - final_env.extend(env.set); - Ok(final_env) - } - fn get_final_env(&self, cmd_filter: Option) -> SEnvOptions { let mut final_behavior = EnvBehavior::default(); let mut final_set = HashMap::new(); @@ -961,9 +707,9 @@ impl OptStack { .unwrap_or(&LinkedHashSet::new()) .iter() .filter(|e| { - !p.set.env_matches(e) - || !p.check.env_matches(e) - || !p.delete.env_matches(e) + //p.set.as_ref().is_some_and(|set| !set.env_matches(e)) || + + !p.check.env_matches(e) || !p.delete.env_matches(e) }) .cloned() .collect(); @@ -972,7 +718,11 @@ impl OptStack { .as_ref() .unwrap_or(&LinkedHashSet::new()) .iter() - .filter(|e| !p.set.env_matches(e) || !p.delete.env_matches(e)) + .filter(|e| { + //p.set.as_ref().is_some_and(|set| !set.env_matches(e)) + //|| + !p.delete.env_matches(e) + }) .cloned() .collect(); final_delete = p @@ -980,10 +730,15 @@ impl OptStack { .as_ref() .unwrap_or(&LinkedHashSet::new()) .iter() - .filter(|e| !p.set.env_matches(e) || !p.check.env_matches(e)) + .filter(|e| { + //p.set.as_ref().is_some_and(|set| !set.env_matches(e)) || + !p.check.env_matches(e) + }) .cloned() .collect(); - final_set = p.set.clone(); + if let Some(set) = &p.set { + final_set = set.clone(); + } debug!("check: {:?}", final_check); p.default_behavior } @@ -1000,7 +755,9 @@ impl OptStack { .union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new())) .cloned() .collect(); - final_set.extend(p.set.clone()); + if let Some(set) = &p.set { + final_set.extend(set.clone()); + } debug!("check: {:?}", final_check); final_behavior } @@ -1018,130 +775,6 @@ impl OptStack { .build() } - #[allow(dead_code)] - #[cfg(not(tarpaulin_include))] - fn union_all_env( - &self, - ) -> ( - EnvBehavior, - LinkedHashSet, - LinkedHashSet, - LinkedHashSet, - ) { - let mut final_behavior = EnvBehavior::default(); - let mut final_keep = LinkedHashSet::new(); - let mut final_check = LinkedHashSet::new(); - let mut final_delete = LinkedHashSet::new(); - self.iter_in_options(|opt| { - if let Some(p) = opt.env.borrow().as_ref() { - final_behavior = match p.default_behavior { - EnvBehavior::Delete => { - // policy is to delete, so we add whitelist and remove blacklist - final_keep = final_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.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.delete.env_matches(e)) - .cloned() - .collect(); - p.default_behavior - } - EnvBehavior::Keep => { - //policy is to keep, so we remove blacklist and add whitelist - final_delete = final_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.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.keep.env_matches(e)) - .cloned() - .collect(); - p.default_behavior - } - EnvBehavior::Inherit => { - if final_behavior.is_delete() { - final_keep = final_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.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.delete.env_matches(e)) - .cloned() - .collect(); - } else { - final_delete = final_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.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.keep.env_matches(e)) - .cloned() - .collect(); - } - final_behavior - } - }; - } - }); - (final_behavior, final_keep, final_check, final_delete) - } - pub fn get_root_behavior(&self) -> (Level, SPrivileged) { - self.find_in_options(|opt| { - if let Some(p) = &opt.borrow().root { - return Some((opt.level, *p)); - } - None - }) - .unwrap_or((Level::None, SPrivileged::default())) - } - pub fn get_bounding(&self) -> (Level, SBounding) { - self.find_in_options(|opt| { - if let Some(p) = &opt.borrow().bounding { - return Some((opt.level, *p)); - } - None - }) - .unwrap_or((Level::None, SBounding::default())) - } - pub fn get_authentication(&self) -> (Level, SAuthentication) { - self.find_in_options(|opt| { - if let Some(p) = &opt.borrow().authentication { - return Some((opt.level, *p)); - } - None - }) - .unwrap_or((Level::None, SAuthentication::default())) - } - - pub fn get_wildcard(&self) -> (Level, String) { - self.find_in_options(|opt| { - if let Some(p) = opt.borrow().wildcard_denied.borrow().as_ref() { - return Some((opt.level, p.clone())); - } - None - }) - .unwrap_or((Level::None, "".to_owned())) - } - - pub fn get_timeout(&self) -> (Level, STimeout) { - self.find_in_options(|opt| { - if let Some(p) = &opt.borrow().timeout { - return Some((opt.level, p.clone())); - } - None - }) - .unwrap_or((Level::None, STimeout::default())) - } - fn get_level(&self) -> Level { let (level, _) = self .find_in_options(|opt| Some((opt.level, ()))) @@ -1150,7 +783,7 @@ impl OptStack { } pub fn to_opt(&self) -> Rc> { - Opt::builder(self.get_level()) + rc_refcell!(Opt::builder(self.get_level()) .path(self.get_final_path()) .env(self.get_final_env(None)) .maybe_root( @@ -1181,76 +814,13 @@ impl OptStack { 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 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() - .unwrap_or(&default) - .symmetric_difference(other_path.add.as_ref().unwrap_or(&default)) - .count() - == 0 - && path - .sub - .as_ref() - .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 - && self.get_bounding().1 == other.get_bounding().1 - && self.get_wildcard().1 == other.get_wildcard().1 - && self.get_authentication().1 == other.get_authentication().1 - && self.get_timeout().1 == other.get_timeout().1; - debug!( - "final_behavior == other_path.behavior : {} - && add {:?} - other_add {:?} == 0 : {} - && sub - other_sub == 0 : {} - && self.get_root_behavior().1 == other.get_root_behavior().1 : {} - && self.get_bounding().1 == other.get_bounding().1 : {} - && self.get_wildcard().1 == other.get_wildcard().1 : {} - && self.get_authentication().1 == other.get_authentication().1 : {} - && self.get_timeout().1 == other.get_timeout().1 : {}", - path.default_behavior == other_path.default_behavior, - path.add, - other_path.add, - path.add - .as_ref() - .unwrap_or(&default) - .symmetric_difference(other_path.add.as_ref().unwrap_or(&default)) - .count() - == 0, - path.sub - .as_ref() - .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, - self.get_bounding().1 == other.get_bounding().1, - self.get_wildcard().1 == other.get_wildcard().1, - self.get_authentication().1 == other.get_authentication().1, - self.get_timeout().1 == other.get_timeout().1 - ); - debug!("OPT check: {}", res); - res + .build()) } } #[cfg(test)] mod tests { - use nix::unistd::Pid; - use super::super::options::*; use super::super::structs::*; @@ -1318,96 +888,8 @@ mod tests { ); } - #[cfg(feature = "finder")] #[test] - fn test_get_path() { - 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 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 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() { + fn test_env_global_to_task() { let config = SConfig::builder() .role( SRole::builder("test") @@ -1573,6 +1055,19 @@ mod tests { .clone(), vec![EnvKey::from("env2")] )); + assert_eq!( + global_options + .env + .as_ref() + .unwrap() + .keep + .as_ref() + .unwrap_or(&LinkedHashSet::new()) + .iter() + .map(|e| e.clone().into()) + .collect::>(), + vec!["env2".to_string()] + ); assert_eq!(global_options.root.unwrap(), SPrivileged::Privileged); assert_eq!(global_options.bounding.unwrap(), SBounding::Ignore); assert_eq!( @@ -1682,166 +1177,24 @@ mod tests { } #[test] - fn test_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()); - } - - #[test] - fn test_get_root_behavior() { - 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 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 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")); - assert!(!tz_is_safe("/America/New_York")); - assert!(!tz_is_safe("America/New_York/..")); - //assert path max - assert!(!tz_is_safe( - String::from_utf8(vec![b'a'; (PATH_MAX + 1).try_into().unwrap()]) - .unwrap() - .as_str() - )); - } - - #[cfg(feature = "finder")] - #[test] - fn test_check_env() { - 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::builder() - .user_id(0) - .group_id(0) - .ppid(Pid::from_raw(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"); + 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_.*")); } - #[cfg(feature = "finder")] #[test] - fn test_override_env() { + fn test_get_final_env_set_inherit() { let config = SConfig::builder() .role( SRole::builder("test") .task( - STask::builder(IdTask::Number(1)) + STask::builder(1) .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Inherit) - .keep(["env1"]) - .unwrap() + .set([("env1", "value3")]) .build(), ) .build() @@ -1851,8 +1204,7 @@ mod tests { .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Inherit) - .check(["env2"]) - .unwrap() + .set([("env2", "value2")]) .build(), ) .build() @@ -1862,147 +1214,40 @@ mod tests { .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Delete) - .check(["env3"]) - .unwrap() - .set([("env4".to_string(), "value4".to_string())]) + .set([("env1", "value1")]) .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(); - - 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()); - std::env::set_var("PATH", "/sys:./proc:/tmp:/bin"); - 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); + let stack = OptStack::from_task(config.task("test", 1).unwrap()); + let opt = stack.to_opt(); + let options = opt.as_ref().borrow(); + assert_eq!( + options + .env + .as_ref() + .unwrap() + .set + .as_ref() + .unwrap_or(&HashMap::new()) + .get("env1") + .unwrap(), + "value3" + ); } #[test] - fn test_inherit_keep_path() { - let path = std::env::var("PATH").unwrap(); + fn test_get_final_path_inherit() { let config = SConfig::builder() .role( SRole::builder("test") .task( - STask::builder(IdTask::Number(1)) + STask::builder(1) .options(|opt| { opt.path( SPathOptions::builder(PathBehavior::Inherit) - .add(Vec::::new()) - .sub(["/sys"]) + .sub(["/path3"]) .build(), ) .build() @@ -2011,47 +1256,8 @@ mod tests { ) .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() + SPathOptions::builder(PathBehavior::Inherit) + .sub(["/path2"]) .build(), ) .build() @@ -2059,78 +1265,68 @@ mod tests { .build(), ) .options(|opt| { - opt.env( - SEnvOptions::builder(EnvBehavior::Keep) - .delete(["env3"]) - .unwrap() + opt.path( + SPathOptions::builder(PathBehavior::KeepSafe) + .sub(["/path1"]) .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"); + let stack = OptStack::from_task(config.task("test", 1).unwrap()); + let opt = stack.to_opt(); + let options = opt.as_ref().borrow(); + assert!(options + .path + .as_ref() + .unwrap() + .sub + .as_ref() + .unwrap() + .contains("/path1")); + assert!(options + .path + .as_ref() + .unwrap() + .sub + .as_ref() + .unwrap() + .contains("/path2")); + assert!(options + .path + .as_ref() + .unwrap() + .sub + .as_ref() + .unwrap() + .contains("/path3")); } #[test] - fn test_opt_filter_env() { + fn test_find_in_options_none() { 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(), - ) + .task(STask::builder(1).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(), + let stack = OptStack::from_task(config.task("test", 1).unwrap()); + let res: Option<(Level, SPathOptions)> = stack.find_in_options(|_| None); + assert_eq!(res, None); + } + + #[test] + fn test_invalid_envkey() { + let invalid_env = "3TE(ST_a"; + let env_key = EnvKey::new(invalid_env.to_string()); + assert!(env_key.is_err()); + assert_eq!( + env_key.unwrap_err(), + format!( + "env key {}, must be a valid env, or a valid regex", + invalid_env ) - .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/score.rs b/rar-common/src/database/score.rs new file mode 100644 index 00000000..aa02b64b --- /dev/null +++ b/rar-common/src/database/score.rs @@ -0,0 +1,671 @@ +use std::cmp::Ordering; + +use bon::Builder; +use strum::EnumIs; + +use super::actor::{DGroupType, DGroups, DUserType, SGroupType, SGroups, SUserType}; + +#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, EnumIs, Default)] +#[repr(u32)] +// Matching user groups for the role +pub enum ActorMatchMin { + UserMatch, + GroupMatch(usize), + #[default] + NoMatch, +} + +impl ActorMatchMin { + pub fn better(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Less + } + pub fn matching(&self) -> bool { + *self != ActorMatchMin::NoMatch + } +} + +#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] + +// Matching setuid and setgid for the role +pub struct SetuidMin { + is_root: bool, +} + +impl From for SetuidMin { + fn from(s: SUserType) -> Self { + SetuidMin { + is_root: user_is_root(&s), + } + } +} + +impl From<&DUserType<'_>> for SetuidMin { + fn from(s: &DUserType) -> Self { + SetuidMin { + is_root: duser_is_root(s), + } + } +} + +impl From for SetuidMin { + fn from(s: u32) -> Self { + SetuidMin { is_root: s == 0 } + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub struct SetgidMin { + is_root: bool, + nb_groups: usize, +} + +impl From for SetgidMin { + fn from(s: SGroups) -> Self { + SetgidMin { + is_root: groups_contains_root(Some(&s)), + nb_groups: groups_len(Some(&s)), + } + } +} + +impl From<&DGroups<'_>> for SetgidMin { + fn from(s: &DGroups<'_>) -> Self { + SetgidMin { + is_root: dgroups_contains_root(Some(s)), + nb_groups: dgroups_len(Some(&s)), + } + } +} + +impl From<&DGroupType<'_>> for SetgidMin { + fn from(s: &DGroupType<'_>) -> Self { + SetgidMin { + is_root: dgroup_is_root(&s), + nb_groups: 1, + } + } +} + +impl From<&Vec> for SetgidMin { + fn from(s: &Vec) -> Self { + SetgidMin { + is_root: s.iter().any(|id| *id == 0), + nb_groups: s.len(), + } + } +} + +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 { + pub uid: Option, + pub 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, Default)] +pub struct CmdMin(u32); + +bitflags::bitflags! { + + impl CmdMin: u32 { + const Match = 0b00001; + const WildcardPath = 0b00010; + const RegexArgs = 0b00100; + const FullRegexArgs = 0b01000; + const FullWildcardPath = 0b10000; + } +} + +impl CmdMin { + pub fn better(&self, other: &Self) -> bool { + (self.matching() && !other.matching()) + || (self.matching() && self.cmp(other) == Ordering::Less) + } + pub fn matching(&self) -> bool { + !self.is_empty() + } +} + +#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, Default)] +pub enum CapsMin { + #[default] + Undefined, + NoCaps, + CapsNoAdmin(usize), + CapsAdmin(usize), + CapsAll, +} + +#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, Default)] +pub struct SecurityMin(u32); + +bitflags::bitflags! { + + impl SecurityMin: u32 { + const DisableBounding = 0b000001; + const EnableRoot = 0b000010; + const KeepEnv = 0b000100; + const KeepPath = 0b001000; + const KeepUnsafePath = 0b010000; + const SkipAuth = 0b100000; + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, Default, Builder)] +pub struct TaskScore { + #[builder(default)] + pub cmd_min: CmdMin, + #[builder(default)] + pub caps_min: CapsMin, + #[builder(default)] + pub setuser_min: SetUserMin, +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, Default, Builder)] +pub struct Score { + pub user_min: ActorMatchMin, + pub cmd_min: CmdMin, + pub caps_min: CapsMin, + pub setuser_min: SetUserMin, + pub security_min: SecurityMin, +} + +impl Score { + pub fn set_cmd_score(&mut self, cmd_min: CmdMin) { + self.cmd_min = cmd_min; + } + pub fn set_task_score(&mut self, task_score: &TaskScore) { + self.cmd_min = task_score.cmd_min; + self.caps_min = task_score.caps_min; + self.setuser_min = task_score.setuser_min; + } + pub fn set_role_score(&mut self, role_score: &ActorMatchMin) { + self.user_min = *role_score; + } + pub fn prettyprint(&self) -> String { + format!( + "{:?}, {:?}, {:?}, {:?}, {:?}", + self.user_min, self.cmd_min, self.caps_min, self.setuser_min, self.security_min + ) + } + + pub fn user_cmp(&self, other: &Score) -> Ordering { + self.user_min.cmp(&other.user_min) + } + + /// Compare the score of tasks results + pub fn cmd_cmp(&self, other: &Score) -> Ordering { + self.cmd_min + .cmp(&other.cmd_min) + .then(self.caps_min.cmp(&other.caps_min)) + .then(self.setuser_min.cmp(&other.setuser_min)) + .then(self.security_min.cmp(&other.security_min)) + } + + pub fn user_matching(&self) -> bool { + self.user_min != ActorMatchMin::NoMatch + } + + pub fn command_matching(&self) -> bool { + !self.cmd_min.is_empty() + } + + pub fn fully_matching(&self) -> bool { + self.user_matching() && self.command_matching() + } + + /// Return true if the score is better than the other + pub fn better_command(&self, other: &Score) -> bool { + (self.command_matching() && !other.command_matching()) + || (self.command_matching() && self.cmd_cmp(other) == Ordering::Less) + } + + pub fn better_user(&self, other: &Score) -> bool { + (self.user_matching() && !other.user_matching()) + || (self.user_matching() && self.user_cmp(other) == Ordering::Less) + } + + pub fn better_fully(&self, other: &Score) -> bool { + (self.fully_matching() && !other.fully_matching()) + || (self.fully_matching() && self.cmp(other) == Ordering::Less) + } +} + +impl PartialOrd for Score { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Score { + fn cmp(&self, other: &Self) -> Ordering { + self.cmd_cmp(other).then(self.user_cmp(other)) + } + + fn max(self, other: Self) -> Self { + std::cmp::max_by(self, other, Ord::cmp) + } + + fn min(self, other: Self) -> Self { + std::cmp::min_by(self, other, Ord::cmp) + } + + fn clamp(self, min: Self, max: Self) -> Self { + self.max(min).min(max) + } +} + +fn group_is_root(actortype: &SGroupType) -> bool { + (*actortype).fetch_id().map_or(false, |id| id == 0) +} +fn dgroup_is_root(actortype: &DGroupType<'_>) -> 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 duser_is_root(actortype: &DUserType<'_>) -> 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) => group_is_root(group), + SGroups::Multiple(groups) => groups.iter().any(group_is_root), + } + } else { + false + } +} + +fn dgroups_contains_root(list: Option<&DGroups<'_>>) -> bool { + if let Some(list) = list { + match list { + DGroups::Single(group) => dgroup_is_root(group), + DGroups::Multiple(groups) => groups.iter().any(dgroup_is_root), + } + } else { + false + } +} + +fn groups_len(groups: Option<&SGroups>) -> usize { + match groups { + Some(groups) => groups.len(), + None => 0, + } +} + +fn dgroups_len(groups: Option<&DGroups<'_>>) -> usize { + match groups { + Some(groups) => groups.len(), + None => 0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::database::actor::{DGroupType, DGroups, DUserType, SGroupType, SGroups, SUserType}; + use std::borrow::Cow; + + #[test] + fn test_group_is_root() { + let root_group = SGroupType::from(0); + let non_root_group = SGroupType::from(1); + assert!(group_is_root(&root_group)); + assert!(!group_is_root(&non_root_group)); + } + + #[test] + fn test_dgroup_is_root() { + let root_group = DGroupType::from(0); + let non_root_group = DGroupType::from(1); + assert!(dgroup_is_root(&root_group)); + assert!(!dgroup_is_root(&non_root_group)); + } + + #[test] + fn test_user_is_root() { + let root_user = SUserType::from(0); + let non_root_user = SUserType::from(1); + assert!(user_is_root(&root_user)); + assert!(!user_is_root(&non_root_user)); + } + + #[test] + fn test_duser_is_root() { + let root_user = DUserType::from(0); + let non_root_user = DUserType::from(1); + assert!(duser_is_root(&root_user)); + assert!(!duser_is_root(&non_root_user)); + } + + #[test] + fn test_groups_contains_root() { + let root_group = SGroupType::from(0); + let non_root_group = SGroupType::from(1); + let single = SGroups::Single(root_group.clone()); + let multiple = SGroups::from(vec![non_root_group.clone(), root_group.clone()]); + let none = None; + assert!(groups_contains_root(Some(&single))); + assert!(groups_contains_root(Some(&multiple))); + assert!(!groups_contains_root(Some(&SGroups::Single( + non_root_group + )))); + assert!(!groups_contains_root(none)); + } + + #[test] + fn test_dgroups_contains_root() { + let root_group = DGroupType::from(0); + let non_root_group = DGroupType::from(1); + let single = DGroups::Single(root_group.clone()); + let multiple = + DGroups::Multiple(Cow::Owned(vec![non_root_group.clone(), root_group.clone()])); + let none = None; + assert!(dgroups_contains_root(Some(&single))); + assert!(dgroups_contains_root(Some(&multiple))); + assert!(!dgroups_contains_root(Some(&DGroups::Single( + non_root_group + )))); + assert!(!dgroups_contains_root(none)); + } + + #[test] + fn test_groups_len() { + let group1 = SGroupType::from(0); + let single = SGroups::Single(group1); + let multiple = SGroups::from(vec![SGroupType::from(0), SGroupType::from(1)]); + assert_eq!(groups_len(Some(&single)), 1); + assert_eq!(groups_len(Some(&multiple)), 2); + assert_eq!(groups_len(None), 0); + } + + #[test] + fn test_dgroups_len() { + let group1 = DGroupType::from(0); + let single = DGroups::Single(group1); + let multiple = + DGroups::Multiple(Cow::Owned(vec![DGroupType::from(0), DGroupType::from(1)])); + assert_eq!(dgroups_len(Some(&single)), 1); + assert_eq!(dgroups_len(Some(&multiple)), 2); + assert_eq!(dgroups_len(None), 0); + } + + #[test] + fn test_setgidmin_from_sgroups() { + let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from(1)]); + let setgid = SetgidMin::from(groups); + assert!(setgid.is_root); + assert_eq!(setgid.nb_groups, 2); + } + + #[test] + fn test_setgidmin_from_dgroups() { + let groups = DGroups::from(vec![DGroupType::from(1), DGroupType::from(2)]); + let setgid = SetgidMin::from(&groups); + assert!(!setgid.is_root); + assert_eq!(setgid.nb_groups, 2); + } + + #[test] + fn test_setgidmin_from_vec_u32() { + let groups = vec![0, 1, 2]; + let setgid = SetgidMin::from(&groups); + assert!(setgid.is_root); + assert_eq!(setgid.nb_groups, 3); + } + + #[test] + fn test_setgidmin_from_dgrouptype() { + let group = DGroupType::from(0); + let setgid = SetgidMin::from(&group); + assert!(setgid.is_root); + assert_eq!(setgid.nb_groups, 1); + } + + #[test] + fn test_setuidmin_from_susertype() { + let user = SUserType::from(0); + let setuid = SetuidMin::from(user); + assert!(setuid.is_root); + } + + #[test] + fn test_setuidmin_from_dusertype() { + let user = DUserType::from(1); + let setuid = SetuidMin::from(&user); + assert!(!setuid.is_root); + } + + #[test] + fn test_setuidmin_from_u32() { + let setuid = SetuidMin::from(0); + assert!(setuid.is_root); + let setuid = SetuidMin::from(1); + assert!(!setuid.is_root); + } + + #[test] + fn test_score_ordering() { + let mut score1 = Score::default(); + let mut score2 = Score::default(); + score1.cmd_min = CmdMin::from_bits_truncate(0b00001); + score2.cmd_min = CmdMin::from_bits_truncate(0b00010); + assert!(score1 < score2 || score1 == score2 || score1 > score2); + } + + #[test] + fn test_score_prettyprint() { + let score = Score::default(); + let s = score.prettyprint(); + assert!(s.contains("NoMatch")); + } + + #[test] + fn test_cmdmin_better_and_matching() { + let a = CmdMin::from_bits_truncate(0b00001); + let b = CmdMin::from_bits_truncate(0b00000); + assert!(a.matching()); + assert!(!b.matching()); + assert!(!b.better(&a)); + assert!(a.better(&b)); + } + + #[test] + fn test_score_better_methods() { + let mut score1 = Score::default(); + let mut score2 = Score::default(); + score1.cmd_min = CmdMin::from_bits_truncate(0b00001); + score2.cmd_min = CmdMin::from_bits_truncate(0b00000); + assert!(score1.better_command(&score2)); + assert!(!score2.better_command(&score1)); + } + + #[test] + fn test_setuser_min_ordering() { + let setuser1 = SetUserMin { + uid: Some(SetuidMin::from(0)), + gid: Some(SetgidMin::from(&vec![0])), + }; + let setuser2 = SetUserMin { + uid: Some(SetuidMin::from(1)), + gid: Some(SetgidMin::from(&vec![1])), + }; + assert!(setuser1 > setuser2); + } + + #[test] + fn test_setgidmin_ordering() { + let setgid1 = SetgidMin { + is_root: true, + nb_groups: 2, + }; + let setgid2 = SetgidMin { + is_root: false, + nb_groups: 3, + }; + assert!(setgid1 > setgid2); + assert!(setgid2 < setgid1); + assert!(setgid1 != setgid2); + let setgid2 = SetgidMin { + is_root: true, + nb_groups: 3, + }; + assert!(setgid1 < setgid2); + assert!(setgid2 > setgid1); + assert!(setgid1 != setgid2); + } + + #[test] + fn test_actor_match_min() { + let setuser = ActorMatchMin::UserMatch; + assert!(setuser.matching()); + let setuser_other = ActorMatchMin::NoMatch; + assert!(!setuser_other.matching()); + assert!(setuser.better(&setuser_other)); + } + + #[test] + fn test_security_min() { + let security = SecurityMin::empty(); + assert!(security.is_empty()); + let security_other = SecurityMin::DisableBounding; + assert!(!security_other.is_empty()); + assert!(security < security_other); + assert!(security_other > security); + assert!(security_other != security); + let security = SecurityMin::EnableRoot; + assert!(security > security_other); + assert!(security_other < security); + assert!(security_other != security); + let security_other = SecurityMin::KeepEnv; + assert!(security_other > security); + assert!(security < security_other); + assert!(security_other != security); + let security = SecurityMin::KeepPath; + assert!(security > security_other); + assert!(security_other < security); + assert!(security_other != security); + let security_other = SecurityMin::KeepUnsafePath; + assert!(security_other > security); + assert!(security < security_other); + assert!(security_other != security); + let security = SecurityMin::SkipAuth; + assert!(security > security_other); + assert!(security_other < security); + assert!(security_other != security); + let security_other = SecurityMin::empty(); + assert!(security > security_other); + assert!(security_other < security); + } + #[test] + fn test_set_score() { + let mut score = Score::default(); + let task_score = TaskScore { + cmd_min: CmdMin::from_bits_truncate(0b00001), + caps_min: CapsMin::NoCaps, + setuser_min: SetUserMin::default(), + }; + score.set_task_score(&task_score); + assert_eq!(score.cmd_min, CmdMin::from_bits_truncate(0b00001)); + assert_eq!(score.caps_min, CapsMin::NoCaps); + assert_eq!(score.setuser_min, SetUserMin::default()); + let role_score = ActorMatchMin::UserMatch; + score.set_role_score(&role_score); + assert_eq!(score.user_min, ActorMatchMin::UserMatch); + assert_eq!(score.cmd_min, CmdMin::from_bits_truncate(0b00001)); + assert_eq!(score.caps_min, CapsMin::NoCaps); + assert_eq!(score.setuser_min, SetUserMin::default()); + assert_eq!(score.security_min, SecurityMin::empty()); + score.set_cmd_score(CmdMin::from_bits_truncate(0b00010)); + assert_eq!(score.cmd_min, CmdMin::from_bits_truncate(0b00010)); + assert_eq!(score.caps_min, CapsMin::NoCaps); + assert_eq!(score.setuser_min, SetUserMin::default()); + assert_eq!(score.user_min, ActorMatchMin::UserMatch); + assert_eq!(score.security_min, SecurityMin::empty()); + } + + #[test] + fn test_score_matching() { + let mut score = Score::default(); + assert!(!score.user_matching()); + assert!(!score.command_matching()); + assert!(!score.fully_matching()); + score.user_min = ActorMatchMin::UserMatch; + assert!(score.user_matching()); + assert!(!score.command_matching()); + assert!(!score.fully_matching()); + score.cmd_min = CmdMin::from_bits_truncate(0b00001); + assert!(score.user_matching()); + assert!(score.command_matching()); + assert!(score.fully_matching()); + score.user_min = ActorMatchMin::NoMatch; + assert!(!score.user_matching()); + assert!(score.command_matching()); + assert!(!score.fully_matching()); + } + + #[test] + fn test_score_better() { + let mut score1 = Score::default(); + let mut score2 = Score::default(); + score1.cmd_min = CmdMin::from_bits_truncate(0b00001); + score2.cmd_min = CmdMin::from_bits_truncate(0b00010); + assert!(!score2.better_command(&score1)); + assert!(score1.better_command(&score2)); + assert!(!score1.better_user(&score2)); + assert!(!score2.better_user(&score1)); + assert!(!score1.better_fully(&score2)); + assert!(!score2.better_fully(&score1)); + score1.user_min = ActorMatchMin::UserMatch; + score2.user_min = ActorMatchMin::GroupMatch(1); + assert!(score1.better_user(&score2)); + assert!(!score2.better_user(&score1)); + assert!(score1.better_fully(&score2)); + assert!(!score2.better_fully(&score1)); + } + + #[test] + fn test_score_max_min_clamp() { + let mut score1 = Score::default(); + let mut score2 = Score::default(); + score1.cmd_min = CmdMin::from_bits_truncate(0b00001); + score2.cmd_min = CmdMin::from_bits_truncate(0b00010); + assert_eq!(score1.max(score2), score2); + assert_eq!(score2.max(score1), score2); + assert_eq!(score1.min(score2), score1); + assert_eq!(score2.min(score1), score1); + let mut score3 = Score::default(); + score3.cmd_min = CmdMin::from_bits_truncate(0b00011); + assert_eq!(score1.clamp(score2, score3), score2); + assert_eq!(score2.clamp(score1, score3), score2); + } +} diff --git a/rar-common/src/database/ser.rs b/rar-common/src/database/ser.rs new file mode 100644 index 00000000..ddb89e4d --- /dev/null +++ b/rar-common/src/database/ser.rs @@ -0,0 +1,539 @@ +use serde::{ + ser::{SerializeMap, SerializeSeq}, + Serialize, +}; + +use super::{is_default, structs::*}; + +impl Serialize for SConfig { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + if let Some(options) = &self.options { + map.serialize_entry("options", options)?; + } + if !self.roles.is_empty() { + map.serialize_entry("roles", &self.roles)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + if let Some(options) = &self.options { + map.serialize_entry("o", options)?; + } + if !self.roles.is_empty() { + map.serialize_entry("r", &self.roles)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } + } +} + +impl Serialize for SRole { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("name", &self.name)?; + if let Some(options) = &self.options { + map.serialize_entry("options", options)?; + } + if !self.actors.is_empty() { + map.serialize_entry("actors", &self.actors)?; + } + if !self.tasks.is_empty() { + map.serialize_entry("tasks", &self.tasks)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("n", &self.name)?; + if let Some(options) = &self.options { + map.serialize_entry("o", options)?; + } + if !self.actors.is_empty() { + map.serialize_entry("a", &self.actors)?; + } + if !self.tasks.is_empty() { + map.serialize_entry("t", &self.tasks)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } + } +} + +impl Serialize for SetBehavior { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + return serializer.serialize_str(&self.to_string()); + } else { + return serializer.serialize_u8(*self as u8); + } + } +} + +impl Serialize for SSetuidSet { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("default", &self.default)?; + if let Some(fallback) = &self.fallback { + map.serialize_entry("fallback", fallback)?; + } + 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)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("d", &(self.default as u8))?; + if let Some(fallback) = &self.fallback { + map.serialize_entry("f", fallback)?; + } + if !self.add.is_empty() { + let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("a", &v)?; + } + if !self.sub.is_empty() { + let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("s", &v)?; + } + map.end() + } + } +} + +impl Serialize for SSetgidSet { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.default.is_none() && self.sub.is_empty() && self.add.is_empty() { + serializer.serialize_some(&self.fallback) + } else if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("default", &self.default)?; + if !self.fallback.is_empty() { + map.serialize_entry("fallback", &self.fallback)?; + } + 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)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("d", &(self.default as u8))?; + if !self.fallback.is_empty() { + map.serialize_entry("f", &self.fallback)?; + } + + if !self.add.is_empty() { + let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("a", &v)?; + } + if !self.sub.is_empty() { + let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("s", &v)?; + } + map.end() + } + } +} + +impl Serialize for SCapabilities { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.default_behavior.is_none() && self.sub.is_empty() { + super::serialize_capset(&self.add, serializer) + } else { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(Some(3))?; + if self.default_behavior.is_all() { + 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)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(Some(3))?; + if self.default_behavior.is_all() { + map.serialize_entry("d", &(self.default_behavior as u8))?; + } + if !self.add.is_empty() { + let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("a", &v)?; + } + if !self.sub.is_empty() { + let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("s", &v)?; + } + map.end() + } + } + } +} + +impl Serialize for SCredentials { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + if self.setuid.is_some() { + map.serialize_entry("setuid", &self.setuid)?; + } + if self.setgid.is_some() { + map.serialize_entry("setgid", &self.setgid)?; + } + if self.capabilities.is_some() { + map.serialize_entry("capabilities", &self.capabilities)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + if self.setuid.is_some() { + map.serialize_entry("u", &self.setuid)?; + } + if self.setgid.is_some() { + map.serialize_entry("g", &self.setgid)?; + } + if self.capabilities.is_some() { + map.serialize_entry("c", &self.capabilities)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } + } +} + +impl Serialize for STask { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("name", &self.name)?; + if let Some(options) = &self.options { + map.serialize_entry("options", options)?; + } + if let Some(purpose) = &self.purpose { + map.serialize_entry("purpose", purpose)?; + } + if !is_default(&self.cred) { + map.serialize_entry("cred", &self.cred)?; + } + if !cmds_is_default(&self.commands) { + map.serialize_entry("commands", &self.commands)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("n", &self.name)?; + if let Some(options) = &self.options { + map.serialize_entry("o", options)?; + } + if let Some(purpose) = &self.purpose { + map.serialize_entry("p", purpose)?; + } + if !is_default(&self.cred) { + map.serialize_entry("i", &self.cred)?; + } + if !cmds_is_default(&self.commands) { + map.serialize_entry("c", &self.commands)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } + } +} + +impl Serialize for SCommands { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.sub.is_empty() && self._extra_fields.is_empty() { + if self.add.is_empty() { + return serializer.serialize_bool( + self.default_behavior + .as_ref() + .is_some_and(|b| *b == SetBehavior::All), + ); + } else if !self.add.is_empty() + && self + .default_behavior + .as_ref() + .is_none_or(|b| *b == SetBehavior::None) + { + let mut seq = serializer.serialize_seq(Some(self.add.len()))?; + for cmd in &self.add { + seq.serialize_element(cmd)?; + } + return seq.end(); + } + } + if serializer.is_human_readable() { + 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() { + map.serialize_entry("add", &self.add)?; + } + if !self.sub.is_empty() { + map.serialize_entry("del", &self.sub)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(Some(3))?; + if let Some(behavior) = &self.default_behavior { + map.serialize_entry("d", &(*behavior as u8))?; + } + if !self.add.is_empty() { + map.serialize_entry("a", &self.add)?; + } + if !self.sub.is_empty() { + map.serialize_entry("s", &self.sub)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } + } +} + +#[cfg(test)] +mod tests { + use capctl::Cap; + use serde_json::{json, to_value}; + + use crate::database::actor::SActor; + + use super::*; + + #[test] + fn test_sconfig_human_readable() { + let config = SConfig { + options: Some(Default::default()), + roles: vec![], + _extra_fields: Default::default(), + }; + let value = to_value(&config).unwrap(); + assert!(value.get("options").is_some()); + } + + #[test] + fn test_srole_binary() { + let role = SRole::builder("admin") + .actor(SActor::user(0).build()) + .task(STask::builder("test").build()) + .options(|o| { + o.bounding(crate::database::options::SBounding::Ignore) + .build() + }) + .build(); + //cbor4ii encode + let bin: Vec = Vec::new(); + let mut writer = cbor4ii::core::utils::BufWriter::new(bin); + let mut serializer = cbor4ii::serde::Serializer::new(&mut writer); + role.serialize(&mut serializer).unwrap(); + assert!(!writer.buffer().is_empty()); + assert!(!writer + .buffer() + .windows("tasks".len()) + .any(|window| window == "tasks".as_bytes())); + assert!(!writer + .buffer() + .windows("name".len()) + .any(|window| window == "name".as_bytes())); + assert!(!writer + .buffer() + .windows("options".len()) + .any(|window| window == "options".as_bytes())); + assert!(!writer + .buffer() + .windows("actors".len()) + .any(|window| window == "actors".as_bytes())); + } + + #[test] + fn test_setbehavior_serialize() { + let b = SetBehavior::All; + let value = to_value(&b).unwrap(); + assert_eq!(value, json!("all")); + let b = SetBehavior::None; + let bin: Vec = Vec::new(); + let mut writer = cbor4ii::core::utils::BufWriter::new(bin); + let mut serializer = cbor4ii::serde::Serializer::new(&mut writer); + b.serialize(&mut serializer).unwrap(); + assert!(!writer.buffer().is_empty()); + assert!(writer.buffer() == [0x00]); + } + + #[test] + fn test_ssetuidset_human_readable() { + let set = SSetuidSet::builder() + .default(SetBehavior::None) + .fallback(1) + .add(vec![1.into(), 3.into()]) + .sub(vec![4.into(), 5.into()]) + .build(); + let value = to_value(&set).unwrap(); + assert!(value.get("add").is_some()); + } + + #[test] + fn test_ssetgidset_seq() { + let set = SSetgidSet::builder(SetBehavior::None, vec![0, 1]).build(); + let value = to_value(&set).unwrap(); + assert!(value.is_array()); + assert_eq!(value.as_array().unwrap().len(), 2); + assert_eq!(value.as_array().unwrap()[0], json!(0)); + assert_eq!(value.as_array().unwrap()[1], json!(1)); + } + + #[test] + fn test_scapabilities_minimal() { + let caps = SCapabilities::builder(SetBehavior::None) + .add_cap(Cap::SYS_ADMIN) + .build(); + let value = to_value(&caps).unwrap(); + assert!(value.is_array()); + } + + #[test] + fn test_scredentials_human_readable() { + let creds = SCredentials::builder() + .setuid(1) + .setgid(2) + .capabilities( + SCapabilities::builder(SetBehavior::None) + .add_cap(Cap::SYS_ADMIN) + .build(), + ) + .build(); + let value = to_value(&creds).unwrap(); + assert!(value.get("setuid").is_some()); + assert!(value.get("setgid").is_some()); + assert!(value.get("capabilities").is_some()); + } + + #[test] + fn test_stask_binary() { + let task = STask::builder("test") + .options(|o| { + o.bounding(crate::database::options::SBounding::Ignore) + .build() + }) + .cred(SCredentials::builder().setuid(1).setgid(2).build()) + .commands( + SCommands::builder(SetBehavior::All) + .add(vec!["ls".into()]) + .build(), + ) + .build(); + let bin: Vec = Vec::new(); + let mut writer = cbor4ii::core::utils::BufWriter::new(bin); + let mut serializer = cbor4ii::serde::Serializer::new(&mut writer); + task.serialize(&mut serializer).unwrap(); + assert!(!writer.buffer().is_empty()); + assert!(!writer + .buffer() + .windows("name".len()) + .any(|window| window == "name".as_bytes())); + assert!(!writer + .buffer() + .windows("options".len()) + .any(|window| window == "options".as_bytes())); + assert!(!writer + .buffer() + .windows("cred".len()) + .any(|window| window == "cred".as_bytes())); + assert!(!writer + .buffer() + .windows("commands".len()) + .any(|window| window == "commands".as_bytes())); + assert!(writer + .buffer() + .windows("test".len()) + .any(|window| window == "test".as_bytes())); + } + + #[test] + fn test_scommands_bool() { + let cmds = SCommands { + default_behavior: Some(SetBehavior::All), + add: vec![], + sub: vec![], + _extra_fields: Default::default(), + }; + let value = to_value(&cmds).unwrap(); + assert!(value.is_boolean()); + } + + #[test] + fn test_scommands_seq() { + let cmds = SCommands::builder(SetBehavior::None) + .add(vec!["ls".into()]) + .build(); + let value = to_value(&cmds).unwrap(); + assert!(value.is_array()); + } +} diff --git a/rar-common/src/database/structs.rs b/rar-common/src/database/structs.rs index cbfa1ab9..5f14ac4d 100644 --- a/rar-common/src/database/structs.rs +++ b/rar-common/src/database/structs.rs @@ -1,13 +1,9 @@ use bon::{bon, builder, Builder}; use capctl::{Cap, CapSet}; use derivative::Derivative; -use serde::{ - de::{self, MapAccess, SeqAccess, Visitor}, - ser::SerializeMap, - Deserialize, Deserializer, Serialize, -}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{Map, Value}; -use strum::{Display, EnumIs}; +use strum::{Display, EnumIs, EnumString, FromRepr}; use std::{ cell::RefCell, @@ -17,24 +13,20 @@ use std::{ rc::{Rc, Weak}, }; +use crate::rc_refcell; + use super::{ - actor::{SActor, SGroups, SUserType}, - is_default, + actor::{SActor, SGroupType, SGroups, SUserType}, options::{Level, Opt, OptBuilder}, }; -#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)] +#[derive(Deserialize, PartialEq, Eq, Debug)] pub struct SConfig { - #[serde( - default, - skip_serializing_if = "Option::is_none", - deserialize_with = "sconfig_opt" - )] + #[serde(default, deserialize_with = "sconfig_opt", alias = "o")] pub options: Option>>, - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(default, alias = "r")] pub roles: Vec>>, - #[serde(default)] - #[serde(flatten, skip_serializing_if = "Map::is_empty")] + #[serde(default, flatten)] pub _extra_fields: Map, } @@ -42,15 +34,20 @@ fn sconfig_opt<'de, D>(deserializer: D) -> Result>>, D::E where D: Deserializer<'de>, { - let mut opt = Opt::deserialize(deserializer)?; - opt.level = Level::Global; - Ok(Some(Rc::new(RefCell::new(opt)))) + let opt: Option>> = Option::deserialize(deserializer)?; + if let Some(opt) = opt { + opt.as_ref().borrow_mut().level = Level::Global; + Ok(Some(opt)) + } else { + Ok(None) + } } -#[derive(Serialize, Deserialize, Debug, Derivative)] +#[derive(Deserialize, Debug, Derivative)] #[serde(rename_all = "kebab-case")] #[derivative(PartialEq, Eq)] pub struct SRole { + #[serde(default, skip_serializing_if = "String::is_empty")] pub name: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub actors: Vec, @@ -73,12 +70,16 @@ fn srole_opt<'de, D>(deserializer: D) -> Result>>, D::Err where D: Deserializer<'de>, { - let mut opt = Opt::deserialize(deserializer)?; - opt.level = Level::Role; - Ok(Some(Rc::new(RefCell::new(opt)))) + let opt: Option>> = Option::deserialize(deserializer)?; + if let Some(opt) = opt { + opt.as_ref().borrow_mut().level = Level::Role; + Ok(Some(opt)) + } else { + Ok(None) + } } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] #[serde(untagged)] pub enum IdTask { Name(String), @@ -94,18 +95,28 @@ impl std::fmt::Display for IdTask { } } -#[derive(Serialize, Deserialize, Debug, Derivative)] +pub(super) fn cmds_is_default(cmds: &SCommands) -> bool { + cmds.default_behavior + .as_ref() + .is_none_or(|b| *b == Default::default()) + && cmds.add.is_empty() + && cmds.sub.is_empty() + && cmds._extra_fields.is_empty() +} + +#[derive(Deserialize, Debug, Derivative)] #[derivative(PartialEq, Eq)] pub struct STask { - #[serde(default, skip_serializing_if = "IdTask::is_number")] + #[serde(alias = "n", default, skip_serializing_if = "IdTask::is_number")] pub name: IdTask, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(alias = "p", skip_serializing_if = "Option::is_none")] pub purpose: Option, - #[serde(default, skip_serializing_if = "is_default")] + #[serde(alias = "i", default, skip_serializing_if = "is_default")] pub cred: SCredentials, - #[serde(default, skip_serializing_if = "is_default")] + #[serde(alias = "c", default, skip_serializing_if = "cmds_is_default")] pub commands: SCommands, #[serde( + alias = "o", default, skip_serializing_if = "Option::is_none", deserialize_with = "stask_opt" @@ -122,25 +133,26 @@ fn stask_opt<'de, D>(deserializer: D) -> Result>>, D::Err where D: Deserializer<'de>, { - let mut opt = Opt::deserialize(deserializer)?; - opt.level = Level::Task; - Ok(Some(Rc::new(RefCell::new(opt)))) + let opt: Option>> = Option::deserialize(deserializer)?; + if let Some(opt) = opt { + opt.as_ref().borrow_mut().level = Level::Task; + Ok(Some(opt)) + } else { + Ok(None) + } } -#[derive(Serialize, Deserialize, Debug, Builder, PartialEq, Eq)] +#[derive(Deserialize, Debug, Builder, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct SCredentials { - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(alias = "u", skip_serializing_if = "Option::is_none")] #[builder(into)] pub setuid: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(alias = "g", skip_serializing_if = "Option::is_none")] #[builder(into)] pub setgid: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(default, alias = "c", 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, @@ -177,40 +189,53 @@ impl From for SUserChooser { } } -#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq)] +#[derive(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)] + #[serde( + alias = "d", + rename = "default", + default, + skip_serializing_if = "is_default" + )] + #[builder(default)] pub default: SetBehavior, - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[builder(into)] + #[serde(alias = "f", skip_serializing_if = "Option::is_none")] + pub fallback: Option, + #[serde(default, alias = "a", skip_serializing_if = "Vec::is_empty")] #[builder(default, with = FromIterator::from_iter)] pub add: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde( + default, + alias = "del", + alias = "s", + 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(PartialEq, Eq, Display, Debug, EnumIs, Clone, Copy, FromRepr, EnumString)] +#[strum(serialize_all = "lowercase")] #[derive(Default)] +#[repr(u8)] pub enum SetBehavior { - All, #[default] None, + All, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(untagged)] pub enum SGroupschooser { - Group(SGroups), + Group(SGroupType), + Groups(SGroups), StructChooser(SSetgidSet), } impl From for SGroupschooser { fn from(group: SGroups) -> Self { - SGroupschooser::Group(group) + SGroupschooser::Groups(group) } } @@ -232,17 +257,23 @@ impl From for SGroupschooser { } } -#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq)] +#[derive(Deserialize, Debug, Clone, Builder, PartialEq, Eq)] pub struct SSetgidSet { - #[builder(start_fn, into)] - pub fallback: SGroups, - #[serde(rename = "default", default, skip_serializing_if = "is_default")] + #[serde( + rename = "default", + alias = "d", + default, + skip_serializing_if = "is_default" + )] #[builder(start_fn)] pub default: SetBehavior, - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(alias = "f")] + #[builder(start_fn, into)] + pub fallback: SGroups, + #[serde(default, alias = "a", skip_serializing_if = "Vec::is_empty")] #[builder(default, with = FromIterator::from_iter)] pub add: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(default, alias = "s", skip_serializing_if = "Vec::is_empty")] #[builder(default, with = FromIterator::from_iter)] pub sub: Vec, } @@ -255,8 +286,6 @@ pub struct SCapabilities { pub add: CapSet, #[builder(field)] pub sub: CapSet, - #[builder(default, with = <_>::from_iter)] - pub _extra_fields: Map, } impl SCapabilitiesBuilder { @@ -278,117 +307,6 @@ impl SCapabilitiesBuilder { } } -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 { @@ -396,15 +314,11 @@ pub enum SCommand { Complex(Value), } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug)] pub struct SCommands { - #[serde(rename = "default")] pub default_behavior: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] pub add: Vec, - #[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, } @@ -455,7 +369,6 @@ impl Default for SCredentials { setuid: None, setgid: None, capabilities: Some(SCapabilities::default()), - additional_auth: None, _extra_fields: Map::default(), } } @@ -478,14 +391,13 @@ impl Default for SCapabilities { default_behavior: SetBehavior::default(), add: CapSet::empty(), sub: CapSet::empty(), - _extra_fields: Map::default(), } } } impl Default for SSetuidSet { fn default() -> Self { - SSetuidSet::builder(0, SetBehavior::None).build() + SSetuidSet::builder().build() } } @@ -546,7 +458,7 @@ impl SConfig { #[builder] pub fn new( #[builder(field)] roles: Vec>>, - #[builder(with = |f : fn(OptBuilder) -> Rc> | f(Opt::builder(Level::Global)))] + #[builder(with = |f : fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Global))))] options: Option>>, _extra_fields: Option>, ) -> Rc> { @@ -644,7 +556,7 @@ impl SRole { #[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)))] + #[builder(with = |f : fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Role))))] options: Option>>, #[builder(default)] _extra_fields: Map, ) -> Rc> { @@ -679,7 +591,7 @@ impl STask { purpose: Option, #[builder(default)] cred: SCredentials, #[builder(default)] commands: SCommands, - #[builder(with = |f : fn(OptBuilder) -> Rc> | f(Opt::builder(Level::Task)))] + #[builder(with = |f : fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Task))))] options: Option>>, #[builder(default)] _extra_fields: Map, _role: Option>>, @@ -745,14 +657,15 @@ impl SCapabilities { } } +/* Confusing impl PartialEq for SUserChooser { fn eq(&self, other: &str) -> bool { match self { SUserChooser::Actor(actor) => actor == &SUserType::from(other), - SUserChooser::ChooserStruct(chooser) => chooser.fallback == *other, + SUserChooser::ChooserStruct(chooser) => chooser.fallback.as_ref().is_some_and(|f| *f == *other), } } -} +}*/ #[cfg(test)] mod tests { @@ -776,7 +689,6 @@ mod tests { #[test] fn test_deserialize() { - println!("START"); let config = r#" { "options": { @@ -842,7 +754,6 @@ mod tests { ] } "#; - println!("STEP 1"); 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(); @@ -900,18 +811,18 @@ 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; - let setuidstruct = SSetuidSet { - fallback: "user1".into(), - default: SetBehavior::All, - add: ["user2".into()].into(), - sub: ["user3".into()].into(), - }; + let setuidstruct = SSetuidSet::builder() + .fallback("user1") + .default(SetBehavior::All) + .add(["user2".into()]) + .sub(["user3".into()]) + .build(); assert!( matches!(cred.setuid.as_ref().unwrap(), SUserChooser::ChooserStruct(set) if set == &setuidstruct) ); assert_eq!( *cred.setgid.as_ref().unwrap(), - SGroupschooser::Group(SGroups::from("setgid1")) + SGroupschooser::Group(SGroupType::from("setgid1")) ); let capabilities = cred.capabilities.as_ref().unwrap(); @@ -977,8 +888,7 @@ mod tests { "capabilities": { "default": "all", "add": ["cap_dac_override"], - "sub": ["cap_dac_override"], - "unknown": "unknown" + "sub": ["cap_dac_override"] }, "unknown": "unknown" }, @@ -1002,8 +912,6 @@ mod tests { let binding = config.options.unwrap(); let options = binding.as_ref().borrow(); - let path = options.path.as_ref().unwrap(); - assert_eq!(path._extra_fields.get("unknown").unwrap(), "unknown"); let env = &options.env.as_ref().unwrap(); assert_eq!(env._extra_fields.get("unknown").unwrap(), "unknown"); assert_eq!(options._extra_fields.get("unknown").unwrap(), "unknown"); @@ -1042,11 +950,6 @@ mod tests { let role = config.roles[0].as_ref().borrow(); let cred = &role[0].as_ref().borrow().cred; assert_eq!(cred._extra_fields.get("unknown").unwrap(), "unknown"); - let capabilities = cred.capabilities.as_ref().unwrap(); - assert_eq!( - capabilities._extra_fields.get("unknown").unwrap(), - "unknown" - ); let commands = &as_borrow!(role[0]).commands; assert_eq!(commands._extra_fields.get("unknown").unwrap(), "unknown"); } @@ -1169,7 +1072,7 @@ mod tests { ); assert_eq!( *cred.setgid.as_ref().unwrap(), - SGroupschooser::Group(SGroups::from("setgid1")) + SGroupschooser::Group(SGroupType::from("setgid1")) ); let capabilities = cred.capabilities.as_ref().unwrap(); @@ -1200,12 +1103,14 @@ mod tests { .cred( SCredentials::builder() .setuid(SUserChooser::ChooserStruct( - SSetuidSet::builder("user1", SetBehavior::All) + SSetuidSet::builder() + .fallback("user1") + .default(SetBehavior::All) .add(["user2".into()]) .sub(["user3".into()]) .build(), )) - .setgid(SGroupschooser::Group(SGroups::from("setgid1"))) + .setgid(SGroupschooser::Group(SGroupType::from("setgid1"))) .capabilities( SCapabilities::builder(SetBehavior::All) .add_cap(Cap::NET_BIND_SERVICE) @@ -1253,8 +1158,7 @@ mod tests { .build() }) .build(); - let config = serde_json::to_string_pretty(&config).unwrap(); - println!("{}", config); + serde_json::to_string_pretty(&config).unwrap(); } #[test] diff --git a/rar-common/src/database/versionning.rs b/rar-common/src/database/versionning.rs index 4563fb9e..326be208 100644 --- a/rar-common/src/database/versionning.rs +++ b/rar-common/src/database/versionning.rs @@ -3,8 +3,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Debug; use super::migration::Migration; -use crate::{SettingsFile, PACKAGE_VERSION}; - +use crate::{FullSettingsFile, PACKAGE_VERSION}; #[derive(Deserialize, Serialize, Debug)] pub struct Versioning { @@ -31,4 +30,4 @@ impl Default for Versioning { } } -pub(crate) const SETTINGS_MIGRATIONS: &[Migration] = &[]; +pub(crate) const SETTINGS_MIGRATIONS: &[Migration] = &[]; diff --git a/rar-common/src/lib.rs b/rar-common/src/lib.rs index 16586bd8..1e7073f0 100644 --- a/rar-common/src/lib.rs +++ b/rar-common/src/lib.rs @@ -49,45 +49,83 @@ const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); -use std::{cell::RefCell, error::Error, path::{Path, PathBuf}, rc::Rc}; +use std::{ + cell::RefCell, + error::Error, + io::BufReader, + path::{Path, PathBuf}, + rc::Rc, +}; use bon::Builder; -use log::debug; +use libc::dev_t; +use log::{debug, warn}; +use nix::unistd::{getgroups, Group, Pid, Uid, User}; use semver::Version; use serde::{Deserialize, Serialize}; -pub mod api; +//pub mod api; pub mod database; -pub mod plugin; +//pub mod plugin; pub mod util; +use strum::EnumString; use util::{ - dac_override_effective, open_with_privileges, read_effective, toggle_lock_config, write_cbor_config, write_json_config, ImmutableLock + dac_override_effective, open_with_privileges, read_effective, toggle_lock_config, + write_cbor_config, write_json_config, ImmutableLock, }; use database::{ - migration::Migration, structs::SConfig, versionning::{Versioning, SETTINGS_MIGRATIONS} + migration::Migration, + structs::SConfig, + versionning::{Versioning, SETTINGS_MIGRATIONS}, }; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Builder)] +pub struct Cred { + #[builder(field = getgroups().unwrap().iter().map(|gid| Group::from_gid(*gid).unwrap().unwrap()) + .collect())] + 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, +} + +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Eq, + Default, + Copy, + EnumString, + strum::VariantNames, +)] #[serde(rename_all = "lowercase")] +#[repr(u8)] pub enum StorageMethod { + #[default] + #[strum(ascii_case_insensitive)] JSON, + #[strum(ascii_case_insensitive)] CBOR, // SQLite, // PostgreSQL, // MySQL, // LDAP, - #[serde(other)] - Unknown, } -pub enum Storage { - SConfig(Rc>), +#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq, Default)] +pub struct SettingsFile { + pub storage: Settings, } -#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq)] -pub struct SettingsFile { +#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq, Default)] +pub struct FullSettingsFile { pub storage: Settings, #[serde(flatten)] pub config: Option>>, @@ -95,7 +133,7 @@ pub struct SettingsFile { #[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq)] pub struct Settings { - #[builder(default = StorageMethod::JSON)] + #[builder(default = StorageMethod::JSON, into)] pub method: StorageMethod, #[serde(skip_serializing_if = "Option::is_none")] pub settings: Option, @@ -168,15 +206,6 @@ pub struct LdapSettings { pub group_filter: String, } -impl Default for SettingsFile { - fn default() -> Self { - Self { - storage: Settings::default(), - config: None, - } - } -} - // Default implementation for Settings impl Default for Settings { fn default() -> Self { @@ -197,36 +226,52 @@ pub fn make_weak_config(config: &Rc>) { } } -pub fn save_settings(path: &S,settings: Rc>, privileged: bool) -> Result<(), Box> +pub fn full_save_settings( + path: &S, + settings: Rc>, + privileged: bool, +) -> Result<(), Box> where S: AsRef, { - Migration::migrate(&Version::parse(PACKAGE_VERSION).unwrap(), &mut *settings.as_ref().borrow_mut(), SETTINGS_MIGRATIONS)?; - let immuable = settings.as_ref().borrow().storage.settings.as_ref().unwrap_or(&RemoteStorageSettings::default()).immutable.unwrap_or(env!("RAR_CFG_IMMUTABLE") == "true") && privileged; + Migration::migrate( + &Version::parse(PACKAGE_VERSION).unwrap(), + &mut *settings.as_ref().borrow_mut(), + SETTINGS_MIGRATIONS, + )?; + let immuable = settings + .as_ref() + .borrow() + .storage + .settings + .as_ref() + .unwrap_or(&RemoteStorageSettings::default()) + .immutable + .unwrap_or(env!("RAR_CFG_IMMUTABLE") == "true") + && privileged; let separate = if let Some(rss) = &settings.as_ref().borrow().storage.settings { let default_data_path = env!("RAR_CFG_DATA_PATH").to_string().into(); let data_path = rss.path.as_ref().unwrap_or(&default_data_path); if data_path != path.as_ref() { Some(data_path.clone()) - } - else { + } else { None } } else { None }; - if let Some(data_path ) = separate { + if let Some(data_path) = separate { debug!("Saving settings in separate file"); return separate_save(&path, &data_path, settings.clone(), immuable); } - + if immuable { debug!("Toggling immutable off for config file"); toggle_lock_config(path, ImmutableLock::Unset)?; } // a single file - let versionned: Versioning>> = Versioning::new(settings.clone()); + let versionned: Versioning>> = Versioning::new(settings.clone()); write_json_config(&versionned, path)?; if immuable { debug!("Toggling immutable on for config file"); @@ -235,7 +280,12 @@ where Ok(()) } -fn separate_save(settings_path: &S, data_path: &T, settings: Rc>, immutable: bool) -> Result<(), Box> +fn separate_save( + settings_path: &S, + data_path: &T, + settings: Rc>, + immutable: bool, +) -> Result<(), Box> where S: AsRef, T: AsRef, @@ -249,7 +299,11 @@ where debug!("Toggling immutable off for config file"); toggle_lock_config(data_path, ImmutableLock::Unset)?; } - debug!("Saving in {} : {}", data_path.as_ref().display(), serde_json::to_string_pretty(&versioned_config).unwrap()); + debug!( + "Saving in {} : {}", + data_path.as_ref().display(), + serde_json::to_string_pretty(&versioned_config).unwrap() + ); match storage_method { StorageMethod::JSON => { write_json_config(&versioned_config, data_path)?; @@ -257,7 +311,6 @@ where StorageMethod::CBOR => { write_cbor_config(&versioned_config, data_path)?; } - StorageMethod::Unknown => todo!(), } if immutable { debug!("Toggling immutable on for config file"); @@ -265,12 +318,17 @@ where } } settings.as_ref().borrow_mut().config = None; - let versioned_settings: Versioning>> = Versioning::new(settings.clone()); + let versioned_settings: Versioning>> = + Versioning::new(settings.clone()); if immutable { debug!("Toggling immutable off for config file"); toggle_lock_config(settings_path, ImmutableLock::Unset)?; } - debug!("Saving in {} : {}", settings_path.as_ref().display(), serde_json::to_string_pretty(&versioned_settings).unwrap()); + debug!( + "Saving in {} : {}", + settings_path.as_ref().display(), + serde_json::to_string_pretty(&versioned_settings).unwrap() + ); write_json_config(&versioned_settings, settings_path)?; if immutable { debug!("Toggling immutable on for config file"); @@ -279,24 +337,24 @@ where Ok(()) } -pub fn get_settings(path: &S) -> Result>, Box> +pub fn get_full_settings(path: &S) -> Result>, Box> where S: AsRef, { // if file does not exist, return default settings if !std::path::Path::new(path.as_ref()).exists() { - return Ok(rc_refcell!(SettingsFile::default())); + return Ok(rc_refcell!(FullSettingsFile::default())); } // if user does not have read permission, try to enable privilege let file = open_with_privileges(path.as_ref())?; - let value: Versioning = serde_json::from_reader(file) + let value: Versioning = serde_json::from_reader(file) .inspect_err(|e| { debug!("Error reading file: {}", e); }) - .unwrap_or_default(); + .unwrap(); read_effective(false).or(dac_override_effective(false))?; - debug!("{}", serde_json::to_string_pretty(&value)?); let settingsfile = rc_refcell!(value.data); + debug!("settingsfile: {:?}", settingsfile); let default_remote = RemoteStorageSettings::default(); let into = env!("RAR_CFG_DATA_PATH").to_string().into(); { @@ -311,13 +369,15 @@ where .unwrap_or(&into); if data_path != path.as_ref() { binding.config = Some(retrieve_sconfig(&binding.storage.method, data_path)?); + } else { + make_weak_config(binding.config.as_ref().unwrap()); } } - make_weak_config(settingsfile.as_ref().borrow_mut().config.as_ref().unwrap()); + Ok(settingsfile.clone()) } -fn retrieve_sconfig( +pub fn retrieve_sconfig( file_type: &StorageMethod, path: &PathBuf, ) -> Result>, Box> { @@ -328,19 +388,37 @@ fn retrieve_sconfig( debug!("Error reading file: {}", e); }) .unwrap_or_default(), - StorageMethod::CBOR => ciborium::from_reader(file) + StorageMethod::CBOR => cbor4ii::serde::from_reader(BufReader::new(file)) .inspect_err(|e| { debug!("Error reading file: {}", e); }) .unwrap_or_default(), - StorageMethod::Unknown => todo!(), }; - read_effective(false).or(dac_override_effective(false))?; + make_weak_config(&value.data); + //read_effective(false).or(dac_override_effective(false))?; //assert_eq!(value.version.to_string(), PACKAGE_VERSION, "Version mismatch"); debug!("{}", serde_json::to_string_pretty(&value)?); Ok(value.data) } +pub fn get_settings(path: &S) -> Result> +where + S: AsRef, +{ + // if user does not have read permission, try to enable privilege + let file = open_with_privileges(path.as_ref())?; + let value: Versioning = serde_json::from_reader(file) + .inspect_err(|e| { + debug!("Error reading file: {}", e); + }) + .unwrap_or_else(|_| { + warn!("Using default settings file!!"); + Default::default() + }); + //read_effective(false).or(dac_override_effective(false))?; + debug!("{}", serde_json::to_string_pretty(&value)?); + Ok(value.data) +} #[cfg(test)] mod tests { @@ -352,35 +430,74 @@ mod tests { use super::*; + pub struct Defer(Option); + + impl Defer { + pub fn new(f: F) -> Self { + Defer(Some(f)) + } + } + + impl Drop for Defer { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } + } + + pub fn defer(f: F) -> Defer { + Defer::new(f) + } + #[test] fn test_get_settings_same_file() { // Create a test JSON file - let value = "test_get_settings_same_file.json"; - let config = Versioning::new(Rc::new(RefCell::new(SettingsFile::builder() - .storage(Settings::builder() - .method(StorageMethod::JSON) - .settings(RemoteStorageSettings::builder() - .path(value) - .not_immutable() - .build()) - .build()) - .config(SConfig::builder() - .role(SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task(STask::builder("test_task") - .cred(SCredentials::builder() - .setuid(0) - .setgid(0) - .build()) - .commands(SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) - .build()) - .build()) - .build()) - .build()) - .build()))); + let value = "/tmp/test_get_settings_same_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(value).canonicalize().unwrap_or(value.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let config = Versioning::new(Rc::new(RefCell::new( + FullSettingsFile::builder() + .storage( + Settings::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(value) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SConfig::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple( + "/usr/bin/true".to_string(), + )]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ))); write_json_config(&config, value).unwrap(); - let settings = get_settings(&value).unwrap(); + let settings = get_full_settings(&value).unwrap(); assert_eq!(*config.data.borrow(), *settings.borrow()); fs::remove_file(value).unwrap(); } @@ -388,105 +505,181 @@ mod tests { #[test] fn test_get_settings_different_file() { // Create a test JSON file - let external_file = "test_get_settings_different_file_external.json"; - let test_file = "test_get_settings_different_file.json"; - let settings_config = Versioning::new(Rc::new(RefCell::new(SettingsFile::builder() - .storage(Settings::builder() - .method(StorageMethod::JSON) - .settings(RemoteStorageSettings::builder() - .path(external_file) - .not_immutable() - .build()) - .build()) - .config(SConfig::builder() - .role(SRole::builder("IGNORED").build()) - .build()) - .build()))); + let external_file = "/tmp/test_get_settings_different_file_external.json"; + let test_file = "/tmp/test_get_settings_different_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or(test_file.into()); + if std::fs::remove_file(&test_file).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let _cleanup2 = defer(|| { + let filename = PathBuf::from(external_file) + .canonicalize() + .unwrap_or(external_file.into()); + if std::fs::remove_file(&external_file).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let settings_config = Versioning::new(Rc::new(RefCell::new( + FullSettingsFile::builder() + .storage( + Settings::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(external_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SConfig::builder() + .role(SRole::builder("IGNORED").build()) + .build(), + ) + .build(), + ))); write_json_config(&settings_config, test_file).unwrap(); let config = SConfig::builder() - .role(SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task(STask::builder("test_task") - .cred(SCredentials::builder() - .setuid(0) - .setgid(0) - .build()) - .commands(SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) - .build()) - .build()) - .build()) + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) + .build(), + ) + .build(), + ) + .build(), + ) .build(); write_json_config(&Versioning::new(config.clone()), &external_file).unwrap(); - let settings = get_settings(&test_file).unwrap(); - assert_eq!(*config.borrow(), *settings.as_ref().borrow().config.as_ref().unwrap().borrow()); + let settings = get_full_settings(&test_file).unwrap(); + assert_eq!( + *config.borrow(), + *settings.as_ref().borrow().config.as_ref().unwrap().borrow() + ); fs::remove_file(test_file).unwrap(); fs::remove_file(external_file).unwrap(); } #[test] fn test_save_settings_same_file() { - let test_file = "test_save_settings_same_file.json"; + let test_file = "/tmp/test_save_settings_same_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or(test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); // Create a test JSON file - let config = Rc::new(RefCell::new(SettingsFile::builder() - .storage(Settings::builder() - .method(StorageMethod::JSON) - .settings(RemoteStorageSettings::builder() - .path(test_file) - .not_immutable() - .build()) - .build()) - .config(SConfig::builder() - .role(SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task(STask::builder("test_task") - .cred(SCredentials::builder() - .setuid(0) - .setgid(0) - .build()) - .commands(SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) - .build()) - .build()) - .build()) - .build()) - .build())); - save_settings(&test_file, config.clone(), false).unwrap(); - let settings = get_settings(&test_file).unwrap(); + let config = Rc::new(RefCell::new( + FullSettingsFile::builder() + .storage( + Settings::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(test_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SConfig::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple( + "/usr/bin/true".to_string(), + )]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + )); + full_save_settings(&test_file, config.clone(), false).unwrap(); + let settings = get_full_settings(&test_file).unwrap(); assert_eq!(*config.borrow(), *settings.borrow()); fs::remove_file(test_file).unwrap(); } #[test] fn test_save_settings_different_file() { - let external_file = "test_save_settings_different_file_external.json"; - let test_file = "test_save_settings_different_file.json"; + let external_file = "/tmp/test_save_settings_different_file_external.json"; + let test_file = "/tmp/test_save_settings_different_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or(test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let _cleanup2 = defer(|| { + let filename = PathBuf::from(external_file) + .canonicalize() + .unwrap_or(external_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); let sconfig = SConfig::builder() - .role(SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task(STask::builder("test_task") - .cred(SCredentials::builder() - .setuid(0) - .setgid(0) - .build()) - .commands(SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) - .build()) - .build()) - .build()) - .build(); + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); // Create a test JSON file - let config = Rc::new(RefCell::new(SettingsFile::builder() - .storage(Settings::builder() - .method(StorageMethod::JSON) - .settings(RemoteStorageSettings::builder() - .path(external_file) - .not_immutable() - .build()) - .build()) - .config(sconfig.clone()) - .build())); - save_settings(&test_file, config.clone(), false).unwrap(); + let config = Rc::new(RefCell::new( + FullSettingsFile::builder() + .storage( + Settings::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(external_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config(sconfig.clone()) + .build(), + )); + full_save_settings(&test_file, config.clone(), false).unwrap(); //assert that test_external.json contains /usr/bin/true let mut file = open_with_privileges(external_file).unwrap(); let mut content = String::new(); @@ -498,8 +691,11 @@ mod tests { file.read_to_string(&mut content).unwrap(); assert!(!content.contains("/usr/bin/true")); - let settings = get_settings(&test_file).unwrap(); - assert_eq!(*sconfig.borrow(), *settings.borrow().config.as_ref().unwrap().borrow()); + let settings = get_full_settings(&test_file).unwrap(); + assert_eq!( + *sconfig.borrow(), + *settings.borrow().config.as_ref().unwrap().borrow() + ); settings.as_ref().borrow_mut().config = None; assert_eq!(*config.borrow(), *settings.borrow()); fs::remove_file(test_file).unwrap(); @@ -508,41 +704,67 @@ mod tests { #[test] fn test_save_cbor_format() { - let external_file = "test_save_cbor_format.bin"; - let test_file = "test_save_cbor_format.json"; + let external_file = "/tmp/test_save_cbor_format.bin"; + let test_file = "/tmp/test_save_cbor_format.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or(test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let _cleanup2 = defer(|| { + let filename = PathBuf::from(external_file) + .canonicalize() + .unwrap_or(external_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); let sconfig = SConfig::builder() - .role(SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task(STask::builder("test_task") - .cred(SCredentials::builder() - .setuid(0) - .setgid(0) - .build()) - .commands(SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) - .build()) - .build()) - .build()) - .build(); - let settings = Rc::new(RefCell::new(SettingsFile::builder() - .storage(Settings::builder() - .method(StorageMethod::CBOR) - .settings(RemoteStorageSettings::builder() - .path(external_file) - .not_immutable() - .build()) - .build()) - .config(sconfig.clone()) - .build())); - save_settings(&test_file, settings.clone(), false).unwrap(); + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + let settings = Rc::new(RefCell::new( + FullSettingsFile::builder() + .storage( + Settings::builder() + .method(StorageMethod::CBOR) + .settings( + RemoteStorageSettings::builder() + .path(external_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config(sconfig.clone()) + .build(), + )); + full_save_settings(&test_file, settings.clone(), false).unwrap(); //asset that external_file is a binary file let mut file = open_with_privileges(external_file).unwrap(); // try to parse as ciborium let mut content = Vec::new(); file.read_to_end(&mut content).unwrap(); - let deserialized: Versioning>> = ciborium::de::from_reader(&content[..]).unwrap(); + let deserialized: Versioning>> = + cbor4ii::serde::from_reader(&content[..]).unwrap(); assert_eq!(deserialized.version.to_string(), PACKAGE_VERSION); fs::remove_file(test_file).unwrap(); fs::remove_file(external_file).unwrap(); } -} \ No newline at end of file +} diff --git a/rar-common/src/plugin/hashchecker.rs b/rar-common/src/plugin/hashchecker.rs index 3babc952..dde5e780 100644 --- a/rar-common/src/plugin/hashchecker.rs +++ b/rar-common/src/plugin/hashchecker.rs @@ -1,10 +1,10 @@ -use std::{fs::File, io::Read, os::fd::AsRawFd}; +use std::{error::Error, fs::File, io::Read, os::fd::AsRawFd}; use crate::{ api::PluginManager, database::structs::SCommand, open_with_privileges, - util::{final_path, parse_conf_command}, + util::{first_path, parse_conf_command}, }; use log::{debug, warn}; use nix::unistd::{access, AccessFlags}; @@ -77,34 +77,7 @@ fn complex_command_parse( debug!("Checking command {:?}", checker); match checker { Ok(checker) => { - let cmd = parse_conf_command(&checker.command)?; - let path = final_path(&cmd[0]); - if access(&path, AccessFlags::W_OK).is_ok() { - if checker.read_only.is_some_and(|read_only| read_only) { - return Err("Executor must not have write access to the executable".into()); - } - warn!("Executor has write access to the executable, this could lead to a race condition vulnerability"); - } - let mut open = open_with_privileges(&path)?; - if !is_immutable(&open)? && checker.immutable.is_some_and(|immutable| immutable) { - return Err("Executable file must be immutable".into()); - } - let mut buf = Vec::new(); - open.read_to_end(&mut buf)?; - let hash = compute(&checker.hash_type, &buf); - let config_hash = hex::decode(checker.hash.as_bytes())?; - debug!( - "Hash: {:?}, Config Hash: {:?}", - hex::encode(&hash), - hex::encode(&config_hash) - ); - if hash == config_hash { - debug!("Hashes match"); - parse_conf_command(&checker.command) - } else { - debug!("Hashes do not match"); - Err("Hashes do not match".into()) - } + process_hash_check(checker) } Err(e) => { debug!("Error parsing command {:?}", e); @@ -113,6 +86,45 @@ fn complex_command_parse( } } +fn process_hash_check(checker: HashChecker) -> Result, Box> { + let cmd = parse_conf_command(&checker.command)?; + let path = first_path(&cmd[0]).find( + |path| access(path, AccessFlags::W_OK).is_ok() + ); + if path.is_some() { + if checker.read_only.is_some_and(|read_only| read_only) { + return Err("Executor must not have write access to the executable".into()); + } + warn!("Executor has write access to the executable, this could lead to a race condition vulnerability"); + } + if let Some(path) = path { + let mut open = open_with_privileges(&path)?; + if !is_immutable(&open)? && checker.immutable.is_some_and(|immutable| immutable) { + return Err("Executable file must be immutable".into()); + } + let mut buf = Vec::new(); + open.read_to_end(&mut buf)?; + let hash = compute(&checker.hash_type, &buf); + let config_hash = hex::decode(checker.hash.as_bytes())?; + debug!( + "Hash: {:?}, Config Hash: {:?}", + hex::encode(&hash), + hex::encode(&config_hash) + ); + if hash == config_hash { + debug!("Hashes match"); + parse_conf_command(&checker.command) + } else { + debug!("Hashes do not match"); + Err("Hashes do not match".into()) + } + } else { + debug!("Path not found"); + Err("Path not found".into()) + } + +} + pub fn register() { PluginManager::subscribe_complex_command_parser(complex_command_parse) } @@ -128,7 +140,7 @@ mod tests { use super::*; use crate::database::actor::SActor; - use crate::database::finder::{Cred, TaskMatcher}; + //use crate::database::finder::{Cred, TaskMatcher}; use crate::{ database::structs::{IdTask, SCommand, SCommands, SConfig, SRole, STask}, rc_refcell, @@ -175,7 +187,7 @@ mod tests { let matching = config .matches(&cred, &None, &vec!["/tmp/hashchecker".to_string()]) .unwrap(); - assert!(matching.fully_matching()); + assert!(matching.score.fully_matching()); std::fs::remove_file("/tmp/hashchecker").unwrap(); } } diff --git a/rar-common/src/plugin/hierarchy.rs b/rar-common/src/plugin/hierarchy.rs index 91ffc4cc..7b6c1f71 100644 --- a/rar-common/src/plugin/hierarchy.rs +++ b/rar-common/src/plugin/hierarchy.rs @@ -29,7 +29,7 @@ fn find_in_parents( matcher: &mut TaskMatch, ) -> PluginResultAction { //precondition matcher user matches - if !matcher.user_matching() { + if !matcher.score.user_matching() { return PluginResultAction::Ignore; } let mut result = PluginResultAction::Ignore; @@ -43,8 +43,8 @@ fn find_in_parents( match role.as_ref().borrow().tasks.matches(user, filter, command) { Ok(matches) => { debug!("Parent role {} matched", parent); - if !matcher.command_matching() - || (matches.command_matching() + if !matcher.score.command_matching() + || (matches.score.command_matching() && matches.score.cmd_cmp(&matcher.score) == Ordering::Less) { debug!("Parent role {} is better", parent); diff --git a/rar-common/src/util.rs b/rar-common/src/util.rs index 24aeabb1..f1a784c0 100644 --- a/rar-common/src/util.rs +++ b/rar-common/src/util.rs @@ -1,5 +1,4 @@ use std::{ - env, error::Error, fs::File, io, @@ -9,14 +8,14 @@ use std::{ use capctl::{prctl, CapState}; use capctl::{Cap, CapSet, ParseCapError}; + use libc::{FS_IOC_GETFLAGS, FS_IOC_SETFLAGS}; use log::{debug, warn}; use serde::Serialize; use strum::EnumIs; #[cfg(feature = "finder")] -use crate::api::PluginManager; -use crate::database::structs::SCommand; +use crate::database::score::CmdMin; pub const RST: &str = "\x1B[0m"; pub const BOLD: &str = "\x1B[1m"; @@ -186,73 +185,71 @@ fn remove_outer_quotes(input: &str) -> String { } } -pub fn parse_conf_command(command: &SCommand) -> Result, Box> { - match command { - SCommand::Simple(command) => parse_simple_command(command), - SCommand::Complex(command) => parse_complex_command(command), - } -} - -fn parse_simple_command(command: &str) -> Result, Box> { - shell_words::split(command).map_err(Into::into) -} - -fn parse_complex_command(command: &serde_json::Value) -> Result, Box> { - if let Some(array) = command.as_array() { - let result: Result, _> = array - .iter() - .map(|item| { - item.as_str() - .map(|s| s.to_string()) - .ok_or_else(|| "Invalid command".into()) - }) - .collect(); - result - } else { - parse_complex_command_with_finder(command) - } +pub fn all_paths_from_env>(env_path: &[&str], exe_name: P) -> Vec { + env_path + .iter() + .filter_map(|dir| { + let full_path = Path::new(dir).join(&exe_name); + debug!("Checking path: {:?}", full_path); + full_path.is_file().then_some(full_path) + }) + .collect() } #[cfg(feature = "finder")] -fn parse_complex_command_with_finder( - command: &serde_json::Value, -) -> Result, Box> { - let res = PluginManager::notify_complex_command_parser(command); - debug!("Parsed command {:?}", res); - res -} - -#[cfg(not(feature = "finder"))] -fn parse_complex_command_with_finder( - _command: &serde_json::Value, -) -> Result, Box> { - Err("Invalid command".into()) -} - -pub fn find_from_envpath>(exe_name: P) -> Option { - env::var_os("PATH").and_then(|paths| { - env::split_paths(&paths) - .filter_map(|dir| { - let full_path = dir.join(&exe_name); - if full_path.is_file() { - Some(full_path) - } else { - None - } - }) - .next() - }) +pub fn match_single_path(cmd_path: &PathBuf, role_path: &str) -> CmdMin { + use glob::Pattern; + if !role_path.ends_with(cmd_path.to_str().unwrap()) || !role_path.starts_with("/") { + // the files could not be the same + return CmdMin::empty(); + } + let mut match_status = CmdMin::empty(); + debug!("Matching path {:?} with {:?}", cmd_path, role_path); + if cmd_path == Path::new(role_path) { + match_status |= CmdMin::Match; + } else if let Ok(pattern) = Pattern::new(role_path) { + if pattern.matches_path(&cmd_path) { + match_status |= CmdMin::WildcardPath; + } + } + if match_status.is_empty() { + debug!( + "No match for path ``{:?}`` for evaluated path : ``{:?}``", + cmd_path, role_path + ); + } + match_status +} + +/* +pub fn all_paths_from_env>(exe_name: P) -> impl Iterator { + env::var_os("PATH") + .into_iter() + .flat_map(|path| { + env::split_paths(&path).collect::>() + }) + .filter_map(move |dir| { + let full_path = dir.join(&exe_name); + debug!("Checking path: {:?}", full_path); + if full_path.is_file() { + Some(full_path) + } else { + None + } + }) } -pub fn final_path(path: &str) -> PathBuf { - if let Some(env_path) = find_from_envpath(path) { - env_path - } else if let Ok(canon_path) = std::fs::canonicalize(path) { +pub fn first_path

(path: P) -> PathBuf +where + P: AsRef, { + if let Some(path) = all_paths_from_env(&path).next() { + path + }else if let Ok(canon_path) = std::fs::canonicalize(&path) { canon_path } else { - PathBuf::from(path) + path.as_ref().to_path_buf() } -} +}*/ #[cfg(debug_assertions)] pub fn subsribe(_: &str) -> Result<(), Box> { @@ -329,7 +326,7 @@ where S: std::convert::AsRef + Clone, { let file = create_with_privileges(path)?; - ciborium::into_writer(&settings, file)?; + cbor4ii::serde::to_writer(file, &settings)?; Ok(()) } @@ -398,6 +395,26 @@ mod test { use super::*; + pub struct Defer(Option); + + impl Defer { + pub fn new(f: F) -> Self { + Defer(Some(f)) + } + } + + impl Drop for Defer { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } + } + + pub fn defer(f: F) -> Defer { + Defer::new(f) + } + #[test] fn test_remove_outer_quotes() { assert_eq!(remove_outer_quotes("'test'"), "test"); @@ -472,7 +489,11 @@ mod test { #[test] fn test_toggle_lock_config() { - let path = PathBuf::from("/tmp/test"); + let path = PathBuf::from("/tmp/rar_test_lock_config.lock"); + let _defer = defer(|| { + // Clean up the test file after the test is done + let _ = fs::remove_file(&path); + }); let file = File::create(&path).expect("Failed to create file"); let res = toggle_lock_config(&path, ImmutableLock::Set); let status = fs::read_to_string("/proc/self/status").unwrap(); diff --git a/resources/rootasrole.json b/resources/rootasrole.json index 4d9c01f2..6bf274ed 100644 --- a/resources/rootasrole.json +++ b/resources/rootasrole.json @@ -1,5 +1,5 @@ { - "version": "3.0.5", + "version": "3.1.0", "storage": { "method": "json", "settings": { @@ -61,6 +61,7 @@ ], "set": {} }, + "authentication": "perform", "root": "user", "bounding": "strict", "wildcard-denied": ";&|" diff --git a/src/chsr/cli/cli.pest b/src/chsr/cli/cli.pest index fd58dcc9..d63282c3 100644 --- a/src/chsr/cli/cli.pest +++ b/src/chsr/cli/cli.pest @@ -1,5 +1,5 @@ cli = { SOI ~ args ~ EOI } -args = { help | list | role | options_operations } +args = { help | convert_op | list | role | options_operations } list = { ("show" | "list" | "l") } set = { "set" | "s" } @@ -13,12 +13,24 @@ whitelist = { "whitelist" | "wl" } blacklist = { "blacklist" | "bl" } checklist = { "checklist" | "cl" } setlist = { "setlist" | "sl" } +convert = { "convert" | "c" } all = { "all" } name = @{ (!WHITESPACE ~ ANY)+ } +// ======================== +// convert +// ======================== +convert_op = { convert ~ convert_reconfigure? ~ convert_args } +convert_args = { from? ~ to } +from = { "--from" ~ convert_type ~ path } +to = { convert_type ~ path } +convert_type = _{ "json" | "cbor" } +convert_reconfigure = { "--reconfigure" | "-r" } + + // ======================== // role // ======================== diff --git a/src/chsr/cli/data.rs b/src/chsr/cli/data.rs index ab2b6235..51d549ce 100644 --- a/src/chsr/cli/data.rs +++ b/src/chsr/cli/data.rs @@ -1,17 +1,21 @@ -use std::collections::HashMap; +use std::{collections::HashMap, path::PathBuf}; +use bon::Builder; use capctl::CapSet; use chrono::Duration; 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, +use rar_common::{ + database::{ + actor::{SActor, SGroups, SUserType}, + options::{ + EnvBehavior, EnvKey, OptType, PathBehavior, SAuthentication, SBounding, SPrivileged, + TimestampType, + }, + structs::{IdTask, SetBehavior}, }, - structs::{IdTask, SetBehavior}, + StorageMethod, }; #[derive(Parser)] @@ -40,6 +44,7 @@ pub enum InputAction { Add, Del, Purge, + Convert, None, } @@ -89,6 +94,16 @@ pub struct Inputs { pub options_bounding: Option, pub options_wildcard: Option, pub options_auth: Option, + pub convertion: Option, + pub convert_reconfigure: bool, +} + +#[derive(Builder, Debug, Default)] +pub struct Convertion { + pub from_type: Option, + pub from: Option, + pub to_type: StorageMethod, + pub to: PathBuf, } impl Default for Inputs { @@ -122,6 +137,8 @@ impl Default for Inputs { options_bounding: None, options_wildcard: None, options_auth: None, + convertion: None, + convert_reconfigure: false, } } } diff --git a/src/chsr/cli/mod.rs b/src/chsr/cli/mod.rs index 5e106d00..0cb972d1 100644 --- a/src/chsr/cli/mod.rs +++ b/src/chsr/cli/mod.rs @@ -3,7 +3,7 @@ pub(crate) mod pair; pub(crate) mod process; pub(crate) mod usage; -use std::error::Error; +use std::{cell::RefCell, error::Error, rc::Rc}; use data::{Cli, Inputs, Rule}; @@ -11,12 +11,12 @@ use log::debug; use pair::recurse_pair; use pest::Parser; use process::process_input; +use rar_common::FullSettingsFile; use usage::print_usage; use crate::util::escape_parser_string_vec; -use rar_common::Storage; -pub fn main(storage: &Storage, args: I) -> Result> +pub fn main(storage: Rc>, args: I) -> Result> where I: IntoIterator, S: AsRef, @@ -34,7 +34,7 @@ where recurse_pair(pair, &mut inputs)?; } debug!("Inputs : {:?}", inputs); - process_input(storage, inputs) + process_input(&storage, inputs) } #[cfg(test)] @@ -50,9 +50,9 @@ mod tests { structs::{SCredentials, *}, versionning::Versioning, }, - get_settings, + get_full_settings, util::remove_with_privileges, - RemoteStorageSettings, Settings, SettingsFile, Storage, StorageMethod, + FullSettingsFile, RemoteStorageSettings, Settings, StorageMethod, }; use crate::ROOTASROLE; @@ -63,10 +63,30 @@ mod tests { use log::error; use test_log::test; - fn setup(name: &str) { + pub struct Defer(Option); + + impl Defer { + pub fn new(f: F) -> Self { + Defer(Some(f)) + } + } + + impl Drop for Defer { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } + } + + pub fn defer(f: F) -> Defer { + Defer::new(f) + } + + fn setup(name: &str) -> Defer { let file_path = format!("{}.{}", ROOTASROLE, name); let versionned = Versioning::new( - SettingsFile::builder() + FullSettingsFile::builder() .storage( Settings::builder() .method(StorageMethod::JSON) @@ -204,7 +224,7 @@ mod tests { .cred( SCredentials::builder() .setuid("user1") - .setgid(SGroupschooser::Group(SGroups::from([ + .setgid(SGroupschooser::Groups(SGroups::from([ "setgid1", "setgid2", ]))) .capabilities( @@ -235,13 +255,11 @@ mod tests { let jsonstr = serde_json::to_string_pretty(&versionned).unwrap(); file.write_all(jsonstr.as_bytes()).unwrap(); file.flush().unwrap(); + defer(move || { + remove_with_privileges(file_path).unwrap(); + }) } - fn teardown(name: &str) { - //Remove json test file - let path = format!("{}.{}", ROOTASROLE, name); - remove_with_privileges(path).unwrap(); - } // we need to test every commands // chsr r r1 create // chsr r r1 delete @@ -278,12 +296,10 @@ mod tests { #[test] fn test_all_main() { - setup("all_main"); + let _defer = setup("all_main"); let path = format!("{}.{}", ROOTASROLE, "all_main"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main(&Storage::SConfig(config.clone()), vec!["--help"],) + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), vec!["--help"],) .inspect_err(|e| { error!("{}", e); }) @@ -291,150 +307,106 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), - "r r1 create".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), - "r complete delete".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); - teardown("all_main"); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r r1 create".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete delete".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); } #[test] fn test_r_complete_show_actors() { - setup("r_complete_show_actors"); + let _defer = setup("r_complete_show_actors"); let path = format!("{}.{}", ROOTASROLE, "r_complete_show_actors"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), - "r complete show actors".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), - "r complete show tasks".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), - "r complete show all".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), - "r complete purge actors".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); - teardown("r_complete_show_actors"); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete show actors".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete show tasks".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete show all".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!( + main(settings.clone(), "r complete purge actors".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b) + ); } #[test] fn test_purge_tasks() { - setup("purge_tasks"); + let _defer = setup("purge_tasks"); let path = format!("{}.{}", ROOTASROLE, "purge_tasks"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), - "r complete purge tasks".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); - teardown("purge_tasks"); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete purge tasks".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); } #[test] fn test_r_complete_purge_all() { - setup("r_complete_purge_all"); + let _defer = setup("r_complete_purge_all"); let path = format!("{}.{}", ROOTASROLE, "r_complete_purge_all"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), - "r complete purge all".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); - teardown("r_complete_purge_all"); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete purge all".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); } #[test] fn test_r_complete_grant_u_user1_g_group1_g_group2_group3() { - setup("r_complete_grant_u_user1_g_group1_g_group2_group3"); + let _defer = setup("r_complete_grant_u_user1_g_group1_g_group2_group3"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_grant_u_user1_g_group1_g_group2_group3" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete grant -u user1 -g group1 -g group2&group3".split(" "), ) .inspect_err(|e| { @@ -444,23 +416,44 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::user("user1").build())); - assert!(config.as_ref().borrow()[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::group("group1").build())); - assert!(config.as_ref().borrow()[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::group(["group2", "group3"]).build())); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete revoke -u user1 -g group1 -g group2&group3".split(" "), ) .inspect_err(|e| { @@ -470,32 +463,50 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::user("user1").build())); - assert!(!config.as_ref().borrow()[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::group("group1").build())); - assert!(!config.as_ref().borrow()[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::group(["group2", "group3"]).build())); - teardown("r_complete_grant_u_user1_g_group1_g_group2_group3"); } #[test] fn test_r_complete_task_t_complete_show_all() { - setup("r_complete_task_t_complete_show_all"); + let _defer = setup("r_complete_task_t_complete_show_all"); let path = format!("{}.{}", ROOTASROLE, "r_complete_task_t_complete_show_all"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete task t_complete show all".split(" "), ) .inspect_err(|e| { @@ -505,11 +516,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete task t_complete show cmd".split(" "), ) .inspect_err(|e| { @@ -519,11 +528,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete task t_complete show cred".split(" "), ) .inspect_err(|e| { @@ -533,11 +540,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete task t_complete purge all".split(" "), ) .inspect_err(|e| { @@ -547,17 +552,14 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - teardown("r_complete_task_t_complete_show_all"); } #[test] fn test_r_complete_task_t_complete_purge_cmd() { - setup("r_complete_task_t_complete_purge_cmd"); + let _defer = setup("r_complete_task_t_complete_purge_cmd"); let path = format!("{}.{}", ROOTASROLE, "r_complete_task_t_complete_purge_cmd"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete task t_complete purge cmd".split(" "), ) .inspect_err(|e| { @@ -567,17 +569,14 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - teardown("r_complete_task_t_complete_purge_cmd"); } #[test] fn test_r_complete_task_t_complete_purge_cred() { - setup("r_complete_task_t_complete_purge_cred"); + let _defer = setup("r_complete_task_t_complete_purge_cred"); let path = format!("{}.{}", ROOTASROLE, "r_complete_task_t_complete_purge_cred"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete task t_complete purge cred".split(" "), ) .inspect_err(|e| { @@ -589,54 +588,76 @@ mod tests { .is_ok_and(|b| b)); debug!("====="); let path = format!("{}.{}", ROOTASROLE, "r_complete_task_t_complete_purge_cred"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - let task_count = config.as_ref().borrow()[0].as_ref().borrow().tasks.len(); - assert!(main( - &Storage::SConfig(config.clone()), - "r complete t t1 add".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + let task_count = settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks + .len(); + assert!(main(settings.clone(), "r complete t t1 add".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks.len(), + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks + .len(), task_count + 1 ); - assert!(main( - &Storage::SConfig(config.clone()), - "r complete t t1 del".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); + assert!(main(settings.clone(), "r complete t t1 del".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks.len(), + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks + .len(), task_count ); - teardown("r_complete_task_t_complete_purge_cred"); } #[test] fn test_r_complete_t_t_complete_cmd_setpolicy_deny_all() { - setup("r_complete_t_t_complete_cmd_setpolicy_deny_all"); + let _defer = setup("r_complete_t_t_complete_cmd_setpolicy_deny_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cmd_setpolicy_deny_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete cmd setpolicy deny-all".split(" "), ) .inspect_err(|e| { @@ -647,27 +668,34 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .default_behavior, Some(SetBehavior::None) ); - teardown("r_complete_t_t_complete_cmd_setpolicy_deny_all"); } #[test] fn test_r_complete_t_t_complete_cmd_setpolicy_allow_all() { - setup("r_complete_t_t_complete_cmd_setpolicy_allow_all"); + let _defer = setup("r_complete_t_t_complete_cmd_setpolicy_allow_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cmd_setpolicy_allow_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete cmd setpolicy allow-all".split(" "), ) .inspect_err(|e| { @@ -678,27 +706,34 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .default_behavior, Some(SetBehavior::All) ); - teardown("r_complete_t_t_complete_cmd_setpolicy_allow_all"); } #[test] fn test_r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces() { - setup("r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces"); + let _defer = setup("r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete cmd whitelist add super command with spaces".split(" "), ) .inspect_err(|e| { @@ -708,14 +743,24 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .add .contains(&SCommand::Simple("super command with spaces".to_string()))); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete cmd blacklist add super command with spaces".split(" "), ) .inspect_err(|e| { @@ -725,14 +770,24 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .sub .contains(&SCommand::Simple("super command with spaces".to_string()))); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete cmd whitelist del super command with spaces".split(" "), ) .inspect_err(|e| { @@ -742,26 +797,33 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .add .contains(&SCommand::Simple("super command with spaces".to_string()))); - teardown("r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces"); } #[test] fn test_r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces() { - setup("r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces"); + let _defer = setup("r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), vec![ "r", "complete", @@ -783,23 +845,30 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .sub .contains(&SCommand::Simple("super command with spaces".to_string()))); - teardown("r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces"); } #[test] fn test_r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2( ) { - setup("r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2"); + let _defer = setup("r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2"); let path = format!("{}.{}",ROOTASROLE,"r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main(&Storage::SConfig(config.clone()), "r complete t t_complete cred set --caps cap_dac_override,cap_sys_admin,cap_sys_boot --setuid user1 --setgid group1,group2".split(" "), + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete t t_complete cred set --caps cap_dac_override,cap_sys_admin,cap_sys_boot --setuid user1 --setgid group1,group2".split(" "), ) .inspect_err(|e| { error!("{}", e); @@ -808,7 +877,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -817,7 +896,17 @@ mod tests { .unwrap() .default_behavior .is_none()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -826,7 +915,17 @@ mod tests { .unwrap() .add .has(Cap::DAC_OVERRIDE)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -835,7 +934,17 @@ mod tests { .unwrap() .add .has(Cap::SYS_ADMIN)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -845,7 +954,17 @@ mod tests { .add .has(Cap::SYS_BOOT)); assert!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -857,7 +976,17 @@ mod tests { == 0 ); assert!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -869,7 +998,7 @@ mod tests { == 3 ); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete cred unset --caps cap_dac_override,cap_sys_admin,cap_sys_boot --setuid user1 --setgid group1,group2".split(" "), ) .inspect_err(|e| { @@ -879,7 +1008,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -888,32 +1027,49 @@ mod tests { .unwrap() .add .is_empty()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred .setuid .is_none()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred .setgid .is_none()); - teardown("r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2"); } #[test] fn test_r_complete_t_t_complete_cred_caps_setpolicy_deny_all() { - setup("r_complete_t_t_complete_cred_caps_setpolicy_deny_all"); + let _defer = setup("r_complete_t_t_complete_cred_caps_setpolicy_deny_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cred_caps_setpolicy_deny_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete cred caps setpolicy deny-all".split(" "), ) .inspect_err(|e| { @@ -924,7 +1080,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -934,20 +1100,17 @@ mod tests { .default_behavior, SetBehavior::None ); - teardown("r_complete_t_t_complete_cred_caps_setpolicy_deny_all"); } #[test] fn test_r_complete_t_t_complete_cred_caps_setpolicy_allow_all() { - setup("r_complete_t_t_complete_cred_caps_setpolicy_allow_all"); + let _defer = setup("r_complete_t_t_complete_cred_caps_setpolicy_allow_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cred_caps_setpolicy_allow_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete cred caps setpolicy allow-all".split(" "), ) .inspect_err(|e| { @@ -958,7 +1121,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -968,17 +1141,14 @@ mod tests { .default_behavior, SetBehavior::All ); - teardown("r_complete_t_t_complete_cred_caps_setpolicy_allow_all"); } #[test] fn test_r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot( ) { - setup("r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); + let _defer = setup("r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); let path = format!("{}.{}",ROOTASROLE,"r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main(&Storage::SConfig(config.clone()), "r complete t t_complete cred caps whitelist add cap_dac_override cap_sys_admin cap_sys_boot".split(" ")) + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete t t_complete cred caps whitelist add cap_dac_override cap_sys_admin cap_sys_boot".split(" ")) .inspect_err(|e| { error!("{}", e); }) @@ -986,7 +1156,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -995,7 +1175,17 @@ mod tests { .unwrap() .add .has(Cap::DAC_OVERRIDE)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1004,7 +1194,17 @@ mod tests { .unwrap() .add .has(Cap::SYS_ADMIN)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1013,17 +1213,14 @@ mod tests { .unwrap() .add .has(Cap::SYS_BOOT)); - teardown("r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); } #[test] fn test_r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot( ) { - setup("r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); + let _defer = setup("r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); let path = format!("{}.{}",ROOTASROLE,"r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main(&Storage::SConfig(config.clone()), "r complete t t_complete cred caps blacklist add cap_dac_override cap_sys_admin cap_sys_boot".split(" "), + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete t t_complete cred caps blacklist add cap_dac_override cap_sys_admin cap_sys_boot".split(" "), ) .inspect_err(|e| { error!("{}", e); @@ -1032,7 +1229,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1041,7 +1248,17 @@ mod tests { .unwrap() .sub .has(Cap::DAC_OVERRIDE)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1050,7 +1267,17 @@ mod tests { .unwrap() .sub .has(Cap::SYS_ADMIN)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1061,7 +1288,7 @@ mod tests { .has(Cap::SYS_BOOT)); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete cred caps whitelist del cap_dac_override cap_sys_admin cap_sys_boot".split(" "), ) .inspect_err(|e| { @@ -1071,7 +1298,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1080,7 +1317,17 @@ mod tests { .unwrap() .add .has(Cap::DAC_OVERRIDE)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1089,7 +1336,17 @@ mod tests { .unwrap() .add .has(Cap::SYS_ADMIN)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1100,7 +1357,7 @@ mod tests { .has(Cap::SYS_BOOT)); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete cred caps blacklist del cap_dac_override cap_sys_admin cap_sys_boot".split(" "), ) .inspect_err(|e| { @@ -1110,7 +1367,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1119,7 +1386,17 @@ mod tests { .unwrap() .sub .has(Cap::DAC_OVERRIDE)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1128,7 +1405,17 @@ mod tests { .unwrap() .sub .has(Cap::SYS_ADMIN)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1137,45 +1424,35 @@ mod tests { .unwrap() .sub .has(Cap::SYS_BOOT)); - teardown("r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); } #[test] fn test_options_show_all() { - setup("options_show_all"); + let _defer = setup("options_show_all"); let path = format!("{}.{}", ROOTASROLE, "options_show_all"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), - "options show all".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), - "r complete options show path".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "options show all".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + + assert!( + main(settings.clone(), "r complete options show path".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b) + ); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main( + settings.clone(), "r complete options show bounding".split(" "), ) .inspect_err(|e| { @@ -1185,20 +1462,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); - teardown("options_show_all"); } #[test] fn test_r_complete_t_t_complete_options_show_env() { - setup("r_complete_t_t_complete_options_show_env"); + let _defer = setup("r_complete_t_t_complete_options_show_env"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_options_show_env" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete options show env".split(" "), ) .inspect_err(|e| { @@ -1208,11 +1482,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete options show root".split(" "), ) .inspect_err(|e| { @@ -1222,11 +1494,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete options show bounding".split(" "), ) .inspect_err(|e| { @@ -1236,11 +1506,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete options show wildcard-denied".split(" "), ) .inspect_err(|e| { @@ -1250,11 +1518,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path set /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1264,20 +1530,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - teardown("r_complete_t_t_complete_options_show_env"); } #[test] fn test_r_complete_t_t_complete_o_path_setpolicy_delete_all() { - setup("r_complete_t_t_complete_o_path_setpolicy_delete_all"); + let _defer = setup("r_complete_t_t_complete_o_path_setpolicy_delete_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_path_setpolicy_delete_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path setpolicy delete-all".split(" "), ) .inspect_err(|e| { @@ -1287,7 +1550,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1300,20 +1573,17 @@ mod tests { .unwrap() .default_behavior .is_delete()); - teardown("r_complete_t_t_complete_o_path_setpolicy_delete_all"); } #[test] fn test_r_complete_t_t_complete_o_path_setpolicy_keep_unsafe() { - setup("r_complete_t_t_complete_o_path_setpolicy_keep_unsafe"); + let _defer = setup("r_complete_t_t_complete_o_path_setpolicy_keep_unsafe"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_path_setpolicy_keep_unsafe" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path setpolicy keep-unsafe".split(" "), ) .inspect_err(|e| { @@ -1323,7 +1593,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1337,7 +1617,7 @@ mod tests { .default_behavior .is_keep_unsafe()); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path setpolicy keep-safe".split(" "), ) .inspect_err(|e| { @@ -1347,7 +1627,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1362,7 +1652,7 @@ mod tests { .is_keep_safe()); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path setpolicy inherit".split(" "), ) .inspect_err(|e| { @@ -1372,7 +1662,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1385,20 +1685,17 @@ mod tests { .unwrap() .default_behavior .is_inherit()); - teardown("r_complete_t_t_complete_o_path_setpolicy_keep_unsafe"); } #[test] fn test_r_complete_t_t_complete_o_path_whitelist_add() { - setup("r_complete_t_t_complete_o_path_whitelist_add"); + let _defer = setup("r_complete_t_t_complete_o_path_whitelist_add"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_path_whitelist_add" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path whitelist add /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1409,7 +1706,17 @@ mod tests { }) .is_ok_and(|b| b)); let default = LinkedHashSet::new(); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1424,7 +1731,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/usr/bin".to_string())); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1440,7 +1757,7 @@ mod tests { .unwrap_or(&default) .contains(&"/bin".to_string())); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path whitelist del /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1450,7 +1767,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1465,7 +1792,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/usr/bin".to_string())); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1482,7 +1819,7 @@ mod tests { .contains(&"/bin".to_string())); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path whitelist purge".split(" "), ) .inspect_err(|e| { @@ -1492,7 +1829,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1509,7 +1856,7 @@ mod tests { .is_empty()); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path whitelist set /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1519,7 +1866,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1534,7 +1891,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/usr/bin".to_string())); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1550,7 +1917,17 @@ mod tests { .unwrap_or(&default) .contains(&"/bin".to_string())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1569,7 +1946,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path blacklist set /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1581,7 +1958,7 @@ mod tests { .is_ok_and(|b| b)); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path blacklist add /tmp".split(" "), ) .inspect_err(|e| { @@ -1591,7 +1968,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1607,7 +1994,7 @@ mod tests { .unwrap_or(&default) .contains(&"/tmp".to_string())); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path blacklist del /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1619,7 +2006,17 @@ mod tests { .is_ok_and(|b| b)); debug!( "add : {:?}", - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1633,7 +2030,17 @@ mod tests { .sub ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1650,7 +2057,17 @@ mod tests { .len(), 1 ); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1665,7 +2082,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/tmp".to_string())); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1680,7 +2107,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/usr/bin".to_string())); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1695,20 +2132,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/bin".to_string())); - teardown("r_complete_t_t_complete_o_path_whitelist_add"); } #[test] fn test_r_complete_t_t_complete_o_path_blacklist_purge() { - setup("r_complete_t_t_complete_o_path_blacklist_purge"); + let _defer = setup("r_complete_t_t_complete_o_path_blacklist_purge"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_path_blacklist_purge" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o path blacklist purge".split(" "), ) .inspect_err(|e| { @@ -1718,20 +2152,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - teardown("r_complete_t_t_complete_o_path_blacklist_purge"); } #[test] fn test_r_complete_t_t_complete_o_env_keep_only_myvar_var2() { - setup("r_complete_t_t_complete_o_env_keep_only_MYVAR_VAR2"); + let _defer = setup("r_complete_t_t_complete_o_env_keep_only_MYVAR_VAR2"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_keep_only_MYVAR_VAR2" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env keep-only MYVAR,VAR2".split(" "), ) .inspect_err(|e| { @@ -1741,7 +2172,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1754,7 +2195,17 @@ mod tests { .unwrap() .default_behavior .is_delete()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1769,7 +2220,17 @@ mod tests { .as_ref() .unwrap() .contains(&"MYVAR".to_string().into())); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1785,7 +2246,17 @@ mod tests { .unwrap() .contains(&"VAR2".to_string().into())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1802,20 +2273,17 @@ mod tests { .len(), 2 ); - teardown("r_complete_t_t_complete_o_env_keep_only_MYVAR_VAR2"); } #[test] fn test_r_complete_t_t_complete_o_env_delete_only_myvar_var2() { - setup("r_complete_t_t_complete_o_env_delete_only_MYVAR_VAR2"); + let _defer = setup("r_complete_t_t_complete_o_env_delete_only_MYVAR_VAR2"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_delete_only_MYVAR_VAR2" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env delete-only MYVAR,VAR2".split(" "), ) .inspect_err(|e| { @@ -1825,7 +2293,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1838,7 +2316,17 @@ mod tests { .unwrap() .default_behavior .is_keep()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1853,7 +2341,17 @@ mod tests { .as_ref() .unwrap() .contains(&"MYVAR".to_string().into())); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1869,7 +2367,17 @@ mod tests { .unwrap() .contains(&"VAR2".to_string().into())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1886,20 +2394,17 @@ mod tests { .len(), 2 ); - teardown("r_complete_t_t_complete_o_env_delete_only_MYVAR_VAR2"); } #[test] fn test_r_complete_t_t_complete_o_env_set_myvar_value_var2_value2() { - setup("r_complete_t_t_complete_o_env_set_MYVAR_value_VAR2_value2"); + let _defer = setup("r_complete_t_t_complete_o_env_set_MYVAR_value_VAR2_value2"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_set_MYVAR_value_VAR2_value2" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), r#"r complete t t_complete o env set MYVAR=value,VAR2="value2""#.split(" "), ) .inspect_err(|e| { @@ -1910,7 +2415,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1922,12 +2437,24 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("MYVAR") .unwrap(), (&"MYVAR".to_string(), &"value".to_string()) ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1939,12 +2466,24 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("VAR2") .unwrap(), (&"VAR2".to_string(), &"value2".to_string()) ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1956,23 +2495,22 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .len(), 2 ); - teardown("r_complete_t_t_complete_o_env_set_MYVAR_value_VAR2_value2"); } #[test] fn test_r_complete_t_t_complete_o_env_add_myvar_value_var2_value2() { - setup("r_complete_t_t_complete_o_env_add_MYVAR_value_VAR2_value2"); + let _defer = setup("r_complete_t_t_complete_o_env_add_MYVAR_value_VAR2_value2"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_add_MYVAR_value_VAR2_value2" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), r#"r complete t t_complete o env setlist set VAR3=value3"#.split(" "), ) .inspect_err(|e| { @@ -1983,7 +2521,7 @@ mod tests { }) .is_ok_and(|b| b)); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), r#"r complete t t_complete o env setlist add MYVAR=value,VAR2="value2""#.split(" "), ) .inspect_err(|e| { @@ -1994,7 +2532,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2006,12 +2554,24 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("MYVAR") .unwrap(), (&"MYVAR".to_string(), &"value".to_string()) ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2023,12 +2583,24 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("VAR2") .unwrap(), (&"VAR2".to_string(), &"value2".to_string()) ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2040,12 +2612,24 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("VAR3") .unwrap(), (&"VAR3".to_string(), &"value3".to_string()) ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2057,11 +2641,13 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .len(), 3 ); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), r#"r complete t t_complete o env setlist del MYVAR,VAR2"#.split(" "), ) .inspect_err(|e| { @@ -2072,7 +2658,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2084,10 +2680,22 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .len(), 1 ); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2099,9 +2707,21 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("MYVAR") .is_none()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2113,10 +2733,12 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("VAR2") .is_none()); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), r#"r complete t t_complete o env setlist purge"#.split(" "), ) .inspect_err(|e| { @@ -2126,7 +2748,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2138,21 +2770,18 @@ mod tests { .as_ref() .unwrap() .set - .is_empty()); - teardown("r_complete_t_t_complete_o_env_add_MYVAR_value_VAR2_value2"); + .is_none()); } #[test] fn test_r_complete_t_t_complete_o_env_setpolicy_delete_all() { - setup("r_complete_t_t_complete_o_env_setpolicy_delete_all"); + let _defer = setup("r_complete_t_t_complete_o_env_setpolicy_delete_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_setpolicy_delete_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env setpolicy delete-all".split(" "), ) .inspect_err(|e| { @@ -2163,7 +2792,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2177,20 +2816,17 @@ mod tests { .default_behavior, EnvBehavior::Delete ); - teardown("r_complete_t_t_complete_o_env_setpolicy_delete_all"); } #[test] fn test_r_complete_t_t_complete_o_env_setpolicy_keep_all() { - setup("r_complete_t_t_complete_o_env_setpolicy_keep_all"); + let _defer = setup("r_complete_t_t_complete_o_env_setpolicy_keep_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_setpolicy_keep_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env setpolicy keep-all".split(" "), ) .inspect_err(|e| { @@ -2201,7 +2837,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2215,20 +2861,17 @@ mod tests { .default_behavior, EnvBehavior::Keep ); - teardown("r_complete_t_t_complete_o_env_setpolicy_keep_all"); } #[test] fn test_r_complete_t_t_complete_o_env_setpolicy_inherit() { - setup("r_complete_t_t_complete_o_env_setpolicy_inherit"); + let _defer = setup("r_complete_t_t_complete_o_env_setpolicy_inherit"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_setpolicy_inherit" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env setpolicy inherit".split(" "), ) .inspect_err(|e| { @@ -2239,7 +2882,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2253,20 +2906,17 @@ mod tests { .default_behavior, EnvBehavior::Inherit ); - teardown("r_complete_t_t_complete_o_env_setpolicy_inherit"); } #[test] fn test_r_complete_t_t_complete_o_env_whitelist_add_myvar() { - setup("r_complete_t_t_complete_o_env_whitelist_add_MYVAR"); + let _defer = setup("r_complete_t_t_complete_o_env_whitelist_add_MYVAR"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_whitelist_add_MYVAR" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env whitelist add MYVAR".split(" "), ) .inspect_err(|e| { @@ -2276,7 +2926,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2292,7 +2952,17 @@ mod tests { .unwrap() .contains(&"MYVAR".to_string().into())); assert!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2310,7 +2980,7 @@ mod tests { > 1 ); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env whitelist del MYVAR".split(" "), ) .inspect_err(|e| { @@ -2320,7 +2990,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2337,7 +3017,7 @@ mod tests { .contains(&"MYVAR".to_string().into())); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env whitelist set MYVAR".split(" "), ) .inspect_err(|e| { @@ -2347,7 +3027,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2363,7 +3053,17 @@ mod tests { .unwrap() .contains(&"MYVAR".to_string().into())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2380,20 +3080,17 @@ mod tests { .len(), 1 ); - teardown("r_complete_t_t_complete_o_env_whitelist_add_MYVAR"); } #[test] fn test_r_complete_t_t_complete_o_env_whitelist_purge() { - setup("r_complete_t_t_complete_o_env_whitelist_purge"); + let _defer = setup("r_complete_t_t_complete_o_env_whitelist_purge"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_whitelist_purge" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env whitelist purge".split(" "), ) .inspect_err(|e| { @@ -2403,7 +3100,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2416,20 +3123,17 @@ mod tests { .unwrap() .keep .is_none()); - teardown("r_complete_t_t_complete_o_env_whitelist_purge"); } #[test] fn test_r_complete_t_t_complete_o_env_blacklist_add_myvar() { - setup("r_complete_t_t_complete_o_env_blacklist_add_MYVAR"); + let _defer = setup("r_complete_t_t_complete_o_env_blacklist_add_MYVAR"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_blacklist_add_MYVAR" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env blacklist add MYVAR".split(" "), ) .inspect_err(|e| { @@ -2439,7 +3143,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2455,7 +3169,7 @@ mod tests { .unwrap() .contains(&"MYVAR".to_string().into())); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env blacklist del MYVAR".split(" "), ) .inspect_err(|e| { @@ -2465,7 +3179,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2480,20 +3204,17 @@ mod tests { .as_ref() .unwrap() .contains(&"MYVAR".to_string().into())); - teardown("r_complete_t_t_complete_o_env_blacklist_add_MYVAR"); } #[test] fn test_r_complete_t_t_complete_o_env_blacklist_set_myvar() { - setup("r_complete_t_t_complete_o_env_blacklist_set_MYVAR"); + let _defer = setup("r_complete_t_t_complete_o_env_blacklist_set_MYVAR"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_blacklist_set_MYVAR" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env blacklist set MYVAR".split(" "), ) .inspect_err(|e| { @@ -2503,7 +3224,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2519,7 +3250,17 @@ mod tests { .unwrap() .contains(&"MYVAR".to_string().into())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2536,20 +3277,17 @@ mod tests { .len(), 1 ); - teardown("r_complete_t_t_complete_o_env_blacklist_set_MYVAR"); } #[test] fn test_r_complete_t_t_complete_o_env_blacklist_purge() { - setup("r_complete_t_t_complete_o_env_blacklist_purge"); + let _defer = setup("r_complete_t_t_complete_o_env_blacklist_purge"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_blacklist_purge" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env blacklist purge".split(" "), ) .inspect_err(|e| { @@ -2559,7 +3297,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2572,20 +3320,17 @@ mod tests { .unwrap() .delete .is_none()); - teardown("r_complete_t_t_complete_o_env_blacklist_purge"); } #[test] fn test_r_complete_t_t_complete_o_env_checklist_add_myvar() { - setup("r_complete_t_t_complete_o_env_checklist_add_MYVAR"); + let _defer = setup("r_complete_t_t_complete_o_env_checklist_add_MYVAR"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_checklist_add_MYVAR" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env checklist add MYVAR".split(" "), ) .inspect_err(|e| { @@ -2595,7 +3340,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2612,7 +3367,7 @@ mod tests { .contains(&"MYVAR".to_string().into())); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env checklist del MYVAR".split(" "), ) .inspect_err(|e| { @@ -2622,7 +3377,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2639,7 +3404,7 @@ mod tests { .contains(&"MYVAR".to_string().into())); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env checklist set MYVAR".split(" "), ) .inspect_err(|e| { @@ -2649,7 +3414,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2665,7 +3440,17 @@ mod tests { .unwrap() .contains(&"MYVAR".to_string().into())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2684,7 +3469,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o env checklist purge".split(" "), ) .inspect_err(|e| { @@ -2694,7 +3479,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2707,20 +3502,17 @@ mod tests { .unwrap() .check .is_none()); - teardown("r_complete_t_t_complete_o_env_checklist_add_MYVAR"); } #[test] fn test_r_complete_t_t_complete_o_root_privileged() { - setup("r_complete_t_t_complete_o_root_privileged"); + let _defer = setup("r_complete_t_t_complete_o_root_privileged"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_root_privileged" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o root privileged".split(" "), ) .inspect_err(|e| { @@ -2731,7 +3523,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2746,7 +3548,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o root user".split(" "), ) .inspect_err(|e| { @@ -2757,7 +3559,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2772,7 +3584,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o root inherit".split(" "), ) .inspect_err(|e| { @@ -2783,7 +3595,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2796,20 +3618,17 @@ mod tests { .unwrap(), &SPrivileged::Inherit ); - teardown("r_complete_t_t_complete_o_root_privileged"); } #[test] fn test_r_complete_t_t_complete_o_bounding_strict() { - setup("r_complete_t_t_complete_o_bounding_strict"); + let _defer = setup("r_complete_t_t_complete_o_bounding_strict"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_bounding_strict" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o bounding strict".split(" "), ) .inspect_err(|e| { @@ -2820,7 +3639,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2833,20 +3662,17 @@ mod tests { .unwrap(), &SBounding::Strict ); - teardown("r_complete_t_t_complete_o_bounding_strict"); } #[test] fn test_r_complete_t_t_complete_o_bounding_ignore() { - setup("r_complete_t_t_complete_o_bounding_ignore"); + let _defer = setup("r_complete_t_t_complete_o_bounding_ignore"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_bounding_ignore" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o bounding ignore".split(" "), ) .inspect_err(|e| { @@ -2857,7 +3683,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2870,20 +3706,17 @@ mod tests { .unwrap(), &SBounding::Ignore ); - teardown("r_complete_t_t_complete_o_bounding_ignore"); } #[test] fn test_r_complete_t_t_complete_o_bounding_inherit() { - setup("r_complete_t_t_complete_o_bounding_inherit"); + let _defer = setup("r_complete_t_t_complete_o_bounding_inherit"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_bounding_inherit" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o bounding inherit".split(" "), ) .inspect_err(|e| { @@ -2894,7 +3727,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2907,17 +3750,14 @@ mod tests { .unwrap(), &SBounding::Inherit ); - teardown("r_complete_t_t_complete_o_bounding_inherit"); } #[test] fn test_r_complete_t_t_complete_o_auth_skip() { - setup("r_complete_t_t_complete_o_auth_skip"); + let _defer = setup("r_complete_t_t_complete_o_auth_skip"); let path = format!("{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_auth_skip"); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o auth skip".split(" "), ) .inspect_err(|e| { @@ -2928,7 +3768,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2943,7 +3793,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o auth perform".split(" "), ) .inspect_err(|e| { @@ -2954,7 +3804,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2969,7 +3829,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o auth inherit".split(" "), ) .inspect_err(|e| { @@ -2980,7 +3840,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2993,20 +3863,17 @@ mod tests { .unwrap(), &SAuthentication::Inherit ); - teardown("r_complete_t_t_complete_o_auth_skip"); } #[test] fn test_r_complete_t_t_complete_o_wildcard_denied_set() { - setup("r_complete_t_t_complete_o_wildcard_denied_set"); + let _defer = setup("r_complete_t_t_complete_o_wildcard_denied_set"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_wildcard_denied_set" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o wildcard-denied set *".split(" "), ) .inspect_err(|e| { @@ -3017,7 +3884,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -3032,7 +3909,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o wildcard-denied add ~".split(" "), ) .inspect_err(|e| { @@ -3043,7 +3920,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -3058,7 +3945,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o wildcard-denied del *".split(" "), ) .inspect_err(|e| { @@ -3069,7 +3956,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -3087,11 +3984,9 @@ mod tests { "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_wildcard_denied_set" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o timeout set --type uid --duration 15:05:10 --max-usage 7" .split(" "), ) @@ -3103,8 +3998,9 @@ mod tests { }) .is_ok_and(|b| b)); { - let binding = config.as_ref().borrow(); - let bindingrole = binding[0].as_ref().borrow(); + let bindingsettings = settings.as_ref().borrow(); + let bindingconfig = bindingsettings.config.as_ref().unwrap().as_ref().borrow(); + let bindingrole = bindingconfig[0].as_ref().borrow(); let bindingtask = bindingrole.tasks[0].as_ref().borrow(); let bindingopt = bindingtask.options.as_ref().unwrap().as_ref().borrow(); let timeout = bindingopt.timeout.as_ref().unwrap(); @@ -3114,7 +4010,7 @@ mod tests { } debug!("====="); assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o timeout unset --type --max-usage".split(" "), ) .inspect_err(|e| { @@ -3125,8 +4021,9 @@ mod tests { }) .is_ok_and(|b| b)); { - let binding = config.as_ref().borrow(); - let bindingrole = binding[0].as_ref().borrow(); + let bindingsettings = settings.as_ref().borrow(); + let bindingconfig = bindingsettings.config.as_ref().unwrap().as_ref().borrow(); + let bindingrole = bindingconfig[0].as_ref().borrow(); let bindingtask = bindingrole.tasks[0].as_ref().borrow(); let bindingopt = bindingtask.options.as_ref().unwrap().as_ref().borrow(); let timeout = bindingopt.timeout.as_ref().unwrap(); @@ -3134,7 +4031,7 @@ mod tests { assert_eq!(timeout.type_field, None); } assert!(main( - &Storage::SConfig(config.clone()), + settings.clone(), "r complete t t_complete o timeout unset --type --duration --max-usage".split(" "), ) .inspect_err(|e| { @@ -3145,26 +4042,21 @@ mod tests { }) .is_ok_and(|b| b)); { - let binding = config.as_ref().borrow(); - let bindingrole = binding[0].as_ref().borrow(); + let bindingsettings = settings.as_ref().borrow(); + let bindingconfig = bindingsettings.config.as_ref().unwrap().as_ref().borrow(); + let bindingrole = bindingconfig[0].as_ref().borrow(); let bindingtask = bindingrole.tasks[0].as_ref().borrow(); let bindingopt = bindingtask.options.as_ref().unwrap().as_ref().borrow(); assert!(bindingopt.timeout.as_ref().is_none()); } - let settings = get_settings(&path).expect("Failed to get settings"); - let binding = settings.as_ref().borrow(); - let config = binding.config.as_ref().unwrap(); - assert!(main( - &Storage::SConfig(config.clone()), - "r complete tosk".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_err()); - teardown("r_complete_t_t_complete_o_wildcard_denied_set"); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete tosk".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_err()); } } diff --git a/src/chsr/cli/pair.rs b/src/chsr/cli/pair.rs index 79f1655b..ed0d871b 100644 --- a/src/chsr/cli/pair.rs +++ b/src/chsr/cli/pair.rs @@ -5,14 +5,19 @@ use chrono::Duration; use linked_hash_set::LinkedHashSet; use log::{debug, warn}; use pest::iterators::Pair; +use strum::VariantNames; use crate::cli::data::{RoleType, TaskType}; -use rar_common::database::{ - actor::{SActor, SGroupType}, - options::{ - EnvBehavior, OptType, PathBehavior, SAuthentication, SBounding, SPrivileged, TimestampType, +use rar_common::{ + database::{ + actor::{SActor, SGroupType}, + options::{ + EnvBehavior, OptType, PathBehavior, SAuthentication, SBounding, SPrivileged, + TimestampType, + }, + structs::{IdTask, SetBehavior}, }, - structs::{IdTask, SetBehavior}, + StorageMethod, }; use super::data::*; @@ -67,6 +72,40 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) -> Result<(), Box { inputs.setlist_type = Some(SetListType::Set); } + Rule::convert => { + inputs.action = InputAction::Convert; + } + Rule::to => { + let mut inner = pair.clone().into_inner(); + let temp_convertion = Default::default(); + let convertion = inputs.convertion.get_or_insert(temp_convertion); + convertion.to_type = inner.next().unwrap().as_str().parse().map_err(|e| { + warn!( + "Unknown type {}, types available : {}", + e, + StorageMethod::VARIANTS.join(", ") + ); + e + })?; + convertion.to = inner.next().unwrap().as_str().into(); + } + Rule::from => { + let mut inner = pair.clone().into_inner(); + let temp_convertion = Default::default(); + let convertion = inputs.convertion.get_or_insert(temp_convertion); + convertion.from_type = Some(inner.next().unwrap().as_str().parse().map_err(|e| { + warn!( + "Unknown type {}, types available : {}", + e, + StorageMethod::VARIANTS.join(", ") + ); + e + })?); + convertion.from = Some(inner.next().unwrap().as_str().into()); + } + Rule::convert_reconfigure => { + inputs.convert_reconfigure = true; + } // === setpolicies === Rule::cmd_policy => { inputs.action = InputAction::Set; @@ -443,7 +482,6 @@ mod test { fn get_inputs(args: &str) -> Inputs { let binding = make_args(args); - println!("{}", binding); let args = Cli::parse(Rule::cli, &binding); let args = match args { Ok(v) => v, diff --git a/src/chsr/cli/process.rs b/src/chsr/cli/process.rs index 6a4927e5..d427c061 100644 --- a/src/chsr/cli/process.rs +++ b/src/chsr/cli/process.rs @@ -1,3 +1,4 @@ +mod convert; mod json; use std::{cell::RefCell, error::Error, rc::Rc}; @@ -11,7 +12,7 @@ use rar_common::{ options::{Opt, OptType}, structs::{IdTask, RoleGetter}, }, - Storage, + FullSettingsFile, }; use super::{ @@ -19,7 +20,12 @@ use super::{ usage, }; -pub fn process_input(storage: &Storage, inputs: Inputs) -> Result> { +pub fn process_input( + storage: &Rc>, + inputs: Inputs, +) -> Result> { + let binding = storage.as_ref().borrow(); + let rconfig = binding.config.as_ref().unwrap(); match inputs { Inputs { action: InputAction::Help, @@ -34,29 +40,27 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => { - debug!("chsr list"); - match json::list_json( - rconfig, - role_id, - task_id, - options, - options_type, - task_type, - role_type, - ) { - Ok(_) => { - debug!("chsr list ok"); - Ok(false) - } - Err(e) => { - debug!("chsr list err {:?}", e); - Err(e) - } + } => { + debug!("chsr list"); + match json::list_json( + rconfig, + role_id, + task_id, + options, + options_type, + task_type, + role_type, + ) { + Ok(_) => { + debug!("chsr list ok"); + Ok(false) + } + Err(e) => { + debug!("chsr list err {:?}", e); + Err(e) } } - }, + } Inputs { // chsr role r1 add|del action, @@ -67,9 +71,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => role_add_del(rconfig, action, role_id, role_type), - }, + } => role_add_del(rconfig, action, role_id, role_type), + Inputs { // chsr role r1 grant|revoke -u u1 -u u2 -g g1,g2 action, @@ -77,9 +80,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => grant_revoke(rconfig, role_id, action, actors), - }, + } => grant_revoke(rconfig, role_id, action, actors), Inputs { // chsr role r1 task t1 add|del @@ -96,9 +97,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => task_add_del(rconfig, role_id, action, task_id, task_type), - }, + } => task_add_del(rconfig, role_id, action, task_id, task_type), + Inputs { //chsr role r1 task t1 cred set --caps "cap_net_raw,cap_sys_admin" action: InputAction::Set, @@ -112,16 +112,15 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => cred_set( - rconfig, - role_id, - task_id, - cred_caps, - cred_setuid, - cred_setgid, - ), - }, + } => cred_set( + rconfig, + role_id, + task_id, + cred_caps, + cred_setuid, + cred_setgid, + ), + Inputs { //chsr role r1 task t1 cred unset --caps "cap_net_raw,cap_sys_admin" action: InputAction::Del, @@ -135,16 +134,15 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => cred_unset( - rconfig, - role_id, - task_id, - cred_caps, - cred_setuid, - cred_setgid, - ), - }, + } => cred_unset( + rconfig, + role_id, + task_id, + cred_caps, + cred_setuid, + cred_setgid, + ), + Inputs { action, role_id: Some(role_id), @@ -154,20 +152,15 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => { - cred_caps(rconfig, role_id, task_id, setlist_type, action, pcred_caps) - } - }, + } => cred_caps(rconfig, role_id, task_id, setlist_type, action, pcred_caps), Inputs { role_id: Some(role_id), task_id: Some(task_id), cred_policy: Some(cred_policy), options: false, .. - } => match storage { - Storage::SConfig(rconfig) => cred_setpolicy(rconfig, role_id, task_id, cred_policy), - }, + } => cred_setpolicy(rconfig, role_id, task_id, cred_policy), + Inputs { // chsr role r1 task t1 command whitelist add c1 action, @@ -176,19 +169,14 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => { - cmd_whitelist_action(rconfig, role_id, task_id, cmd_id, setlist_type, action) - } - }, + } => cmd_whitelist_action(rconfig, role_id, task_id, cmd_id, setlist_type, action), Inputs { role_id: Some(role_id), task_id: Some(task_id), cmd_policy: Some(cmd_policy), .. - } => match storage { - Storage::SConfig(rconfig) => cmd_setpolicy(rconfig, role_id, task_id, cmd_policy), - }, + } => cmd_setpolicy(rconfig, role_id, task_id, cmd_policy), + // Set options Inputs { // chsr o env set A,B,C @@ -199,11 +187,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => { - env_set_policylist(rconfig, role_id, task_id, options_env, options_env_policy) - } - }, + } => env_set_policylist(rconfig, role_id, task_id, options_env, options_env_policy), Inputs { // chsr o root set privileged action: InputAction::Set, @@ -211,9 +195,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => set_privileged(rconfig, role_id, task_id, options_root), - }, + } => set_privileged(rconfig, role_id, task_id, options_root), + Inputs { // chsr o bounding set strict action: InputAction::Set, @@ -221,9 +204,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => set_bounding(rconfig, role_id, task_id, options_bounding), - }, + } => set_bounding(rconfig, role_id, task_id, options_bounding), + Inputs { // chsr o bounding set strict action: InputAction::Set, @@ -231,9 +213,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => set_authentication(rconfig, role_id, task_id, options_auth), - }, + } => set_authentication(rconfig, role_id, task_id, options_auth), + Inputs { // chsr o wildcard-denied set ";&*$" action, @@ -242,11 +223,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => { - json_wildcard(rconfig, role_id, task_id, action, options_wildcard) - } - }, + } => json_wildcard(rconfig, role_id, task_id, action, options_wildcard), Inputs { // chsr o path whitelist set a:b:c action: InputAction::Set, @@ -256,11 +233,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => { - path_set(rconfig, role_id, task_id, setlist_type, options_path) - } - }, + } => path_set(rconfig, role_id, task_id, setlist_type, options_path), Inputs { // chsr o path whitelist set a:b:c action: InputAction::Purge, @@ -270,9 +243,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => path_purge(rconfig, role_id, task_id, setlist_type), - }, + } => path_purge(rconfig, role_id, task_id, setlist_type), + Inputs { // chsr o env whitelist set A,B,C action: InputAction::Set, @@ -284,11 +256,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => { - env_whitelist_set(rconfig, role_id, task_id, setlist_type, options_env) - } - }, + } => env_whitelist_set(rconfig, role_id, task_id, setlist_type, options_env), Inputs { // chsr o timeout unset --type --duration --max-usage action: InputAction::Del, @@ -298,9 +266,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => unset_timeout(rconfig, role_id, task_id, timeout_arg), - }, + } => unset_timeout(rconfig, role_id, task_id, timeout_arg), + Inputs { // chsr o timeout set --type tty --duration 00:00:00 --max-usage 1 action: InputAction::Set, @@ -312,16 +279,14 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => set_timeout( - rconfig, - role_id, - task_id, - timeout_type, - timeout_duration, - timeout_max_usage, - ), - }, + } => set_timeout( + rconfig, + role_id, + task_id, + timeout_type, + timeout_duration, + timeout_max_usage, + ), Inputs { // chsr o path setpolicy delete-all @@ -331,11 +296,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => { - path_setpolicy(rconfig, role_id, task_id, options_path_policy) - } - }, + } => path_setpolicy(rconfig, role_id, task_id, options_path_policy), Inputs { // chsr o path whitelist add path1:path2:path3 action, @@ -347,17 +308,16 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => env_setlist_add( - rconfig, - role_id, - task_id, - setlist_type, - action, - options_key_env, - options_env_values, - ), - }, + } => env_setlist_add( + rconfig, + role_id, + task_id, + setlist_type, + action, + options_key_env, + options_env_values, + ), + Inputs { // chsr o path whitelist add path1:path2:path3 action, @@ -367,16 +327,15 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => path_setlist2( - rconfig, - role_id, - task_id, - setlist_type, - action, - options_path, - ), - }, + } => path_setlist2( + rconfig, + role_id, + task_id, + setlist_type, + action, + options_path, + ), + Inputs { // chsr o env setpolicy delete-all role_id, @@ -385,10 +344,14 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::SConfig(rconfig) => env_setpolicy(rconfig, role_id, task_id, options_env_policy), - }, + } => env_setpolicy(rconfig, role_id, task_id, options_env_policy), + Inputs { + action: InputAction::Convert, + convertion: Some(convertion), + convert_reconfigure, + .. + } => convert::convert(storage, convertion, convert_reconfigure), _ => Err("Unknown Input".into()), } } diff --git a/src/chsr/cli/process/convert.rs b/src/chsr/cli/process/convert.rs new file mode 100644 index 00000000..bd5b9fc7 --- /dev/null +++ b/src/chsr/cli/process/convert.rs @@ -0,0 +1,91 @@ +use std::{ + cell::RefCell, + error::Error, + fs::File, + io::{BufWriter, Write}, + path::Path, + rc::Rc, +}; + +use log::{debug, error}; +use rar_common::{retrieve_sconfig, FullSettingsFile, StorageMethod}; + +use crate::{cli::data::Convertion, ROOTASROLE}; + +pub fn convert( + settings: &Rc>, + convertion: Convertion, + convert_reconfigure: bool, +) -> Result> { + debug!("chsr convert"); + let mut settings = settings.borrow_mut(); + let default = Default::default(); + let default_path = Default::default(); + let path = settings + .storage + .settings + .as_ref() + .unwrap_or(&default) + .path + .as_ref() + .unwrap_or(&default_path); + let config = match convertion.from { + Some(ref from) => { + debug!("Convert from: {:?}", from); + let from_type = convertion.from_type.expect("Impossible state"); + if from == &convertion.to { + error!("The source and destination paths are the same"); + return Ok(false); + } + if from != path { + retrieve_sconfig(&from_type, from)? + } else { + settings + .config + .as_ref() + .expect("A configuration should be loaded") + .clone() + } + } + None => settings + .config + .clone() + .expect("A configuration should be loaded"), + }; + if !convert_reconfigure && convertion.to != *path { + write_config_file(&convertion, config) + } else if convert_reconfigure { + if convertion.to_type != StorageMethod::JSON && convertion.to == Path::new(ROOTASROLE) { + error!("The general settings file cannot be converted to another format than JSON\nThis file is used to determine the policy location and format. Please specify another path."); + return Ok(false); + } + settings.storage.method = convertion.to_type; + let mut remote = settings.storage.settings.clone().unwrap_or_default(); + remote.path = Some(convertion.to); + settings.storage.settings = Some(remote); + Ok(true) + } else { + error!("You are overwriting the current configuration file but you not specified the reconfigure (-r) option, this would break the current configuration"); + Ok(false) + } +} + +fn write_config_file( + convertion: &Convertion, + config: Rc>, +) -> Result> { + match convertion.to_type { + StorageMethod::JSON => { + let json = serde_json::to_string_pretty(&config)?; + let file = File::create(&convertion.to)?; + let mut writer = BufWriter::new(file); + writer.write_all(json.as_bytes())?; + } + StorageMethod::CBOR => { + let file = File::create(&convertion.to)?; + let writer = BufWriter::new(file); + cbor4ii::serde::to_writer(writer, &config)?; + } + } + Ok(false) +} diff --git a/src/chsr/cli/process/json.rs b/src/chsr/cli/process/json.rs index f7443870..81d3ec42 100644 --- a/src/chsr/cli/process/json.rs +++ b/src/chsr/cli/process/json.rs @@ -27,7 +27,6 @@ pub fn list_json( role_type: Option, ) -> Result<(), Box> { let config = rconfig.as_ref().borrow(); - debug!("list_json {:?}", config); if let Some(role_id) = role_id { if let Some(role) = rconfig.role(&role_id) { list_task(task_id, &role, options, options_type, task_type, role_type) @@ -51,6 +50,7 @@ fn list_task( if let Some(task_id) = task_id { if let Some(task) = role.as_ref().borrow().task(&task_id) { if options { + debug!("task {:?}", task); let rcopt = OptStack::from_task(task.clone()).to_opt(); let opt = rcopt.as_ref().borrow(); if let Some(opttype) = options_type { @@ -326,7 +326,7 @@ pub fn cred_set( } if let Some(setgid) = cred_setgid { task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::Group(setgid.clone())); + Some(SGroupschooser::Groups(setgid.clone())); } Ok(true) } @@ -1003,21 +1003,26 @@ pub fn env_setlist_add( Some(SetListType::Set) => match action { InputAction::Add => { debug!("options_env_values: {:?}", options_env_values); - env.set.extend(options_env_values.as_ref().unwrap().clone()); + env.set + .get_or_insert_default() + .extend(options_env_values.as_ref().unwrap().clone()); } InputAction::Del => { debug!("options_env_values: {:?}", options_env_values); options_key_env.as_ref().unwrap().into_iter().for_each(|k| { - env.set.remove(&k.to_string()); + if let Some(env) = &mut env.set { + env.remove(&k.to_string()); + } }); } InputAction::Purge => { debug!("options_env_values: {:?}", options_env_values); - env.set = HashMap::new(); + env.set = None; } InputAction::Set => { debug!("options_env_values: {:?}", options_env_values); - env.set = options_env_values.as_ref().unwrap().clone(); + env.set + .replace(options_env_values.as_ref().unwrap().clone()); } _ => unreachable!("Unknown action {:?}", action), }, diff --git a/src/chsr/main.rs b/src/chsr/main.rs index e1a1d743..3e06db51 100644 --- a/src/chsr/main.rs +++ b/src/chsr/main.rs @@ -1,11 +1,6 @@ //extern crate sudoers_reader; -use log::error; -use rar_common::{ - plugin::register_plugins, - util::{drop_effective, read_effective, subsribe}, - Storage, -}; +use rar_common::util::{drop_effective, read_effective, subsribe}; mod cli; mod util; @@ -17,23 +12,15 @@ const ROOTASROLE: &str = "target/rootasrole.json"; #[cfg(not(tarpaulin_include))] fn main() -> Result<(), Box> { - use rar_common::{get_settings, save_settings, StorageMethod}; + use rar_common::{full_save_settings, get_full_settings}; subsribe("chsr")?; drop_effective()?; - register_plugins(); - let settings = get_settings(&ROOTASROLE.to_string()).expect("Error on config read"); - let config = match settings.clone().as_ref().borrow().storage.method { - StorageMethod::JSON | StorageMethod::CBOR => Storage::SConfig(settings.as_ref().borrow().config.clone().unwrap()), - StorageMethod::Unknown => { - error!("Unknown storage method"); - return Err("Unknown storage method".into()); - }, - }; + let settings = get_full_settings(&ROOTASROLE.to_string()).expect("Error on config read"); read_effective(false).expect("Operation not permitted"); - if cli::main(&config, std::env::args().skip(1)).is_ok_and(|b| b) { - save_settings(&ROOTASROLE.to_string(), settings, true) + if cli::main(settings.clone(), std::env::args().skip(1)).is_ok_and(|b| b) { + full_save_settings(&ROOTASROLE.to_string(), settings, true) } else { Ok(()) } diff --git a/src/sr/finder/api/hashchecker.rs b/src/sr/finder/api/hashchecker.rs new file mode 100644 index 00000000..bbb03bbe --- /dev/null +++ b/src/sr/finder/api/hashchecker.rs @@ -0,0 +1,632 @@ +use std::{error::Error, fs::File, io::Read, os::fd::AsRawFd, path::PathBuf}; + +use ::serde::{Deserialize, Serialize}; +use libc::FS_IOC_GETFLAGS; +use log::{debug, warn}; +use nix::unistd::{access, AccessFlags}; +use rar_common::{ + database::score::CmdMin, + util::{all_paths_from_env, match_single_path, open_with_privileges}, +}; +use serde_json::to_value; +use sha2::Digest; + +use crate::finder::cmd::match_args; + +use super::{Api, ApiEvent, EventKey}; + +#[derive(Debug, Serialize, Deserialize, Hash, Eq, PartialEq)] +#[serde(rename_all = "lowercase", untagged)] +pub enum HashElement { + SHA224 { + #[serde(rename = "sha224")] + sha224: String, + }, + SHA256 { + #[serde(rename = "sha256")] + sha256: String, + }, + SHA384 { + #[serde(rename = "sha384")] + sha384: String, + }, + SHA512 { + #[serde(rename = "sha512")] + sha512: String, + }, +} + +#[derive(Debug, Serialize, Deserialize, Hash, Eq, PartialEq)] +struct HashChecker { + #[serde(flatten)] + hash: Option, + #[serde(alias = "read-only")] + read_only: Option, + immutable: Option, + command: String, +} + +fn new_complex_command(event: &mut ApiEvent) -> Result<(), Box> { + if let ApiEvent::ProcessComplexCommand( + value, + env_path, + cmd_path, + cmd_args, + cmd_min, + final_path, + ) = event + { + let hash_checker: HashChecker = serde_json::from_value(to_value(value)?)?; + process_hash_check( + hash_checker, + env_path, + cmd_path, + cmd_args, + *cmd_min, + *final_path, + ); + } + Ok(()) +} + +fn evaluate_hash(hashtype: &HashElement, hash: &[u8]) -> bool { + match hashtype { + HashElement::SHA224 { sha224 } => { + let mut hasher = sha2::Sha224::new(); + hasher.update(hash); + hasher.finalize().to_vec() == hex::decode(sha224).unwrap() + } + HashElement::SHA256 { sha256 } => { + let mut hasher = sha2::Sha256::new(); + hasher.update(hash); + hasher.finalize().to_vec() == hex::decode(sha256).unwrap() + } + HashElement::SHA384 { sha384 } => { + let mut hasher = sha2::Sha384::new(); + hasher.update(hash); + hasher.finalize().to_vec() == hex::decode(sha384).unwrap() + } + HashElement::SHA512 { sha512 } => { + let mut hasher = sha2::Sha512::new(); + hasher.update(hash); + hasher.finalize().to_vec() == hex::decode(sha512).unwrap() + } + } +} + +const FS_IMMUTABLE_FL: u32 = 0x00000010; + +fn is_immutable(file: &File) -> Result> { + let mut val = 0; + let fd = file.as_raw_fd(); + if unsafe { nix::libc::ioctl(fd, FS_IOC_GETFLAGS, &mut val) } < 0 { + debug!("Error getting flags {:?}", std::io::Error::last_os_error()); + return Err("Error getting flags".into()); + } + Ok(val & FS_IMMUTABLE_FL != 0) +} + +fn match_path( + checker: &HashChecker, + env_path: &[&str], + cmd_path: &PathBuf, + role_path: &String, + final_path: &mut Option, +) -> CmdMin { + if role_path == "**" { + return CmdMin::FullWildcardPath; + } else if cmd_path.is_absolute() { + let min = match_single_path(cmd_path, role_path); + verify_executable_conditions(checker, final_path, cmd_path, min).unwrap_or_default() + } else { + all_paths_from_env(env_path, cmd_path) + .iter() + .find_map(|cmd_path| { + let min = match_single_path(cmd_path, role_path); + verify_executable_conditions(checker, final_path, cmd_path, min) + }) + .unwrap_or_default() + } +} + +fn verify_executable_conditions( + checker: &HashChecker, + final_path: &mut Option, + cmd_path: &PathBuf, + min: CmdMin, +) -> Option { + if min.matching() { + if checker.read_only.is_some_and(|read_only| read_only) { + if access(cmd_path, AccessFlags::W_OK).is_ok() { + warn!("File should be read only but has write access"); + return None; + } + warn!("Executor has write access to the executable, this could lead to a race condition vulnerability"); + } + let open = open_with_privileges(cmd_path); + if open.is_err() { + return None; + } + let mut open = open.unwrap(); + if checker.immutable.is_some_and(|immutable| immutable) { + let is_immutable = is_immutable(&open); + if is_immutable.is_err() { + return None; + } + if !is_immutable.unwrap() { + warn!("File should be immutable but is not"); + return None; + } + } + if let Some(hash_element) = &checker.hash { + let mut buf = Vec::new(); + let res = open.read_to_end(&mut buf); + if res.is_err() { + warn!("Error reading file {:?}", res); + return None; + } + if !evaluate_hash(&hash_element, &buf) { + warn!("Hash does not match"); + return None; + } + } + *final_path = Some(cmd_path.clone()); + Some(min) + } else { + None + } +} + +/// Check if input command line is matching with role command line and return the score +fn match_command_line( + checker: &HashChecker, + env_path: &[&str], + cmd_path: &PathBuf, + cmd_args: &[String], + role_command: &[String], + cmd_min: &mut CmdMin, + final_path: &mut Option, +) { + let mut result = match_path(checker, env_path, cmd_path, &role_command[0], final_path); + if result.is_empty() || role_command.len() == 1 { + *cmd_min = result; + return; + } + match match_args(cmd_args, &shell_words::join(&role_command[1..])) { + Ok(args_result) => result |= args_result, + Err(err) => { + debug!("Error: {}", err); + return; + } + } + *cmd_min = result; +} + +fn process_hash_check( + checker: HashChecker, + env_path: &[&str], + cmd_path: &PathBuf, + cmd_args: &[String], + min_score: &mut CmdMin, + final_path: &mut Option, +) { + match shell_words::split(&checker.command) + .map_err(|e| Into::>::into(e)) + { + Ok(command) => { + match_command_line( + &checker, env_path, cmd_path, cmd_args, &command, min_score, final_path, + ); + } + Err(err) => { + warn!("Error: {}", err); + } + } +} + +pub fn register() { + Api::register(EventKey::NewComplexCommand, new_complex_command); +} + +#[cfg(test)] +mod tests { + use std::{ + fs::File, + io::Read, + path::{Path, PathBuf}, + }; + + use capctl::{CapSet, CapState}; + use log::debug; + use nix::sys::stat::{fchmodat, Mode}; + use rar_common::{ + database::score::CmdMin, + util::{immutable_effective, toggle_lock_config}, + }; + use serde::de::DeserializeSeed; + use sha2::{Digest, Sha224, Sha256, Sha384, Sha512}; + + use crate::finder::{api::hashchecker::register, de::DCommandDeserializer}; + pub struct Defer(Option); + + impl Defer { + pub fn new(f: F) -> Self { + Defer(Some(f)) + } + } + + impl Drop for Defer { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } + } + + pub fn defer(f: F) -> Defer { + Defer::new(f) + } + + fn set_read_only(path: &Path) -> nix::Result<()> { + // Set permissions to read-only for owner, group, and others + fchmodat( + None, // Relative to the current directory + path, + Mode::S_IRUSR // Owner read + | Mode::S_IRGRP // Group read + | Mode::S_IROTH, // Others read + nix::sys::stat::FchmodatFlags::NoFollowSymlink, // No special flags + )?; + Ok(()) + } + + #[test] + fn test_dcommand_seed_hashchecker() { + register(); + let filename = "test.sh"; + let _cleanup = defer(|| { + let filename = PathBuf::from(filename) + .canonicalize() + .unwrap_or(filename.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + //create the file + File::create(&filename).unwrap(); + let filename = PathBuf::from(filename).canonicalize().unwrap(); + //call sha256sum on the file + + let mut file = File::open(&filename).unwrap(); + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).unwrap(); + + let mut sha224hasher = Sha224::new(); + sha224hasher.update(&buffer); + let sha224 = sha224hasher.finalize(); + let json = format!( + r#"{{"sha224": "{:x}", "command": "{} -l"}}"#, + sha224, + &filename.display() + ); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + assert_eq!(final_path, Some(PathBuf::from(&filename))); + assert_eq!(cmd_min, CmdMin::Match); + + let mut sha256hasher = Sha256::new(); + sha256hasher.update(&buffer); + let sha256 = sha256hasher.finalize(); + + let json = format!( + r#"{{"sha256": "{:x}", "command": "{} -l"}}"#, + sha256, + &filename.display() + ); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + assert_eq!(final_path, Some(PathBuf::from(&filename))); + assert_eq!(cmd_min, CmdMin::Match); + + let mut sha384hasher = Sha384::new(); + sha384hasher.update(&buffer); + let sha384 = sha384hasher.finalize(); + + let json = format!( + r#"{{"sha384": "{:x}", "command": "{} -l"}}"#, + sha384, + &filename.display() + ); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + assert_eq!(final_path, Some(PathBuf::from(&filename))); + assert_eq!(cmd_min, CmdMin::Match); + + let mut sha512hasher = Sha512::new(); + sha512hasher.update(&buffer); + let sha512 = sha512hasher.finalize(); + let json = format!( + r#"{{"sha512": "{:x}", "command": "{} -l"}}"#, + sha512, + &filename.display() + ); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + assert_eq!(final_path, Some(PathBuf::from(&filename))); + assert_eq!(cmd_min, CmdMin::Match); + } + + #[test] + fn test_read_only_immutable() { + register(); + // remove root privileges + let current = CapState::get_current(); + let mut current = current.unwrap(); + current.effective = CapSet::empty(); + current.permitted = CapSet::empty(); + current.inheritable = CapSet::empty(); + current.set_current().unwrap(); + let filename = "/tmp/test_ro.sh"; + let _cleanup = defer(|| { + let filename = PathBuf::from(filename) + .canonicalize() + .unwrap_or(filename.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + //create the file + File::create(&filename).unwrap(); + + let filename = PathBuf::from(filename).canonicalize().unwrap(); + //call sha256sum on the file + + let json = format!( + r#"{{"read-only": true, "immutable": true, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + + let json = format!( + r#"{{"read-only": true, "immutable": false, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + + let json = format!( + r#"{{"read-only": false, "immutable": true, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + + let json = format!( + r#"{{"read-only": false, "immutable": false, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, PathBuf::from(&filename).canonicalize().ok()); + assert_eq!(cmd_min, CmdMin::Match); + + set_read_only(filename.as_path()).unwrap(); + + let json = format!( + r#"{{"read-only": true, "immutable": false, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, PathBuf::from(&filename).canonicalize().ok()); + assert_eq!(cmd_min, CmdMin::Match); + + let json = format!( + r#"{{"read-only": true, "immutable": true, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + + let json = format!( + r#"{{"read-only": true, "immutable": true, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + + let mut immutable = false; + if immutable_effective(true).is_ok() { + toggle_lock_config(&filename, rar_common::util::ImmutableLock::Unset).unwrap(); + immutable = true; + immutable_effective(false).unwrap(); + } + let json = format!( + r#"{{"read-only": true, "immutable": {}, "command": "{}"}}"#, + immutable, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, PathBuf::from(&filename).canonicalize().ok()); + assert_eq!(cmd_min, CmdMin::Match); + + let json = format!( + r#"{{"read-only": true, "immutable": true, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + if immutable { + assert_eq!(final_path, PathBuf::from(&filename).canonicalize().ok()); + assert_eq!(cmd_min, CmdMin::Match); + } else { + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + } + } +} diff --git a/src/sr/finder/api/hierarchy.rs b/src/sr/finder/api/hierarchy.rs new file mode 100644 index 00000000..a89d06ae --- /dev/null +++ b/src/sr/finder/api/hierarchy.rs @@ -0,0 +1,69 @@ +use std::error::Error; + +use bon::builder; +use serde_json_borrow::Value; + +use crate::{ + finder::{de::DLinkedRole, options::BorrowedOptStack, BestExecSettings}, + Cli, +}; + +/// This module is not thread-safe. +use super::{Api, ApiEvent, EventKey}; + +fn find_in_parents(event: &mut ApiEvent) -> Result<(), Box> { + if let ApiEvent::BestRoleSettingsFound(cli, role, opt_stack, env_path, settings, matching) = + event + { + return match role.role()._extra_values.get("parents") { + Some(Value::Array(parents)) => { + let mut parents = parents.iter(); + while let Some(Value::Str(parent)) = parents.next() { + evaluate_parent_role() + .parent(parent.as_ref()) + .cli(cli) + .role(role) + .opt_stack(opt_stack) + .settings(settings) + .matching(matching) + .env_path(&env_path) + .call()?; + } + Ok(()) + } + Some(Value::Str(parent)) => evaluate_parent_role() + .parent(parent.as_ref()) + .cli(cli) + .role(role) + .opt_stack(opt_stack) + .settings(settings) + .matching(matching) + .env_path(&env_path) + .call(), + Some(_) => Err("Invalid parent value".into()), + None => Ok(()), + }; + } + Ok(()) +} + +#[builder] +fn evaluate_parent_role<'a>( + parent: &str, + cli: &mut &Cli, + role: &mut &DLinkedRole<'_, 'a>, + opt_stack: &mut BorrowedOptStack<'a>, + settings: &mut &mut BestExecSettings, + matching: &mut &mut bool, + env_path: &[&str], +) -> Result<(), Box> { + Ok(if let Some(role) = role.config().role(parent) { + for task in role.tasks() { + **matching |= settings.task_settings(cli, &task, opt_stack, env_path)?; + } + }) +} + +pub fn register() { + Api::register(EventKey::BestRoleSettings, find_in_parents); +} diff --git a/src/sr/finder/api/mod.rs b/src/sr/finder/api/mod.rs new file mode 100644 index 00000000..ada4a059 --- /dev/null +++ b/src/sr/finder/api/mod.rs @@ -0,0 +1,125 @@ +use std::{cell::UnsafeCell, collections::HashMap, error::Error, path::PathBuf}; + +use once_cell::sync::Lazy; +use rar_common::database::score::{CmdMin, Score}; +use serde_json_borrow::Value; +use strum::Display; + +use crate::Cli; + +use super::{ + de::{DConfigFinder, DLinkedRole, DLinkedTask}, + options::BorrowedOptStack, + BestExecSettings, +}; + +mod hashchecker; +mod hierarchy; +mod ssd; + +thread_local! { + static API: Lazy> = Lazy::new(|| UnsafeCell::new(Api::new())); +} + +pub struct Api { + callbacks: + HashMap Result<(), Box> + Send>>>, +} + +#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Display)] +pub enum EventKey { + BestGlobalSettings, + BestRoleSettings, + BestTaskSettings, + NewComplexCommand, + ActorMatching, +} + +#[allow(dead_code)] +pub enum ApiEvent<'a, 't, 'c, 'f, 'g, 'h, 'i, 'j, 'k> { + BestGlobalSettingsFound( + &'f Cli, + &'g DConfigFinder<'a>, + &'j mut BorrowedOptStack<'a>, + &'h mut BestExecSettings, + &'i mut bool, + ), + BestRoleSettingsFound( + &'f Cli, + &'g DLinkedRole<'c, 'a>, + &'h mut BorrowedOptStack<'a>, + &'k &'k [&'k str], + &'i mut BestExecSettings, + &'j mut bool, + ), + BestTaskSettingsFound( + &'f Cli, + &'g DLinkedTask<'t, 'c, 'a>, + &'j mut BorrowedOptStack<'a>, + &'h mut BestExecSettings, + &'i mut Score, + ), + // NewComplexCommand (Value, env_path, cmd_path, cmd_args, cmd_min, final_path), + ProcessComplexCommand( + &'f Value<'a>, + &'g [&'g str], + &'h PathBuf, + &'i [String], + &'j mut CmdMin, + &'k mut Option, + ), + ActorMatching( + &'f DLinkedRole<'c, 'a>, + &'g mut BestExecSettings, + &'h mut bool, + ), +} + +impl ApiEvent<'_, '_, '_, '_, '_, '_, '_, '_, '_> { + fn get_key(&self) -> EventKey { + match self { + ApiEvent::BestGlobalSettingsFound(..) => EventKey::BestGlobalSettings, + ApiEvent::BestRoleSettingsFound(..) => EventKey::BestRoleSettings, + ApiEvent::BestTaskSettingsFound(..) => EventKey::BestTaskSettings, + ApiEvent::ProcessComplexCommand(..) => EventKey::NewComplexCommand, + ApiEvent::ActorMatching(..) => EventKey::ActorMatching, + } + } +} + +impl Api { + fn new() -> Self { + Api { + callbacks: HashMap::new(), + } + } + pub fn notify(mut event: ApiEvent) -> Result<(), Box> { + let key = event.get_key(); + API.with(|api| -> Result<(), Box> { + let api = unsafe { &mut *api.get() }; + if let Some(callbacks) = api.callbacks.get(&key) { + for callback in callbacks.iter() { + callback(&mut event)?; + } + } + Ok(()) + })?; + Ok(()) + } + pub fn register(event: EventKey, function: F) + where + F: Fn(&mut ApiEvent) -> Result<(), Box> + Send + 'static, + { + API.with(|api| unsafe { + let api = &mut *api.get(); + let callbacks = api.callbacks.entry(event).or_insert_with(Vec::new); + callbacks.push(Box::new(function)); + }); + } +} + +pub(super) fn register_plugins() { + ssd::register(); + hashchecker::register(); + hierarchy::register(); +} diff --git a/src/sr/finder/api/ssd.rs b/src/sr/finder/api/ssd.rs new file mode 100644 index 00000000..a76476aa --- /dev/null +++ b/src/sr/finder/api/ssd.rs @@ -0,0 +1,76 @@ +use std::{ + collections::HashSet, + error::Error, + hash::{DefaultHasher, Hash, Hasher}, +}; + +use log::error; +use serde_json_borrow::Value; + +use crate::finder::de::DLinkedRole; + +use super::{Api, ApiEvent}; + +fn calculate_hash(value: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() +} + +fn check_ssd_recursive( + role: &DLinkedRole, + visited: &mut HashSet, +) -> Result> { + if let Some(Value::Array(ssd)) = role.role()._extra_values.get("ssd") { + for ssd in ssd.iter() { + if let Value::Str(ssd) = ssd { + if visited.contains(&calculate_hash(ssd)) { + continue; // Avoid infinite recursion + } + visited.insert(calculate_hash(ssd)); + if let Some(r) = role.config().role(ssd) { + if r.role().user_min.matching() { + return Ok(true); + } + if check_ssd_recursive(&r, visited)? { + return Ok(true); + } + } + } + } + } else if let Some(Value::Str(ssd)) = role.role()._extra_values.get("ssd") { + if visited.contains(&calculate_hash(ssd)) { + return Ok(false); // Avoid infinite recursion + } + visited.insert(calculate_hash(ssd)); + if let Some(r) = role.config().role(ssd) { + if r.role().user_min.matching() { + return Ok(true); + } + if check_ssd_recursive(&r, visited)? { + return Ok(true); + } + } + } else if let Some(_) = role.role()._extra_values.get("ssd") { + error!("Invalid SSD value"); + return Err("Invalid SSD value".into()); + } + Ok(false) +} + +fn check_ssd(event: &mut ApiEvent) -> Result<(), Box> { + if let ApiEvent::ActorMatching(role, _settings, matching) = event { + if role.role().user_min.matching() { + let mut visited: HashSet = HashSet::new(); + if check_ssd_recursive(role, &mut visited)? { + **matching = false; + return Ok(()); + } + } + } + Ok(()) +} + +pub fn register() { + Api::register(super::EventKey::ActorMatching, check_ssd); +} diff --git a/src/sr/finder/cmd.rs b/src/sr/finder/cmd.rs new file mode 100644 index 00000000..1095874e --- /dev/null +++ b/src/sr/finder/cmd.rs @@ -0,0 +1,533 @@ +use log::{debug, warn}; +use rar_common::{ + database::score::CmdMin, + util::{all_paths_from_env, match_single_path}, +}; +use std::path::PathBuf; + +fn match_path( + env_path: &[&str], + cmd_path: &PathBuf, + role_path: &String, + previous_min: &CmdMin, + final_path: &mut Option, +) -> CmdMin { + if role_path == "**" { + return CmdMin::FullWildcardPath; + } else if cmd_path.is_absolute() { + let min = match_single_path(cmd_path, role_path); + if min.better(&previous_min) { + *final_path = Some(cmd_path.clone()); + } + return min; + } else { + all_paths_from_env(env_path, cmd_path) + .iter() + .find_map(|cmd_path| { + let min = match_single_path(cmd_path, role_path); + if min.better(&previous_min) { + *final_path = Some(cmd_path.clone()); + Some(min) + } else { + None + } + }) + .unwrap_or_default() + } +} + +/// Check if input args is matching with role args and return the score +/// role args can contains regex +/// input args is the command line args +pub(super) fn match_args( + input_args: &[String], + role_args: &str, +) -> Result> { + if role_args == "'^.*$'" { + return Ok(CmdMin::FullRegexArgs); + } + let commandline = shell_words::join(input_args); + if role_args.starts_with("\'^") && role_args.ends_with("$\'") { + evaluate_regex_cmd(role_args.trim_matches('\''), &commandline).inspect_err(|e| { + debug!("{:?},No match for args {:?}", e, input_args); + }) + } else if commandline == role_args { + Ok(CmdMin::Match) + } else { + Ok(CmdMin::empty()) + } +} + +#[cfg(feature = "pcre2")] +fn evaluate_regex_cmd( + role_args: &str, + commandline: &str, +) -> Result> { + use pcre2::bytes::RegexBuilder; + + let regex = RegexBuilder::new().build(&role_args)?; + if regex.is_match(commandline.as_bytes())? { + Ok(CmdMin::RegexArgs) + } else { + Ok(CmdMin::empty()) + } +} + +#[cfg(not(feature = "pcre2"))] +fn evaluate_regex_cmd( + _role_args: &str, + _commandline: &str, +) -> Result> { + Ok(CmdMin::empty()) +} + +/// Check if input command line is matching with role command line and return the score +fn match_command_line( + env_path: &[&str], + cmd_path: &PathBuf, + cmd_args: &[String], + role_command: &[String], + previous_min: &CmdMin, + final_path: &mut Option, +) -> CmdMin { + if role_command.is_empty() { + return CmdMin::empty(); + } + let mut result = match_path( + env_path, + &cmd_path, + &role_command[0], + previous_min, + final_path, + ); + if result.is_empty() || role_command.len() == 1 { + debug!("result : {:?}", result); + return result; + } + match match_args(cmd_args, &shell_words::join(&role_command[1..])) { + Ok(args_result) => { + if args_result.is_empty() { + return CmdMin::empty(); + } + result |= args_result; + } + Err(err) => { + debug!("Error: {}", err); + return CmdMin::empty(); + } + } + result +} + +pub fn evaluate_command_match( + env_path: &[&str], + cmd_path: &PathBuf, + cmd_args: &[String], + role_cmd: &str, + previous_min: &CmdMin, + final_path: &mut Option, +) -> CmdMin { + match shell_words::split(role_cmd).map_err(|e| Into::>::into(e)) { + Ok(role_cmd) => match_command_line( + env_path, + cmd_path, + cmd_args, + &role_cmd, + previous_min, + final_path, + ), + Err(err) => { + warn!("Error: {}", err); + CmdMin::empty() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use test_log::test; + + #[test] + fn test_match_path_full_wildcard() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let role_path = String::from("**"); + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_path( + &env_path, + &cmd_path, + &role_path, + &previous_min, + &mut final_path, + ); + assert_eq!(result, CmdMin::FullWildcardPath); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_path() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let role_path = String::from("/bin/ls"); + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_path( + &env_path, + &cmd_path, + &role_path, + &previous_min, + &mut final_path, + ); + assert!(result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_match_path_absolute_no_match() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("/usr/local/bin/ls"); + let role_path = String::from("/bin/ls"); + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_path( + &env_path, + &cmd_path, + &role_path, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_path_absolute_match() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("/bin/ls"); + let role_path = String::from("/bin/ls"); + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_path( + &env_path, + &cmd_path, + &role_path, + &previous_min, + &mut final_path, + ); + assert!(result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_match_path_not_found_in_env() { + let env_path = ["/usr/local/sbin"]; + let cmd_path = PathBuf::from("ls"); + let role_path = String::from("/bin/ls"); + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_path( + &env_path, + &cmd_path, + &role_path, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_args() { + let input_args = vec!["-l".to_string(), "/tmp".to_string()]; + let role_args = "-l /tmp"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::Match); + } + + #[cfg(feature = "pcre2")] + #[test] + fn test_match_args_full_regex() { + let input_args = vec!["foo".to_string(), "bar".to_string()]; + let role_args = "'^.*$'"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::FullRegexArgs); + } + + #[cfg(feature = "pcre2")] + #[test] + fn test_match_args_full_regex_empty_input() { + let input_args: Vec = vec![]; + let role_args = "'^.*$'"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::FullRegexArgs); + } + + #[cfg(feature = "pcre2")] + #[test] + fn test_match_args_regex_args() { + let input_args: Vec = vec!["a".to_string(), "A".to_string()]; + let role_args = "'^[Aa ]*$'"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::RegexArgs); + let role_args = "'^[Aa]*$'"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::empty()); + } + + #[test] + fn test_match_args_no_match() { + let input_args = vec!["-a".to_string()]; + let role_args = "-l"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::empty()); + } + + #[test] + fn test_match_args_input_longer_than_role() { + let input_args = vec!["-l".to_string(), "/tmp".to_string(), "extra".to_string()]; + let role_args = "-l /tmp"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::empty()); + } + + #[test] + fn test_match_args_input_shorter_than_role() { + let input_args = vec!["-l".to_string()]; + let role_args = "-l /tmp"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::empty()); + } + + #[test] + fn test_match_command_line() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args = vec!["-l".to_string(), "/tmp".to_string()]; + let role_command = vec!["/bin/ls".to_string(), "-l".to_string(), "/tmp".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert!(result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[cfg(feature = "pcre2")] + #[test] + fn test_match_command_line_args_mismatch() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args = vec!["-a".to_string()]; + let role_command = vec!["/bin/ls".to_string(), "-l".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_match_command_line_empty_cmd_args_multi_role_args() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec![]; + let role_command = vec!["/bin/ls".to_string(), "-l".to_string(), "/tmp".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_match_command_line_single_arg() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec![]; + let role_command = vec!["/bin/ls".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert!(result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_match_command_line_empty_role_command() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec![]; + let role_command: Vec = vec![]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_command_line_role_command_only_args() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args = vec!["-l".to_string()]; + let role_command = vec!["-l".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + // Should not match, as the binary is not specified + assert!(!result.matching()); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_command_line_role_command_only_wildcard() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec![]; + let role_command = vec!["**".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert_eq!(result, CmdMin::FullWildcardPath); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_command_line_previous_min_set() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec!["-l".to_string()]; + let role_command = vec!["/bin/l*".to_string(), "^.*$".to_string()]; + let previous_min = CmdMin::Match; // better than regex + let mut final_path = Some("/usr/bin/ls".into()); + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert_eq!(result, CmdMin::empty()); + assert_eq!(final_path, Some("/usr/bin/ls".into())); + } + + #[test] + fn test_evaluate_command_match() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args = vec!["-l".to_string(), "/tmp".to_string()]; + let role_cmd = "/bin/ls -l /tmp"; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = evaluate_command_match( + &env_path, + &cmd_path, + &cmd_args, + role_cmd, + &previous_min, + &mut final_path, + ); + assert!(result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_evaluate_command_match_invalid_role_cmd() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args = vec!["-l".to_string(), "/tmp".to_string()]; + let role_cmd = "\"unterminated string"; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = evaluate_command_match( + &env_path, + &cmd_path, + &cmd_args, + role_cmd, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, None); + } + + #[test] + fn test_evaluate_command_match_only_wildcard() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec![]; + let role_cmd = "**"; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = evaluate_command_match( + &env_path, + &cmd_path, + &cmd_args, + role_cmd, + &previous_min, + &mut final_path, + ); + assert_eq!(result, CmdMin::FullWildcardPath); + assert_eq!(final_path, None); + } +} diff --git a/src/sr/finder/de.rs b/src/sr/finder/de.rs new file mode 100644 index 00000000..574d7671 --- /dev/null +++ b/src/sr/finder/de.rs @@ -0,0 +1,2826 @@ +use std::{borrow::Cow, collections::HashMap, fmt::Display, ops::Deref, path::PathBuf}; + +use bon::Builder; +use capctl::CapSet; +use derivative::Derivative; +use log::{debug, info}; +use nix::unistd::Group; +use rar_common::{ + database::{ + actor::{DActor, DGroupType, DGroups, DUserType}, + options::Level, + score::{ + ActorMatchMin, CapsMin, CmdMin, Score, SecurityMin, SetgidMin, SetuidMin, TaskScore, + }, + structs::{SCapabilities, SetBehavior}, + }, + util::capabilities_are_exploitable, + Cred, +}; +use serde::{ + de::{DeserializeSeed, IgnoredAny, Visitor}, + Deserialize, +}; +use serde_json_borrow::Value; +use strum::EnumIs; + +use crate::{ + finder::{ + api::{Api, ApiEvent}, + cmd, + options::DPathOptions, + }, + Cli, +}; + +use super::options::Opt; + +#[cfg_attr(test, derive(Builder))] +#[derive(PartialEq, Eq, Debug, Default)] +pub struct DConfigFinder<'a> { + pub options: Option>, + pub roles: Vec>, +} + +#[cfg_attr(test, derive(Builder))] +#[derive(Debug, Derivative)] +#[derivative(PartialEq, Eq)] +pub struct DRoleFinder<'a> { + #[cfg_attr(test, builder(default))] + pub user_min: ActorMatchMin, + #[cfg_attr(test, builder(into))] + pub role: Cow<'a, str>, + #[cfg_attr(test, builder(default))] + pub tasks: Vec>, + pub options: Option>, + #[cfg_attr(test, builder(default))] + pub _extra_values: HashMap, Value<'a>>, +} + +#[derive(Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] +#[serde(untagged)] +pub enum IdTask<'a> { + Name(#[serde(borrow)] Cow<'a, str>), + Number(usize), +} + +impl Display for IdTask<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IdTask::Name(name) => write!(f, "{}", name), + IdTask::Number(num) => write!(f, "{}", num), + } + } +} + +#[derive(Debug, Derivative, Builder)] +#[derivative(PartialEq, Eq)] +pub struct DTaskFinder<'a> { + pub id: IdTask<'a>, + #[builder(default)] + pub score: TaskScore, + pub setuid: Option>, + pub setgroups: Option>, + pub caps: Option, + pub commands: Option>, + pub options: Option>, + pub final_path: Option, + #[builder(default)] + pub _extra_values: HashMap, Value<'a>>, +} + +#[derive(Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] +#[serde(untagged)] +pub enum DCommand<'a> { + Simple(#[serde(borrow)] Cow<'a, str>), + Complex(Value<'a>), +} + +#[cfg(test)] +impl<'a> DCommand<'a> { + pub fn simple(cmd: &'a str) -> Self { + DCommand::Simple(Cow::Borrowed(cmd)) + } + pub fn complex(cmd: Value<'a>) -> Self { + DCommand::Complex(cmd) + } +} + +pub struct ConfigFinderDeserializer<'a> { + pub cli: &'a Cli, + pub cred: &'a Cred, + pub env_path: &'a [&'a str], +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for ConfigFinderDeserializer<'a> { + type Value = DConfigFinder<'a>; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field<'a> { + Options, + Roles, + #[serde(untagged, borrow)] + #[allow(dead_code)] + Unknown(Cow<'a, str>), + } + + struct ConfigFinderVisitor<'a> { + cli: &'a Cli, + cred: &'a Cred, + env_path: &'a [&'a str], + human_readable: bool, + } + + impl<'de: 'a, 'a> Visitor<'de> for ConfigFinderVisitor<'a> { + type Value = DConfigFinder<'a>; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("policy") + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut options = None; + let mut roles = Vec::new(); + let mut spath = DPathOptions::default_path(); + while let Some(key) = map.next_key()? { + match key { + Field::Options => { + debug!("ConfigFinderVisitor: options"); + let mut opt: Opt = map.next_value()?; + opt.level = Level::Global; + if self.human_readable { + if let Some(path) = opt.path.as_ref() { + spath.union(path.clone()); + } + } + options = Some(opt); + } + Field::Roles => { + debug!("ConfigFinderVisitor: roles"); + roles = map.next_value_seed(RoleListFinderDeserializer { + cli: self.cli, + cred: self.cred, + spath: &mut spath, + env_path: self.env_path, + })?; + } + Field::Unknown(_) => { + debug!("ConfigFinderVisitor: unknown"); + let _ = map.next_value::(); + } + } + } + Ok(DConfigFinder { options, roles }) + } + } + const FIELDS: &[&str] = &["options", "roles", "version"]; + let human_readable = deserializer.is_human_readable(); + let res = deserializer.deserialize_struct( + "Config", + FIELDS, + ConfigFinderVisitor { + cli: self.cli, + cred: self.cred, + human_readable, + env_path: self.env_path, + }, + ); + res + } +} + +struct RoleListFinderDeserializer<'a, 'b> { + cli: &'a Cli, + cred: &'a Cred, + spath: &'b mut DPathOptions<'a>, + env_path: &'a [&'a str], +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for RoleListFinderDeserializer<'a, '_> { + type Value = Vec>; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct RoleListFinderVisitor<'a, 'b> { + cli: &'a Cli, + cred: &'a Cred, + spath: &'b mut DPathOptions<'a>, + env_path: &'a [&'a str], + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for RoleListFinderVisitor<'a, '_> { + type Value = Vec>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("RoleList sequence") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + debug!("RoleListFinderVisitor: visit_seq"); + let mut roles = Vec::new(); + while let Some(role) = seq.next_element_seed(RoleFinderDeserializer { + cli: self.cli, + cred: self.cred, + spath: self.spath, + env_path: self.env_path, + })? { + roles.push(role); + } + Ok(roles) + } + } + deserializer.deserialize_seq(RoleListFinderVisitor { + cli: self.cli, + cred: self.cred, + spath: self.spath, + env_path: self.env_path, + }) + } +} + +struct RoleFinderDeserializer<'a, 'b> { + cli: &'a Cli, + cred: &'a Cred, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for RoleFinderDeserializer<'a, '_> { + type Value = DRoleFinder<'a>; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field<'a> { + #[serde(alias = "n")] + Name, + #[serde(alias = "a", alias = "users")] + Actors, + #[serde(alias = "t")] + Tasks, + #[serde(alias = "o")] + Options, + #[serde(untagged, borrow)] + Unknown(Cow<'a, str>), + } + + struct RoleFinderVisitor<'a, 'b> { + cli: &'a Cli, + cred: &'a Cred, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, + _human_readable: bool, + } + + impl<'de: 'a, 'a> Visitor<'de> for RoleFinderVisitor<'a, '_> { + type Value = DRoleFinder<'a>; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a role") + } + fn visit_map(mut self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + debug!("RoleFinderVisitor: visit_map"); + let mut role = None; + let mut tasks: Vec> = Vec::new(); + let mut options = None; + let mut extra_values = HashMap::new(); + let mut user_min = ActorMatchMin::default(); + while let Some(key) = map.next_key()? { + match key { + Field::Options => { + debug!("RoleFinderVisitor: options"); + let mut opt: Opt = map.next_value()?; + opt.level = Level::Role; + if let Some(path) = opt.path.as_ref() { + self.spath.union(path.clone().into()); + } + options = Some(opt); + } + Field::Name => { + debug!("RoleFinderVisitor: name"); + role = Some(map.next_value()?); + } + Field::Actors => { + debug!("RoleFinderVisitor: actors"); + user_min = + map.next_value_seed(ActorsFinderDeserializer { cred: self.cred })?; + } + Field::Tasks => { + debug!("RoleFinderVisitor: tasks"); + tasks = map.next_value_seed(TaskListFinderDeserializer { + cli: self.cli, + spath: &mut self.spath, + env_path: self.env_path, + })?; + } + Field::Unknown(key) => { + debug!("RoleFinderVisitor: unknown {}", key); + let unknown: Value = map.next_value()?; + extra_values.insert(key, unknown); + } + } + } + Ok(DRoleFinder { + user_min, + role: role.unwrap_or_default(), + tasks, + options, + _extra_values: extra_values, + }) + } + } + const FIELDS: &[&str] = &["name", "tasks", "options"]; + let _human_readable = deserializer.is_human_readable(); + deserializer.deserialize_struct( + "Role", + FIELDS, + RoleFinderVisitor { + cli: self.cli, + cred: self.cred, + spath: self.spath, + env_path: self.env_path, + _human_readable, + }, + ) + } +} + +struct ActorsFinderDeserializer<'a> { + cred: &'a Cred, +} + +impl<'de> DeserializeSeed<'de> for ActorsFinderDeserializer<'_> { + type Value = ActorMatchMin; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ActorsFinderVisitor<'a> { + cred: &'a Cred, + } + + impl<'de> Visitor<'de> for ActorsFinderVisitor<'_> { + type Value = ActorMatchMin; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a set of users") + } + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut user_matches = ActorMatchMin::NoMatch; + while let Some(actor) = seq.next_element::()? { + debug!("ActorsSettingsVisitor: actor {:?}", actor); + let temp = self.user_matches(self.cred, &actor); + if temp != ActorMatchMin::NoMatch && temp < user_matches { + info!("ActorsSettingsVisitor: Better actor found {:?}", temp); + user_matches = temp; + } + } + Ok(user_matches) + } + } + + impl ActorsFinderVisitor<'_> { + fn match_groups(groups: &[Group], role_groups: &[&DGroups<'_>]) -> bool { + for role_group in role_groups { + if match role_group { + DGroups::Single(group) => groups.iter().any(|g| group == g), + DGroups::Multiple(multiple_actors) => multiple_actors + .iter() + .all(|actor| groups.iter().any(|g| actor == g)), + } { + return true; + } + } + false + } + fn user_matches(&self, user: &Cred, actor: &DActor<'_>) -> ActorMatchMin { + match actor { + DActor::User { id, .. } => { + if *id == user.user { + return ActorMatchMin::UserMatch; + } + } + DActor::Group { groups, .. } => { + if Self::match_groups(&user.groups, &[groups]) { + return ActorMatchMin::GroupMatch(groups.len()); + } + } + DActor::Unknown(element) => { + unimplemented!("Unknown actor type: {:?}", element); + } + } + ActorMatchMin::NoMatch + } + } + + deserializer.deserialize_seq(ActorsFinderVisitor { cred: self.cred }) + } +} + +struct TaskListFinderDeserializer<'a, 'b> { + cli: &'a Cli, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for TaskListFinderDeserializer<'a, '_> { + type Value = Vec>; + + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct TaskListFinderVisitor<'a, 'b> { + cli: &'a Cli, + spath: &'b mut DPathOptions<'a>, + env_path: &'a [&'a str], + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for TaskListFinderVisitor<'a, '_> { + type Value = Vec>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("TaskList sequence") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut tasks = Vec::new(); + let mut i = 0; + while let Some(element) = seq.next_element_seed(TaskFinderDeserializer { + cli: self.cli, + spath: self.spath, + env_path: self.env_path, + i, + })? { + if let Some(task) = element { + debug!("adding task {:?}", task); + tasks.push(task); + i += 1; + } + } + Ok(tasks) + } + } + deserializer.deserialize_seq(TaskListFinderVisitor { + cli: self.cli, + spath: self.spath, + env_path: self.env_path, + }) + } +} + +struct TaskFinderDeserializer<'a, 'b> { + cli: &'a Cli, + i: usize, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for TaskFinderDeserializer<'a, '_> { + type Value = Option>; + + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field<'a> { + #[serde(alias = "n")] + Name, + #[serde(alias = "i", alias = "credentials")] + Cred, + #[serde(alias = "c", alias = "cmds")] + Commands, + #[serde(alias = "o")] + Options, + #[serde(untagged, borrow)] + Unknown(Cow<'a, str>), + } + + struct TaskFinderVisitor<'a, 'b> { + cli: &'a Cli, + i: usize, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, + human_readable: bool, + } + + impl<'de: 'a, 'a> serde::de::Visitor<'de> for TaskFinderVisitor<'a, '_> { + type Value = Option>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("STask structure") + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + // Use local temporaries for each field + let mut id = IdTask::Number(self.i); + let mut score = TaskScore::default(); + let mut setuid = None; + let mut setgroups = None; + let mut caps = None; + let mut commands = None; + let mut options = None; + let mut final_path = None; + let mut extra_values = HashMap::new(); + + while let Some(key) = map.next_key()? { + match key { + Field::Options => { + debug!("TaskFinderVisitor: options"); + let mut opt: Opt = map.next_value()?; + opt.level = Level::Task; + if let Some(path) = opt.path.as_ref() { + self.spath.union(path.clone().into()); + } + options = Some(opt); + } + Field::Name => { + debug!("TaskFinderVisitor: name"); + id = map.next_value()?; + } + Field::Cred => { + debug!("TaskFinderVisitor: cred"); + let (su, sg, ca, sc, ok) = map + .next_value_seed(CredFinderDeserializerReturn { cli: self.cli })?; + setuid = su; + setgroups = sg; + caps = ca; + score.setuser_min = sc.setuser_min; + score.caps_min = sc.caps_min; + if !ok { + while map.next_entry::()?.is_some() {} + return Ok(None); + } + } + Field::Commands => { + debug!("TaskFinderVisitor: commands"); + // if is_human_readable -> next_value + // else -> next_value_seed -> no memory allocation, just the result, thus highly optimizing + if self.human_readable { + commands = Some(map.next_value()?); + } else { + map.next_value_seed(DCommandListDeserializer { + env_path: &self.spath.calc_path(self.env_path), + cmd_path: &self.cli.cmd_path, + cmd_args: &self.cli.cmd_args, + final_path: &mut final_path, + cmd_min: &mut score.cmd_min, + blocker: false, + })?; + } + } + Field::Unknown(key) => { + debug!("TaskFinderVisitor: unknown"); + let unknown: Value = map.next_value()?; + extra_values.insert(key, unknown); + } + } + } + debug!("TaskFinderVisitor: final_path {:?}", final_path); + Ok(Some(DTaskFinder { + id, + score, + setuid, + setgroups, + caps, + commands, + options, + final_path, + _extra_values: extra_values, + })) + } + } + + const FIELDS: &[&str] = &["name", "cred", "commands", "options"]; + let human_readable = deserializer.is_human_readable(); + deserializer.deserialize_struct( + "STask", + FIELDS, + TaskFinderVisitor { + i: self.i, + cli: self.cli, + env_path: self.env_path, + spath: self.spath, + human_readable, + }, + ) + } +} + +struct CredFinderDeserializerReturn<'a> { + cli: &'a Cli, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for CredFinderDeserializerReturn<'a> { + type Value = ( + Option>, + Option>, + Option, + TaskScore, + bool, + ); + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + enum Field<'a> { + #[serde(alias = "u")] + Setuid, + #[serde(alias = "g", alias = "setgroups")] + Setgid, + #[serde(alias = "c", alias = "capabilities")] + Caps, + #[serde(untagged, borrow)] + Other(Cow<'a, str>), + } + + struct CredFinderVisitor<'a> { + cli: &'a Cli, + } + + fn get_caps_min(caps: &CapSet) -> CapsMin { + if caps.is_empty() { + CapsMin::NoCaps + } else if *caps == !CapSet::empty() { + CapsMin::CapsAll + } else if capabilities_are_exploitable(caps) { + CapsMin::CapsAdmin(caps.size()) + } else { + CapsMin::CapsNoAdmin(caps.size()) + } + } + + impl<'de: 'a, 'a> serde::de::Visitor<'de> for CredFinderVisitor<'a> { + type Value = ( + Option>, + Option>, + Option, + TaskScore, + bool, + ); + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Cred structure") + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut setuid = None; + let mut setgroups = None; + let mut caps = None; + let mut score = TaskScore::default(); + let mut ok = true; + while let Some(key) = map.next_key()? { + match key { + Field::Setuid => { + debug!("CredFinderVisitor: setuid"); + let (user, setuser_min, user_ok) = + map.next_value_seed(SetUserDeserializerReturn { cli: self.cli })?; + setuid = user; + score.setuser_min.uid = setuser_min; + if !user_ok { + ok = false; + } + } + Field::Setgid => { + debug!("CredFinderVisitor: setgid"); + let (groups, setuser_min, groups_ok) = + map.next_value_seed(SetGroupsDeserializerReturn { cli: self.cli })?; + setgroups = groups; + score.setuser_min.gid = setuser_min; + if !groups_ok { + ok = false; + } + } + Field::Caps => { + debug!("CredFinderVisitor: capabilities"); + let scaps: SCapabilities = map.next_value()?; + let capset = scaps.to_capset(); + score.caps_min = get_caps_min(&capset); + caps = Some(capset); + } + Field::Other(n) => { + return Err(serde::de::Error::custom(format!( + "Unknown Cred field {}", + n + ))); + } + } + } + debug!("CredFinderVisitor: end"); + Ok((setuid, setgroups, caps, score, ok)) + } + } + const FIELDS: &[&str] = &["setuid", "setgroups", "capabilities", "0", "1", "2"]; + let (setuid, setgroups, caps, score, ok) = + deserializer.deserialize_struct("Cred", FIELDS, CredFinderVisitor { cli: self.cli })?; + Ok((setuid, setgroups, caps, score, ok)) + } +} + +// New deserializer for SetGroups that returns values instead of using &mut +struct SetGroupsDeserializerReturn<'a> { + cli: &'a Cli, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for SetGroupsDeserializerReturn<'a> { + type Value = (Option>, Option, bool); + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + #[serde(alias = "d")] + Default, + #[serde(alias = "f")] + Fallback, + #[serde(alias = "a")] + Add, + #[serde(alias = "s", alias = "sub")] + Del, + } + struct SGroupsChooserVisitor<'a> { + cli: &'a Cli, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for SGroupsChooserVisitor<'a> { + type Value = (Option>, Option, bool); + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("SGroups structure") + } + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_borrowed_str"); + let group: DGroupType<'_> = if let Ok(gid) = v.parse::() { + gid.into() + } else { + v.into() + }; + let score = Some(SetgidMin::from(&group)); + let ok = true; + if let Some(y) = &self + .cli + .opt_filter + .as_ref() + .map(|x| x.group.as_ref()) + .flatten() + { + if y.len() == 1 + && y[0] + != group + .fetch_id() + .ok_or(serde::de::Error::custom("Group does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(DGroups::Single(group)), score, ok)) + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_str"); + self.visit_string(v.to_string()) + } + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_string"); + let group: DGroupType<'_> = if let Ok(gid) = v.parse::() { + gid.into() + } else { + Cow::::from(v).into() + }; + let score = Some(SetgidMin::from(&group)); + let ok = true; + if let Some(y) = &self + .cli + .opt_filter + .as_ref() + .map(|x| x.group.as_ref()) + .flatten() + { + if y.len() == 1 + && y[0] + != group + .fetch_id() + .ok_or(serde::de::Error::custom("Group does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(DGroups::Single(group)), score, ok)) + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_u64"); + if v > u32::MAX as u64 { + return Err(serde::de::Error::custom("Group id too large")); + } + let group: DGroupType<'_> = (v as u32).into(); + let score = Some(SetgidMin::from(&group)); + let ok = true; + if let Some(y) = &self + .cli + .opt_filter + .as_ref() + .map(|x| x.group.as_ref()) + .flatten() + { + if y.len() == 1 + && y[0] + != group + .fetch_id() + .ok_or(serde::de::Error::custom("Group does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(DGroups::Single(group)), score, ok)) + } + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + debug!("SGroupsChooserVisitor: visit_seq"); + let mut groups = None; + let mut score = None; + let mut ok = false; + let filter = self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()); + while let Some(group) = seq.next_element::()? { + if let Some(u) = filter { + let parsed_ids: Vec = + (&group).try_into().map_err(serde::de::Error::custom)?; + if *u == parsed_ids { + ok = true; + groups = Some(group.to_owned()); + score.replace((&group).into()); + while seq.next_element::()?.is_some() {} + break; + } + } else { + groups = Some(group.to_owned()); + ok = true; + score.replace((&group).into()); + } + } + Ok((groups, score, ok)) + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut groups = None; + let mut score = None; + let mut ok = false; + let filter = self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()); + 'fields: while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("SGroupsChooserVisitor: default"); + let default = map.next_value::()?; + if default.is_all() { + ok = true; + } + } + Field::Fallback => { + debug!("SGroupsChooserVisitor: fallback"); + let value = map.next_value::()?; + if let Some(u) = filter { + let parsed_ids: Vec = + (&value).try_into().map_err(serde::de::Error::custom)?; + if *u == parsed_ids { + ok = true; + groups = Some(value.to_owned()); + score.replace((&value).into()); + } + } else { + groups = Some(value.to_owned()); + ok = true; + score.replace((&value).into()); + } + } + Field::Add => { + debug!("SGroupsChooserVisitor: add"); + if filter.is_some() { + let add = map.next_value::>()?; + for group in add.iter() { + let v: Vec = + group.try_into().map_err(serde::de::Error::custom)?; + if v == *filter.unwrap() { + ok = true; + groups = Some(group.to_owned()); + score.replace(group.into()); + while map.next_entry::()?.is_some() + { + } + break; + } + } + } else { + map.next_value::()?; + } + } + Field::Del => { + debug!("SGroupsChooserVisitor: del"); + if let Some(u) = filter { + for group in map.next_value::>()?.iter() { + if let Some(v) = TryInto::>::try_into(group).ok() { + if v == *u { + while map + .next_entry::()? + .is_some() + { + } + ok = false; + groups = None; + score = None; + break 'fields; + } + } else { + return Err(serde::de::Error::custom("Invalid group")); + } + } + } else { + map.next_value::()?; + } + } + } + } + Ok((groups, score, ok)) + } + } + deserializer.deserialize_any(SGroupsChooserVisitor { cli: self.cli }) + } +} + +// New deserializer for SetUser that returns values instead of using &mut +struct SetUserDeserializerReturn<'a> { + cli: &'a Cli, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for SetUserDeserializerReturn<'a> { + type Value = (Option>, Option, bool); + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + #[serde(alias = "d")] + Default, + #[serde(alias = "f")] + Fallback, + #[serde(alias = "a")] + Add, + #[serde(alias = "s", alias = "sub")] + Del, + } + struct SetUserVisitor<'a> { + cli: &'a Cli, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for SetUserVisitor<'a> { + type Value = (Option>, Option, bool); + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("SUser structure") + } + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_borrowed_str"); + let user = if let Ok(uid) = v.parse::() { + DUserType::from(uid) + } else { + DUserType::from(v) + }; + let score = Some(SetuidMin::from(&user)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().map(|x| x.user).flatten() { + if *y + != user + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(user), score, ok)) + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_str"); + self.visit_string(v.to_string()) + } + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_string"); + let user = if let Ok(uid) = v.parse::() { + DUserType::from(uid) + } else { + DUserType::from(v) + }; + let score = Some(SetuidMin::from(&user)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().map(|x| x.user).flatten() { + if *y + != user + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(user), score, ok)) + } + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_i64"); + if v > u32::MAX as u64 { + return Err(serde::de::Error::custom("User id too large")); + } + let user = DUserType::from(v as u32); + let score = Some(SetuidMin::from(&user)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().map(|x| x.user).flatten() { + if *y + != user + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(user), score, ok)) + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut user = None; + let mut score = None; + let mut ok = false; + let filter = self.cli.opt_filter.as_ref().and_then(|x| x.user.as_ref()); + 'fields: while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("SUserChooserVisitor: default"); + let default = map.next_value::()?; + if default.is_all() { + ok = true; + } + } + Field::Fallback => { + debug!("SUserChooserVisitor: fallback"); + let value = map.next_value::()?; + if let Some(u) = filter { + let userid = value + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))?; + if u == &userid { + score.replace((&value).into()); + user = Some(value.into()); + ok = true; + } + } else { + ok = true; + score.replace((&value).into()); + user = Some(value); + } + } + Field::Add => { + debug!("SUserChooserVisitor: add"); + if filter.is_some() { + let users = map.next_value::>()?; + for user_item in users.iter() { + let user_id = user_item + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))?; + if user_id == *filter.unwrap() { + ok = true; + user = Some(user_item.to_owned()); + score.replace(user_item.into()); + break; + } + } + } else { + map.next_value::()?; + } + } + Field::Del => { + debug!("SUserChooserVisitor: del"); + if let Some(u) = filter { + let users = map.next_value::>()?; + for user_item in users.iter() { + let user_id = user_item + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))?; + if user_id == *u { + while map.next_entry::()?.is_some() + { + } + score = None; + user = None; + ok = false; + break 'fields; + } + } + } else { + map.next_value::()?; + } + } + } + } + Ok((user, score, ok)) + } + } + deserializer.deserialize_any(SetUserVisitor { cli: self.cli }) + } +} + +/// This struct keeps the list of commands because options may be written after +#[cfg_attr(test, derive(Builder))] +#[derive(PartialEq, Eq, Debug)] +pub struct DCommandList<'a> { + #[cfg_attr(test, builder(start_fn, into))] + pub default_behavior: Option, + #[cfg_attr(test, builder(default, into))] + pub add: Cow<'a, [DCommand<'a>]>, + #[cfg_attr(test, builder(default, into))] + pub del: Cow<'a, [DCommand<'a>]>, +} + +impl<'de: 'a, 'a> Deserialize<'de> for DCommandList<'a> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + Default, + Add, + Del, + } + #[derive(Default)] + struct DCommandListVisitor<'a> { + _phantom: std::marker::PhantomData<&'a ()>, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandListVisitor<'a> { + type Value = DCommandList<'a>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("CommandList structure") + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut default_behavior = None; + let mut add: Cow<'_, [DCommand<'_>]> = Cow::Borrowed(&[]); + let mut del: Cow<'_, [DCommand<'_>]> = Cow::Borrowed(&[]); + while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("DCommandListVisitor: default"); + default_behavior = Some(map.next_value()?); + } + Field::Add => { + debug!("DCommandListVisitor: add"); + add = map.next_value()?; + } + Field::Del => { + debug!("DCommandListVisitor: del"); + del = map.next_value()?; + } + } + } + Ok(DCommandList { + default_behavior, + add, + del, + }) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut add = Vec::new(); + while let Some(command) = seq.next_element()? { + add.push(command); + } + return Ok(DCommandList { + default_behavior: None, + add: Cow::Owned(add), + del: Cow::Borrowed(&[]), + }); + } + + fn visit_bool(self, v: bool) -> Result + where + E: serde::de::Error, + { + return Ok(DCommandList { + default_behavior: Some(if v { + SetBehavior::All + } else { + SetBehavior::None + }), + add: Cow::Borrowed(&[]), + del: Cow::Borrowed(&[]), + }); + } + } + deserializer.deserialize_any(DCommandListVisitor::default()) + } +} + +/// This struct evaluates commands directly from deserialization +pub struct DCommandListDeserializer<'a> { + env_path: &'a [&'a str], + cmd_path: &'a PathBuf, + cmd_args: &'a [String], + pub final_path: &'a mut Option, + pub cmd_min: &'a mut CmdMin, + pub blocker: bool, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for DCommandListDeserializer<'a> { + type Value = bool; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(self) + } +} + +impl<'a> DCommandListDeserializer<'a> { + fn generate_dcommand_deserializer(&mut self) -> DCommandDeserializer<'_> { + DCommandDeserializer { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + } + } +} + +impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandListDeserializer<'a> { + type Value = bool; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("CommandList structure") + } + + fn visit_seq(mut self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut result = false; + while let Some(bool) = seq.next_element_seed(self.generate_dcommand_deserializer())? { + if bool && self.blocker { + return Ok(true); + } + result |= bool; + } + Ok(result) + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + #[serde(alias = "d")] + Default, + #[serde(alias = "a")] + Add, + #[serde(alias = "s", alias = "sub")] + Del, + } + let mut result = false; + let mut default = SetBehavior::None; + while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("DCommandListVisitor: default"); + default = map.next_value()?; + } + Field::Del => { + let deserializer = DCommandListDeserializer { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + blocker: true, + }; + let res = map.next_value_seed(deserializer)?; + if res { + while map.next_entry::()?.is_some() {} + return Ok(false); + } + } + Field::Add => { + if default.is_all() { + let _ = map.next_value::(); + } else { + let deserializer = DCommandListDeserializer { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + blocker: false, + }; + result |= map.next_value_seed(deserializer)?; + } + } + } + } + Ok(result) + } +} + +pub(super) struct DCommandDeserializer<'a> { + pub(super) env_path: &'a [&'a str], + pub(super) cmd_path: &'a PathBuf, + pub(super) cmd_args: &'a [String], + pub(super) final_path: &'a mut Option, + pub(super) cmd_min: &'a mut CmdMin, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for DCommandDeserializer<'a> { + type Value = bool; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct DCommandVisitor<'a> { + env_path: &'a [&'a str], + cmd_path: &'a PathBuf, + cmd_args: &'a [String], + final_path: &'a mut Option, + cmd_min: &'a mut CmdMin, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandVisitor<'a> { + type Value = bool; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Command structure") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let mut final_path = None; + let mut result = false; + debug!("DCommandVisitor: command {}", v); + let cmd_min = cmd::evaluate_command_match( + self.env_path, + self.cmd_path, + self.cmd_args, + v, + self.cmd_min, + &mut final_path, + ); + if cmd_min.better(&self.cmd_min) { + debug!("DCommandVisitor: better command found"); + result = true; + *self.final_path = final_path; + *self.cmd_min = cmd_min; + } + Ok(result) + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut map_value = Vec::new(); + while let Some((key, value)) = map.next_entry::<&str, Value>()? { + map_value.push((key, value)); + } + Api::notify(ApiEvent::ProcessComplexCommand( + &Value::Object(map_value.into()), + self.env_path, + self.cmd_path, + self.cmd_args, + self.cmd_min, + self.final_path, + )) + .map(|_| true) + .map_err(|_| serde::de::Error::custom("Failed to notify process complex command")) + } + } + deserializer.deserialize_any(DCommandVisitor { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + }) + } +} + +impl<'a> DConfigFinder<'a> { + pub fn roles<'s>(&'s self) -> impl Iterator> { + self.roles.iter().map(|role| DLinkedRole::new(self, role)) + } + + pub fn role<'s>(&'s self, role_name: &str) -> Option> { + self.roles + .iter() + .find(|r| r.role == role_name) + .map(|role| DLinkedRole::new(self, role)) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct DLinkedRole<'c, 'a> { + parent: &'c DConfigFinder<'a>, + role: &'c DRoleFinder<'a>, +} + +impl<'c, 'a> DLinkedRole<'c, 'a> { + fn new(parent: &'c DConfigFinder<'a>, role: &'c DRoleFinder<'a>) -> Self { + Self { parent, role } + } + + pub fn tasks<'t>(&'t self) -> impl Iterator> { + self.role + .tasks + .iter() + .map(|task| DLinkedTask::new(self, task)) + } + + pub fn role(&self) -> &DRoleFinder<'a> { + self.role + } + + pub fn config(&self) -> &DConfigFinder<'a> { + self.parent + } +} + +#[derive(Clone, Copy, Debug)] +pub struct DLinkedTask<'t, 'c, 'a> { + parent: &'t DLinkedRole<'c, 'a>, + pub task: &'t DTaskFinder<'a>, +} + +impl<'t, 'c, 'a> DLinkedTask<'t, 'c, 'a> { + fn new(parent: &'t DLinkedRole<'c, 'a>, task: &'t DTaskFinder<'a>) -> Self { + Self { parent, task } + } + + pub fn commands<'l>(&'l self) -> Option> { + self.task + .commands + .as_ref() + .map(|list| DLinkedCommandList::new(self, list)) + } + + pub fn role(&self) -> &DLinkedRole<'c, 'a> { + self.parent + } + + pub fn task(&self) -> &DTaskFinder<'a> { + self.task + } + + pub fn score(&self, cmd_min: CmdMin, security_min: SecurityMin) -> Score { + Score::builder() + .user_min(self.role().role.user_min) + .caps_min(self.score.caps_min) + .cmd_min(cmd_min) + .security_min(security_min) + .setuser_min(self.score.setuser_min) + .build() + } +} + +impl<'t, 'c, 'a> Deref for DLinkedTask<'t, 'c, 'a> { + type Target = DTaskFinder<'a>; + fn deref(&self) -> &Self::Target { + self.task + } +} + +pub struct DLinkedCommandList<'l, 't, 'c, 'a> { + #[allow(dead_code)] // TODO: remove this + parent: &'l DLinkedTask<'t, 'c, 'a>, + command_list: &'l DCommandList<'a>, +} + +impl<'l, 't, 'c, 'a> DLinkedCommandList<'l, 't, 'c, 'a> { + fn new(parent: &'l DLinkedTask<'t, 'c, 'a>, list: &'l DCommandList<'a>) -> Self { + Self { + parent, + command_list: list, + } + } + + pub fn add<'d>(&'d self) -> impl Iterator> { + self.command_list + .add + .iter() + .map(|cmd| DLinkedCommand::new(self, cmd)) + } + + pub fn del<'d>(&'d self) -> impl Iterator> { + self.command_list + .del + .iter() + .map(|cmd| DLinkedCommand::new(self, cmd)) + } +} + +impl<'l, 't, 'c, 'a> Deref for DLinkedCommandList<'l, 't, 'c, 'a> { + type Target = DCommandList<'a>; + fn deref(&self) -> &Self::Target { + self.command_list + } +} + +pub struct DLinkedCommand<'d, 'l, 't, 'c, 'a> { + #[allow(dead_code)] // TODO: remove this + parent: &'d DLinkedCommandList<'l, 't, 'c, 'a>, + pub command: &'d DCommand<'a>, +} + +impl<'d, 'l, 't, 'c, 'a> DLinkedCommand<'d, 'l, 't, 'c, 'a> { + fn new(parent: &'d DLinkedCommandList<'l, 't, 'c, 'a>, command: &'d DCommand<'a>) -> Self { + Self { parent, command } + } + + #[allow(dead_code)] // TODO: remove this + pub fn task(&self) -> &DLinkedTask<'t, 'c, 'a> { + self.parent.parent + } +} + +impl<'d, 'l, 't, 'c, 'a> Deref for DLinkedCommand<'d, 'l, 't, 'c, 'a> { + type Target = DCommand<'a>; + fn deref(&self) -> &Self::Target { + self.command + } +} + +#[cfg(test)] +mod tests { + + use std::fs; + + use super::*; + use capctl::Cap; + use cbor4ii::core::utils::SliceReader; + use nix::unistd::{getgid, getuid}; + use rar_common::database::{ + actor::{DGroupType, SGroupType, SGroups}, + score::{SetUserMin, SetgidMin, SetuidMin}, + FilterMatcher, + }; + use test_log::test; + + fn get_non_root_uid(nth: usize) -> Option { + // 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() + }) + .filter(|uid| *uid != 0) + .nth(nth); + } + + fn get_non_root_gid(nth: usize) -> Option { + // list all users + let passwd = fs::read_to_string("/etc/group").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() + }) + .filter(|uid| *uid != 0) + .nth(nth); + } + + fn convert_json_to_cbor(json: &str) -> Vec { + let value: Value = serde_json::from_str(json).unwrap(); + let cbor = cbor4ii::serde::to_vec(Vec::new(), &value).unwrap(); + cbor + } + + #[test] + fn test_idtask_display() { + let name = IdTask::Name(Cow::Borrowed("test")); + let number = IdTask::Number(42); + assert_eq!(format!("{}", name), "test"); + assert_eq!(format!("{}", number), "42"); + } + + #[test] + fn test_dcommandlist_deserialize_seq() { + let json = r#"["ls", "cat"]"#; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.add.len(), 2); + assert!(matches!(list.add[0], DCommand::Simple(_))); + } + + #[test] + fn test_dcommandlist_deserialize_map() { + let json = r#"{"default": "all", "add": ["ls"], "del": ["rm"]}"#; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior.unwrap(), SetBehavior::All); + assert_eq!(list.add.len(), 1); + assert_eq!(list.del.len(), 1); + } + + #[test] + fn test_dcommandlist_deserialize_bool() { + let json = "true"; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior, Some(SetBehavior::All)); + assert_eq!(list.add.len(), 0); + assert_eq!(list.del.len(), 0); + let json = "false"; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior, Some(SetBehavior::None)); + assert_eq!(list.add.len(), 0); + assert_eq!(list.del.len(), 0); + } + + #[test] + fn test_dcommandlist_deserialize_empty() { + let json = "{}"; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior, None); + assert_eq!(list.add.len(), 0); + assert_eq!(list.del.len(), 0); + } + + #[test] + fn test_dcommandlist_deserialize_invalid() { + let json = r#"{"default": "invalid", "add": ["ls"], "del": ["rm"]}"#; + let result: Result = serde_json::from_str(json); + assert!(result.is_err()); + } + + #[test] + fn test_dcommandlist_seed() { + let json = r#"{"default": "none", "add": ["/usr/bin/ls"], "del": ["/usr/bin/rm"]}"#; + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandListDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from("/usr/bin/ls"), + cmd_args: &vec![], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + blocker: false, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(final_path, Some(PathBuf::from("/usr/bin/ls"))); + assert!(result); + } + + #[test] + fn test_dcommand_seed() { + let json = r#""/usr/bin/ls""#; + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from("/usr/bin/ls"), + cmd_args: &vec![], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(final_path, Some(PathBuf::from("/usr/bin/ls"))); + assert!(result); + } + + #[test] + fn test_setuserdeserializerreturn() { + let json = + r#"{"default": "none", "fallback": "user1", "add": ["user2"], "del": ["user3"]}"#; + let cli = Cli::builder().build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from("user1"); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + } + + #[test] + fn test_setuserdeserializerreturn_filter() { + let uid1 = get_non_root_uid(0).unwrap(); + let uid2 = get_non_root_uid(1).unwrap(); + let json = format!( + r#"{{"default": "none", "fallback": "root", "add": [{}], "del": [{}]}}"#, + uid1, uid2 + ); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from(uid1); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from("root"); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid2).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(user, None); + let json = "\"root\""; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from("root"); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(user, None); + let json = "0"; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(user, None); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from(0); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + } + + #[test] + fn test_no_fallback() { + let json = r#"{"default": "all"}"#; + let cli = Cli::builder().build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(score, None); + assert_eq!(user, None); + } + + #[test] + fn test_setgroupsdeserializerreturn() { + let json = r#"{"default": "none", "fallback": [1, 2], "add": [[3, 4]], "del": [[5, 6]]}"#; + let cli = Cli::builder().build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::from(vec![1.into(), 2.into()]); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + } + + #[test] + fn test_setgroupsdeserializerreturn_filter() { + let gid1 = get_non_root_gid(0).unwrap(); + let gid2 = get_non_root_gid(1).unwrap(); + let json = format!( + r#"{{"default": "none", "fallback": ["root"], "add": [[{}]], "del": [[{}]]}}"#, + gid1, gid2 + ); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single("root".into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single(gid1.into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid2).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + let json = "\"root\""; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single("root".into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + let json = "0"; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single(0.into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let json = "[[\"root\", 1]]"; + let cli = Cli::builder() + .opt_filter( + FilterMatcher::builder() + .group(vec!["root".into(), Into::::into(1)]) + .unwrap() + .build(), + ) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::from(vec!["root".into(), 1.into()]); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::from(vec!["root".into(), 1.into()]); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + } + + #[test] + fn test_no_fallback_groups() { + let json = r#"{"default": "all"}"#; + let cli = Cli::builder().build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(score, None); + assert_eq!(groups, None); + } + + #[test] + fn test_cred_deserializer() { + let json = r#"{"setuid":"root", "setgid":"root", "caps": ["CAP_SYS_ADMIN"]}"#; + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (user, groups, caps, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(user, Some("root".into())); + assert_eq!(groups, Some(DGroups::from(vec!["root".into()]))); + assert_eq!(caps, Some(CapSet::from_iter(vec![Cap::SYS_ADMIN]))); + assert_eq!(score.setuser_min.uid, Some(SetuidMin::from(&"root".into()))); + assert_eq!( + score.setuser_min.gid, + Some(SetgidMin::from(&Into::>::into("root"))) + ); + assert_eq!(score.caps_min, CapsMin::CapsAdmin(1)); + + let uid = get_non_root_uid(0).unwrap(); + let gid = get_non_root_gid(0).unwrap(); + let json = format!(r#"{{"setuid":{}, "setgid":[[{}]]}}"#, uid, gid); + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (user, groups, caps, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(user, Some(uid.into())); + assert_eq!(groups, Some(DGroups::from(vec![gid.into()]))); + assert_eq!(caps, None); + assert_eq!(score.setuser_min.uid, Some(SetuidMin::from(&uid.into()))); + assert_eq!( + score.setuser_min.gid, + Some(SetgidMin::from(&Into::>::into(uid))) + ); + assert_eq!(score.caps_min, CapsMin::Undefined); + + let uid = get_non_root_uid(0).unwrap(); + let gid = get_non_root_gid(0).unwrap(); + let json = format!(r#"{{"setuid":"{}", "setgid":["{}"]}}"#, uid, gid); + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (user, groups, caps, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(user, Some(uid.into())); + assert_eq!(groups, Some(DGroups::from(vec![gid.into()]))); + assert_eq!(caps, None); + assert_eq!(score.setuser_min.uid, Some(SetuidMin::from(&uid.into()))); + assert_eq!( + score.setuser_min.gid, + Some(SetgidMin::from(&Into::>::into(uid))) + ); + assert_eq!(score.caps_min, CapsMin::Undefined); + } + + #[test] + fn test_cred_deserializer_invalid() { + let json = r#"{"setuid":-1, "setgid":"invalid", "caps": ["CAP_SYS_ADMIN"]}"#; + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let json = r#"{"setuid":"invalid", "setgid":-1, "caps": ["CAP_SYS_ADMIN"]}"#; + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + } + + #[test] + fn test_task_deserializer() { + let json = r#"{"name": "test", "cred": {"setuid":"0", "setgid":["0", 0], "caps": []}, "commands": ["ls"]}}"#; + let cli = Cli::builder().build(); + let deserializer = TaskFinderDeserializer { + cli: &cli, + i: 0, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let task = result.unwrap().unwrap(); + assert_eq!(task.id, IdTask::Name("test".into())); + assert_eq!(task.score.setuser_min.uid, Some(SetuidMin::from(&0.into()))); + assert_eq!(task.score.setuser_min.gid, Some(SetgidMin::from(&vec![0]))); + assert_eq!(task.score.caps_min, CapsMin::NoCaps); + let commands = task.commands.unwrap(); + assert_eq!(commands.add.len(), 1); + assert_eq!(commands.add[0], DCommand::Simple("ls".into())); + } + + #[test] + fn test_task_list_deserializer() { + let json = r#"[{"name": "test", "cred": {"setuid":"0", "setgid":["0", 0], "caps": []}, "commands": ["ls"]}]"#; + let cli = Cli::builder().build(); + let deserializer = TaskListFinderDeserializer { + cli: &cli, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let task = &result.unwrap()[0]; + assert_eq!(task.id, IdTask::Name("test".into())); + assert_eq!(task.score.setuser_min.uid, Some(SetuidMin::from(&0.into()))); + assert_eq!(task.score.setuser_min.gid, Some(SetgidMin::from(&vec![0]))); + assert_eq!(task.score.caps_min, CapsMin::NoCaps); + let commands = task.commands.as_ref().unwrap(); + assert_eq!(commands.add.len(), 1); + assert_eq!(commands.add[0], DCommand::Simple("ls".into())); + } + + #[test] + fn test_actors_finder_deserializer() { + let json = format!(r#"[{{"type": "user", "id": {}}}]"#, getuid().as_raw()); + let deserializer = ActorsFinderDeserializer { + cred: &Cred::builder().build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let user_min = result.unwrap(); + assert_eq!(user_min, ActorMatchMin::UserMatch); + } + + #[test] + fn test_role_finder_deserializer() { + let json = format!( + r#"{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let role = result.unwrap(); + assert_eq!(role.role, "r_test"); + assert_eq!(role.tasks.len(), 1); + assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); + } + + #[test] + fn test_role_list_finder_deserializer() { + let json = format!( + r#"[{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleListFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let role = &result.unwrap()[0]; + assert_eq!(role.role, "r_test"); + assert_eq!(role.tasks.len(), 1); + assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); + let json = format!( + r#"[{{"name":"r_test","actors":[{{"type": "group", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]"#, + getgid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleListFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let role = &result.unwrap()[0]; + assert_eq!(role.role, "r_test"); + assert_eq!(role.tasks.len(), 1); + assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); + let json = format!( + r#"[{{"name":"r_test","actors":[{{"type": "user", "id": "874510"}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]"# + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleListFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let result = result.unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].user_min, ActorMatchMin::NoMatch); + } + + #[test] + fn test_config_finder_deserializer() { + let json = format!( + r#"{{"roles":[{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]}}"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let config = result.unwrap(); + assert_eq!(config.roles.len(), 1); + assert_eq!(config.roles[0].role, "r_test"); + } + + #[test] + fn test_config_finder_implementation() { + let json = format!( + r#"{{"roles":[{{"name":"r_test","actors":[{{"type":"user","id":{}}}],"tasks":[{{"name":"test","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/ls"]}},{{"name":"test2","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/ls","/usr/bin/cat"]}}]}},{{"name":"r_test2","actors":[{{"type":"group","names":[{}, {}]}}],"tasks":[{{"name":"test3","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/cat","/usr/bin/ls"]}}]}}]}}"#, + getuid().as_raw(), + getgid().as_raw(), + getgid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let config = result.unwrap(); + let mut roles = config.roles(); + let role_a = roles.next().unwrap(); + assert_eq!(role_a.role().role, "r_test"); + let mut tasks = role_a.tasks(); + let task_a = tasks.next().unwrap(); + assert_eq!(task_a.task().id, IdTask::Name("test".into())); + let commands = task_a.commands().unwrap(); + assert_eq!(commands.add().count(), 1); + assert_eq!( + *commands.add().next().unwrap().command, + DCommand::Simple("/usr/bin/ls".into()) + ); + let task_b = tasks.next().unwrap(); + assert_eq!(task_b.task().id, IdTask::Name("test2".into())); + let commands = task_b.commands().unwrap(); + assert_eq!(commands.add().count(), 2); + assert_eq!( + *commands.add().next().unwrap().command, + DCommand::Simple("/usr/bin/ls".into()) + ); + assert_eq!( + *commands.add().nth(1).unwrap().command, + DCommand::Simple("/usr/bin/cat".into()) + ); + assert!(tasks.next().is_none()); + let role_b = roles.next().unwrap(); + assert_eq!(role_b.role().role, "r_test2"); + let mut tasks = role_b.tasks(); + let task_a = tasks.next().unwrap(); + assert_eq!(task_a.task().id, IdTask::Name("test3".into())); + let commands = task_a.commands().unwrap(); + assert_eq!(commands.add().count(), 2); + assert_eq!( + *commands.add().next().unwrap().command, + DCommand::Simple("/usr/bin/cat".into()) + ); + assert_eq!( + *commands.add().nth(1).unwrap().command, + DCommand::Simple("/usr/bin/ls".into()) + ); + assert_eq!(commands.del().count(), 0); + assert!(tasks.next().is_none()); + assert!(roles.next().is_none()); + assert!(config.options.is_none()); + assert!(config.roles[0].options.is_none()); + assert!(config.roles[0].tasks[0].options.is_none()); + assert!(config.roles[0].tasks[1].options.is_none()); + assert!(config.roles[1].options.is_none()); + assert!(config.roles[1].tasks[0].options.is_none()); + assert!(config.role("r_test").is_some()); + assert!(config.role("r_test2").is_some()); + assert!(config.role("r_test3").is_none()); + assert_eq!(*config.role("r_test").unwrap().config(), config); + assert_eq!(*config.role("r_test2").unwrap().config(), config); + assert_eq!( + *config + .role("r_test") + .unwrap() + .tasks() + .next() + .unwrap() + .role(), + config.role("r_test").unwrap() + ); + assert_eq!( + *config + .role("r_test2") + .unwrap() + .tasks() + .next() + .unwrap() + .role(), + config.role("r_test2").unwrap() + ); + assert_eq!( + config + .role("r_test") + .unwrap() + .tasks() + .next() + .unwrap() + .score(CmdMin::Match, SecurityMin::empty()), + Score::builder() + .user_min(ActorMatchMin::UserMatch) + .setuser_min(SetUserMin { + uid: Some(SetuidMin::from(0)), + gid: Some(SetgidMin::from(SGroups::from(vec![0]))) + }) + .caps_min(CapsMin::NoCaps) + .security_min(SecurityMin::empty()) + .cmd_min(CmdMin::Match) + .build() + ); + } + + #[test] + fn test_config_with_options() { + let json = format!( + r#"{{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:05:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep1" + ], + "check": [ + "check1" + ], + "delete": [ + "del1" + ], + "set": {{ + "set1": "value1", + "set2": "value2" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "roles": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:06:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep2" + ], + "check": [ + "check2" + ], + "delete": [ + "del2" + ], + "set": {{ + "set1": "value2", + "set3": "value3" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "name": "role1", + "actors": [ + {{ + "type": "user", + "id": {} + }} + ], + "tasks": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:07:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep3" + ], + "check": [ + "check3" + ], + "delete": [ + "del3" + ], + "set": {{ + "set1": "value3", + "set4": "value4" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "name": "task1", + "cred": {{ + "setuid": 0, + "setgid": 0, + "caps": [ + "CAP_SYS_ADMIN", + "CAP_SYS_RESOURCE" + ] + }} + }} + ] + }} + ] +}}"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let config = result.unwrap(); + assert_eq!(config.roles.len(), 1); + assert_eq!(config.roles[0].role, "role1"); + assert_eq!(config.roles[0].tasks.len(), 1); + assert_eq!(config.roles[0].tasks[0].id, IdTask::Name("task1".into())); + assert!(config.options.is_some()); + assert!(config.roles[0].options.is_some()); + assert!(config.roles[0].tasks[0].options.is_some()); + } + + #[test] + fn test_config_optimized_with_options() { + let json = format!( + r#"{{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:05:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep1" + ], + "check": [ + "check1" + ], + "delete": [ + "del1" + ], + "set": {{ + "set1": "value1", + "set2": "value2" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "roles": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:06:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep2" + ], + "check": [ + "check2" + ], + "delete": [ + "del2" + ], + "set": {{ + "set1": "value2", + "set3": "value3" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "name": "role1", + "actors": [ + {{ + "type": "group", + "id": {} + }} + ], + "tasks": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:07:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep3" + ], + "check": [ + "check3" + ], + "delete": [ + "del3" + ], + "set": {{ + "set1": "value3", + "set4": "value4" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "name": "task1", + "cred": {{ + "setuid": 0, + "setgid": 0, + "caps": [ + "CAP_SYS_ADMIN", + "CAP_SYS_RESOURCE" + ] + }}, + "commands": ["/usr/bin/ls"] + }} + ] + }} + ] +}}"#, + getgid().as_raw() + ); + //convert json to cbor4ii + let cbor = convert_json_to_cbor(&json); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + }; + let result: Result, _> = deserializer.deserialize( + &mut cbor4ii::serde::Deserializer::new(SliceReader::new(cbor.as_slice())), + ); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let config = result.unwrap(); + assert_eq!(config.roles.len(), 1); + assert_eq!(config.roles[0].role, "role1"); + assert_eq!(config.roles[0].tasks.len(), 1); + assert_eq!(config.roles[0].tasks[0].id, IdTask::Name("task1".into())); + assert!(config.options.is_some()); + assert!(config.roles[0].options.is_some()); + assert!(config.roles[0].tasks[0].options.is_some()); + assert_eq!(config.roles[0].user_min, ActorMatchMin::GroupMatch(1)); + assert_eq!(config.roles[0].tasks[0].score.cmd_min, CmdMin::Match); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.uid, + Some(SetuidMin::from(&0.into())) + ); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.gid, + Some(SetgidMin::from(&vec![0])) + ); + assert_eq!( + config.roles[0].tasks[0].score.caps_min, + CapsMin::CapsAdmin(2) + ); + assert!(config.roles[0].tasks[0].commands.is_none()); + assert_eq!( + config.roles[0].tasks[0].final_path, + Some(PathBuf::from("/usr/bin/ls")) + ); + } + + #[test] + fn test_optimized_config() { + let uid = getuid().as_raw(); + let json = format!( + r#"{{"roles":[{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0"], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]}}"#, + uid + ); + //convert json to cbor4ii + let cbor = convert_json_to_cbor(&json); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + }; + let result: Result, _> = deserializer.deserialize( + &mut cbor4ii::serde::Deserializer::new(SliceReader::new(cbor.as_slice())), + ); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let config = result.unwrap(); + assert_eq!(config.roles[0].user_min, ActorMatchMin::UserMatch); + assert_eq!(config.roles[0].tasks[0].score.cmd_min, CmdMin::Match); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.uid, + Some(SetuidMin::from(&0.into())) + ); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.gid, + Some(SetgidMin::from(&vec![0])) + ); + assert_eq!(config.roles[0].tasks[0].score.caps_min, CapsMin::NoCaps); + assert!(config.roles[0].tasks[0].commands.is_none()); + assert_eq!( + config.roles[0].tasks[0].final_path, + Some(PathBuf::from("/usr/bin/ls")) + ); + } + + #[test] + fn test_expecting_error() { + let seq = "[1, 2, 3]"; + let map = "{\"1\": 2, \"3\": 4}"; + let int = "1"; + let float = "1.0"; + let cli = Cli::builder().build(); + let config_finder = ConfigFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder().build(), + }; + let result = config_finder.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + + let role_list = RoleListFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = role_list.deserialize(&mut serde_json::Deserializer::from_str(map)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let task_list = TaskListFinderDeserializer { + cli: &cli, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = task_list.deserialize(&mut serde_json::Deserializer::from_str(map)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let task = TaskFinderDeserializer { + cli: &cli, + i: 0, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = task.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + assert!(serde_json::from_str::(int).is_err()); + let mut var_name = None; + let mut cmd_min = CmdMin::Match; + let dcommand = DCommandDeserializer { + env_path: &[], + cmd_path: &cli.cmd_path, + cmd_args: &cli.cmd_args, + final_path: &mut var_name, + cmd_min: &mut cmd_min, + }; + let result = dcommand.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let cred = CredFinderDeserializerReturn { cli: &cli }; + let result = cred.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let setuser = SetUserDeserializerReturn { cli: &cli }; + let result = setuser.deserialize(&mut serde_json::Deserializer::from_str(float)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let setgroups = SetGroupsDeserializerReturn { cli: &cli }; + let result = setgroups.deserialize(&mut serde_json::Deserializer::from_str(float)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let actors = ActorsFinderDeserializer { + cred: &Cred::builder().build(), + }; + let result = actors.deserialize(&mut serde_json::Deserializer::from_str(int)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let role = RoleFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = role.deserialize(&mut serde_json::Deserializer::from_str(int)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + } + + // this test is to check if the deserializer can handle unknown types... It might evolve in the future + #[test] + fn test_unknown_type() { + let json = r#"{"unknown": "unknown"}"#; + let cli = Cli::builder().build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder().build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Expected error, got: {:?}", result); + + let deserializer = RoleFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Expected error, got: {:?}", result); + + let deserializer = TaskFinderDeserializer { + cli: &cli, + i: 0, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Expected error, got: {:?}", result); + + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + } +} diff --git a/src/sr/finder/mod.rs b/src/sr/finder/mod.rs new file mode 100644 index 00000000..577532f9 --- /dev/null +++ b/src/sr/finder/mod.rs @@ -0,0 +1,491 @@ +/// This file implements a finder algorithm within deserialization of the settings +/// It is much more efficient to do it this way, way less memory allocation and manipulation +/// Only the settings that are needed are kept in memory +use std::{ + collections::HashMap, + io::BufReader, + path::{Path, PathBuf}, +}; + +use api::{register_plugins, Api, ApiEvent}; +use capctl::CapSet; +use de::{ConfigFinderDeserializer, DConfigFinder, DLinkedCommand, DLinkedRole, DLinkedTask}; +use log::debug; +use options::BorrowedOptStack; +use rar_common::{ + database::{ + actor::DGroups, + options::{SAuthentication, SBounding, SPrivileged, STimeout}, + score::{CmdMin, Score}, + }, + util::{all_paths_from_env, open_with_privileges}, + Cred, StorageMethod, +}; +use serde::de::DeserializeSeed; + +use crate::Cli; + +mod api; +mod cmd; +mod de; +mod options; + +#[derive(Debug, Default, Clone)] +pub struct BestExecSettings { + pub score: Score, + pub final_path: PathBuf, + pub setuid: Option, + pub setgroups: Option>, + pub caps: Option, + pub task: Option, + pub role: String, + pub env: HashMap, + pub env_path: Vec, + pub bounding: SBounding, + pub timeout: STimeout, + pub auth: SAuthentication, + pub root: SPrivileged, +} + +pub fn find_best_exec_settings<'de: 'a, 'a, P>( + cli: &'a Cli, + cred: &'a Cred, + path: &'a P, + env_vars: impl IntoIterator, impl Into)>, + env_path: &[&str], +) -> Result> +where + P: AsRef, +{ + register_plugins(); + let settings_file = rar_common::get_settings(path)?; + let config_finder_deserializer = ConfigFinderDeserializer { + cli, + cred, + env_path, + }; + match settings_file.storage.method { + StorageMethod::CBOR => { + let file_path = settings_file + .storage + .settings + .unwrap_or_default() + .path + .ok_or("Settings file variable not found")?; + let file = open_with_privileges(&file_path)?; + let reader = BufReader::new(file); // Use BufReader for efficient streaming + let mut io_reader = cbor4ii::core::utils::IoReader::new(reader); // Use IoReader for streaming + Ok(BestExecSettings::retrieve_settings( + cli, + cred, + &config_finder_deserializer + .deserialize(&mut cbor4ii::serde::Deserializer::new(&mut io_reader))?, + env_vars, + &env_path, + )?) + } + StorageMethod::JSON => { + let file_path = settings_file + .storage + .settings + .unwrap_or_default() + .path + .ok_or("Settings file variable not found")?; + let file = open_with_privileges(&file_path)?; + let reader = BufReader::new(file); + let io_reader = serde_json::de::IoRead::new(reader); + Ok(BestExecSettings::retrieve_settings( + cli, + cred, + &config_finder_deserializer + .deserialize(&mut serde_json::Deserializer::new(io_reader))?, + env_vars, + &env_path, + )?) + } + } +} + +impl BestExecSettings { + fn retrieve_settings<'a>( + cli: &'a Cli, + cred: &'a Cred, + data: &'a DConfigFinder<'a>, + env_vars: impl IntoIterator, impl Into)>, + env_path: &[&str], + ) -> Result> { + let mut result = Self::default(); + let mut matching = false; + let mut opt_stack = BorrowedOptStack::new(data.options.clone()); + for role in data.roles() { + matching |= result.role_settings(cli, &role, &mut opt_stack, env_path)?; + } + if !matching { + return Err("No matching role found".into()); + } + result.env = opt_stack + .calc_temp_env(&opt_stack.calc_override_behavior(), &cli.opt_filter) + .calc_final_env(env_vars, env_path, cred)?; + result.auth = opt_stack.calc_authentication(); + result.bounding = opt_stack.calc_bounding(); + result.timeout = opt_stack.calc_timeout(); + result.root = opt_stack.calc_privileged(); + Ok(result) + } + + pub fn role_settings<'c, 'a>( + &mut self, + cli: &'c Cli, + data: &DLinkedRole<'c, 'a>, + opt_stack: &mut BorrowedOptStack<'a>, + env_path: &[&str], + ) -> Result> { + debug!("role_settings: {:?}", data.role().role); + if !self.actors_settings(data)? { + return Ok(false); + } + let mut res = false; + for task in data.tasks() { + res |= self.task_settings(cli, &task, opt_stack, env_path)?; + } + Ok(res) + } + + pub fn actors_settings<'c, 'a>( + &mut self, + data: &DLinkedRole<'c, 'a>, + ) -> Result> { + let mut res = !data.role().user_min.is_no_match(); + Api::notify(ApiEvent::ActorMatching(data, self, &mut res))?; + Ok(res) + } + + pub fn task_settings<'t, 'c, 'a>( + &mut self, + cli: &'t Cli, + data: &DLinkedTask<'t, 'c, 'a>, + opt_stack: &mut BorrowedOptStack<'a>, + env_path: &[&str], + ) -> Result> { + debug!("task_settings: {:?}", data.id); + let temp_opt_stack = BorrowedOptStack::from_task(data); + let mut found = false; + let mut f_env_path = None; + if let Some(commands) = data.commands() { + let t_env_path = opt_stack.calc_path(env_path); + for command in commands.del() { + if self.command_settings( + &t_env_path.iter().map(|s| s.as_str()).collect::>(), + cli, + &command, + )? { + return Ok(false); + } + } + if commands.default_behavior.is_some_and(|b| b.is_all()) { + debug!("default behavior is all"); + let t_env_path = opt_stack.calc_path(env_path); + found = true; + debug!("{:?}", &cli.cmd_path); + if let Ok(path) = cli.cmd_path.canonicalize() { + self.final_path = path; + } else { + self.final_path = all_paths_from_env( + &t_env_path.iter().map(|s| s.as_str()).collect::>(), + &cli.cmd_path, + ) + .first() + .ok_or_else::, _>(|| { + "No path found".to_string().into() + })? + .to_path_buf(); + } + self.score.cmd_min = CmdMin::FullWildcardPath | CmdMin::RegexArgs; + } else { + for command in commands.add() { + found = self.command_settings( + &t_env_path.iter().map(|s| s.as_str()).collect::>(), + cli, + &command, + )?; + } + } + f_env_path = Some(t_env_path); + } else if let Some(final_path) = &data.final_path { + debug!("final_path already found: {:?}", final_path); + found = self.update_command_score(final_path.to_path_buf(), data.score.cmd_min); + } + let mut score = data.score(self.score.cmd_min, temp_opt_stack.calc_security_min()); + Api::notify(ApiEvent::BestTaskSettingsFound( + &cli, &data, opt_stack, self, &mut score, + ))?; + if found && score.better_fully(&self.score) { + debug!("found better task settings"); + self.role = data.role().role().role.to_string(); + self.task = Some(data.id.to_string()); + self.env_path = f_env_path + .unwrap_or(opt_stack.calc_path(env_path)) + .iter() + .map(|s| s.to_string()) + .collect(); + self.score = score; + self.setuid = data.setuid.clone().map(|u| u.fetch_id()).flatten(); + self.setgroups = data.setgroups.clone().and_then(|g| match g { + DGroups::Single(g) => Some(vec![g.fetch_id()].into_iter().flatten().collect()), + DGroups::Multiple(g) => Some(g.iter().filter_map(|g| g.fetch_id()).collect()), + }); + self.caps = data.caps.clone(); + opt_stack.set_role(data.role().role().options.clone()); + opt_stack.set_task(data.task().options.clone()); + } + + Ok(found) + } + + pub fn command_settings<'d, 'l, 't, 'c, 'a>( + &mut self, + env_path: &[&str], + cli: &'d Cli, + data: &DLinkedCommand<'d, 'l, 't, 'c, 'a>, + ) -> Result> { + debug!("env_path: {:?}", env_path); + Ok(match &**data { + de::DCommand::Simple(role_cmd) => { + let mut final_path = None; + let cmd_min = cmd::evaluate_command_match( + env_path, + &cli.cmd_path, + &cli.cmd_args, + role_cmd, + &self.score.cmd_min, + &mut final_path, + ); + if let Some(final_path) = final_path { + self.update_command_score(final_path, cmd_min) + } else { + false + } + } + de::DCommand::Complex(value) => { + let mut cmd_min = CmdMin::empty(); + let mut final_path = None; + Api::notify(ApiEvent::ProcessComplexCommand( + value, + env_path, + &cli.cmd_path, + &cli.cmd_args, + &mut cmd_min, + &mut final_path, + ))?; + if let Some(final_path) = final_path { + self.update_command_score(final_path, cmd_min) + } else { + false + } + } + }) + } + + fn update_command_score(&mut self, final_path: PathBuf, res: CmdMin) -> bool { + if res.better(&self.score.cmd_min) { + self.score.cmd_min = res; + self.final_path = final_path; + true + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use super::de::{DCommand, DCommandList, DRoleFinder, DTaskFinder, IdTask}; + use super::*; + use rar_common::database::score::{ActorMatchMin, CmdMin, Score}; + use rar_common::database::structs::SetBehavior; + use serde_json_borrow::Value; + use std::path::PathBuf; + + use crate::Cli; + use rar_common::Cred; + + // Helper: Dummy implementations for required traits/structs + fn dummy_cli() -> Cli { + Cli::builder() + .cmd_path("/usr/bin/ls".to_string()) + .cmd_args(vec!["-l".to_string()]) + .build() + } + + fn dummy_cred() -> Cred { + Cred::builder().build() + } + + fn dummy_dconfigfinder<'a>() -> DConfigFinder<'a> { + DConfigFinder::builder() + .roles(vec![ + DRoleFinder::builder() + .user_min(ActorMatchMin::UserMatch) + .role("test") + .tasks(vec![ + DTaskFinder::builder() + .id(IdTask::Number(0)) + .caps(!CapSet::empty()) + .commands( + DCommandList::builder(SetBehavior::None) + .add(vec![DCommand::simple("/usr/bin/ls -l")]) + .build(), + ) + .build(), + DTaskFinder::builder() + .id(IdTask::Number(1)) + .caps(CapSet::empty()) + .commands( + DCommandList::builder(SetBehavior::None) + .add(vec![ + DCommand::simple("/usr/bin/ls ^.*$"), + DCommand::complex(Value::Object( + [("key".into(), Value::Str("value".into()))] + .into_iter() + .collect::>() + .into(), + )), + ]) + .build(), + ) + .build(), + ]) + .build(), + DRoleFinder::builder() + .user_min(ActorMatchMin::UserMatch) + .role("test2") + .tasks(vec![ + DTaskFinder::builder() + .id(IdTask::Number(0)) + .caps(!CapSet::empty()) + .commands( + DCommandList::builder(SetBehavior::None) + .add(vec![DCommand::simple("/usr/bin/ls -l")]) + .build(), + ) + .build(), + DTaskFinder::builder() + .id(IdTask::Number(1)) + .caps(CapSet::empty()) + .commands( + DCommandList::builder(SetBehavior::None) + .add(vec![DCommand::simple("/usr/bin/ls ^.*$")]) + .build(), + ) + .build(), + ]) + .build(), + ]) + .build() + } + + #[test] + fn test_retrieve_settings_no_matching_role() { + let cli = dummy_cli(); + let cred = dummy_cred(); + let data = dummy_dconfigfinder(); + let env_vars = vec![("KEY", "VALUE")]; + let env_path = &["/bin"]; + let result = BestExecSettings::retrieve_settings(&cli, &cred, &data, env_vars, env_path); + assert!(result.is_ok()); + } + + #[test] + fn test_role_settings_calls_actors_and_tasks() { + let mut best = BestExecSettings::default(); + let cli = dummy_cli(); + let binding = dummy_dconfigfinder(); + let data = binding.roles().nth(0).unwrap(); + let mut opt_stack = BorrowedOptStack::new(None); + let env_path = &["/bin"]; + let result = best.role_settings(&cli, &data, &mut opt_stack, env_path); + assert!(result.is_ok()); + } + + #[test] + fn test_actors_settings_returns_bool() { + let mut best = BestExecSettings::default(); + let binding = dummy_dconfigfinder(); + let data = binding.roles().nth(0).unwrap(); + let result = best.actors_settings(&data); + assert!(result.is_ok()); + assert!(matches!(result, Ok(_))); + } + + #[test] + fn test_task_settings_sets_fields_on_found() { + let mut best = BestExecSettings::default(); + let cli = dummy_cli(); + let binding = dummy_dconfigfinder(); + let binding = binding.roles().nth(0).unwrap(); + let data = binding.tasks().nth(0).unwrap(); + let mut opt_stack = BorrowedOptStack::new(None); + let env_path = &["/bin"]; + let result = best.task_settings(&cli, &data, &mut opt_stack, env_path); + assert!(result.is_ok()); + } + + #[cfg(feature = "pcre2")] + #[test] + fn test_command_settings_simple_and_complex() { + let mut best = BestExecSettings::default(); + let cli = dummy_cli(); + let env_path = &["/usr/bin"]; + let binding = dummy_dconfigfinder(); + let binding = binding.roles().nth(0).unwrap(); + let binding = binding.tasks().nth(1).unwrap(); + let binding = binding.commands().unwrap(); + let data = binding.add().nth(0).unwrap(); + let result = best.command_settings(env_path, &cli, &data); + assert!(result.is_ok()); + assert!(result.unwrap()); + let data = binding.add().nth(1).unwrap(); + let result = best.command_settings(env_path, &cli, &data); + assert!( + result.is_ok(), + "Failed to process complex command : {}", + result.unwrap_err() + ); + assert!(!result.unwrap()) + } + + #[test] + fn test_update_command_score_better() { + let mut settings = BestExecSettings { + score: Score { + cmd_min: CmdMin::RegexArgs, + ..Default::default() + }, + final_path: PathBuf::from("/old/path"), + ..Default::default() + }; + let new_cmd_min = CmdMin::Match; + let new_path = PathBuf::from("/new/path"); + let updated = settings.update_command_score(new_path.clone(), new_cmd_min.clone()); + assert!(updated); + assert_eq!(settings.score.cmd_min, new_cmd_min); + assert_eq!(settings.final_path, new_path); + } + + #[test] + fn test_update_command_score_not_better() { + let mut settings = BestExecSettings { + score: Score { + cmd_min: CmdMin::Match, + ..Default::default() + }, + final_path: PathBuf::from("/old/path"), + ..Default::default() + }; + let worse_cmd_min = CmdMin::RegexArgs; + let new_path = PathBuf::from("/new/path"); + let updated = settings.update_command_score(new_path, worse_cmd_min); + assert!(!updated); + assert_eq!(settings.final_path, PathBuf::from("/old/path")); + } +} diff --git a/src/sr/finder/options.rs b/src/sr/finder/options.rs new file mode 100644 index 00000000..522cb904 --- /dev/null +++ b/src/sr/finder/options.rs @@ -0,0 +1,1082 @@ +use std::collections::HashSet; +use std::error::Error; +use std::{borrow::Cow, collections::HashMap}; + +use bon::{bon, builder, Builder}; +use chrono::Duration; + +use libc::PATH_MAX; +use rar_common::database::options::{ + EnvBehavior, Level, PathBehavior, SAuthentication, SBounding, SPathOptions, SPrivileged, + STimeout, +}; +use rar_common::database::score::SecurityMin; +use rar_common::database::FilterMatcher; +use std::hash::Hash; + +#[cfg(feature = "pcre2")] +use pcre2::bytes::Regex; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +use log::debug; + +use crate::Cred; + +use super::de::DLinkedTask; + +//#[cfg(feature = "finder")] +//use super::finder::Cred; +//#[cfg(feature = "finder")] +//use super::finder::SecurityMin; +#[cfg(not(tarpaulin_include))] +fn default<'a>() -> Opt<'a> { + Opt::builder(Level::Default) + .maybe_root(env!("RAR_USER_CONSIDERED").parse().ok()) + .maybe_bounding(env!("RAR_BOUNDING").parse().ok()) + .path(DPathOptions::default_path()) + .maybe_authentication(env!("RAR_AUTHENTICATION").parse().ok()) + .env( + DEnvOptions::builder( + env!("RAR_ENV_DEFAULT") + .parse() + .unwrap_or(EnvBehavior::Delete), + ) + .keep(env!("RAR_ENV_KEEP_LIST").split(',').collect::>()) + .unwrap() + .check(env!("RAR_ENV_CHECK_LIST").split(',').collect::>()) + .unwrap() + .delete( + env!("RAR_ENV_DELETE_LIST") + .split(',') + .collect::>(), + ) + .unwrap() + .set( + serde_json::from_str(env!("RAR_ENV_SET_LIST")) + .unwrap_or_else(|_| Map::default()) + .into_iter() + .filter_map(|(k, v)| { + if let Some(v) = v.as_str() { + Some((k.to_string(), v.to_string())) + } else { + None + } + }), + ) + .maybe_override_behavior(env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().ok()) + .build(), + ) + .timeout( + STimeout::builder() + .maybe_type_field(env!("RAR_TIMEOUT_TYPE").parse().ok()) + .maybe_duration( + convert_string_to_duration(&env!("RAR_TIMEOUT_DURATION").to_string()) + .ok() + .flatten(), + ) + .build(), + ) + .wildcard_denied(env!("RAR_WILDCARD_DENIED")) + .build() +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Builder, Default)] +pub struct DPathOptions<'a> { + #[serde(rename = "default", default, skip_serializing_if = "is_default")] + #[builder(start_fn)] + pub default_behavior: PathBehavior, + #[serde(borrow, default, skip_serializing_if = "Option::is_none")] + #[builder(with = |v : impl IntoIterator>>| { v.into_iter().map(|s| s.into()).collect() })] + pub add: Option]>>, + #[serde( + borrow, + default, + skip_serializing_if = "Option::is_none", + alias = "del" + )] + #[builder(with = |v : impl IntoIterator>>| { v.into_iter().map(|s| s.into()).collect() })] + pub sub: Option]>>, +} + +#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Default, Builder)] +pub struct DEnvOptions<'a> { + #[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(borrow, default, skip_serializing_if = "HashMap::is_empty")] + #[builder(default, with = |iter: impl IntoIterator>, impl Into>)>| { + let mut map = HashMap::with_hasher(Default::default()); + map.extend(iter.into_iter().map(|(k, v)| (k.into(), v.into()))); + map + })] + pub set: HashMap, Cow<'a, str>>, + #[serde(borrow, default, skip_serializing_if = "HashSet::is_empty")] + #[builder(default, with = |v : impl IntoIterator>>| -> Result<_,Cow<'a,str>> { let mut res = HashSet::new(); for s in v { res.insert(s.into()); } Ok(res)})] + pub keep: HashSet>, + #[serde(borrow, default, skip_serializing_if = "HashSet::is_empty")] + #[builder(default, with = |v : impl IntoIterator>>| -> Result<_,Cow<'a,str>> { let mut res = HashSet::new(); for s in v { res.insert(s.into()); } Ok(res)})] + pub check: HashSet>, + #[serde(borrow, default, skip_serializing_if = "HashSet::is_empty")] + #[builder(default, with = |v : impl IntoIterator>>| -> Result<_,Cow<'a,str>> { let mut res = HashSet::new(); for s in v { res.insert(s.into()); } Ok(res)})] + pub delete: HashSet>, +} + +#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Default)] +#[serde(rename_all = "kebab-case")] +pub struct Opt<'a> { + #[serde(skip)] + pub level: Level, + #[serde(borrow, skip_serializing_if = "Option::is_none")] + pub path: Option>, + #[serde(borrow, skip_serializing_if = "Option::is_none")] + pub env: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub root: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bounding: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub authentication: Option, + #[serde(borrow, skip_serializing_if = "Option::is_none")] + pub wildcard_denied: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout: Option, + #[serde(default, flatten)] + pub _extra_fields: Value, +} + +#[bon] +impl<'a> Opt<'a> { + #[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: Value, + ) -> Self { + Self { + level, + path, + env, + root, + bounding, + authentication, + wildcard_denied, + timeout, + _extra_fields, + } + } +} + +impl<'a> DEnvOptions<'a> { + pub fn calc_final_env( + &self, + env_vars: impl IntoIterator, impl Into)>, + env_path: &[&str], + target: &Cred, + ) -> Result, Box> { + let mut final_set = match self.default_behavior { + EnvBehavior::Inherit => Err("Internal Error with environment behavior".to_string()), + EnvBehavior::Delete => Ok(env_vars + .into_iter() + .filter_map(|(key, value)| { + let needle = key.into().into(); + let value: String = value.into(); + if env_matches(&self.keep, &needle) + || (env_matches(&self.check, &needle) && check_env(&needle, &value)) + { + Some((needle.to_string(), value)) + } else { + None + } + }) + .collect::>()), + EnvBehavior::Keep => Ok(env_vars + .into_iter() + .filter_map(|(key, value)| { + let needle = key.into().into(); + let value: String = value.into(); + if !env_matches(&self.delete, &needle) + || (env_matches(&self.check, &needle) && check_env(&needle, &value)) + { + Some((needle.to_string(), value)) + } else { + None + } + }) + .collect::>()), + }?; + final_set.insert( + "PATH".into(), + env_path.iter().fold(String::new(), |acc, path| { + if acc.is_empty() { + path.to_string() + } else { + format!("{}:{}", acc, path) + } + }), + ); + final_set.insert("LOGNAME".into(), target.user.name.clone()); + final_set.insert("USER".into(), target.user.name.clone()); + final_set.insert("HOME".into(), target.user.dir.to_string_lossy().to_string()); + final_set + .entry("TERM".into()) + .or_insert_with(|| "unknown".into()); + final_set.insert( + "SHELL".into(), + target.user.shell.to_string_lossy().to_string(), + ); + final_set.extend( + self.set + .iter() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect::>(), + ); + Ok(final_set) + } +} + +impl Into for Opt<'_> { + fn into(self) -> rar_common::database::options::Opt { + rar_common::database::options::Opt::builder(self.level) + .maybe_path(if let Some(spath) = self.path { + Some( + rar_common::database::options::SPathOptions::builder(spath.default_behavior) + .maybe_add( + spath + .add + .map(|v| v.into_iter().map(|s| s.to_string()).collect::>()), + ) + .maybe_sub( + spath + .sub + .map(|v| v.into_iter().map(|s| s.to_string()).collect::>()), + ) + .build(), + ) + } else { + None + }) + .maybe_env(if let Some(senv) = self.env { + Some( + rar_common::database::options::SEnvOptions::builder(senv.default_behavior) + .maybe_override_behavior(senv.override_behavior) + .set( + senv.set + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>(), + ) + .keep( + senv.keep + .into_iter() + .map(|v| v.to_string()) + .collect::>(), + ) + .unwrap() + .check( + senv.check + .into_iter() + .map(|v| v.to_string()) + .collect::>(), + ) + .unwrap() + .delete( + senv.delete + .into_iter() + .map(|v| v.to_string()) + .collect::>(), + ) + .unwrap() + .build(), + ) + } else { + None + }) + .maybe_root(self.root) + .maybe_bounding(self.bounding) + .maybe_authentication(self.authentication) + .maybe_wildcard_denied(self.wildcard_denied) + .maybe_timeout(self.timeout) + .build() + } +} + +impl Into for DPathOptions<'_> { + fn into(self) -> SPathOptions { + SPathOptions::builder(self.default_behavior) + .maybe_add( + self.add + .map(|v| v.into_iter().map(|s| s.to_string()).collect::>()), + ) + .maybe_sub( + self.sub + .map(|v| v.into_iter().map(|s| s.to_string()).collect::>()), + ) + .build() + } +} + +impl DPathOptions<'_> { + pub fn default_path<'a>() -> DPathOptions<'a> { + DPathOptions::builder( + env!("RAR_PATH_DEFAULT") + .parse() + .unwrap_or(PathBehavior::Delete), + ) + .add(env!("RAR_PATH_ADD_LIST").split(':').collect::>()) + .sub( + env!("RAR_PATH_REMOVE_LIST") + .split(':') + .collect::>(), + ) + .build() + } + pub fn calc_path<'a>(&'a self, path_var: &'a [&'a str]) -> Vec<&'a str> { + let default = Default::default(); + match self.default_behavior { + PathBehavior::Inherit | PathBehavior::Delete => { + if let Some(add) = &self.add { + let sub = self.sub.as_ref().unwrap_or(&default); + add.iter() + .filter(|item| !sub.contains(*item)) + .map(|s| s.as_ref()) + .collect() + } else { + Vec::new() + } + } + is_safe => { + let sub = self.sub.as_ref(); + self.add + .as_ref() + .map(|cow| cow.iter()) + .into_iter() + .flatten() + .map(|s| s.as_ref()) + .chain(path_var.iter().copied()) + .filter(move |s| { + let not_in_sub = !sub.is_some_and(|set| set.iter().any(|p| *s == p)); + not_in_sub && (!is_safe.is_keep_safe() || !s.starts_with('/')) + }) + .collect() + } + } + } +} + +impl<'a> DPathOptions<'a> { + pub fn union(&mut self, path_options: DPathOptions<'a>) { + match path_options.default_behavior { + PathBehavior::Inherit => { + if let Some(add) = &path_options.add { + self.add + .get_or_insert_with(Default::default) + .to_mut() + .extend_from_slice(&add); + } + if let Some(sub) = &path_options.sub { + self.sub + .get_or_insert_with(Default::default) + .to_mut() + .extend_from_slice(&sub); + } + } + behaviors => { + self.add = path_options.add.clone(); + self.sub = path_options.sub.clone(); + self.default_behavior = behaviors; + } + } + } +} + +fn check_env(key: impl AsRef, value: impl AsRef) -> bool { + debug!("Checking env: {}={}", key.as_ref(), value.as_ref()); + match key.as_ref() { + "TZ" => tz_is_safe(value.as_ref()), + _ => !value.as_ref().chars().any(|c| c == '/' || c == '%'), + } +} + +fn env_matches(set: &HashSet, needle: &K) -> bool +where + K: AsRef + Eq + Hash, +{ + set.contains(&needle) || set.iter().any(|key| test_pattern(&needle, key.as_ref())) +} + +fn is_valid_env_name(s: &str) -> bool { + let mut chars = s.chars(); + + // Check if the first character is a letter or underscore + if let Some(first_char) = chars.next() { + if !(first_char.is_ascii_alphabetic() || first_char == '_') { + return false; + } + } else { + return false; // Empty string + } + + // Check if the remaining characters are alphanumeric or underscores + chars.all(|c| c.is_ascii_alphanumeric() || c == '_') +} + +#[cfg(feature = "pcre2")] +fn is_regex(s: impl AsRef) -> bool { + Regex::new(s.as_ref()).is_ok() +} + +#[cfg(not(feature = "pcre2"))] +fn is_regex(_s: impl AsRef) -> bool { + false // Always return false if regex feature is disabled +} + +#[cfg(feature = "pcre2")] +fn test_pattern(pattern: impl AsRef, subject: impl AsRef) -> bool { + Regex::new(&format!("^{}$", pattern.as_ref())) // convert to regex + .and_then(|r| r.is_match(subject.as_ref().as_bytes())) + .is_ok_and(|m| m) +} + +#[cfg(not(feature = "pcre2"))] +fn test_pattern(_: impl AsRef, _: impl AsRef) -> bool { + false +} + +fn tz_is_safe(tzval: &str) -> bool { + // tzcode treats a value beginning with a ':' as a path. + let tzval = if let Some(val) = tzval.strip_prefix(':') { + val + } else { + tzval + }; + + // Reject fully-qualified TZ that doesn't begin with the zoneinfo dir. + if tzval.starts_with('/') { + return false; + } + + // Make sure TZ only contains printable non-space characters + // and does not contain a '..' path element. + let mut lastch = '/'; + for cp in tzval.chars() { + if cp.is_ascii_whitespace() || !cp.is_ascii_graphic() { + return false; + } + if lastch == '/' + && cp == '.' + && tzval + .chars() + .nth(tzval.chars().position(|c| c == '.').unwrap() + 1) + == Some('.') + && (tzval + .chars() + .nth(tzval.chars().position(|c| c == '.').unwrap() + 2) + == Some('/') + || tzval + .chars() + .nth(tzval.chars().position(|c| c == '.').unwrap() + 2) + .is_none()) + { + return false; + } + lastch = cp; + } + + // Reject extra long TZ values (even if not a path). + if tzval.len() >= >::try_into(PATH_MAX).unwrap() { + return false; + } + + true +} + +pub fn is_default(t: &T) -> bool { + t == &T::default() +} + +fn convert_string_to_duration(s: &String) -> Result, Box> { + let mut parts = s.split(':'); + //unwrap or error + if let (Some(hours), Some(minutes), Some(seconds)) = (parts.next(), parts.next(), parts.next()) + { + let hours: i64 = hours.parse()?; + let minutes: i64 = minutes.parse()?; + let seconds: i64 = seconds.parse()?; + return Ok(Some( + Duration::hours(hours) + Duration::minutes(minutes) + Duration::seconds(seconds), + )); + } + Err("Invalid duration format".into()) +} + +pub struct BorrowedOptStack<'a> { + default_opt: Opt<'a>, + config: Option>, + role: Option>, + task: Option>, +} + +impl<'a, 'b, 'c, 't> BorrowedOptStack<'a> { + pub fn new(config: Option>) -> Self { + Self { + default_opt: default(), + config, + role: None, + task: None, + } + } + pub fn set_role(&mut self, role: Option>) { + self.role = role; + } + pub fn set_task(&mut self, task: Option>) { + self.task = task; + } + pub fn from_task(task: &DLinkedTask<'t, 'c, 'a>) -> Self { + let default_opt = default(); + let config = task.role().config().options.clone(); + let role = task.role().role().options.clone(); + let task_opt = task.task.options.clone(); + Self { + default_opt, + config, + role, + task: task_opt, + } + } + pub fn calc_path(&self, path_var: &[&str]) -> Vec { + // Preallocate with a reasonable guess, but will only allocate once. + let mut combined_paths: Vec = Vec::with_capacity(path_var.len()); + + // Stack of options in order: default, config, role, task + let stack = [ + Some(&self.default_opt), + self.config.as_ref(), + self.role.as_ref(), + self.task.as_ref(), + ]; + + for opt in stack.iter().flatten() { + if let Some(ref path_opt) = opt.path { + match path_opt.default_behavior { + PathBehavior::Inherit => { + if let Some(ref add_paths) = path_opt.add { + combined_paths.extend(add_paths.iter().map(|p| p.to_string())); + } + if let Some(ref sub_paths) = path_opt.sub { + // Avoid allocation by using retain and Cow::Borrowed + combined_paths.retain(|path| !sub_paths.contains(&Cow::Borrowed(path))); + } + } + PathBehavior::Delete => { + combined_paths.clear(); + if let Some(ref add_paths) = path_opt.add { + combined_paths.extend(add_paths.iter().map(|p| p.to_string())); + } + } + ref is_safe => { + combined_paths.clear(); + combined_paths.extend( + path_var + .iter() + .map(|s| s.to_string()) + .filter(|path| is_safe.is_keep_unsafe() || path.starts_with('/')), + ); + } + } + } + } + combined_paths + } + + pub fn calc_security_min(&self) -> SecurityMin { + let mut security_min = SecurityMin::default(); + // todo: fix the algorithm + [ + self.task.as_ref(), + self.role.as_ref(), + self.config.as_ref(), + Some(&self.default_opt), + ] + .iter() + .flatten() + .for_each(|o| { + if !security_min.contains(SecurityMin::DisableBounding) + && o.bounding.is_some_and(|b| b.is_ignore()) + { + security_min |= SecurityMin::DisableBounding; + } + if !security_min.contains(SecurityMin::EnableRoot) + && o.root.is_some_and(|r| r == SPrivileged::Privileged) + { + security_min |= SecurityMin::EnableRoot; + } + if !security_min.contains(SecurityMin::SkipAuth) + && o.authentication.is_some_and(|a| a == SAuthentication::Skip) + { + security_min |= SecurityMin::SkipAuth; + } + if !security_min.contains(SecurityMin::KeepEnv) + && o.env.as_ref().is_some_and(|e| { + e.default_behavior.is_keep() || e.override_behavior.as_ref().is_some_and(|o| *o) + }) + { + security_min |= SecurityMin::KeepEnv; + } + if !security_min.contains(SecurityMin::KeepPath) + && o.path + .as_ref() + .is_some_and(|p| p.default_behavior.is_keep_safe()) + { + security_min |= SecurityMin::KeepPath; + } + if !security_min.contains(SecurityMin::KeepUnsafePath) + && o.path + .as_ref() + .is_some_and(|p| p.default_behavior.is_keep_unsafe()) + { + security_min |= SecurityMin::KeepUnsafePath; + } + }); + security_min + } + + pub fn calc_override_behavior(&self) -> Option { + [ + self.task.as_ref(), + self.role.as_ref(), + self.config.as_ref(), + Some(&self.default_opt), + ] + .iter() + .flatten() + .filter_map(|o| o.env.as_ref()) + .find_map(|o| o.override_behavior) + } + pub fn calc_temp_env( + &self, + override_behavior: &Option, + opt_filter: &Option, + ) -> DEnvOptions<'_> { + let mut result = DEnvOptions::default(); + fn determine_final_behavior<'a>( + override_behavior: &Option, + opt_filter: &Option, + final_behavior: &mut EnvBehavior, + overriden: &mut bool, + o: &DEnvOptions<'_>, + ) { + if !*overriden { + if let Some(behavior) = opt_filter + .as_ref() + .and_then(|f| { + if override_behavior.is_some_and(|o| o) { + *overriden = true; + f.env_behavior + } else { + None + } + }) + .or_else(|| { + if o.default_behavior.is_inherit() { + None + } else { + Some(o.default_behavior) + } + }) + { + *final_behavior = behavior; + } + } + } + let mut overriden = false; + [ + Some(&self.default_opt), + self.config.as_ref(), + self.role.as_ref(), + self.task.as_ref(), + ] + .iter() + .flatten() + .filter_map(|o| o.env.as_ref()) + .for_each(|o| { + determine_final_behavior( + &override_behavior, + &opt_filter, + &mut result.default_behavior, + &mut overriden, + o, + ); + if o.default_behavior.is_keep() || o.default_behavior.is_delete() { + result.set.clear(); + result.keep.clear(); + result.delete.clear(); + result.check.clear(); + } + result.set.extend( + o.set + .iter() + .filter(|(k, _)| is_valid_env_name(k.as_ref())) + .map(|(k, v)| (k.to_string().into(), v.to_string().into())), + ); + result.keep.extend( + o.keep + .iter() + .cloned() + .filter(|p| is_valid_env_name(p.as_ref()) || is_regex(p.as_ref())) + .map(|k| k.to_string().into()), + ); + result.delete.extend( + o.delete + .iter() + .cloned() + .filter(|p| is_valid_env_name(p.as_ref()) || is_regex(p.as_ref())) + .map(|k| k.to_string().into()), + ); + result.check.extend( + o.check + .iter() + .cloned() + .filter(|p| is_valid_env_name(p.as_ref()) || is_regex(p.as_ref())) + .map(|k| k.to_string().into()), + ); + }); + result + } + + pub fn calc_bounding(&self) -> SBounding { + [ + self.task.as_ref(), + self.role.as_ref(), + self.config.as_ref(), + Some(&self.default_opt), + ] + .iter() + .flatten() + .filter_map(|o| o.bounding) + .next() + .unwrap_or(SBounding::default()) + } + pub fn calc_timeout(&self) -> STimeout { + [ + self.task.as_ref(), + self.role.as_ref(), + self.config.as_ref(), + Some(&self.default_opt), + ] + .iter() + .flatten() + .filter_map(|o| o.timeout.clone()) + .next() + .unwrap_or(STimeout::default()) + } + pub fn calc_authentication(&self) -> SAuthentication { + [ + self.task.as_ref(), + self.role.as_ref(), + self.config.as_ref(), + Some(&self.default_opt), + ] + .iter() + .flatten() + .filter_map(|o| o.authentication) + .next() + .unwrap_or(SAuthentication::default()) + } + pub fn calc_privileged(&self) -> SPrivileged { + [ + self.task.as_ref(), + self.role.as_ref(), + self.config.as_ref(), + Some(&self.default_opt), + ] + .iter() + .flatten() + .filter_map(|o| o.root) + .next() + .unwrap_or(SPrivileged::default()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tz_is_safe() { + assert!(tz_is_safe("America/New_York")); + assert!(!tz_is_safe("/America/New_York")); + assert!(!tz_is_safe("America/New_York/..")); + //assert path max + assert!(!tz_is_safe( + String::from_utf8(vec![b'a'; (PATH_MAX + 1).try_into().unwrap()]) + .unwrap() + .as_str() + )); + } + + #[test] + fn test_is_valid_env_name() { + assert!(is_valid_env_name("VAR_NAME")); + assert!(is_valid_env_name("_VAR_NAME")); + assert!(!is_valid_env_name("1_VAR_NAME")); + assert!(!is_valid_env_name("VAR-NAME")); + assert!(!is_valid_env_name("VAR NAME")); + assert!(!is_valid_env_name("")); + } + #[test] + fn test_is_regex() { + #[cfg(feature = "pcre2")] + assert!(is_regex("^[a-zA-Z0-9_]+$")); + #[cfg(not(feature = "pcre2"))] + assert!(!is_regex("^[a-zA-Z0-9_]+$")); + assert!(!is_regex("[a-z")); + } + + #[test] + fn test_test_pattern() { + #[cfg(feature = "pcre2")] + assert!(test_pattern("^[a-zA-Z0-9_]+$", "test")); + #[cfg(not(feature = "pcre2"))] + assert!(!test_pattern("^[a-zA-Z0-9_]+$", "test")); + assert!(!test_pattern("[a-z", "test")); + } + + #[test] + fn test_check_env() { + assert!(check_env("TZ", "America/New_York")); + assert!(!check_env("TZ", "/America/New_York")); + assert!(!check_env("TZ", "America/New_York/..")); + assert!(!check_env("VAR_NAME", "VAR%NAME")); + assert!(check_env("VAR_NAME", "VAR_NAME")); + } + + #[test] + fn test_env_matches() { + let set: HashSet = ["VAR1", "VAR2"].iter().map(|s| s.to_string()).collect(); + assert!(env_matches(&set, &"VAR1".to_string())); + assert!(!env_matches(&set, &"VAR3".to_string())); + } + + #[test] + fn test_calc_path() { + let path_options = DPathOptions::builder(PathBehavior::Inherit) + .add(vec!["/usr/local/bin", "/usr/bin"]) + .sub(vec!["/usr/bin"]) + .build(); + let path_var = ["/bin", "/usr/bin"]; + let result = path_options.calc_path(&path_var); + assert_eq!(result, vec!["/usr/local/bin"]); + } + + #[test] + fn test_calc_env() { + let env_options = DEnvOptions::builder(EnvBehavior::Delete) + .set(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")]) + .keep(vec!["VAR3"]) + .unwrap() + .delete(vec!["VAR4"]) + .unwrap() + .check(vec!["VAR5"]) + .unwrap() + .build(); + let env_vars = vec![ + ("VAR1", "AAAA"), + ("VAR3", "VALUE3"), + ("VAR4", "VALUE4"), + ("VAR5", "VALUE5"), + ]; + let env_path = vec!["/usr/local/bin", "/usr/bin"]; + let target = Cred::builder().build(); + let result = env_options.calc_final_env(env_vars, &env_path, &target); + assert!( + result.is_ok(), + "Failed to calculate final env {}", + result.unwrap_err() + ); + let final_env = result.unwrap(); + assert_eq!(final_env.get("PATH").unwrap(), "/usr/local/bin:/usr/bin"); + assert_eq!(*final_env.get("LOGNAME").unwrap(), target.user.name); + assert_eq!(*final_env.get("USER").unwrap(), target.user.name); + assert_eq!( + *final_env.get("HOME").unwrap(), + target.user.dir.to_string_lossy() + ); + assert_eq!(final_env.get("TERM").unwrap(), "unknown"); + assert_eq!( + *final_env.get("SHELL").unwrap(), + target.user.shell.to_string_lossy() + ); + assert_eq!(final_env.get("VAR1").unwrap(), "VALUE1"); + assert_eq!(final_env.get("VAR2").unwrap(), "VALUE2"); + assert_eq!(final_env.get("VAR3").unwrap(), "VALUE3"); + assert!(final_env.get("VAR4").is_none()); + assert!(final_env.get("VAR5").unwrap() == "VALUE5"); + + let env_options = DEnvOptions::builder(EnvBehavior::Keep) + .set(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")]) + .keep(vec!["VAR3"]) + .unwrap() + .delete(vec!["VAR4"]) + .unwrap() + .check(vec!["VAR5"]) + .unwrap() + .build(); + let env_vars = vec![ + ("VAR1", "AAAA"), + ("VAR3", "VALUE3"), + ("VAR4", "VALUE4"), + ("VAR5", "VALUE5"), + ]; + let env_path = vec!["/usr/local/bin", "/usr/bin"]; + let target = Cred::builder().build(); + let result = env_options.calc_final_env(env_vars, &env_path, &target); + assert!( + result.is_ok(), + "Failed to calculate final env {}", + result.unwrap_err() + ); + let final_env = result.unwrap(); + assert_eq!(final_env.get("PATH").unwrap(), "/usr/local/bin:/usr/bin"); + assert_eq!(*final_env.get("LOGNAME").unwrap(), target.user.name); + assert_eq!(*final_env.get("USER").unwrap(), target.user.name); + assert_eq!( + *final_env.get("HOME").unwrap(), + target.user.dir.to_string_lossy() + ); + assert_eq!(final_env.get("TERM").unwrap(), "unknown"); + assert_eq!( + *final_env.get("SHELL").unwrap(), + target.user.shell.to_string_lossy() + ); + assert_eq!(final_env.get("VAR1").unwrap(), "VALUE1"); + assert_eq!(final_env.get("VAR2").unwrap(), "VALUE2"); + assert_eq!(final_env.get("VAR3").unwrap(), "VALUE3"); + assert!(final_env.get("VAR4").is_none()); + assert!(final_env.get("VAR5").unwrap() == "VALUE5"); + + let env_options = DEnvOptions::builder(EnvBehavior::Inherit) + .set(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")]) + .keep(vec!["VAR3"]) + .unwrap() + .delete(vec!["VAR4"]) + .unwrap() + .check(vec!["VAR5"]) + .unwrap() + .build(); + let env_vars = vec![ + ("VAR1", "AAAA"), + ("VAR3", "VALUE3"), + ("VAR4", "VALUE4"), + ("VAR5", "VALUE5"), + ]; + let env_path = vec!["/usr/local/bin", "/usr/bin"]; + let target = Cred::builder().build(); + let result = env_options.calc_final_env(env_vars, &env_path, &target); + assert!(result.is_err()); + } + + #[test] + fn test_is_default() { + let default = Opt::default(); + assert!(is_default(&default)); + let non_default = Opt::builder(Level::Default).build(); + assert!(!is_default(&non_default)); + } + + #[test] + fn test_convert_string_to_duration() { + let duration = convert_string_to_duration(&"01:30:00".to_string()); + assert!(duration.is_ok()); + assert_eq!( + duration.unwrap(), + Some(Duration::hours(1) + Duration::minutes(30)) + ); + let invalid_duration = convert_string_to_duration(&"invalid".to_string()); + assert!(invalid_duration.is_err()); + } + + #[test] + fn test_borrowed_opt_stack() { + let config = Some( + Opt::builder(Level::Global) + .env( + DEnvOptions::builder(EnvBehavior::Delete) + .check(["CHECKME"]) + .unwrap() + .set([("VAR1", "VALUE1"), ("VAR2", "VALUE2")]) + .build(), + ) + .build(), + ); + let role = Some( + Opt::builder(Level::Role) + .env( + DEnvOptions::builder(EnvBehavior::Inherit) + .delete(["DELETEME"]) + .unwrap() + .build(), + ) + .build(), + ); + let task = Some( + Opt::builder(Level::Task) + .env( + DEnvOptions::builder(EnvBehavior::Inherit) + .keep(["KEEPME"]) + .unwrap() + .build(), + ) + .build(), + ); + let mut stack = BorrowedOptStack::new(config); + stack.set_role(role); + stack.set_task(task); + assert_eq!( + stack.calc_path(&["/test"]), + env!("RAR_PATH_ADD_LIST").split(':').collect::>() + ); + let env = stack.calc_temp_env(&None, &None); + assert_eq!(env.delete, HashSet::from(["DELETEME".into()])); + assert_eq!(env.keep, HashSet::from(["KEEPME".into()])); + assert_eq!(env.check, HashSet::from(["CHECKME".into()])); + assert_eq!( + env.set, + HashMap::from([ + ("VAR1".into(), "VALUE1".into()), + ("VAR2".into(), "VALUE2".into()) + ]) + ); + } + + #[test] + fn test_opt_into_opt() { + let opt = Opt::builder(Level::Default) + .path( + DPathOptions::builder(PathBehavior::Inherit) + .add(["/usr/local/bin"]) + .build(), + ) + .env( + DEnvOptions::builder(EnvBehavior::Keep) + .set([("VAR1", "VALUE1")]) + .build(), + ) + .build(); + let rar_opt: rar_common::database::options::Opt = opt.clone().into(); + assert_eq!(rar_opt.level, Level::Default); + assert_eq!( + rar_opt.path.unwrap().default_behavior, + PathBehavior::Inherit + ); + assert_eq!(rar_opt.env.unwrap().default_behavior, EnvBehavior::Keep); + } +} diff --git a/src/sr/main.rs b/src/sr/main.rs index f6062777..db000cd2 100644 --- a/src/sr/main.rs +++ b/src/sr/main.rs @@ -1,35 +1,30 @@ +mod finder; pub mod pam; mod timeout; +use bon::Builder; use capctl::CapState; use const_format::formatcp; -use nix::{ - libc::dev_t, - sys::stat, - unistd::{getgroups, getuid, isatty, Group, User}, -}; -use rar_common::database::{ - actor::{SGroupType, SGroups, SUserType}, - finder::{Cred, TaskMatch, TaskMatcher}, - options::EnvBehavior, - FilterMatcher, -}; -use rar_common::database::{options::OptStack, structs::SConfig}; +use finder::BestExecSettings; +use nix::{sys::stat, unistd::isatty}; use rar_common::util::escape_parser_string; +use rar_common::{ + database::{ + actor::{SGroupType, SGroups, SUserType}, + options::EnvBehavior, + FilterMatcher, + }, + Cred, +}; use log::{debug, error}; use pam::PAM_PROMPT; use pty_process::blocking::{Command, Pty}; -use std::{cell::RefCell, error::Error, io::stdout, os::fd::AsRawFd, rc::Rc}; +use std::{error::Error, io::stdout, os::fd::AsRawFd, path::PathBuf}; -use rar_common::plugin::register_plugins; -use rar_common::{ - self, - util::{ - activates_no_new_privs, dac_override_effective, drop_effective, read_effective, - setgid_effective, setpcap_effective, setuid_effective, subsribe, BOLD, RST, UNDERLINE, - }, - Storage, +use rar_common::util::{ + activates_no_new_privs, drop_effective, setgid_effective, setpcap_effective, setuid_effective, + subsribe, BOLD, RST, UNDERLINE, }; #[cfg(not(test))] @@ -81,37 +76,39 @@ const USAGE: &str = formatcp!( RST = RST ); -#[derive(Debug)] +#[derive(Debug, Builder)] struct Cli { /// Role option allows you to select a specific role to use. opt_filter: Option, + #[builder(into)] /// Prompt option allows you to override the default password prompt and use a custom one. - prompt: String, + prompt: Option, + #[builder(default, with = || true)] /// Display rights of executor info: bool, + #[builder(default, with = || true)] /// Display help help: bool, - /// Command to execute - command: Vec, + #[builder(default, into)] + /// A non-absolute path to the command that needs to be found in the PATH + cmd_path: PathBuf, + #[builder(default, with = |i : impl IntoIterator> | i.into_iter().map(|s| s.into()).collect())] + /// Command arguments + cmd_args: Vec, + + #[builder(default, with = || true)] /// Use stdin for password prompt stdin: bool, } impl Default for Cli { fn default() -> Self { - Cli { - opt_filter: None, - prompt: PAM_PROMPT.to_string(), - info: false, - help: false, - stdin: false, - command: vec![], - } + Cli::builder().prompt(PAM_PROMPT).build() } } @@ -124,16 +121,6 @@ fn cap_effective_error(caplist: &str) -> String { ) } -fn from_json_execution_settings( - args: &Cli, - config: &Rc>, - user: &Cred, -) -> Result> { - config - .matches(user, &args.opt_filter, &args.command) - .map_err(|m| m.into()) -} - fn getopt(s: I) -> Result> where I: IntoIterator, @@ -179,10 +166,11 @@ where env.replace(EnvBehavior::Keep); } "-p" | "--prompt" => { - args.prompt = iter - .next() - .map(|s| escape_parser_string(s)) - .unwrap_or_default(); + args.prompt = Some( + iter.next() + .map(|s| escape_parser_string(s)) + .expect("Missing prompt for -p option"), + ); } "-i" | "--info" => { args.info = true; @@ -194,7 +182,7 @@ where if arg.as_ref().starts_with('-') { return Err(format!("Unknown option: {}", arg.as_ref()).into()); } else { - args.command.push(escape_parser_string(arg)); + args.cmd_path = arg.as_ref().into(); break; } } @@ -205,23 +193,25 @@ where .maybe_role(role) .maybe_task(task) .maybe_env_behavior(env) - .maybe_user(user) - .maybe_group(group) + .maybe_user(user)? + .maybe_group(group)? .build(), ); for arg in iter { - args.command.push(escape_parser_string(arg)); + args.cmd_args.push(escape_parser_string(arg)); } Ok(args) } #[cfg(not(tarpaulin_include))] fn main() -> Result<(), Box> { + use std::env; + use crate::{pam::check_auth, ROOTASROLE}; + use finder::find_best_exec_settings; subsribe("sr")?; drop_effective()?; - register_plugins(); let args = std::env::args(); if args.len() < 2 { println!("{}", USAGE); @@ -233,47 +223,39 @@ fn main() -> Result<(), Box> { println!("{}", USAGE); return Ok(()); } - read_effective(true) - .or(dac_override_effective(true)) - .unwrap_or_else(|_| panic!("{}", cap_effective_error("dac_read_search or dac_override"))); - let settings = rar_common::get_settings(&ROOTASROLE.to_string()).expect("Failed to get settings"); - read_effective(false) - .and(dac_override_effective(false)) - .unwrap_or_else(|_| panic!("{}", cap_effective_error("dac_read"))); - let config = match settings.clone().as_ref().borrow().storage.method { - rar_common::StorageMethod::JSON | rar_common::StorageMethod::CBOR => { - Storage::SConfig(settings.as_ref().borrow().config.clone().unwrap()) - }, - _ => { - return Err("Unsupported storage method".into()); - } - }; let user = make_cred(); - let taskmatch = match config { - Storage::SConfig(ref config) => from_json_execution_settings(&args, config, &user) - .inspect_err(|e| { - error!("{}", e); - }) - .unwrap_or_default(), - }; - let execcfg = &taskmatch.settings; - - let optstack = &execcfg.opt; - check_auth(optstack, &config, &user, &args.prompt)?; - - if !taskmatch.fully_matching() { + let execcfg = find_best_exec_settings( + &args, + &user, + &ROOTASROLE.to_string(), + env::vars(), + env::var("PATH") + .unwrap_or_default() + .split(':') + .collect::>() + .as_slice(), + )?; + + check_auth( + &execcfg.auth, + &execcfg.timeout, + &user, + &args.prompt.unwrap_or(PAM_PROMPT.to_string()), + )?; + + if !execcfg.score.fully_matching() { println!("You are not allowed to execute this command, this incident will be reported."); error!( - "User {} tried to execute command : {:?} without the permission.", - &user.user.name, args.command + "User {} tried to execute command : {:?} {:?} without the permission.", + &user.user.name, args.cmd_path, args.cmd_args ); std::process::exit(1); } if args.info { - println!("Role: {}", execcfg.role().as_ref().borrow().name); - println!("Task: {}", execcfg.task().as_ref().borrow().name); + //println!("Role: {}", if execcfg.role.is_empty() { "None" } else { &execcfg.role }); + //println!("Task: {}", execcfg.task); println!( "With capabilities: {}", execcfg @@ -286,33 +268,27 @@ fn main() -> Result<(), Box> { } // disable root - if !optstack.get_root_behavior().1.is_privileged() { + if execcfg.root.is_user() { activates_no_new_privs().expect("Failed to activate no new privs"); } debug!("setuid : {:?}", execcfg.setuid); - setuid_setgid(execcfg); - let cred = make_cred(); + setuid_setgid(&execcfg); - set_capabilities(execcfg, optstack); - - //execute command - let envset = optstack - .calculate_filtered_env(args.opt_filter, cred, std::env::vars()) - .expect("Failed to calculate env"); + set_capabilities(&execcfg); let pty = Pty::new().expect("Failed to create pty"); debug!( "Command: {:?} {:?}", - execcfg.exec_path, - execcfg.exec_args.join(" ") + execcfg.final_path, + args.cmd_args.join(" ") ); - let command = Command::new(&execcfg.exec_path) - .args(execcfg.exec_args.iter()) + let command = Command::new(&execcfg.final_path) + .args(args.cmd_args.iter()) .env_clear() - .envs(envset) + .envs(execcfg.env) .stdin(std::process::Stdio::inherit()) .stdout(std::process::Stdio::inherit()) .stderr(std::process::Stdio::inherit()) @@ -321,7 +297,7 @@ fn main() -> Result<(), Box> { Ok(command) => command, Err(e) => { error!("{}", e); - eprintln!("sr: {} : {}", execcfg.exec_path.display(), e); + eprintln!("sr: {} : {}", execcfg.final_path.display(), e); std::process::exit(1); } }; @@ -330,45 +306,18 @@ fn main() -> Result<(), Box> { } fn make_cred() -> Cred { - let user = User::from_uid(getuid()) - .expect("Failed to get user") - .expect("Failed to get user"); - let mut groups = getgroups() - .expect("Failed to get groups") - .iter() - .map(|g| { - Group::from_gid(*g) - .expect("Failed to get group") - .expect("Failed to get group") - }) - .collect::>(); - groups.insert( - 0, - Group::from_gid(user.gid) - .expect("Failed to get group") - .expect("Failed to get group"), - ); - debug!("User: {} ({}), Groups: {:?}", user.name, user.uid, groups,); - let mut tty: Option = None; - if let Ok(stat) = stat::fstat(stdout().as_raw_fd()) { - if let Ok(istty) = isatty(stdout().as_raw_fd()) { - if istty { - tty = Some(stat.st_rdev); + return Cred::builder() + .maybe_tty(stat::fstat(stdout().as_raw_fd()).ok().and_then(|s| { + if isatty(stdout().as_raw_fd()).ok().unwrap_or(false) { + Some(s.st_rdev) + } else { + None } - } - } - // get parent pid - let ppid = nix::unistd::getppid(); - - Cred { - user, - groups, - tty, - ppid, - } + })) + .build(); } -fn set_capabilities(execcfg: &rar_common::database::finder::ExecSettings, optstack: &OptStack) { +fn set_capabilities(execcfg: &BestExecSettings) { //set capabilities if let Some(caps) = execcfg.caps { // case where capabilities are more than bounding set @@ -378,7 +327,7 @@ fn set_capabilities(execcfg: &rar_common::database::finder::ExecSettings, optsta } setpcap_effective(true).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); let mut capstate = CapState::empty(); - if !optstack.get_bounding().1.is_ignore() { + if execcfg.bounding.is_strict() { for cap in (!caps).iter() { capctl::bounding::drop(cap).expect("Failed to set bounding cap"); } @@ -393,158 +342,66 @@ fn set_capabilities(execcfg: &rar_common::database::finder::ExecSettings, optsta setpcap_effective(false).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); } else { setpcap_effective(true).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); - if !optstack.get_bounding().1.is_ignore() { + if execcfg.bounding.is_strict() { capctl::bounding::clear().expect("Failed to clear bounding cap"); } + capctl::ambient::clear().expect("Failed to clear ambient cap"); let capstate = CapState::empty(); capstate.set_current().expect("Failed to set current cap"); - setpcap_effective(false).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); } } -fn setuid_setgid(execcfg: &rar_common::database::finder::ExecSettings) { - let uid = execcfg.setuid.as_ref().and_then(|u| { - let res = u.fetch_user(); - if let Some(user) = res { - Some(user.uid.as_raw()) - } else { - None - } - }); - let gid = execcfg.setgroups.as_ref().and_then(|g| match g { - SGroups::Single(g) => { - let res = g.fetch_group(); - if let Some(group) = res { - Some(group.gid.as_raw()) - } else { - None - } - } - SGroups::Multiple(g) => { - let res = g.first().unwrap().fetch_group(); - if let Some(group) = res { - Some(group.gid.as_raw()) - } else { - None - } - } - }); - let groups = execcfg.setgroups.as_ref().and_then(|g| match g { - SGroups::Single(g) => { - let res = g.fetch_group(); - if let Some(group) = res { - Some(vec![group.gid.as_raw()]) - } else { - None - } - } - SGroups::Multiple(g) => { - 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()); - } - Some(groups) - } - }); +fn setuid_setgid(execcfg: &BestExecSettings) { + let gid = execcfg.setgroups.as_ref().and_then(|g| g.first().cloned()); setgid_effective(true).unwrap_or_else(|_| panic!("{}", cap_effective_error("setgid"))); setuid_effective(true).unwrap_or_else(|_| panic!("{}", cap_effective_error("setuid"))); - capctl::cap_set_ids(uid, gid, groups.as_deref()).expect("Failed to set ids"); + capctl::cap_set_ids(execcfg.setuid, gid, execcfg.setgroups.as_deref()) + .expect("Failed to set ids"); setgid_effective(false).unwrap_or_else(|_| panic!("{}", cap_effective_error("setgid"))); setuid_effective(false).unwrap_or_else(|_| panic!("{}", cap_effective_error("setuid"))); } #[cfg(test)] mod tests { + use capctl::{Cap, CapSet}; use libc::getgid; - use nix::unistd::Pid; - use rar_common::database::actor::SActor; - use rar_common::rc_refcell; + use nix::unistd::{getuid, Pid}; + use rar_common::database::options::SBounding; use super::*; - use rar_common::make_weak_config; - use rar_common::database::structs::{IdTask, SCommand, SCommands, SConfig, SRole, STask}; - - #[test] - fn test_from_json_execution_settings() { - let mut args = Cli { - opt_filter: None, - prompt: PAM_PROMPT.to_string(), - info: false, - help: false, - stdin: false, - command: vec!["ls".to_string(), "-l".to_string()], - }; - let user = Cred { - user: User::from_uid(0.into()).unwrap().unwrap(), - groups: vec![], - tty: None, - ppid: Pid::parent(), - }; - let config = rc_refcell!(SConfig::default()); - let role = rc_refcell!(SRole::default()); - let task = rc_refcell!(STask::default()); - task.as_ref().borrow_mut().name = IdTask::Name("task1".to_owned()); - task.as_ref().borrow_mut().commands = SCommands::default(); - task.as_ref() - .borrow_mut() - .commands - .add - .push(SCommand::Simple("ls -l".to_owned())); - role.as_ref().borrow_mut().name = "role1".to_owned(); - role.as_ref() - .borrow_mut() - .actors - .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()); - task.as_ref().borrow_mut().commands = SCommands::default(); - task.as_ref() - .borrow_mut() - .commands - .add - .push(SCommand::Simple("ls .*".to_owned())); - role.as_ref().borrow_mut().tasks.push(task); - let task = rc_refcell!(STask::default()); - task.as_ref().borrow_mut().name = IdTask::Name("task3".to_owned()); - role.as_ref().borrow_mut().tasks.push(task); - config.as_ref().borrow_mut().roles.push(role); - make_weak_config(&config); - let taskmatch = from_json_execution_settings(&args, &config, &user).unwrap(); - assert!(taskmatch.fully_matching()); - args.opt_filter = Some(FilterMatcher::default()); - args.opt_filter.as_mut().unwrap().role = Some("role1".to_owned()); - let taskmatch = from_json_execution_settings(&args, &config, &user).unwrap(); - assert!(taskmatch.fully_matching()); - args.opt_filter.as_mut().unwrap().task = Some("task1".to_owned()); - let taskmatch = from_json_execution_settings(&args, &config, &user).unwrap(); - assert!(taskmatch.fully_matching()); - args.opt_filter.as_mut().unwrap().task = Some("task2".to_owned()); - let taskmatch = from_json_execution_settings(&args, &config, &user).unwrap(); - assert!(taskmatch.fully_matching()); - args.opt_filter.as_mut().unwrap().task = Some("task3".to_owned()); - let taskmatch = from_json_execution_settings(&args, &config, &user); - assert!(taskmatch.is_err()); - args.opt_filter.as_mut().unwrap().role = None; - let taskmatch = from_json_execution_settings(&args, &config, &user); - assert!(taskmatch.is_err()); - } #[test] fn test_getopt() { let args = getopt(vec![ - "chsr", "-r", "role1", "-t", "task1", "-p", "prompt", "-i", "-h", "ls", "-l", + "sr", + "-u", + "root", + "-g", + "root,root", + "-r", + "role1", + "-t", + "task1", + "-p", + "prompt", + "-E", + "-i", + "-h", + "ls", + "-l", ]) .unwrap(); let opt_filter = args.opt_filter.as_ref().unwrap(); + assert_eq!(opt_filter.user, Some(0)); + assert_eq!(opt_filter.group, Some(vec![0, 0])); assert_eq!(opt_filter.role.as_deref(), Some("role1")); assert_eq!(opt_filter.task.as_deref(), Some("task1")); - assert_eq!(args.prompt, "prompt"); + assert_eq!(args.prompt.unwrap(), "prompt"); assert!(args.info); assert!(args.help); - assert_eq!(args.command, vec!["ls".to_string(), "-l".to_string()]); + assert_eq!(args.cmd_path, PathBuf::from("ls")); + assert_eq!(args.cmd_args, vec!["-l".to_string()]); } #[test] @@ -557,4 +414,73 @@ mod tests { assert_eq!(user.groups[0].gid.as_raw(), gid); assert_eq!(user.ppid, Pid::parent()); } + + #[test] + fn test_setuid_setgid() { + let mut capset = CapState::get_current().unwrap(); + if capset.permitted.has(Cap::SETUID) && capset.permitted.has(Cap::SETGID) { + println!("setuid and setgid are available"); + capset.effective.add(Cap::SETUID); + capset.effective.add(Cap::SETGID); + capset.set_current().unwrap(); + let mut execcfg = BestExecSettings::default(); + execcfg.setuid = Some(1000); + execcfg.setgroups = Some(vec![1000]); + setuid_setgid(&execcfg); + assert_eq!(getuid().as_raw(), execcfg.setuid.unwrap()); + if let Some(gid) = execcfg.setgroups.as_ref().and_then(|g| g.first()) { + assert_eq!(unsafe { getgid() }, *gid); + } + capset.effective.clear(); + capset.set_current().unwrap(); + } + } + + #[test] + fn test_set_capabilities() { + let mut capset = CapState::get_current().unwrap(); + if capset.permitted.has(Cap::SETPCAP) + && capset.permitted.has(Cap::SETUID) + && capset.permitted.has(Cap::SETGID) + { + capset.effective.add(Cap::SETPCAP); + capset.set_current().unwrap(); + let mut execcfg = BestExecSettings::default(); + let mut capset = CapSet::empty(); + capset.add(Cap::SETUID); + capset.add(Cap::SETGID); + capset.add(Cap::SETPCAP); + execcfg.caps = Some(capset); + set_capabilities(&execcfg); + let capset = CapState::get_current().unwrap(); + assert!(capset.permitted.has(Cap::SETUID)); + assert!(capset.permitted.has(Cap::SETGID)); + assert!(capset.permitted.has(Cap::SETPCAP)); + assert!(capset.inheritable.has(Cap::SETUID)); + assert!(capset.inheritable.has(Cap::SETGID)); + assert!(capset.inheritable.has(Cap::SETPCAP)); + assert!(capctl::bounding::probe().has(Cap::SETUID)); + assert!(capctl::bounding::probe().has(Cap::SETGID)); + assert!(capctl::bounding::probe().has(Cap::SETPCAP)); + assert!(capctl::ambient::probe().unwrap().has(Cap::SETUID)); + assert!(capctl::ambient::probe().unwrap().has(Cap::SETGID)); + assert!(capctl::ambient::probe().unwrap().has(Cap::SETPCAP)); + execcfg.caps = None; + execcfg.bounding = SBounding::Strict; + set_capabilities(&execcfg); + let capset = CapState::get_current().unwrap(); + assert!(!capset.permitted.has(Cap::SETUID)); + assert!(!capset.permitted.has(Cap::SETGID)); + assert!(!capset.permitted.has(Cap::SETPCAP)); + assert!(!capset.inheritable.has(Cap::SETUID)); + assert!(!capset.inheritable.has(Cap::SETGID)); + assert!(!capset.inheritable.has(Cap::SETPCAP)); + assert!(!capctl::bounding::probe().has(Cap::SETUID)); + assert!(!capctl::bounding::probe().has(Cap::SETGID)); + assert!(!capctl::bounding::probe().has(Cap::SETPCAP)); + assert!(!capctl::ambient::probe().unwrap().has(Cap::SETUID)); + assert!(!capctl::ambient::probe().unwrap().has(Cap::SETGID)); + assert!(!capctl::ambient::probe().unwrap().has(Cap::SETPCAP)); + } + } } diff --git a/src/sr/pam/mod.rs b/src/sr/pam/mod.rs index ce11c9e8..1a914d50 100644 --- a/src/sr/pam/mod.rs +++ b/src/sr/pam/mod.rs @@ -10,8 +10,8 @@ use pcre2::bytes::RegexBuilder; use crate::timeout; use rar_common::{ - database::{finder::Cred, options::OptStack}, - Storage, + database::options::{SAuthentication, STimeout}, + Cred, }; use self::rpassword::Terminal; @@ -120,19 +120,16 @@ impl ConversationHandler for SrConversationHandler { } pub(super) fn check_auth( - optstack: &OptStack, - config: &Storage, + authentication: &SAuthentication, + timeout: &STimeout, user: &Cred, prompt: &str, ) -> Result<(), Box> { - if optstack.get_authentication().1.is_skip() { + if authentication.is_skip() { warn!("Skipping authentication, this is a security risk!"); return Ok(()); } - let timeout = optstack.get_timeout().1; - let is_valid = match config { - Storage::SConfig(_) => timeout::is_valid(user, user, &timeout), - }; + let is_valid = timeout::is_valid(user, user, &timeout); debug!("need to re-authenticate : {}", !is_valid); if !is_valid { let conv = SrConversationHandler::new(prompt); @@ -141,10 +138,6 @@ pub(super) fn check_auth( context.authenticate(Flag::SILENT)?; context.acct_mgmt(Flag::SILENT)?; } - match config { - Storage::SConfig(_) => { - timeout::update_cookie(user, user, &timeout)?; - }, - } + timeout::update_cookie(user, user, &timeout)?; Ok(()) } diff --git a/src/sr/timeout.rs b/src/sr/timeout.rs index 2f36782b..5408eab8 100644 --- a/src/sr/timeout.rs +++ b/src/sr/timeout.rs @@ -16,14 +16,12 @@ use nix::{ use serde::{Deserialize, Serialize}; use rar_common::{ - database::{ - finder::Cred, - options::{STimeout, TimestampType}, - }, + database::options::{STimeout, TimestampType}, util::{ create_dir_all_with_privileges, create_with_privileges, open_with_privileges, remove_with_privileges, }, + Cred, }; /// This module checks the validity of a user's credentials @@ -176,7 +174,7 @@ fn read_cookies(user: &Cred) -> Result, Box> { write_lockfile(&lockpath); let mut file = open_with_privileges(&path)?; let reader = BufReader::new(&mut file); - let res = ciborium::de::from_reader::, BufReader<_>>(reader)?; + let res = cbor4ii::serde::from_reader::, BufReader<_>>(reader)?; Ok(res) } @@ -188,7 +186,7 @@ fn save_cookies(user: &Cred, cookies: &[CookieVersion]) -> Result<(), Box bool { } pub fn check_filesystem() -> io::Result<()> { - let config = BufReader::new(File::open(ROOTASROLE)?); let mut config: SettingsFile = serde_json::from_reader(config)?; @@ -78,13 +78,13 @@ pub fn check_filesystem() -> io::Result<()> { info!("Failed to get filesystem type, removing immutable flag"); } } - + set_immutable(&mut config, false); File::create(ROOTASROLE)?.write_all(serde_json::to_string_pretty(&config)?.as_bytes())?; Ok(()) } -fn set_options(content : &mut String) -> io::Result<()> { +fn set_options(content: &mut String) -> io::Result<()> { let mut config: SettingsFile = serde_json::from_str(content)?; config.storage.method = env!("RAR_CFG_TYPE").parse().unwrap(); if let Some(settings) = &mut config.storage.settings { @@ -98,7 +98,8 @@ fn set_options(content : &mut String) -> io::Result<()> { config.storage.options = Some(Opt { timeout: Some(STimeout { type_field: Some(env!("RAR_TIMEOUT_TYPE").parse().unwrap()), - duration: convert_string_to_duration(&env!("RAR_TIMEOUT_DURATION").to_string()).unwrap(), + duration: convert_string_to_duration(&env!("RAR_TIMEOUT_DURATION").to_string()) + .unwrap(), max_usage: if env!("RAR_TIMEOUT_MAX_USAGE").len() > 0 { Some(env!("RAR_TIMEOUT_MAX_USAGE").parse().unwrap()) } else { @@ -108,17 +109,54 @@ fn set_options(content : &mut String) -> io::Result<()> { }), path: Some(SPathOptions { default_behavior: env!("RAR_PATH_DEFAULT").parse().unwrap(), - add: Some(env!("RAR_PATH_ADD_LIST").split(":").map(|s| s.to_string()).collect()), - sub: if env!("RAR_PATH_REMOVE_LIST").len() > 0 { Some(env!("RAR_PATH_REMOVE_LIST").split(":").map(|s| s.to_string()).collect()) } else { None }, + add: Some( + env!("RAR_PATH_ADD_LIST") + .split(":") + .map(|s| s.to_string()) + .collect(), + ), + sub: if env!("RAR_PATH_REMOVE_LIST").len() > 0 { + Some( + env!("RAR_PATH_REMOVE_LIST") + .split(":") + .map(|s| s.to_string()) + .collect(), + ) + } else { + None + }, _extra_fields: Value::Null, }), env: Some(SEnvOptions { default_behavior: env!("RAR_ENV_DEFAULT").parse().unwrap(), - override_behavior: if env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().unwrap() { Some(env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().unwrap()) } else { None }, - keep: Some(env!("RAR_ENV_KEEP_LIST").split(",").map(|s| s.to_string()).collect()), - check: Some(env!("RAR_ENV_CHECK_LIST").split(",").map(|s| s.to_string()).collect()), - delete: Some(env!("RAR_ENV_DELETE_LIST").split(",").map(|s| s.to_string()).collect()), - set: if env!("RAR_ENV_SET_LIST").len() > 0 && env!("RAR_ENV_SET_LIST") != "{}" { serde_json::from_str(env!("RAR_ENV_SET_LIST")).unwrap()} else { HashMap::new() }, + override_behavior: if env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().unwrap() { + Some(env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().unwrap()) + } else { + None + }, + keep: Some( + env!("RAR_ENV_KEEP_LIST") + .split(",") + .map(|s| s.to_string()) + .collect(), + ), + check: Some( + env!("RAR_ENV_CHECK_LIST") + .split(",") + .map(|s| s.to_string()) + .collect(), + ), + delete: Some( + env!("RAR_ENV_DELETE_LIST") + .split(",") + .map(|s| s.to_string()) + .collect(), + ), + set: if env!("RAR_ENV_SET_LIST").len() > 0 && env!("RAR_ENV_SET_LIST") != "{}" { + serde_json::from_str(env!("RAR_ENV_SET_LIST")).unwrap() + } else { + HashMap::new() + }, _extra_fields: Value::Null, }), root: Some(env!("RAR_USER_CONSIDERED").parse().unwrap()), @@ -215,7 +253,10 @@ fn deploy_config_file() -> Result { let mut status = ConfigState::Unchanged; // Check if the target file exists if !Path::new(ROOTASROLE).exists() { - info!("Config file {} does not exist, deploying default file", ROOTASROLE); + info!( + "Config file {} does not exist, deploying default file", + ROOTASROLE + ); // If the target file does not exist, copy the default file cap_effective(Cap::DAC_OVERRIDE, true).context("Failed to raise DAC_OVERRIDE")?; deploy_config(ROOTASROLE)?; diff --git a/xtask/src/installer/install.rs b/xtask/src/installer/install.rs index 45007663..64e28a7b 100644 --- a/xtask/src/installer/install.rs +++ b/xtask/src/installer/install.rs @@ -16,7 +16,7 @@ use crate::installer::Profile; use crate::util::{change_dir_to_git_root, detect_priv_bin, BOLD, RED, RST}; use anyhow::{anyhow, Context}; -use super::{CHSR_DEST, SR_DEST, RAR_BIN_PATH}; +use super::{CHSR_DEST, RAR_BIN_PATH, SR_DEST}; use crate::util::cap_clear; fn copy_executables(profile: &Profile) -> Result<(), anyhow::Error> { @@ -29,7 +29,10 @@ fn copy_executables(profile: &Profile) -> Result<(), anyhow::Error> { info!("Current working directory: {}", cwd); info!( "Copying files {}/target/{}/sr to {} and {}", - cwd, profile, sr_dest.to_str().unwrap(), chsr_dest.to_str().unwrap() + cwd, + profile, + sr_dest.to_str().unwrap(), + chsr_dest.to_str().unwrap() ); let s_sr = format!("{}/target/{}/sr", cwd, profile); let sr = Path::new(&s_sr); diff --git a/xtask/src/util.rs b/xtask/src/util.rs index a98b9108..0c2086a8 100644 --- a/xtask/src/util.rs +++ b/xtask/src/util.rs @@ -1,5 +1,11 @@ use std::{ - collections::HashMap, error::Error, fs::{self, File}, io, os::{fd::AsRawFd, unix::fs::MetadataExt}, path::Path, process::Command + collections::HashMap, + error::Error, + fs::{self, File}, + io, + os::{fd::AsRawFd, unix::fs::MetadataExt}, + path::Path, + process::Command, }; use anyhow::{anyhow, Context}; @@ -76,7 +82,6 @@ pub enum StorageMethod { Unknown, } - #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Settings { pub method: StorageMethod, @@ -100,7 +105,9 @@ pub struct RemoteStorageSettings { pub _extra_fields: Value, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] @@ -112,7 +119,9 @@ pub enum PathBehavior { Inherit, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy, Display, EnumString)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy, Display, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] @@ -144,23 +153,18 @@ pub struct STimeout { pub struct SPathOptions { #[serde(rename = "default", default, skip_serializing_if = "is_default")] pub default_behavior: PathBehavior, - #[serde( - default, - skip_serializing_if = "Option::is_none", - )] + #[serde(default, skip_serializing_if = "Option::is_none")] pub add: Option>, - #[serde( - default, - skip_serializing_if = "Option::is_none", - alias = "del" - )] + #[serde(default, skip_serializing_if = "Option::is_none", alias = "del")] pub sub: Option>, #[serde(default)] #[serde(flatten)] pub _extra_fields: Value, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] @@ -179,26 +183,19 @@ pub struct SEnvOptions { pub override_behavior: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub set: HashMap, - #[serde( - default, - skip_serializing_if = "Option::is_none", - )] + #[serde(default, skip_serializing_if = "Option::is_none")] pub keep: Option>, - #[serde( - default, - skip_serializing_if = "Option::is_none", - )] + #[serde(default, skip_serializing_if = "Option::is_none")] pub check: Option>, - #[serde( - default, - skip_serializing_if = "Option::is_none", - )] + #[serde(default, skip_serializing_if = "Option::is_none")] pub delete: Option>, #[serde(default, flatten)] pub _extra_fields: Value, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] @@ -209,7 +206,9 @@ pub enum SBounding { Inherit, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "kebab-case")] #[derive(Default)] @@ -220,7 +219,9 @@ pub enum SPrivileged { Inherit, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] #[strum(ascii_case_insensitive)] #[serde(rename_all = "kebab-case")] #[derive(Default)] @@ -261,7 +262,6 @@ pub enum ImmutableLock { Unset, } - pub fn is_default(t: &T) -> bool { t == &T::default() } @@ -293,8 +293,7 @@ where } } -pub fn convert_string_to_duration(s: &String) -> Result, Box> -{ +pub fn convert_string_to_duration(s: &String) -> Result, Box> { let mut parts = s.split(':'); //unwrap or error if let (Some(hours), Some(minutes), Some(seconds)) = (parts.next(), parts.next(), parts.next())