,
+ pub base_dn: String,
+ pub user_dn: String,
+ pub group_dn: String,
+ pub user_filter: String,
+ pub group_filter: String,
+}
+
+// Default implementation for Settings
+impl Default for Settings {
+ fn default() -> Self {
+ Self {
+ storage_method: StorageMethod::JSON,
+ remote_storage_settings: Some(RemoteStorageSettings::default()),
+ ldap: None,
+ }
+ }
+}
+
+impl Default for RemoteStorageSettings {
+ fn default() -> Self {
+ Self {
+ path: Some(ROOTASROLE.into()),
+ host: None,
+ port: None,
+ auth: None,
+ database: None,
+ schema: None,
+ table_prefix: None,
+ properties: None,
+ }
+ }
+}
+
+pub fn get_settings() -> Settings {
+ // if file does not exist, return default settings
+ if !std::path::Path::new(FILEPATH).exists() {
+ return Settings::default();
+ }
+ let file = std::fs::File::open(FILEPATH).expect("Failed to open file");
+ serde_json::from_reader(file).unwrap_or_default()
+}
\ No newline at end of file
diff --git a/src/config/libxml2.rs b/src/config/libxml2.rs
deleted file mode 100644
index cc97491f..00000000
--- a/src/config/libxml2.rs
+++ /dev/null
@@ -1,274 +0,0 @@
-use std::{
- ffi::{c_char, c_int, c_long, c_uint, c_ulong, c_ushort, c_void, CString},
- fmt::Display,
- path::Path,
- ptr,
-};
-
-enum XmlDtd {}
-enum XmlNode {}
-enum XmlNs {}
-enum XmlValidState {}
-enum XmlAutomata {}
-enum XmlAutomataState {}
-enum XmlSAXHandler {}
-enum XmlParserInput {}
-enum XmlParserNodeInfo {}
-enum XmlParserNodeInfoSeq {}
-enum XmlParserInputState {}
-enum XmlDict {}
-enum XmlStartTag {}
-enum XmlHashTable {}
-enum XmlAttr {}
-enum XmlError {}
-enum XmlParserMode {}
-
-#[repr(C)]
-struct XmlParserCtxt {
- sax: *mut XmlSAXHandler,
- user_data: *mut c_void,
- my_doc: *mut XmlDoc,
- well_formed: c_int,
- replace_entities: c_int,
- version: *const c_char,
- encoding: *const c_char,
- standalone: c_int,
- html: c_int,
- input: *mut XmlParserInput,
- input_nr: c_int,
- input_max: c_int,
- input_tab: *mut *mut XmlParserInput,
- node: *mut XmlNode,
- node_nr: c_int,
- node_max: c_int,
- node_tab: *mut *mut XmlNode,
- record_info: c_int,
- node_seq: XmlParserNodeInfoSeq,
- err_no: c_int,
- has_external_subset: c_int,
- has_pe_refs: c_int,
- external: c_int,
- valid: c_int,
- validate: c_int,
- vctxt: XmlValidCtxt,
- instate: XmlParserInputState,
- token: c_int,
- directory: *mut c_char,
- name: *const c_char,
- name_nr: c_int,
- name_max: c_int,
- name_tab: *const *mut c_char,
- nb_chars: c_long,
- check_index: c_long,
- keep_blanks: c_int,
- disable_sax: c_int,
- in_subset: c_int,
- int_sub_name: *const c_char,
- ext_sub_uri: *mut c_char,
- ext_sub_system: *mut c_char,
- space: *mut c_int,
- space_nr: c_int,
- space_max: c_int,
- space_tab: *mut c_int,
- depth: c_int,
- entity: *mut XmlParserInput,
- charset: c_int,
- nodelen: c_int,
- nodemem: c_int,
- pedantic: c_int,
- _private: *mut c_void,
- loadsubset: c_int,
- linenumbers: c_int,
- catalogs: *mut c_void,
- recovery: c_int,
- progressive: c_int,
- dict: *mut XmlDict,
- atts: *const *mut c_char,
- maxatts: c_int,
- docdict: c_int,
- str_xml: *const c_char,
- str_xmlns: *const c_char,
- str_xml_ns: *const c_char,
- sax2: c_int,
- ns_nr: c_int,
- ns_max: c_int,
- ns_tab: *const *mut c_char,
- attallocs: *mut c_int,
- push_tab: *mut XmlStartTag,
- atts_default: *mut XmlHashTable,
- atts_special: *mut XmlHashTable,
- ns_well_formed: c_int,
- options: c_int,
- dict_names: c_int,
- free_elems_nr: c_int,
- free_elems: *mut XmlNode,
- free_attrs_nr: c_int,
- free_attrs: *mut XmlAttr,
- last_error: XmlError,
- parse_mode: XmlParserMode,
- nbentities: c_ulong,
- sizeentities: c_ulong,
- node_info: *mut XmlParserNodeInfo,
- node_info_nr: c_int,
- node_info_max: c_int,
- node_info_tab: *mut XmlParserNodeInfo,
- input_id: c_int,
- sizeentcopy: c_ulong,
- end_check_state: c_int,
- nb_errors: c_ushort,
- nb_warnings: c_ushort,
-}
-
-enum XmlElementType {}
-
-enum _XmlDict {}
-
-#[repr(C)]
-struct XmlValidCtxt {
- user_data: *mut c_void,
- error: *mut extern "C" fn(*mut c_void, *const c_char, ...),
- warning: *mut extern "C" fn(*mut c_void, *const c_char, ...),
- node: *mut XmlNode,
- node_nr: c_int,
- node_max: c_int,
- node_tab: *mut *mut XmlNode,
- flags: c_uint,
- doc: *mut XmlDoc,
- valid: c_int,
- vstate: *mut XmlValidState,
- vstate_nr: c_int,
- vstate_max: c_int,
- vstate_tab: *mut XmlValidState,
- am: *mut XmlAutomata,
- state: *mut XmlAutomataState,
-}
-
-#[repr(C)]
-struct XmlDoc {
- _private: *mut c_void,
- type_: XmlElementType,
- name: *const c_char,
- children: *mut XmlNode,
- last: *mut XmlNode,
- parent: *mut XmlNode,
- next: *mut XmlNode,
- prev: *mut XmlNode,
- doc: *mut XmlDoc,
- compression: c_int,
- standalone: c_int,
- int_subset: *mut XmlDtd,
- ext_subset: *mut XmlDtd,
- oldns: *mut XmlNs,
- version: *const c_char,
- encoding: *const c_char,
- ids: *mut c_void,
- refs: *mut c_void,
- url: *const c_char,
- charset: c_int,
- dict: *mut _XmlDict,
- psvi: *mut c_void,
- parse_flags: c_int,
- properties: c_int,
-}
-
-#[derive(Clone, Copy)]
-struct XmlValidCtxtPtr(pub *mut XmlValidCtxt);
-
-unsafe impl Send for XmlValidCtxtPtr {}
-unsafe impl Sync for XmlValidCtxtPtr {}
-
-static XML_PARSE_DTDVALID: c_int = 16;
-static XML_PARSE_NOERROR: c_int = 32;
-static XML_PARSE_NOWARNING: c_int = 64;
-static XML_PARSE_PEDANTIC: c_int = 128;
-static XML_PARSE_NOBLANKS: c_int = 256;
-static XML_PARSE_NONET: c_int = 2048;
-
-#[link(name = "xml2")]
-extern "C" {
-
- fn xmlParseFile(filename: *const ::std::os::raw::c_char) -> *mut XmlDoc;
- fn xmlFreeDoc(cur: *mut XmlDoc);
- fn xmlNewValidCtxt() -> *mut XmlValidCtxt;
- fn xmlFreeValidCtxt(ctxt: *mut XmlValidCtxt);
- fn xmlValidateDocument(ctxt: *mut XmlValidCtxt, doc: *mut XmlDoc) -> ::std::os::raw::c_int;
- fn xmlGetIntSubset(doc: *mut XmlDoc) -> *mut XmlDtd;
- fn xmlValidateDtd(
- ctxt: *mut XmlValidCtxt,
- doc: *mut XmlDoc,
- dtd: *mut XmlDtd,
- ) -> ::std::os::raw::c_int;
- fn xmlValidityErrorFunc(ctx: *mut c_void, msg: *const c_char, ...);
- fn xmlNewParserCtxt() -> *mut XmlParserCtxt;
- fn xmlCtxtReadFile(
- ctxt: *mut XmlParserCtxt,
- filename: *const c_char,
- encoding: *const c_char,
- options: c_int,
- ) -> *mut XmlDoc;
-
-}
-
-/*
-pub(crate) fn validate_Xml_file(filename: &str) -> bool {
- let filename = CString::new(filename).unwrap();
- let doc = unsafe {
- XmlParseFile(
- filename.as_ptr() as *const ::std::os::raw::c_char,
- )
- };
- if doc.is_null() {
- return false;
- }
- let ctxt = unsafe { XmlValidCtxtPtr(XmlNewValidCtxt()) };
-
- if ctxt.0.is_null() {
- unsafe { XmlFreeDoc(doc) };
- return false;
- }
- let ret = unsafe { XmlValidateDocument(ctxt.0, doc) };
-
- if ret != 1 {
- unsafe {
- XmlFreeDoc(doc);
- XmlFreeValidCtxt(ctxt.0);
- }
- return false;
- }
- let dtd = unsafe { XmlGetIntSubset(doc) };
- let ret = unsafe { XmlValidateDtd(ctxt.0, doc, dtd) };
- unsafe {
- XmlFreeDoc(doc);
- XmlFreeValidCtxt(ctxt.0);
- }
- ret == 1
-
-}
-*/
-
-pub(crate) unsafe fn validate_xml_file(filename: &P, silent: bool) -> bool
-where
- P: AsRef + Display,
-{
- let ctxt = xmlNewParserCtxt();
- let filename = CString::new(filename.to_string()).unwrap();
- let options: c_int = if silent {
- XML_PARSE_DTDVALID
- | XML_PARSE_NOBLANKS
- | XML_PARSE_NONET
- | XML_PARSE_NOERROR
- | XML_PARSE_NOWARNING
- } else {
- XML_PARSE_DTDVALID | XML_PARSE_NOBLANKS | XML_PARSE_NONET | XML_PARSE_PEDANTIC
- };
- let doc = xmlCtxtReadFile(ctxt, filename.as_ptr(), ptr::null(), options);
- if doc.is_null() {
- return false;
- }
- if (!ctxt.is_null()) && (*ctxt).valid != 0 {
- xmlFreeDoc(doc);
- return false;
- }
-
- true
-}
diff --git a/src/config/load.rs b/src/config/load.rs
deleted file mode 100644
index 4fdedcda..00000000
--- a/src/config/load.rs
+++ /dev/null
@@ -1,422 +0,0 @@
-use std::{borrow::BorrowMut, cell::RefCell, error::Error, fmt::Display, path::Path, rc::Rc};
-
-use chrono::Duration;
-use sxd_document::{
- dom::{Document, Element},
- Package,
-};
-use tracing::warn;
-
-use super::{
- do_in_main_child, get_groups, libxml2,
- options::{Level, Opt},
- parse_capset, read_xml_file,
- structs::{Config, IdTask, Role, Task},
- version::migrate,
-};
-
-use crate::xml_version::PACKAGE_VERSION;
-
-trait Load {
- fn load(&self, node: Element) -> Result<(), Box>;
-}
-
-/// Checks if an element is enforced.
-/// It checks the enforce attribute of the element.
-/// If the enforce attribute is true, then the element is enforced.
-pub fn is_enforced(node: Element) -> bool {
- let enforce = node.attribute("enforce");
- (enforce.is_some()
- && enforce
- .expect("Unable to retrieve enforce attribute")
- .value()
- == "true")
- || enforce.is_none()
-}
-
-/// Retrieve string from an option element.
-/// It returns the text of the first child of the element.
-fn option_element_string(elem: Element<'_>) -> String {
- elem.children()
- .first()
- .unwrap()
- .text()
- .expect("Cannot read option")
- .text()
- .to_string()
-}
-
-/// Construct an option from the document structure level and the pointed element
-/// It returns an option with the level and the element.
-fn get_options(level: Level, node: Element) -> Opt {
- let mut rc_options = Opt::new(level);
-
- for child in node.children() {
- let mut options = rc_options.borrow_mut();
- if let Some(elem) = child.element() {
- match elem.name().local_part() {
- "path" => options.path = Some(option_element_string(elem)),
- "env-keep" => options.env_whitelist = Some(option_element_string(elem)),
- "env-check" => options.env_checklist = Some(option_element_string(elem)),
- "allow-root" => options.allow_root = Some(is_enforced(elem)),
- "allow-bounding" => options.disable_bounding = Some(is_enforced(elem)),
- "wildcard-denied" => options.wildcard_denied = Some(option_element_string(elem)),
- _ => warn!("Unknown option: {}", elem.name().local_part()),
- }
- }
- }
- rc_options
-}
-
-/// Load a task from an element.
-/// It loads the id, the commands, the options and the purpose of the task.
-/// It also loads the capabilities, the setuid and the setgid if they are present.
-/// It returns an error if the id is not present. The id is required by the DTD.
-impl Load for Rc>> {
- fn load(&self, node: Element) -> Result<(), Box> {
- if let Some(id) = node.attribute_value("id") {
- self.as_ref().borrow_mut().id = IdTask::Name(id.to_string());
- }
- if let Some(capabilities) = node.attribute_value("capabilities") {
- self.as_ref().borrow_mut().capabilities = Some(parse_capset(capabilities)?);
- }
- self.as_ref().borrow_mut().setuid =
- node.attribute_value("setuser").map(|setuid| setuid.into());
- self.as_ref().borrow_mut().setgid = node
- .attribute_value("setgroups")
- .map(|setgid| setgid.split(',').map(|e| e.to_string()).collect());
- for child in node.children() {
- if let Some(elem) = child.element() {
- match elem.name().local_part() {
- "command" => self.as_ref().borrow_mut().commands.push(
- elem.children()
- .first()
- .ok_or("Unable to get text from command")?
- .text()
- .map(|f| f.text().to_string())
- .ok_or("Unable to get text from command")?,
- ),
- "options" => {
- self.as_ref().borrow_mut().options =
- Some(Rc::new(get_options(Level::Task, elem).into()));
- }
- "purpose" => {
- self.as_ref().borrow_mut().purpose = Some(
- elem.children()
- .first()
- .ok_or("Unable to get text from purpose")?
- .text()
- .map(|f| f.text().to_string())
- .ok_or("Unable to get text from purpose")?,
- );
- }
- _ => warn!("Unknown element: {}", elem.name().local_part()),
- }
- }
- }
- Ok(())
- }
-}
-
-/// Obtain every role actors from the actors element.
-fn add_actors(role: &mut Role, node: Element) -> Result<(), Box> {
- for child in node.children() {
- if let Some(elem) = child.element() {
- match elem.name().local_part() {
- "user" => role.users.push(
- elem.attribute_value("name")
- .ok_or("Unable to retrieve user name")?
- .to_string(),
- ),
- "group" => role.groups.push(get_groups(elem)),
- _ => warn!("Unknown element: {}", elem.name().local_part()),
- }
- }
- }
- Ok(())
-}
-
-/// Load a role from an element.
-/// It loads the name, the actors, the tasks and the options.
-impl Load for Rc>> {
- fn load(&self, element: Element) -> Result<(), Box> {
- let mut i: usize = 0;
- for child in element.children() {
- if let Some(element) = child.element() {
- match element.name().local_part() {
- "actors" => add_actors(&mut self.as_ref().borrow_mut(), element)?,
- "task" => {
- i += 1;
- let task = Task::new(IdTask::Number(i), Rc::downgrade(self));
- task.load(element)?;
- self.as_ref().borrow_mut().tasks.push(task);
- }
- "options" => {
- self.as_ref().borrow_mut().options =
- Some(Rc::new(get_options(Level::Role, element).into()))
- }
- _ => warn!(
- "Unknown element: {}",
- child
- .element()
- .expect("Unable to convert unknown to element")
- .name()
- .local_part()
- ),
- }
- }
- }
- // load parents
- if let Some(parents) = element.attribute_value("parents") {
- let parents: Vec<&str> = parents.split(',').collect();
- let confparents = parents
- .iter()
- .map(|e| e.to_string())
- .collect::>();
- let mut parents = Vec::new();
- if let Some(config) = self.as_ref().borrow().get_config() {
- let mut roles = config.as_ref().borrow_mut().roles.clone();
- roles.retain(|e| {
- confparents.contains(&e.as_ref().borrow().name)
- && e.as_ref().borrow().name != self.as_ref().borrow().name
- });
- if roles.len() != confparents.len() {
- warn!(
- "Role {} : Some parents are not found: {:?}",
- self.as_ref().borrow().name,
- confparents
- );
- }
- if !roles.is_empty() {
- for role in roles {
- parents.push(Rc::downgrade(&role));
- }
- }
- }
- if !parents.is_empty() {
- self.as_ref().borrow_mut().parents = Some(parents);
- }
- }
- Ok(())
- }
-}
-
-/// Load the rootasrole element.
-/// It load the timestamp-type, the timestamp-timeout and the password-usage-max attributes.
-/// It also load the roles and the options.
-impl Load for Rc>> {
- fn load(&self, element: Element) -> Result<(), Box> {
- if element.name().local_part() != "rootasrole" {
- return Err("Wrong Element".into());
- }
-
- if let Some(timestamp_type) = element.attribute_value("timestamp-type") {
- self.as_ref().borrow_mut().timestamp.timestamptype = timestamp_type.to_string();
- }
-
- if let Some(timestamp) = element.attribute_value("timestamp-timeout") {
- self.as_ref().borrow_mut().timestamp.offset =
- Duration::seconds(timestamp.parse::()?);
- }
-
- if let Some(usage_max) = element.attribute_value("password-usage-max") {
- self.as_ref().borrow_mut().timestamp.max_usage = Some(usage_max.parse::()?);
- }
-
- for role in element.children() {
- if let Some(element) = role.element() {
- if element.name().local_part() == "roles" {
- let mut ziprole = Vec::new();
- for role in element.children() {
- if let Some(element) = role.element() {
- if element.name().local_part() == "role" {
- let name = element.attribute_value("name").unwrap();
- let role = Role::new(name.to_string(), Some(Rc::downgrade(self)));
- ziprole.push((role, element));
- }
- }
- }
- // Load role after all roles are created, this is because of the hierarchy feature
- for (role, element) in ziprole {
- role.load(element)?;
- self.as_ref().borrow_mut().roles.push(role);
- }
- }
- if element.name().local_part() == "options" {
- self.as_ref().borrow_mut().options =
- Some(Rc::new(get_options(Level::Global, element).into()));
- }
- }
- }
- Ok(())
- }
-}
-
-/// Load a document with the filename, and check for his DTD validity if validate is set to true.
-/// It returns an error if the document is not valid.
-pub fn load_document<'a, P>(filename: &P, validate: bool) -> Result>
-where
- P: AsRef + Display,
-{
- if validate && !unsafe { libxml2::validate_xml_file(filename, true) } {
- return Err("Invalid XML file".into());
- }
- read_xml_file(filename)
-}
-
-/// Load a config from the filename, and check for his DTD validity.
-/// It returns an error if the document is not valid
-pub(crate) fn load_config<'a, P>(filename: &P) -> Result>>, Box>
-where
- P: AsRef + Display,
-{
- load_document(filename, true).and_then(|pkg| load_config_from_doc(&pkg.as_document(), true))
-}
-
-/// Load a config from the filename, and check for his DTD validity.
-/// It also perform a migration if the version of the config is not the same as the package version.
-/// It returns an error if the document is not valid
-pub fn load_config_from_doc<'a>(
- doc: &Document,
- do_migration: bool,
-) -> Result>>, Box> {
- let mut version = PACKAGE_VERSION.to_string();
- do_in_main_child(doc, "rootasrole", |element| {
- if let Some(element) = element.element() {
- version = element
- .attribute_value("version")
- .expect("No version")
- .to_string();
- }
- Ok(())
- })?;
- let rc_roles = Config::new(version.as_str());
- do_in_main_child(doc, "rootasrole", |element| {
- if let Some(element) = element.element() {
- if do_migration {
- migrate(&version, doc)?;
- rc_roles.as_ref().borrow_mut().version = PACKAGE_VERSION.to_string();
- rc_roles.as_ref().borrow_mut().migrated = true;
- }
- rc_roles.load(element)?;
- }
- Ok(())
- })?;
- Ok(rc_roles)
-}
-
-#[cfg(test)]
-mod tests {
-
- use crate::util;
-
- use super::*;
- #[test]
- fn test_load_roles() {
- let roles = load_config(&util::test::test_resources_file(
- "test_xml_manager_case1.xml",
- ));
- if let Err(e) = roles {
- panic!("Unable to load roles: {}", e);
- }
- let binding = roles.unwrap();
- let roles = binding.as_ref().borrow();
- assert_eq!(roles.roles.len(), 2);
- let role = roles.roles.first().unwrap();
- let role = role.as_ref().borrow();
- assert_eq!(role.name, "test1");
- assert_eq!(role.users.len(), 1);
- assert_eq!(role.users.first().unwrap(), "test1");
- assert_eq!(role.groups.len(), 0);
- assert_eq!(role.tasks.len(), 2);
- let task = role.tasks.first().unwrap();
- let task = task.as_ref().borrow();
- assert_eq!(task.id, IdTask::Name("t1_test1".to_string()));
- assert_eq!(task.commands.len(), 1);
- assert_eq!(task.commands.first().unwrap(), "/bin/ls");
- let option = task.options.as_ref();
- assert!(option.is_some());
- let path = task
- .options
- .as_ref()
- .unwrap()
- .as_ref()
- .borrow()
- .to_owned()
- .path;
- assert!(path.is_some());
- assert_eq!(path.unwrap(), "t1_test1");
- assert!(task.capabilities.is_some());
- let capabilities = task.capabilities.to_owned().unwrap();
- assert_eq!(
- capabilities,
- super::parse_capset("cap_dac_override").unwrap()
- );
- let task = role.tasks.last().unwrap();
- let task = task.as_ref().borrow();
- assert_eq!(task.id, IdTask::Name("t1_test2".to_string()));
- assert_eq!(task.commands.len(), 1);
- assert_eq!(task.commands.first().unwrap(), "/bin/ls");
- let option = task.options.as_ref();
- assert!(option.is_some());
- let path = task
- .options
- .as_ref()
- .unwrap()
- .as_ref()
- .borrow()
- .to_owned()
- .path;
- assert!(path.is_some());
- assert_eq!(path.unwrap(), "t1_test2");
- assert!(task.capabilities.is_none());
- let role = roles.roles.last().unwrap();
- let role = role.as_ref().borrow();
- assert_eq!(role.name, "test2");
- assert_eq!(role.users.len(), 1);
- assert_eq!(role.users.first().unwrap(), "test1");
- assert_eq!(role.groups.len(), 0);
- assert_eq!(role.tasks.len(), 1);
- let task = role.tasks.first().unwrap();
- let task = task.as_ref().borrow();
- assert_eq!(task.id, IdTask::Name("t2_test1".to_string()));
- assert_eq!(task.commands.len(), 1);
- assert_eq!(task.commands.first().unwrap(), "/bin/ls");
- let option = task.options.as_ref();
- assert!(option.is_some());
- let path = task
- .options
- .as_ref()
- .unwrap()
- .as_ref()
- .borrow()
- .to_owned()
- .path;
- assert!(path.is_some());
- assert_eq!(path.unwrap(), "t2_test1");
- let allowroot = task
- .options
- .as_ref()
- .unwrap()
- .as_ref()
- .borrow()
- .to_owned()
- .allow_root;
- assert!(allowroot.is_some());
- assert_eq!(allowroot.unwrap(), true);
- assert!(task.capabilities.is_none());
- }
-
- #[test]
- fn test_load_roles_with_hierarchy() {
- let roles = load_config(&util::test::test_resources_file(
- "test_xml_manager_hierarchy.xml",
- ));
- if let Err(e) = roles {
- panic!("Unable to load roles: {}", e);
- }
- let binding = roles.unwrap();
- let roles = binding.as_ref().borrow();
- assert_eq!(roles.roles.len(), 6);
- }
-}
diff --git a/src/config/mod.rs b/src/config/mod.rs
deleted file mode 100644
index 17a19752..00000000
--- a/src/config/mod.rs
+++ /dev/null
@@ -1,245 +0,0 @@
-pub mod load;
-pub mod options;
-pub mod save;
-pub mod structs;
-
-#[allow(dead_code)]
-mod libxml2;
-mod version;
-
-use capctl::{Cap, CapSet, ParseCapError};
-
-use std::{error::Error, fs::File, io::Read, path::Path};
-
-use sxd_document::{
- dom::{ChildOfElement, ChildOfRoot, Document, Element},
- parser, Package,
-};
-
-use self::structs::Groups;
-
-pub(crate) const FILENAME: &str = "/etc/security/rootasrole.xml";
-
-pub(crate) fn read_file(file_path: P, contents: &mut String) -> Result<(), Box>
-where
- P: AsRef,
-{
- let mut file = File::open(file_path)?;
- file.read_to_string(contents)?;
- Ok(())
-}
-
-pub(crate) fn read_xml_file(file_path: &P) -> Result>
-where
- P: AsRef,
-{
- let mut contents = String::new();
- read_file(file_path, &mut contents)?;
- Ok(parser::parse(&contents)?)
-}
-
-pub(crate) fn foreach_child(element: &Element, mut f: F) -> Result<(), Box>
-where
- F: FnMut(ChildOfElement) -> Result<(), Box>,
-{
- if !element.children().is_empty() {
- for child in element.children() {
- f(child)?;
- }
- }
- Ok(())
-}
-
-pub(crate) fn foreach_element_name(
- element: &Element,
- element_name: &str,
- mut f: F,
-) -> Result<(), Box>
-where
- F: FnMut(Element) -> Result<(), Box>,
-{
- if !element.children().is_empty() {
- for child in element.children() {
- if let Some(element) = child.element() {
- if element.name().local_part() == element_name {
- f(element)?;
- }
- }
- }
- }
- Ok(())
-}
-
-pub(crate) fn foreach_inner_elements_names(
- element: &Element,
- elements_structure: &mut Vec<&str>,
- mut f: F,
-) -> Result<(), Box>
-where
- F: FnMut(Element) -> Result<(), Box>,
-{
- if elements_structure.is_empty() {
- return Ok(());
- } else if elements_structure.len() == 1 {
- return foreach_element_name(element, elements_structure.first().unwrap(), f);
- } else if elements_structure.len() > 128 {
- return Err("elements_structure is too big".into());
- }
- for child in element.children() {
- if let Some(element) = child.element() {
- if element.name().local_part() == *elements_structure.first().unwrap() {
- elements_structure.remove(0);
- if elements_structure.is_empty() {
- return f(element);
- } else {
- return foreach_inner_elements_names(&element, elements_structure, f);
- }
- }
- }
- }
- Ok(())
-}
-
-pub(crate) fn do_in_main_child(
- doc: &Document,
- name: &str,
- mut f: F,
-) -> Result<(), Box>
-where
- F: FnMut(ChildOfRoot) -> Result<(), Box>,
-{
- for child in doc.root().children() {
- if let Some(element) = child.element() {
- if element.name().local_part() == name {
- f(child)?;
- }
- }
- }
- Ok(())
-}
-
-pub(crate) fn do_in_main_element(
- doc: &Document,
- name: &str,
- mut f: F,
-) -> Result<(), Box>
-where
- F: FnMut(Element) -> Result<(), Box>,
-{
- do_in_main_child(doc, name, |child| {
- if let Some(element) = child.element() {
- f(element.into())?;
- }
- Ok(())
- })
-}
-
-pub(crate) fn get_groups(node: Element) -> Groups {
- node.attribute("names")
- .expect("Unable to retrieve group names")
- .value()
- .split(',')
- .map(|s| s.to_string())
- .collect()
-}
-
-pub fn capset_to_string(set: &CapSet) -> String {
- set.iter()
- .fold(String::new(), |mut acc, cap| {
- acc.push_str(&format!("CAP_{:?} ", cap));
- acc
- })
- .trim_end()
- .to_string()
-}
-
-pub fn parse_capset(s: &str) -> Result {
- if s.is_empty() || s.eq_ignore_ascii_case("all") {
- return Ok(!CapSet::empty());
- }
-
- let mut res = CapSet::empty();
-
- for part in s.split(',') {
- match part.parse() {
- Ok(cap) => res.add(cap),
- Err(error) => {
- return Err(error);
- }
- }
- }
-
- Ok(res)
-}
-
-/// Reference every capabilities that lead to almost a direct privilege escalation
-pub fn capabilities_are_exploitable(caps: &CapSet) -> bool {
- caps.has(Cap::SYS_ADMIN)
- || caps.has(Cap::SYS_PTRACE)
- || caps.has(Cap::SYS_MODULE)
- || caps.has(Cap::DAC_READ_SEARCH)
- || caps.has(Cap::DAC_OVERRIDE)
- || caps.has(Cap::FOWNER)
- || caps.has(Cap::CHOWN)
- || caps.has(Cap::SETUID)
- || caps.has(Cap::SETGID)
- || caps.has(Cap::SETFCAP)
- || caps.has(Cap::SYS_RAWIO)
- || caps.has(Cap::LINUX_IMMUTABLE)
- || caps.has(Cap::SYS_CHROOT)
- || caps.has(Cap::SYS_BOOT)
- || caps.has(Cap::MKNOD)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use capctl::Cap;
-
- #[test]
- fn capset_to_string_test() {
- let mut set = CapSet::empty();
- set.add(Cap::CHOWN);
- set.add(Cap::DAC_OVERRIDE);
- set.add(Cap::DAC_READ_SEARCH);
- set.add(Cap::FOWNER);
- set.add(Cap::FSETID);
- set.add(Cap::KILL);
- set.add(Cap::SETGID);
- set.add(Cap::SETUID);
- set.add(Cap::SETPCAP);
- set.add(Cap::LINUX_IMMUTABLE);
- set.add(Cap::NET_BIND_SERVICE);
- set.add(Cap::NET_BROADCAST);
- set.add(Cap::NET_ADMIN);
- set.add(Cap::NET_RAW);
- set.add(Cap::IPC_LOCK);
- set.add(Cap::IPC_OWNER);
- set.add(Cap::SYS_MODULE);
- set.add(Cap::SYS_RAWIO);
- set.add(Cap::SYS_CHROOT);
- set.add(Cap::SYS_PTRACE);
- set.add(Cap::SYS_PACCT);
- set.add(Cap::SYS_ADMIN);
- set.add(Cap::SYS_BOOT);
- set.add(Cap::SYS_NICE);
- set.add(Cap::SYS_RESOURCE);
- set.add(Cap::SYS_TIME);
- set.add(Cap::SYS_TTY_CONFIG);
- set.add(Cap::MKNOD);
- set.add(Cap::LEASE);
- set.add(Cap::AUDIT_WRITE);
- set.add(Cap::AUDIT_CONTROL);
- set.add(Cap::SETFCAP);
- set.add(Cap::MAC_OVERRIDE);
- set.add(Cap::MAC_ADMIN);
- set.add(Cap::SYSLOG);
- set.add(Cap::WAKE_ALARM);
- set.add(Cap::BLOCK_SUSPEND);
- set.add(Cap::AUDIT_READ);
-
- assert_eq!(
- capset_to_string(&set),
- "CAP_CHOWN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_FSETID CAP_KILL CAP_SETGID CAP_SETUID CAP_SETPCAP CAP_LINUX_IMMUTABLE CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_ADMIN CAP_NET_RAW CAP_IPC_LOCK CAP_IPC_OWNER CAP_SYS_MODULE CAP_SYS_RAWIO CAP_SYS_CHROOT CAP_SYS_PTRACE CAP_SYS_PACCT CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_NICE CAP_SYS_RESOURCE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_MKNOD CAP_LEASE CAP_AUDIT_WRITE CAP_AUDIT_CONTROL CAP_SETFCAP CAP_MAC_OVERRIDE CAP_MAC_ADMIN CAP_SYSLOG CAP_WAKE_ALARM CAP_BLOCK_SUSPEND CAP_AUDIT_READ");
- }
-}
diff --git a/src/config/options.rs b/src/config/options.rs
deleted file mode 100644
index d17235a4..00000000
--- a/src/config/options.rs
+++ /dev/null
@@ -1,633 +0,0 @@
-use std::{borrow::Borrow, cell::RefCell, rc::Rc};
-
-use tracing::debug;
-
-use super::structs::{Config, Role, Task};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum Level {
- None,
- Default,
- Global,
- Role,
- Task,
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum OptType {
- Path,
- EnvWhitelist,
- EnvChecklist,
- NoRoot,
- Bounding,
- Wildcard,
-}
-
-impl OptType {
- pub fn from_index(index: usize) -> OptType {
- match index {
- 0 => OptType::Path,
- 1 => OptType::EnvWhitelist,
- 2 => OptType::EnvChecklist,
- 3 => OptType::NoRoot,
- 4 => OptType::Bounding,
- _ => panic!("Invalid index for OptType"),
- }
- }
- pub fn as_index(&self) -> usize {
- match self {
- OptType::Path => 0,
- OptType::EnvWhitelist => 1,
- OptType::EnvChecklist => 2,
- OptType::NoRoot => 3,
- OptType::Bounding => 4,
- OptType::Wildcard => 5,
- }
- }
-}
-
-#[derive(Debug)]
-pub enum OptValue {
- String(String),
- Bool(bool),
-}
-
-impl ToString for OptValue {
- fn to_string(&self) -> String {
- match self {
- OptValue::String(s) => s.to_string(),
- OptValue::Bool(b) => b.to_string(),
- }
- }
-}
-
-impl OptValue {
- pub fn as_bool(&self) -> bool {
- match self {
- OptValue::Bool(b) => *b,
- _ => panic!("OptValue is not a bool"),
- }
- }
-}
-
-impl OptValue {
- pub fn get_description(&self, opttype: OptType) -> String {
- match opttype {
- OptType::Path => self
- .to_string()
- .split(':')
- .collect::>()
- .join("\n"),
- OptType::EnvWhitelist => self
- .to_string()
- .split(',')
- .collect::>()
- .join("\n"),
- OptType::EnvChecklist => self
- .to_string()
- .split(',')
- .collect::>()
- .join("\n"),
- OptType::NoRoot => {
- if self.as_bool() {
- String::from("Enforce NoRoot")
- } else {
- String::from("Do not enforce NoRoot")
- }
- }
- OptType::Bounding => {
- if self.as_bool() {
- String::from("Restrict with Bounding")
- } else {
- String::from("Do not restrict with Bounding")
- }
- }
- OptType::Wildcard => self.to_string(),
- }
- }
-}
-
-impl OptType {
- pub fn item_list_str() -> Vec<(OptType, String)> {
- vec![
- (OptType::Path, String::from("Path")),
- (OptType::EnvWhitelist, String::from("Environment Whitelist")),
- (OptType::EnvChecklist, String::from("Environment Checklist")),
- (OptType::NoRoot, String::from("Enforce NoRoot")),
- (OptType::Bounding, String::from("Restrict with Bounding")),
- ]
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Opt {
- level: Level,
- pub path: Option,
- pub env_whitelist: Option,
- pub env_checklist: Option,
- pub wildcard_denied: Option,
- pub allow_root: Option,
- pub disable_bounding: Option,
-}
-
-impl AsRef for Opt {
- fn as_ref(&self) -> &Opt {
- self
- }
-}
-
-impl ToString for Opt {
- fn to_string(&self) -> String {
- let mut str = String::new();
- if let Some(path) = &self.path {
- str.push_str(format!("path={}\n", path).as_str());
- }
- if let Some(env_whitelist) = &self.env_whitelist {
- str.push_str(format!("env_whitelist={}\n", env_whitelist).as_str());
- }
- if let Some(env_checklist) = &self.env_checklist {
- str.push_str(format!("env_checklist={}\n", env_checklist).as_str());
- }
- if let Some(wildcard_denied) = &self.wildcard_denied {
- str.push_str(format!("wildcard_denied={}\n", wildcard_denied).as_str());
- }
- if let Some(no_root) = &self.allow_root {
- str.push_str(format!("no_root={}\n", no_root).as_str());
- }
- if let Some(bounding) = &self.disable_bounding {
- str.push_str(format!("bounding={}\n", bounding).as_str());
- }
- str
- }
-}
-
-impl Default for Opt {
- fn default() -> Opt {
- Opt {
- level: Level::Default,
- path: Some("/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin".to_string()),
- env_whitelist: Some("HOME,USER,LOGNAME,COLORS,DISPLAY,HOSTNAME,KRB5CCNAME,LS_COLORS,PS1,PS2,XAUTHORY,XAUTHORIZATION,XDG_CURRENT_DESKTOP".to_string()),
- env_checklist: Some("COLORTERM,LANG,LANGUAGE,LC_*,LINGUAS,TERM,TZ".to_string()),
- allow_root: Some(true),
- disable_bounding: Some(true),
- wildcard_denied: Some(";&|".to_string())
- }
- }
-}
-
-impl Opt {
- pub fn new(level: Level) -> Opt {
- Opt {
- level,
- path: None,
- env_whitelist: None,
- env_checklist: None,
- allow_root: None,
- disable_bounding: None,
- wildcard_denied: None,
- }
- }
-
- pub fn get_description(&self) -> String {
- let mut description = String::new();
- if let Some(path) = self.path.borrow().as_ref() {
- description.push_str(format!("Path: {}\n", path).as_str());
- }
- if let Some(env_whitelist) = self.env_whitelist.borrow().as_ref() {
- description.push_str(format!("Env whitelist: {}\n", env_whitelist).as_str());
- }
- if let Some(env_checklist) = self.env_checklist.borrow().as_ref() {
- description.push_str(format!("Env checklist: {}\n", env_checklist).as_str());
- }
- if let Some(no_root) = self.allow_root.borrow().as_ref() {
- description.push_str(format!("No root: {}\n", no_root).as_str());
- }
- if let Some(bounding) = self.disable_bounding.borrow().as_ref() {
- description.push_str(format!("Bounding: {}\n", bounding).as_str());
- }
- if let Some(wildcard_denied) = self.wildcard_denied.borrow().as_ref() {
- description.push_str(format!("Wildcard denied: {}\n", wildcard_denied).as_str());
- }
- description
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct OptStack<'a> {
- pub(crate) stack: [Option>>; 5],
- roles: Rc>>,
- role: Option>>>,
- task: Option>>>,
-}
-
-impl<'a> OptStack<'a> {
- pub fn from_task(task: Rc>>) -> Self {
- let mut stack = OptStack::from_role(task.as_ref().borrow().get_role().unwrap());
- stack.task = Some(task.to_owned());
- stack.set_opt(Level::Task, task.as_ref().borrow().options.to_owned());
- stack
- }
- pub fn from_role(role: Rc>>) -> Self {
- let mut stack = OptStack::from_roles(role.as_ref().borrow().get_config().unwrap());
- stack.role = Some(role.to_owned());
- stack.set_opt(Level::Role, role.as_ref().borrow().options.to_owned());
- stack
- }
- pub fn from_roles(roles: Rc>>) -> Self {
- let mut stack = OptStack::new(roles);
- stack.set_opt(
- Level::Global,
- stack.get_roles().as_ref().borrow().options.to_owned(),
- );
- stack
- }
-
- fn new(roles: Rc>>) -> OptStack<'a> {
- OptStack {
- stack: [None, Some(Rc::new(Opt::default().into())), None, None, None],
- roles,
- role: None,
- task: None,
- }
- }
-
- fn get_roles(&self) -> Rc>> {
- self.roles.to_owned()
- }
-
- fn save(&mut self) {
- let level = self.get_level();
- let opt = self.get_opt(level);
- match level {
- Level::Global => {
- self.get_roles().as_ref().borrow_mut().options = opt;
- }
- Level::Role => {
- self.role.to_owned().unwrap().as_ref().borrow_mut().options = opt;
- }
- Level::Task => {
- self.task.to_owned().unwrap().as_ref().borrow_mut().options = opt;
- }
- Level::None | Level::Default => {
- panic!("Cannot save None/default options");
- }
- }
- }
-
- pub fn get_level(&self) -> Level {
- self.stack
- .iter()
- .rev()
- .find(|opt| opt.is_some())
- .unwrap()
- .as_ref()
- .unwrap()
- .as_ref()
- .borrow()
- .level
- }
- fn set_opt(&mut self, level: Level, opt: Option>>) {
- if let Some(opt) = opt {
- self.stack[level as usize] = Some(opt);
- } else {
- self.stack[level as usize] = Some(Rc::new(Opt::new(level).into()));
- }
- }
-
- fn get_opt(&self, level: Level) -> Option>> {
- self.stack[level as usize].to_owned()
- }
-
- fn find_in_options Option<(Level, T)>, T>(&self, f: F) -> Option<(Level, T)> {
- for opt in self.stack.iter().rev() {
- if let Some(opt) = opt.to_owned() {
- let res = f(opt.as_ref().borrow().as_ref());
- if res.is_some() {
- debug!("res: {:?}", res.as_ref().unwrap().0);
- return res;
- }
- }
- }
- None
- }
-
- pub fn get_from_type(&self, opttype: OptType) -> (Level, OptValue) {
- match opttype {
- OptType::Path => {
- let res = self.get_path();
- (res.0, OptValue::String(res.1))
- }
- OptType::EnvWhitelist => {
- let res = self.get_env_whitelist();
- (res.0, OptValue::String(res.1))
- }
- OptType::EnvChecklist => {
- let res = self.get_env_checklist();
- (res.0, OptValue::String(res.1))
- }
- OptType::NoRoot => {
- let res = self.get_no_root();
- (res.0, OptValue::Bool(res.1))
- }
- OptType::Bounding => {
- let res = self.get_bounding();
- (res.0, OptValue::Bool(res.1))
- }
- OptType::Wildcard => {
- let res = self.get_wildcard();
- (res.0, OptValue::String(res.1))
- }
- }
- }
-
- pub fn get_from_level(&self, level: Level, opttype: OptType) -> Option {
- self.stack[level as usize]
- .as_ref()
- .map(|opt| {
- let opt = opt.as_ref().borrow();
- match opttype {
- OptType::Path => {
- if let Some(value) = opt.path.borrow().as_ref() {
- return Some(OptValue::String(value.to_owned()));
- }
- }
- OptType::EnvWhitelist => {
- if let Some(value) = opt.env_whitelist.borrow().as_ref() {
- return Some(OptValue::String(value.to_owned()));
- }
- }
- OptType::EnvChecklist => {
- if let Some(value) = opt.env_checklist.borrow().as_ref() {
- return Some(OptValue::String(value.to_owned()));
- }
- }
- OptType::NoRoot => {
- if let Some(value) = opt.allow_root.borrow().as_ref() {
- return Some(OptValue::Bool(value.to_owned()));
- }
- }
- OptType::Bounding => {
- if let Some(value) = opt.disable_bounding.borrow().as_ref() {
- return Some(OptValue::Bool(value.to_owned()));
- }
- }
- OptType::Wildcard => {
- if let Some(value) = opt.wildcard_denied.borrow().as_ref() {
- return Some(OptValue::String(value.to_owned()));
- }
- }
- }
- None
- })
- .unwrap_or(None)
- }
-
- pub fn get_path(&self) -> (Level, String) {
- self.find_in_options(|opt| {
- if let Some(p) = opt.borrow().path.borrow().as_ref() {
- return Some((opt.borrow().level, p.to_owned()));
- }
- None
- })
- .unwrap_or((Level::None, "".to_string()))
- }
- pub fn get_env_whitelist(&self) -> (Level, String) {
- self.find_in_options(|opt| {
- if let Some(p) = opt.borrow().env_whitelist.borrow().as_ref() {
- return Some((opt.borrow().level, p.to_owned()));
- }
- None
- })
- .unwrap_or((Level::None, "".to_string()))
- }
- pub fn get_env_checklist(&self) -> (Level, String) {
- self.find_in_options(|opt| {
- if let Some(p) = opt.borrow().env_checklist.borrow().as_ref() {
- return Some((opt.borrow().level, p.to_owned()));
- }
- None
- })
- .unwrap_or((Level::None, "".to_string()))
- }
- pub fn get_no_root(&self) -> (Level, bool) {
- self.find_in_options(|opt| {
- if let Some(p) = opt.borrow().allow_root.borrow().as_ref() {
- return Some((opt.borrow().level, p.to_owned()));
- }
- None
- })
- .unwrap_or((Level::None, true))
- }
- pub fn get_bounding(&self) -> (Level, bool) {
- self.find_in_options(|opt| {
- if let Some(p) = opt.borrow().disable_bounding.borrow().as_ref() {
- return Some((opt.borrow().level, p.to_owned()));
- }
- None
- })
- .unwrap_or((Level::None, true))
- }
- 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.borrow().level, p.to_owned()));
- }
- None
- })
- .unwrap_or((Level::None, "".to_string()))
- }
-
- fn set_at_level(&mut self, opttype: OptType, value: Option, level: Level) {
- let ulevel = level as usize;
- if self.stack[ulevel].is_none() {
- self.stack[ulevel] = Some(Rc::new(Opt::new(level).into()));
- return;
- }
- println!("stack : {:?}", self.stack);
- let binding = self.stack[ulevel].as_ref().unwrap();
- let mut opt = binding.as_ref().borrow_mut();
- match opttype {
- OptType::Path => {
- if let Some(OptValue::String(value)) = value.borrow() {
- opt.path.replace(value.to_string());
- }
- }
- OptType::EnvWhitelist => {
- if let Some(OptValue::String(value)) = value.borrow() {
- opt.env_whitelist.replace(value.to_string());
- }
- }
- OptType::EnvChecklist => {
- if let Some(OptValue::String(value)) = value.borrow() {
- opt.env_checklist.replace(value.to_string());
- }
- }
- OptType::NoRoot => {
- if let Some(OptValue::Bool(value)) = value.borrow() {
- opt.allow_root.replace(*value);
- }
- }
- OptType::Bounding => {
- if let Some(OptValue::Bool(value)) = value.borrow() {
- opt.disable_bounding.replace(*value);
- }
- }
- OptType::Wildcard => {
- if let Some(OptValue::String(value)) = value.borrow() {
- opt.wildcard_denied.replace(value.to_string());
- }
- }
- }
- }
-
- /**
- * Set an option at the highest level
- */
- pub fn set_value(&mut self, opttype: OptType, value: Option) {
- self.set_at_level(opttype, value, self.get_level());
- self.save();
- }
-
- pub fn get_description(&self, current_level: Level, opttype: OptType) -> String {
- let (level, value) = self.get_from_type(opttype.to_owned());
- let leveldesc = if level != current_level {
- match level {
- Level::Default => " (Inherited from Default)",
- Level::Global => " (Inherited from Global)",
- Level::Role => " (Inherited from Role)",
- Level::Task => " (Inherited from Commands)",
- Level::None => " (Inherited from None)",
- }
- } else {
- " (setted at this level)"
- };
- format!("{}\n{}", leveldesc, value.get_description(opttype))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use crate::xml_version::PACKAGE_VERSION;
-
- use super::super::options::*;
- use super::super::structs::*;
-
- #[test]
- fn test_find_in_options() {
- let roles = Config::new(PACKAGE_VERSION);
- let role = Role::new("test".to_string(), Some(Rc::downgrade(&roles)));
- roles.as_ref().borrow_mut().roles.push(role);
- let mut options = OptStack::from_role(roles.as_ref().borrow().roles[0].to_owned());
- options.set_at_level(
- OptType::Path,
- Some(OptValue::String("path1".to_string())),
- Level::Global,
- );
- options.set_at_level(
- OptType::Path,
- Some(OptValue::String("path2".to_string())),
- Level::Role,
- );
-
- let res = options.find_in_options(|opt| {
- if let Some(value) = opt.path.borrow().as_ref() {
- Some((opt.level, value.to_owned()))
- } else {
- None
- }
- });
- assert_eq!(res, Some((Level::Role, "path2".to_string())));
- }
-
- #[test]
- fn test_get_description() {
- let mut options = OptStack::from_roles(Config::new(PACKAGE_VERSION));
- println!("{:?}", options);
- options.set_at_level(
- OptType::Path,
- Some(OptValue::String("path1".to_string())),
- Level::Global,
- );
- println!("{:?}", options);
- options.set_at_level(
- OptType::EnvWhitelist,
- Some(OptValue::String("tets".to_string())),
- Level::Role,
- );
- println!("{:?}", options);
- let res = options.get_description(Level::Role, OptType::Path);
- assert_eq!(res, " (Inherited from Global)\npath1");
- }
-
- #[test]
- fn test_get_description_inherited() {
- let mut options = OptStack::from_roles(Config::new(PACKAGE_VERSION));
- options.set_at_level(
- OptType::Path,
- Some(OptValue::String("path1".to_string())),
- Level::Global,
- );
- options.set_at_level(
- OptType::EnvWhitelist,
- Some(OptValue::String("tets".to_string())),
- Level::Global,
- );
-
- let res = options.get_description(Level::Global, OptType::Path);
- assert_eq!(res, " (setted at this level)\npath1");
- }
-
- #[test]
- fn test_task_level() {
- let roles = Config::new(PACKAGE_VERSION);
- let role = Role::new("test".to_string(), Some(Rc::downgrade(&roles)));
- let task = Task::new(IdTask::Number(1), Rc::downgrade(&role));
- let mut options = OptStack::from_task(task);
- options.set_at_level(
- OptType::EnvChecklist,
- Some(OptValue::String("checklist1".to_string())),
- Level::Global,
- );
- options.set_at_level(
- OptType::EnvChecklist,
- Some(OptValue::String("checklist2".to_string())),
- Level::Task,
- );
-
- let res = options.get_description(Level::Task, OptType::EnvChecklist);
- assert_eq!(res, " (setted at this level)\nchecklist2");
- }
-
- #[test]
- fn test_get_from_level() {
- let roles = Config::new(PACKAGE_VERSION);
- let role = Role::new("test".to_string(), Some(Rc::downgrade(&roles)));
- let task = Task::new(IdTask::Number(1), Rc::downgrade(&role));
- let mut options = OptStack::from_task(task);
- options.set_at_level(
- OptType::EnvChecklist,
- Some(OptValue::String("checklist1".to_string())),
- Level::Global,
- );
- options.set_at_level(
- OptType::EnvChecklist,
- Some(OptValue::String("checklist2".to_string())),
- Level::Task,
- );
-
- let res = options.get_from_level(Level::Task, OptType::EnvChecklist);
- assert_eq!(res.unwrap().to_string(), "checklist2");
- }
-
- #[test]
- fn test_set_value() {
- let roles = Config::new(PACKAGE_VERSION);
- let role = Role::new("test".to_string(), Some(Rc::downgrade(&roles)));
- let task = Task::new(IdTask::Number(1), Rc::downgrade(&role));
- let mut options = OptStack::from_task(task);
- options.set_value(OptType::NoRoot, Some(OptValue::Bool(true)));
-
- let res = options.get_from_level(Level::Task, OptType::NoRoot);
- assert_eq!(res.unwrap().to_string(), "true");
- }
-}
diff --git a/src/config/save.rs b/src/config/save.rs
deleted file mode 100644
index 8de97208..00000000
--- a/src/config/save.rs
+++ /dev/null
@@ -1,874 +0,0 @@
-use std::{
- borrow::Borrow, collections::HashSet, error::Error, fs::File, io::Write, os::fd::AsRawFd,
- path::Path, str::from_utf8,
-};
-
-use sxd_document::{
- dom::{Document, Element},
- writer::Writer,
-};
-use tracing::debug;
-
-use crate::xml_version::DTD;
-
-use super::{capset_to_string, do_in_main_child, options::Opt};
-
-use super::{
- foreach_child, read_xml_file,
- structs::{Config, Groups, IdTask, Role, Save, Task},
-};
-
-const FS_IMMUTABLE_FL: u32 = 0x00000010;
-const FS_IOC_GETFLAGS: u64 = 0x80086601;
-const FS_IOC_SETFLAGS: u64 = 0x40086602;
-
-fn toggle_lock_config(file: &str, lock: bool) -> Result<(), String> {
- let file = match File::open(file) {
- Err(e) => return Err(e.to_string()),
- Ok(f) => f,
- };
- let mut val = 0;
- let fd = file.as_raw_fd();
- if unsafe { nix::libc::ioctl(fd, FS_IOC_GETFLAGS, &mut val) } < 0 {
- return Err(std::io::Error::last_os_error().to_string());
- }
- if lock {
- val &= !(FS_IMMUTABLE_FL);
- } else {
- val |= FS_IMMUTABLE_FL;
- }
- if unsafe { nix::libc::ioctl(fd, FS_IOC_SETFLAGS, &mut val) } < 0 {
- return Err(std::io::Error::last_os_error().to_string());
- }
- Ok(())
-}
-
-pub fn sxd_sanitize(element: &mut str) -> String {
- element
- .replace('&', "&")
- .replace('<', "<")
- .replace('>', ">")
- .replace('\"', """)
- .replace('\'', "'")
-}
-
-fn write_xml_config(file: &str, content: Option<&[u8]>) -> Result<(), Box> {
- debug!("Writing config file {}", file);
- let mut file = File::create(file)?;
- if let Some(content) = content {
- let mut content = from_utf8(content).unwrap().to_string();
- content = content.replace("?>", format!("?>\n{}", DTD).as_str());
- file.write_all(content.as_bytes())?;
- } else {
- file.write_fmt(format_args!(
- "\n{}\n\n",
- DTD,
- ))?;
- }
-
- Ok(())
-}
-
-pub fn save_config(filename: &str, config: &Config, lock: bool) -> Result<(), Box> {
- if !Path::new(filename).exists() {
- write_xml_config(filename, None)?;
- }
- let package = read_xml_file(&filename)?;
- let doc = package.as_document();
- config.save(Some(&doc), None)?;
- let mut output = Vec::new();
- Writer::new()
- .set_single_quotes(false)
- .format_document(&doc, &mut output)?;
- if lock {
- toggle_lock_config(filename, false)?;
- }
- write_xml_config(filename, Some(&output))?;
- if lock {
- toggle_lock_config(filename, true)?;
- }
-
- Ok(())
-}
-
-impl<'a> Save for Config<'a> {
- fn save(
- &self,
- doc: Option<&Document>,
- _element: Option<&Element>, // is None
- ) -> Result> {
- let doc = doc.ok_or::>("Unable to retrieve Document".into())?;
- let mut edited = false;
- let mut hasroles = false;
- do_in_main_child(doc, "rootasrole", |element| {
- let element = element.element().unwrap();
- element.set_attribute_value("version", self.version.as_str());
- foreach_child(&element, |child| {
- if let Some(child) = child.element() {
- match child.name().local_part() {
- "roles" => {
- hasroles = true;
- let mut rolesnames = self.get_roles_names();
- foreach_child(&child, |role_element| {
- if let Some(role_element) = role_element.element() {
- let rolename = role_element.attribute_value("name").unwrap();
- if let Some(role) = self.get_role(rolename) {
- if role
- .as_ref()
- .borrow()
- .save(doc.into(), Some(&role_element))?
- {
- edited = true;
- }
- } else {
- role_element.remove_from_parent();
- }
- rolesnames.remove(&rolename.to_string());
- }
- Ok(())
- })?;
- if !rolesnames.is_empty() {
- edited = true;
- }
- for rolename in rolesnames {
- let role = self.get_role(&rolename).unwrap();
- let role_element = doc.create_element("role");
- role_element.set_attribute_value("name", &rolename);
-
- role.as_ref()
- .borrow()
- .save(doc.into(), Some(&role_element))?;
- child.append_child(role_element);
- }
- }
- "options" => {
- if self
- .options
- .as_ref()
- .unwrap()
- .as_ref()
- .borrow()
- .save(doc.into(), Some(&child))?
- {
- edited = true;
- }
- }
- _ => (),
- }
- }
- Ok(())
- })?;
- if !hasroles {
- if let Some(options) = &self.options {
- let options = options.as_ref();
- let options_element = doc.create_element("options");
- options.borrow().save(doc.into(), Some(&options_element))?;
- element.append_child(options_element);
- }
- let roles_element = doc.create_element("roles");
- let rolesnames = self.get_roles_names();
- for rolename in rolesnames {
- let role = self.get_role(&rolename).unwrap();
- let role_element = doc.create_element("role");
- role_element.set_attribute_value("name", &rolename);
-
- role.as_ref()
- .borrow()
- .save(doc.into(), Some(&role_element))?;
- roles_element.append_child(role_element);
- }
- element.append_child(roles_element);
- edited = true;
- }
- Ok(())
- })?;
- Ok(edited)
- }
-}
-
-fn add_actors_to_child_element(
- doc: &Document,
- child: &Element,
- users: &HashSet,
- groups: &HashSet,
-) -> bool {
- if !users.is_empty() || !groups.is_empty() {
- for user in users {
- let actor_element = doc.create_element("user");
- actor_element.set_attribute_value("name", user);
- child.append_child(actor_element);
- }
- for group in groups {
- let actor_element = doc.create_element("group");
- actor_element.set_attribute_value("names", &group.join(","));
- child.append_child(actor_element);
- }
- true
- } else {
- false
- }
-}
-
-impl<'a> Save for Role<'a> {
- fn save(
- &self,
- doc: Option<&Document>,
- element: Option<&Element>,
- ) -> Result> {
- let doc = doc.ok_or::>("Unable to retrieve Document".into())?;
- let element = element.ok_or::>("Unable to retrieve Element".into())?;
- if element.name().local_part() != "role" {
- return Err("Unable to save role".into());
- }
- let mut edited = false;
- if !element.children().is_empty() {
- let mut hasactors = false;
- let mut hasoptions = false;
- let mut hastasks = false;
- let mut taskid = 0;
-
- foreach_child(element, |child| {
- if let Some(child) = child.element() {
- match child.name().local_part() {
- "actors" => {
- hasactors = true;
- let mut users = HashSet::new();
- users.extend(self.users.clone());
- let mut groups = HashSet::new();
- groups.extend(self.groups.clone());
- foreach_child(&child, |actor_element| {
- if let Some(actor_element) = actor_element.element() {
- match actor_element.name().local_part() {
- "user" => {
- let username = actor_element
- .attribute_value("name")
- .unwrap()
- .to_string();
- if !users.contains(&username) {
- actor_element.remove_from_parent();
- edited = true;
- } else {
- users.remove(&username);
- }
- }
- "group" => {
- let groupnames = actor_element
- .attribute_value("names")
- .unwrap()
- .split(',')
- .map(|s| s.to_string())
- .collect::();
- if !groups.contains(&groupnames) {
- actor_element.remove_from_parent();
- edited = true;
- } else {
- groups.remove(&groupnames);
- }
- }
- _ => {}
- }
- }
- Ok(())
- })?;
- edited = add_actors_to_child_element(doc, &child, &users, &groups);
- }
- "task" => {
- hastasks = true;
- if let Some(task) = self.tasks.iter().find(|t| {
- if let Some(id) = child.attribute("id") {
- t.as_ref().borrow().id == IdTask::Name(id.value().to_string())
- } else {
- let ret = t.as_ref().borrow().id == IdTask::Number(taskid);
- taskid += 1;
- ret
- }
- }) {
- if task.as_ref().borrow().save(doc.into(), Some(&child))? {
- edited = true;
- }
- } else {
- child.remove_from_parent();
- edited = true;
- }
- }
- "options" => {
- hasoptions = true;
- if self
- .options
- .clone()
- .unwrap()
- .as_ref()
- .borrow()
- .save(doc.into(), Some(&child))?
- {
- edited = true;
- }
- }
- _ => (),
- }
- }
- Ok(())
- })?;
- if !hasactors && (!self.users.is_empty() || !self.groups.is_empty()) {
- let mut users = HashSet::new();
- users.extend(self.users.clone());
- let mut groups = HashSet::new();
- groups.extend(self.groups.clone());
- let actors_element = doc.create_element("actors");
- add_actors_to_child_element(doc, &actors_element, &users, &groups);
- element.append_child(actors_element);
- edited = true;
- }
- if !hastasks && !self.tasks.is_empty() {
- for task in self.tasks.clone() {
- let element = doc.create_element("task");
- task.as_ref().borrow().save(doc.into(), Some(&element))?;
- }
- edited = true;
- }
- if !hasoptions && self.options.is_some() {
- let element = doc.create_element("options");
- self.options
- .as_ref()
- .unwrap()
- .as_ref()
- .borrow()
- .save(doc.into(), Some(&element))?;
- edited = true;
- }
- } else {
- let actors_element = doc.create_element("actors");
- let mut users = HashSet::new();
- users.extend(self.users.clone());
- let mut groups = HashSet::new();
- groups.extend(self.groups.clone());
- add_actors_to_child_element(doc, &actors_element, &users, &groups);
- for task in self.tasks.clone() {
- let child = doc.create_element("task");
- task.as_ref().borrow().save(doc.into(), Some(&child))?;
- element.append_child(child);
- }
- if let Some(options) = &self.options {
- let options_element = doc.create_element("options");
- options
- .as_ref()
- .borrow()
- .save(doc.into(), Some(&options_element))?;
- element.append_child(options_element);
- }
- edited = true;
- }
- Ok(edited)
- }
-}
-
-impl<'a> Save for Task<'a> {
- fn save(
- &self,
- doc: Option<&Document>,
- element: Option<&Element>,
- ) -> Result> {
- let doc = doc.ok_or::>("Unable to retrieve Document".into())?;
- let element = element.ok_or::>("Unable to retrieve Element".into())?;
- if element.name().local_part() != "task" {
- return Err("Unable to save task".into());
- }
- let mut edited = false;
- if let IdTask::Name(id) = &self.id {
- if let Some(att) = element.attribute_value("id") {
- if att != id.as_str() {
- element.set_attribute_value("id", id.as_str());
- edited = true;
- }
- } else {
- element.set_attribute_value("id", id.as_str());
- edited = true;
- }
- }
- if let Some(capabilities) = &self.capabilities {
- if !capabilities.is_empty() {
- element.set_attribute_value("capabilities", &capset_to_string(capabilities));
- } else if element.attribute_value("capabilities").is_some() {
- element.remove_attribute("capabilities");
- }
- }
- if let Some(setuid) = &self.setuid {
- element.set_attribute_value("setuser", setuid.as_str());
- } else if element.attribute_value("setuser").is_some() {
- element.remove_attribute("setuser");
- }
- if let Some(setgid) = &self.setgid {
- element.set_attribute_value("setgroups", setgid.join(",").as_str());
- } else if element.attribute_value("setgroups").is_some() {
- element.remove_attribute("setgroups");
- }
-
- let mut commands = HashSet::new();
- commands.extend(self.commands.clone());
- let mut hasoptions = false;
- let mut haspurpose = false;
- foreach_child(element, |child| {
- if let Some(child_element) = child.element() {
- match child_element.name().local_part() {
- "command" => {
- let command = child
- .text()
- .ok_or::>("Unable to retrieve command Text".into())?
- .text()
- .to_string();
- if !commands.contains(&command) {
- child_element.remove_from_parent();
- edited = true;
- } else {
- commands.remove(&command);
- }
- }
- "purpose" => {
- haspurpose = true;
- if let Some(purpose) = &self.purpose {
- if child
- .text()
- .ok_or::>("Unable to retrieve command Text".into())?
- .text()
- != purpose
- {
- child_element.set_text(purpose);
- edited = true;
- }
- } else {
- child_element.remove_from_parent();
- edited = true;
- }
- }
- "options" => {
- hasoptions = true;
- if self
- .options
- .as_ref()
- .map(|o| o.as_ref().borrow().save(doc.into(), Some(&child_element)))
- .unwrap()?
- {
- edited = true;
- }
- }
- _ => {}
- }
- }
- Ok(())
- })?;
-
- if !haspurpose && self.purpose.is_some() {
- let purpose_element = doc.create_element("purpose");
- purpose_element.set_text(self.purpose.as_ref().unwrap().as_str());
- element.append_child(purpose_element);
- edited = true;
- }
-
- if !commands.is_empty() {
- for command in commands {
- let command_element = doc.create_element("command");
- command_element.set_text(&command);
- element.append_child(command_element);
- }
- edited = true;
- }
-
- if !hasoptions && self.options.is_some() {
- let options_element = doc.create_element("options");
- self.options
- .as_ref()
- .unwrap()
- .as_ref()
- .borrow()
- .save(doc.into(), Some(&options_element))?;
- element.append_child(options_element);
- edited = true;
- }
- Ok(edited)
- }
-}
-
-impl Save for Opt {
- fn save(
- &self,
- _doc: Option<&Document>,
- element: Option<&Element>,
- ) -> Result> {
- let element = element.ok_or::>("Unable to retrieve Element".into())?;
- if element.name().local_part() != "options" {
- return Err("Unable to save options".into());
- }
- let mut edited = false;
- let mut haspath = false;
- let mut hasenv_whitelist = false;
- let mut hasenv_checklist = false;
- let mut hasallow_root = false;
- let mut hasdisable_bounding = false;
- let mut haswildcard_denied = false;
- foreach_child(element, |child| {
- if let Some(child_element) = child.element() {
- match child_element.name().local_part() {
- "path" => {
- haspath = true;
- if self.path.is_none() {
- child_element.remove_from_parent();
- edited = true;
- } else if child_element
- .children()
- .iter()
- .fold(String::new(), |acc, c| {
- acc + match c.text() {
- Some(t) => t.text(),
- None => "",
- }
- })
- != *self.path.as_ref().unwrap()
- {
- child_element.set_text(self.path.as_ref().unwrap());
- edited = true;
- }
- }
- "env_whitelist" => {
- hasenv_whitelist = true;
- if self.env_whitelist.is_none() {
- child_element.remove_from_parent();
- edited = true;
- } else if *child
- .text()
- .ok_or::>(
- "Unable to retrieve env_whitelist Text".into(),
- )?
- .text()
- != self.env_whitelist.as_ref().unwrap().to_string()
- {
- child_element.set_text(self.env_whitelist.as_ref().unwrap().as_str());
- edited = true;
- }
- }
- "env_checklist" => {
- hasenv_checklist = true;
- if self.env_checklist.is_none() {
- child_element.remove_from_parent();
- edited = true;
- } else if *child
- .text()
- .ok_or::>(
- "Unable to retrieve env_checklist Text".into(),
- )?
- .text()
- != self.env_checklist.as_ref().unwrap().to_string()
- {
- child_element.set_text(self.env_checklist.as_ref().unwrap().as_str());
- edited = true;
- }
- }
- "allow-root" => {
- hasallow_root = true;
- let noroot = child_element
- .attribute("allow-root")
- .ok_or::>("Unable to retrieve allow-root".into())?;
- if self.allow_root.is_none() {
- child_element.remove_from_parent();
- edited = true;
- } else if (noroot.value() == "true") != self.allow_root.unwrap() {
- child_element.set_attribute_value(
- "enforce",
- match self.allow_root.unwrap() {
- true => "true",
- false => "false",
- },
- );
- edited = true;
- }
- }
- "allow-bounding" => {
- hasdisable_bounding = true;
- let noroot = child_element
- .attribute("allow-bounding")
- .ok_or::>("Unable to retrieve allow_bounding".into())?;
- if self.allow_root.is_none() {
- child_element.remove_from_parent();
- edited = true;
- } else if (noroot.value() == "true") != self.allow_root.unwrap() {
- child_element.set_attribute_value(
- "enforce",
- match self.allow_root.unwrap() {
- true => "true",
- false => "false",
- },
- );
- edited = true;
- }
- }
- "wildcard_denied" => {
- haswildcard_denied = true;
- if self.wildcard_denied.is_none() {
- child_element.remove_from_parent();
- edited = true;
- } else if *child.text().unwrap().text()
- != self.wildcard_denied.as_ref().unwrap().to_string()
- {
- child_element.set_text(self.wildcard_denied.as_ref().unwrap().as_str());
- edited = true;
- }
- }
- _ => {}
- }
- }
- Ok(())
- })?;
- if !haspath && self.path.is_some() {
- let path_element = _doc.unwrap().create_element("path");
- path_element.set_text(self.path.as_ref().unwrap());
- element.append_child(path_element);
- edited = true;
- }
- if !hasenv_whitelist && self.env_whitelist.is_some() {
- let env_whitelist_element = _doc.unwrap().create_element("env_whitelist");
- env_whitelist_element.set_text(self.env_whitelist.as_ref().unwrap().as_str());
- element.append_child(env_whitelist_element);
- edited = true;
- }
- if !hasenv_checklist && self.env_checklist.is_some() {
- let env_checklist_element = _doc.unwrap().create_element("env_checklist");
- env_checklist_element.set_text(self.env_checklist.as_ref().unwrap().as_str());
- element.append_child(env_checklist_element);
- edited = true;
- }
- if !hasallow_root && self.allow_root.is_some() {
- let allow_root_element = _doc.unwrap().create_element("allow-root");
- allow_root_element.set_attribute_value(
- "enforce",
- match self.allow_root.unwrap() {
- true => "true",
- false => "false",
- },
- );
- element.append_child(allow_root_element);
- edited = true;
- }
- if !hasdisable_bounding && self.disable_bounding.is_some() {
- let disable_bounding_element = _doc.unwrap().create_element("disable-bounding");
- disable_bounding_element.set_attribute_value(
- "enforce",
- match self.disable_bounding.unwrap() {
- true => "true",
- false => "false",
- },
- );
- element.append_child(disable_bounding_element);
- edited = true;
- }
- if self.wildcard_denied.is_some() {
- let wildcard_denied_element = _doc.unwrap().create_element("wildcard_denied");
- wildcard_denied_element.set_text(self.wildcard_denied.as_ref().unwrap().as_str());
- element.append_child(wildcard_denied_element);
- edited = true;
- }
-
- Ok(edited)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::rc::Rc;
- use test_log::test;
-
- use crate::xml_version::PACKAGE_VERSION;
-
- use super::super::options::*;
- use super::super::structs::*;
- use super::*;
- use capctl::{Cap, CapSet};
-
- #[test]
- fn test_save() {
- let roles = Config::new(PACKAGE_VERSION);
- let binding = "role_test".to_string();
- let role = Role::new(binding, Some(Rc::downgrade(&roles)));
- let task = Task::new(IdTask::Name("task_test".to_string()), Rc::downgrade(&role));
- {
- let mut task_mut = task.as_ref().borrow_mut();
- task_mut.commands.push("test_command1".to_string());
- task_mut.commands.push("test_command2".to_string());
- task_mut.purpose = Some("test_purpose".to_string());
- let mut capset = CapSet::empty();
- capset.add(Cap::BPF);
- task_mut.capabilities = Some(capset);
- task_mut.setuid = Some("test_setuid".to_string());
- task_mut.setgid =
- Some(vec!["test_setgidA1".to_string(), "test_setgidB1".to_string()].into());
- let mut options = Opt::new(Level::Task);
- options.path = Some("task_test_path".to_string().into());
- options.env_whitelist = Some("task_test_env_whitelist".to_string().into());
- options.env_checklist = Some("task_test_env_checklist".to_string().into());
- options.allow_root = Some(false.into());
- options.disable_bounding = Some(false.into());
- options.wildcard_denied = Some("task_test_wildcard_denied".into());
- task_mut.options = Some(Rc::new(options.into()));
- }
- {
- let mut role_mut = role.as_ref().borrow_mut();
- role_mut.users.push("test_user1".to_string());
- role_mut.users.push("test_user2".to_string());
- role_mut
- .groups
- .push(vec!["test_groupA1".to_string()].into());
- role_mut
- .groups
- .push(vec!["test_groupB1".to_string(), "test_groupB2".to_string()].into());
- role_mut.tasks.push(task);
- let mut options = Opt::new(Level::Role);
- options.path = Some("role_test_path".to_string().into());
- options.env_whitelist = Some("role_test_env_whitelist".to_string().into());
- options.env_checklist = Some("role_test_env_checklist".to_string().into());
- options.allow_root = Some(false.into());
- options.disable_bounding = Some(false.into());
- options.wildcard_denied = Some("role_test_wildcard_denied".into());
- role_mut.options = Some(Rc::new(options.into()));
- }
- let mut roles_mut = roles.as_ref().borrow_mut();
- let mut options = Opt::new(Level::Global);
- options.path = Some("global_test_path".to_string().into());
- options.env_whitelist = Some("global_test_env_whitelist".to_string().into());
- options.env_checklist = Some("global_test_env_checklist".to_string().into());
- options.allow_root = Some(false.into());
- options.disable_bounding = Some(false.into());
- options.wildcard_denied = Some("global_test_wildcard_denied".into());
- roles_mut.options = Some(Rc::new(options.into()));
- roles_mut.roles.push(role);
- let package = sxd_document::Package::new();
- let doc = package.as_document();
- let root = doc.create_element("rootasrole");
- root.set_attribute_value("version", "vtest");
- doc.root().append_child(root);
- roles_mut.save(Some(&doc), None).unwrap();
-
- let childs = root.children();
- assert_eq!(childs.len(), 2);
- let roles_options = childs[0].element().unwrap();
- assert_eq!(roles_options.name().local_part(), "options");
- assert_eq!(roles_options.children().len(), 6);
- for option_element in roles_options.children() {
- let option_element = option_element.element().unwrap();
- match option_element.name().local_part() {
- "path" => {
- assert_eq!(
- option_element.children()[0].text().unwrap().text(),
- "global_test_path"
- );
- }
- "env_whitelist" => {
- assert_eq!(
- option_element.children()[0].text().unwrap().text(),
- "global_test_env_whitelist"
- );
- }
- "env_checklist" => {
- assert_eq!(
- option_element.children()[0].text().unwrap().text(),
- "global_test_env_checklist"
- );
- }
- "allow-root" => {
- assert_eq!(
- option_element.attribute("enforce").unwrap().value(),
- "false"
- );
- }
- "allow-bounding" => {
- assert_eq!(
- option_element.attribute("enforce").unwrap().value(),
- "false"
- );
- }
- "wildcard_denied" => {
- assert_eq!(
- option_element.children()[0].text().unwrap().text(),
- "global_test_wildcard_denied"
- );
- }
- _ => {}
- }
- }
- let role_list = childs[1].element().unwrap();
- assert_eq!(role_list.name().local_part(), "roles");
- assert_eq!(role_list.children().len(), 1);
- let role = role_list.children()[0].element().unwrap();
- assert_eq!(role.name().local_part(), "role");
- assert_eq!(role.children().len(), 2);
- let task = role.children()[0].element().unwrap();
- assert_eq!(task.name().local_part(), "task");
- assert_eq!(task.children().len(), 4);
- let task_purpose = task.children()[0].element().unwrap();
- assert_eq!(task_purpose.name().local_part(), "purpose");
- assert_eq!(task_purpose.children().len(), 1);
- assert_eq!(
- task_purpose.children()[0].text().unwrap().text(),
- "test_purpose"
- );
- let task_command1 = task.children()[1].element().unwrap();
- assert_eq!(task_command1.name().local_part(), "command");
- assert_eq!(task_command1.children().len(), 1);
- assert!(task_command1.children()[0]
- .text()
- .unwrap()
- .text()
- .starts_with("test_command"));
- let task_command2 = task.children()[2].element().unwrap();
- assert_eq!(task_command2.name().local_part(), "command");
- assert_eq!(task_command2.children().len(), 1);
- assert!(task_command2.children()[0]
- .text()
- .unwrap()
- .text()
- .starts_with("test_command"));
- let package = read_xml_file(&crate::util::test::test_resources_file(
- "test_xml_manager_case1.xml",
- ))
- .unwrap();
- let doc = package.as_document();
- let element = doc.root().children();
- assert_eq!(element.len(), 3);
- let element = element[1].element().unwrap();
- roles_mut.save(Some(&doc), Some(&element)).unwrap();
- let childs = root.children();
- assert_eq!(childs.len(), 2);
- let roles_options = childs[0].element().unwrap();
- assert_eq!(roles_options.name().local_part(), "options");
- assert_eq!(roles_options.children().len(), 6);
- let role_list = childs[1].element().unwrap();
- assert_eq!(role_list.name().local_part(), "roles");
- assert_eq!(role_list.children().len(), 1);
- let role = role_list.children()[0].element().unwrap();
- assert_eq!(role.name().local_part(), "role");
- assert_eq!(role.children().len(), 2);
- let task = role.children()[0].element().unwrap();
- assert_eq!(task.name().local_part(), "task");
- assert_eq!(task.children().len(), 4);
- let task_purpose = task.children()[0].element().unwrap();
- assert_eq!(task_purpose.name().local_part(), "purpose");
- assert_eq!(task_purpose.children().len(), 1);
- assert_eq!(
- task_purpose.children()[0].text().unwrap().text(),
- "test_purpose"
- );
- let task_command1 = task.children()[1].element().unwrap();
- assert_eq!(task_command1.name().local_part(), "command");
- assert_eq!(task_command1.children().len(), 1);
- assert!(task_command1.children()[0]
- .text()
- .unwrap()
- .text()
- .starts_with("test_command"));
- let task_command2 = task.children()[2].element().unwrap();
- assert_eq!(task_command2.name().local_part(), "command");
- assert_eq!(task_command2.children().len(), 1);
- assert!(task_command2.children()[0]
- .text()
- .unwrap()
- .text()
- .starts_with("test_command"));
- }
-}
diff --git a/src/config/structs.rs b/src/config/structs.rs
deleted file mode 100644
index ef8d8163..00000000
--- a/src/config/structs.rs
+++ /dev/null
@@ -1,575 +0,0 @@
-use std::cell::RefCell;
-use std::collections::HashSet;
-use std::error::Error;
-use std::ffi::CString;
-use std::hash::{Hash, Hasher};
-
-use std::rc::{Rc, Weak};
-use std::str::Split;
-
-use capctl::CapSet;
-use chrono::Duration;
-use nix::unistd::{getgrouplist, Group};
-use sxd_document::dom::{Document, Element};
-
-use crate::util::capset_to_string;
-
-use super::options::Opt;
-
-#[derive(Debug, Clone, PartialEq, Eq, Default)]
-pub struct Groups {
- pub groups: Vec,
-}
-
-impl Hash for Groups {
- fn hash(&self, state: &mut H) {
- for group in self.groups.iter() {
- group.hash(state);
- }
- }
-}
-
-impl FromIterator for Groups {
- fn from_iter>(iter: T) -> Groups {
- let mut groups = Vec::new();
- for group in iter {
- groups.push(group);
- }
- Groups { groups }
- }
-}
-
-impl From> for Groups {
- fn from(groups: Vec) -> Self {
- let mut set = Vec::new();
- for group in groups {
- set.push(group);
- }
- Groups { groups: set }
- }
-}
-
-impl From> for Groups {
- fn from(groups: Split) -> Self {
- let mut set = Vec::new();
- for group in groups {
- set.push(group.to_string());
- }
- Groups { groups: set }
- }
-}
-
-impl Groups {
- pub fn join(&self, sep: &str) -> String {
- self.groups.iter().fold(String::new(), |acc, s| {
- if acc.is_empty() {
- s.to_string()
- } else {
- format!("{}{}{}", acc, sep, s)
- }
- })
- }
- fn to_hashset(&self) -> HashSet {
- self.groups.clone().into_iter().collect()
- }
- pub fn is_subset(&self, other: &Groups) -> bool {
- self.to_hashset().is_subset(&other.to_hashset())
- }
- pub fn is_unix_subset(&self, other: &Vec) -> bool {
- let mut remaining = self.groups.clone();
- for group in other {
- if remaining.is_empty() {
- return true;
- }
- if let Some(index) = remaining
- .iter()
- .position(|x| x == &group.name || x == &group.gid.to_string())
- {
- remaining.remove(index);
- }
- }
- remaining.is_empty()
- }
- pub fn len(&self) -> usize {
- self.groups.len()
- }
- pub fn is_empty(&self) -> bool {
- self.groups.is_empty()
- }
-}
-
-impl From for Vec {
- fn from(val: Groups) -> Self {
- val.groups.into_iter().collect()
- }
-}
-
-#[derive(Clone, Debug)]
-pub enum IdTask {
- Name(String),
- Number(usize),
-}
-
-impl IdTask {
- pub fn is_name(&self) -> bool {
- match self {
- IdTask::Name(_) => true,
- IdTask::Number(_) => false,
- }
- }
-
- pub fn as_ref(&self) -> &IdTask {
- self
- }
-
- pub fn unwrap(&self) -> String {
- match self {
- IdTask::Name(s) => s.to_string(),
- IdTask::Number(s) => s.to_string(),
- }
- }
-}
-
-impl ToString for IdTask {
- fn to_string(&self) -> String {
- match self {
- IdTask::Name(s) => s.to_string(),
- IdTask::Number(n) => format!("Task #{}", n),
- }
- }
-}
-
-impl From for IdTask {
- fn from(s: String) -> Self {
- IdTask::Name(s)
- }
-}
-
-impl From for String {
- fn from(val: IdTask) -> Self {
- match val {
- IdTask::Name(s) => s,
- IdTask::Number(n) => n.to_string(),
- }
- }
-}
-
-impl PartialEq for IdTask {
- fn eq(&self, other: &Self) -> bool {
- match (self, other) {
- (IdTask::Name(a), IdTask::Name(b)) => a == b,
- (IdTask::Number(a), IdTask::Number(b)) => a == b,
- _ => false,
- }
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct Task<'a> {
- role: Weak>>,
- pub id: IdTask,
- pub options: Option>>,
- pub commands: Vec,
- pub capabilities: Option,
- pub setuid: Option,
- pub setgid: Option,
- pub setgroups: Option,
- pub purpose: Option,
-}
-
-#[derive(Debug, Clone)]
-pub struct Role<'a> {
- roles: Option>>>,
- ssd: Option>>>,
- denied_caps: Option,
- pub parents: Option>>>>,
- pub name: String,
- pub users: Vec,
- pub groups: Vec,
- pub tasks: Vec>>>,
- pub options: Option>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct CookieConstraint {
- pub offset: Duration,
- pub timestamptype: String,
- pub max_usage: Option,
-}
-
-#[derive(Debug, Clone)]
-pub struct Config<'a> {
- pub config: Option>>>,
- pub roles: Vec>>>,
- pub options: Option>>,
- pub version: String,
- pub timestamp: CookieConstraint,
- pub migrated: bool,
-}
-
-impl Default for CookieConstraint {
- fn default() -> Self {
- CookieConstraint {
- offset: Duration::seconds(0),
- timestamptype: "tty".to_string(),
- max_usage: None,
- }
- }
-}
-
-impl<'a> Config<'a> {
- pub fn new(version: &str) -> Rc>> {
- Rc::new(
- Config {
- config: None,
- roles: Vec::new(),
- options: None,
- version: version.to_string(),
- timestamp: CookieConstraint::default(),
- migrated: false,
- }
- .into(),
- )
- }
-
- pub fn get_role(&self, name: &str) -> Option>>> {
- for r in self.roles.iter() {
- if r.as_ref().borrow().name == name {
- return Some(r.clone());
- }
- }
- None
- }
-
- pub fn get_roles_names(&self) -> HashSet {
- let mut set = HashSet::new();
- for r in self.roles.iter() {
- set.insert(r.as_ref().borrow().name.to_string());
- }
- set
- }
-}
-
-impl<'a> Role<'a> {
- pub fn new(name: String, roles: Option>>>) -> Rc>> {
- Rc::new(
- Role {
- roles,
- name,
- users: Vec::new(),
- groups: Vec::new(),
- tasks: Vec::new(),
- options: None,
- parents: None,
- ssd: None,
- denied_caps: None,
- }
- .into(),
- )
- }
- pub fn in_config(&self) -> bool {
- self.roles.is_some()
- }
- pub fn get_config(&self) -> Option>>> {
- if let Some(roles) = &self.roles {
- return roles.upgrade();
- }
- None
- }
- pub fn get_task_from_index(&self, index: &usize) -> Option>>> {
- if self.tasks.len() > *index {
- return Some(self.tasks[*index].clone());
- }
- None
- }
- pub fn get_users_info(&self) -> String {
- let mut users_info = String::new();
- users_info.push_str(&format!("Users:\n({})\n", self.users.to_vec().join(", ")));
- users_info
- }
- pub fn get_groups_info(&self) -> String {
- let mut groups_info = String::new();
- groups_info.push_str(&format!(
- "Groups:\n({})\n",
- self.groups
- .iter()
- .cloned()
- .map(|x| x.join(" & "))
- .collect::>()
- .join(")\n(")
- ));
- groups_info
- }
- pub fn get_tasks_info(&self) -> String {
- let mut tasks_info = String::new();
- tasks_info.push_str(&format!(
- "Tasks:\n{}\n",
- self.tasks
- .iter()
- .cloned()
- .map(|x| x.as_ref().borrow().id.to_string())
- .collect::>()
- .join("\n")
- ));
- tasks_info
- }
- pub fn get_options_info(&self) -> String {
- let mut options_info = String::new();
- if let Some(o) = &self.options {
- options_info.push_str(&format!(
- "Options:\n{}",
- o.as_ref().borrow().get_description()
- ));
- }
- options_info
- }
-
- pub fn get_description(&self) -> String {
- let mut description = String::new();
- description.push_str(&self.get_users_info());
- description.push_str(&self.get_groups_info());
- description.push_str(&self.get_tasks_info());
- description.push_str(&self.get_options_info());
- description
- }
-
- pub fn remove_task(&mut self, id: IdTask) {
- self.tasks.retain(|x| x.as_ref().borrow().id != id);
- }
-
- pub fn groups_are_forbidden(&self, groups: &Vec) -> bool {
- return match self.ssd.as_ref() {
- Some(roles) => {
- let mut vgroups = Vec::new();
- for group in groups {
- match nix::unistd::Group::from_name(group) {
- Ok(Some(nixgroup)) => {
- vgroups.push(nixgroup);
- }
- _ => (),
- };
- }
- for role in roles.iter() {
- if let Some(role) = role.upgrade() {
- if role
- .groups
- .iter()
- .any(|group| group.is_unix_subset(&vgroups))
- {
- return true;
- }
- }
- }
- false
- }
- None => false,
- };
- }
-
- pub fn user_is_forbidden(&self, user: &str) -> bool {
- return match self.ssd.as_ref() {
- Some(roles) => match nix::unistd::User::from_name(user) {
- Ok(Some(nixuser)) => {
- let mut groups_to_check = Vec::new();
- if let Ok(groups) = getgrouplist(
- CString::new(nixuser.name.as_str()).unwrap().as_c_str(),
- nixuser.gid,
- ) {
- for group in groups.iter() {
- let group = nix::unistd::Group::from_gid(group.to_owned());
- if let Ok(Some(group)) = group {
- groups_to_check.push(group);
- }
- }
- }
- for role in roles.iter() {
- if let Some(role) = role.upgrade() {
- if role.users.contains(&nixuser.name)
- || role.users.contains(&nixuser.uid.to_string())
- || role
- .groups
- .iter()
- .any(|group| group.is_unix_subset(&groups_to_check))
- {
- return true;
- }
- }
- }
- false
- }
- Ok(None) => false,
- Err(_) => false,
- },
- None => false,
- };
- }
-
- pub fn capabilities_are_denied(&self, caps: CapSet) -> bool {
- !self.denied_capabilities().intersection(caps).is_empty()
- }
-
- pub fn denied_capabilities(&self) -> CapSet {
- let mut denied_caps = if self.denied_caps.is_some() {
- self.denied_caps.as_ref().unwrap().clone()
- } else {
- CapSet::empty()
- };
- if let Some(parents) = &self.parents {
- for parent in parents.iter() {
- if let Some(parent) = parent.upgrade() {
- if let Some(denied) = &parent.as_ref().borrow().denied_caps {
- denied_caps = denied_caps.union(*denied);
- }
- }
- }
- }
- denied_caps
- }
-}
-
-impl<'a> Task<'a> {
- pub fn new(id: IdTask, role: Weak>>) -> Rc>> {
- Rc::new(
- Task {
- role,
- id,
- options: None,
- commands: Vec::new(),
- capabilities: None,
- setuid: None,
- setgid: None,
- setgroups: None,
- purpose: None,
- }
- .into(),
- )
- }
- pub fn get_role(&self) -> Option>>> {
- self.role.upgrade()
- }
-
- pub fn get_description(&self) -> String {
- let mut description = String::new();
-
- if let Some(p) = &self.purpose {
- description.push_str(&format!("Purpose :\n{}\n", p));
- }
-
- if let Some(caps) = &self.capabilities {
- description.push_str(&format!("Capabilities:\n({})\n", capset_to_string(caps)));
- }
- if let Some(setuid) = &self.setuid {
- description.push_str(&format!("Setuid:\n({})\n", setuid));
- }
- if let Some(setgid) = &self.setgid {
- description.push_str(&format!("Setgid:\n({})\n", setgid.join(" & ")));
- }
-
- if let Some(options) = &self.options {
- description.push_str(&format!(
- "Options:\n({})\n",
- options.as_ref().borrow().get_description()
- ));
- }
-
- description.push_str(&format!(
- "Commands:\n{}\n",
- self.commands
- .iter()
- .map(|s| {
- if s.len() < 64 {
- s.to_string()
- } else {
- let mut s = s.to_string().chars().take(64).collect::();
- s.push_str("...");
- s
- }
- })
- .fold(String::new(), |acc, x| acc + &format!("{}\n", x))
- ));
- description
- }
-}
-
-pub trait Save {
- fn save(
- &self,
- doc: Option<&Document>,
- element: Option<&Element>,
- ) -> Result>;
-}
-
-#[cfg(test)]
-mod tests {
- use test_log::test;
-
- use capctl::Cap;
-
- use super::super::{capset_to_string, options::Level};
-
- use super::*;
-
- #[test]
- fn test_get_empty_description() {
- let binding = "test_role".to_string();
- let role = Role::new(binding, None);
- assert_eq!(
- role.as_ref().borrow().get_description(),
- "Users:\n()\nGroups:\n()\nTasks:\n\n"
- );
- let task = Task::new(IdTask::Number(0), Rc::downgrade(&role));
- assert_eq!(task.as_ref().borrow().get_description(), "Commands:\n\n");
- }
-
- #[test]
- fn test_get_description() {
- let binding = "test_role".to_string();
- let role = Role::new(binding, None);
- let task = Task::new(IdTask::Number(0), Rc::downgrade(&role));
- task.as_ref().borrow_mut().commands.push("ls".to_string());
- task.as_ref()
- .borrow_mut()
- .commands
- .push("another".to_string());
- task.as_ref().borrow_mut().purpose = Some("thepurpose".to_string());
- task.as_ref().borrow_mut().setuid = Some("thesetuid".to_string());
- task.as_ref().borrow_mut().setgid =
- Some(vec!["thesetgid".to_string(), "thesecondsetgid".to_string()].into());
- let mut caps = CapSet::empty();
- caps.add(Cap::DAC_READ_SEARCH);
- task.as_ref().borrow_mut().capabilities = Some(caps.clone());
- let mut opt = Opt::new(Level::Task);
- opt.path = Some("thepath".to_string());
- opt.disable_bounding = Some(false);
- opt.allow_root = Some(true);
- opt.wildcard_denied = Some("thewildcard-denied".to_string());
- opt.env_checklist = Some("thechecklist".to_string());
- opt.env_whitelist = Some("thewhitelist".to_string());
- task.as_ref().borrow_mut().options = Some(Rc::new(RefCell::new(opt)));
- let desc = task.as_ref().borrow().get_description();
- println!("{}", desc);
- assert!(desc.contains("ls\nanother\n"));
- assert!(desc.contains("thepurpose"));
- assert!(desc.contains("thesetuid"));
- assert!(desc.contains("thesetgid"));
- assert!(desc.contains("thesecondsetgid"));
- assert!(desc.contains(&capset_to_string(&caps)));
- assert!(desc.contains("Options"));
- assert!(desc.contains("thepath"));
- assert!(desc.contains("thewildcard-denied"));
- assert!(desc.contains("thechecklist"));
- assert!(desc.contains("thewhitelist"));
- assert!(desc.contains("No root: true"));
- assert!(desc.contains("Bounding: false"));
- }
-
- #[test]
- fn test_idtask() {
- let id = IdTask::Number(0);
- assert_eq!(id.to_string(), "Task #0");
- let id = IdTask::Name("test".to_string());
- assert_eq!(id.to_string(), "test");
- let id: IdTask = "test".to_string().into();
- assert_eq!(Into::::into(id), "test");
- }
-}
diff --git a/src/config/version.rs b/src/config/version.rs
deleted file mode 100644
index 01100984..00000000
--- a/src/config/version.rs
+++ /dev/null
@@ -1,354 +0,0 @@
-use std::error::Error;
-
-use crate::xml_version::PACKAGE_VERSION;
-/// This allows to upgrade or downgrade the database schema.
-/// The version is stored in the database and compared to the compiled version.
-use semver::Version;
-use sxd_document::dom::{Document, Element};
-use tracing::debug;
-
-use super::{do_in_main_child, do_in_main_element, foreach_inner_elements_names};
-
-struct Migration {
- from: fn() -> Version,
- to: fn() -> Version,
- up: fn(&Self, &Document) -> Result<(), Box>,
- down: fn(&Self, &Document) -> Result<(), Box>,
-}
-
-#[derive(PartialEq, Eq, Debug)]
-enum ChangeResult {
- UpgradeDirect,
- DowngradeDirect,
- UpgradeIndirect,
- DowngradeIndirect,
- None,
-}
-
-impl Migration {
- fn from(&self) -> Version {
- (self.from)()
- }
- fn to(&self) -> Version {
- (self.to)()
- }
- fn change(
- &self,
- doc: &Document,
- from: &Version,
- to: &Version,
- ) -> Result> {
- debug!("Checking migration from {} to {} :", self.from(), self.to());
- debug!(
- "
-\tself.from() == *from -> {}\tself.from() == *to -> {}
-\tself.to() == *to -> {}\tself.to() == *from -> {}
-\t*from < *to -> {}\tself.to() < *to -> {}\tself.to() > *from -> {}
-\t*from > *to -> {}\tself.from() < *to -> {}\tself.from() > *from -> {}",
- self.from() == *from,
- self.to() == *from,
- self.to() == *to,
- self.to() == *from,
- *from < *to,
- self.to() < *to,
- self.to() > *from,
- *from > *to,
- self.from() < *to,
- self.from() > *from
- );
- if self.from() == *from && self.to() == *to {
- debug!("Direct Upgrading from {} to {}", self.from(), self.to());
- (self.up)(self, doc)?;
- Ok(ChangeResult::UpgradeDirect)
- } else if self.to() == *from && self.from() == *to {
- debug!("Direct Downgrading from {} to {}", self.to(), self.from());
- (self.down)(self, doc)?;
- Ok(ChangeResult::DowngradeDirect)
- } else if *from < *to && self.from() == *from && self.to() < *to && self.to() > *from {
- debug!("Step Upgrading from {} to {}", self.from(), self.to());
- // 1.0.0 -> 2.0.0 -> 3.0.0
- (self.up)(self, doc)?;
- Ok(ChangeResult::UpgradeIndirect)
- } else if *from > *to && self.to() == *from && self.from() > *to && self.from() < *from {
- debug!("Step Downgrading from {} to {}", self.to(), self.from());
- // 3.0.0 -> 2.0.0 -> 1.0.0
- (self.down)(self, doc)?;
- Ok(ChangeResult::DowngradeIndirect)
- } else {
- Ok(ChangeResult::None)
- }
- }
-}
-
-fn _migrate(from: &Version, to: &Version, doc: &Document) -> Result> {
- let mut from = from.clone();
- let to = to.clone();
- debug!("===== Migrating from {} to {} =====", from, to);
- if from != to {
- let mut migrated = ChangeResult::UpgradeIndirect;
- while migrated == ChangeResult::UpgradeIndirect
- || migrated == ChangeResult::DowngradeIndirect
- {
- for migration in MIGRATIONS {
- match migration.change(doc, &from, &to)? {
- ChangeResult::UpgradeDirect | ChangeResult::DowngradeDirect => {
- return Ok(true);
- }
- ChangeResult::UpgradeIndirect => {
- from = migration.to();
- migrated = ChangeResult::UpgradeIndirect;
- break;
- }
- ChangeResult::DowngradeIndirect => {
- from = migration.from();
- migrated = ChangeResult::DowngradeIndirect;
- break;
- }
- ChangeResult::None => {
- migrated = ChangeResult::None;
- }
- }
- }
- if migrated == ChangeResult::None {
- return Err(format!("No migration from {} to {} found", from, to).into());
- }
- }
- }
- Ok(false)
-}
-
-/// Migrate the database schema to the current version.
-/// If the version is already the current version, nothing is done.
-/// If the version is older, the database is upgraded.
-/// If the version is newer, the database is downgraded.
-pub(crate) fn migrate(version: &str, doc: &Document) -> Result> {
- _migrate(
- &Version::parse(version).unwrap(),
- &Version::parse(PACKAGE_VERSION).unwrap(),
- doc,
- )
-}
-
-fn set_to_version(element: &Element, to: &Version) {
- element.set_attribute_value("version", to.to_string().as_str());
-}
-
-fn set_to_version_from_doc(doc: &Document, to: &Version) -> Result<(), Box> {
- do_in_main_child(doc, "rootasrole", |main| {
- if let Some(mainelement) = main.element() {
- set_to_version(&mainelement, to);
- }
- Ok(())
- })
-}
-
-const MIGRATIONS: &[Migration] = &[
- Migration {
- from: || "3.0.0-alpha.2".parse().unwrap(),
- to: || "3.0.0-alpha.3".parse().unwrap(),
- /// Upgrade from 3.0.0-alpha.2 to 3.0.0-alpha.3
- /// The version attribute is set to 3.0.0-alpha.3
- up: |m, doc| {
- do_in_main_element(doc, "rootasrole", |main| {
- set_to_version(&main, &m.to());
- foreach_inner_elements_names(
- &main,
- &mut vec!["roles", "role", "task", "command"],
- |cmdelement| {
- cmdelement.remove_attribute("regex");
- Ok(())
- },
- )?;
- Ok(())
- })?;
- Ok(())
- },
- /// Downgrade from 3.0.0-alpha.3 to 3.0.0-alpha.2
- /// The timestamp-timeout attribute is removed from the root element.
- /// The version attribute is set to 3.0.0-alpha.2
- /// The parents, denied-capabilities and incompatible-with attributes are removed from the role element.
- down: |s: &Migration, doc| {
- do_in_main_element(doc, "rootasrole", |main| {
- set_to_version(&main, &s.from());
- if let Some(a) = main.attribute("timestamp-timeout") {
- a.remove_from_parent();
- }
- return foreach_inner_elements_names(&main, &mut vec!["roles", "role"], |role| {
- if let Some(a) = role.attribute("parents") {
- a.remove_from_parent();
- }
- if let Some(a) = role.attribute("denied-capabilities") {
- a.remove_from_parent();
- }
- if let Some(a) = role.attribute("incompatible-with") {
- a.remove_from_parent();
- }
- Ok(())
- });
- })?;
- Ok(())
- },
- },
- Migration {
- from: || "3.0.0-alpha.1".parse().unwrap(),
- to: || "3.0.0-alpha.2".parse().unwrap(),
- up: |m, doc| set_to_version_from_doc(doc, &m.to()),
- down: |s, doc| set_to_version_from_doc(doc, &s.from()),
- },
-];
-
-#[cfg(test)]
-mod tests {
-
- use super::*;
- use crate::{
- config::{
- self,
- load::{load_config, load_config_from_doc},
- save::save_config,
- },
- util,
- };
- use test_log::test;
-
- #[test]
- fn test_migrate() {
- let pkg = config::load::load_document(
- &util::test::test_resources_file("test_migrate-3.0.0-alpha.1.xml"),
- true,
- )
- .expect("Failed to load config");
- let doc = pkg.as_document();
- let v1 = &Version::parse("3.0.0-alpha.1").unwrap();
- let v2 = &Version::parse("3.0.0-alpha.2").unwrap();
- let v3 = &Version::parse("3.0.0-alpha.3").unwrap();
-
- do_in_main_element(&doc, "rootasrole", |main| {
- assert_eq!(
- main.attribute("version").unwrap().value(),
- v1.to_string().as_str()
- );
- Ok(())
- })
- .expect("Failed to get rootasrole element");
-
- //this migration should remove regex attribute on command element
- assert_eq!(
- _migrate(v1, v2, &doc).expect(format!("Failed to migrate to {}", v3).as_str()),
- true
- );
-
- do_in_main_element(&doc, "rootasrole", |main| {
- assert_eq!(
- main.attribute("version").unwrap().value(),
- v2.to_string().as_str()
- );
- Ok(())
- })
- .expect("Failed to get rootasrole element");
-
- //this migration should remove regex attribute on command element
- assert_eq!(
- _migrate(v2, v3, &doc).expect(format!("Failed to migrate to {}", v3).as_str()),
- true
- );
-
- do_in_main_element(&doc, "rootasrole", |main| {
- assert_eq!(
- main.attribute("version").unwrap().value(),
- v3.to_string().as_str()
- );
- foreach_inner_elements_names(
- &main,
- &mut vec!["roles", "role", "task", "command"],
- |cmdelement| {
- assert_eq!(cmdelement.attribute("regex"), None);
- Ok(())
- },
- )?;
- Ok(())
- })
- .expect("Failed to get rootasrole element");
-
- //this migration should do nothing
- assert_eq!(
- _migrate(v3, v2, &doc).expect(format!("Failed to migrate to {}", v3).as_str()),
- true
- );
-
- do_in_main_element(&doc, "rootasrole", |main| {
- assert_eq!(
- main.attribute("version").unwrap().value(),
- v2.to_string().as_str()
- );
- Ok(())
- })
- .expect("Failed to get rootasrole element");
-
- //this migration should do nothing
- assert_eq!(
- _migrate(v2, v3, &doc).expect(format!("Failed to migrate to {}", v3).as_str()),
- true
- );
- do_in_main_element(&doc, "rootasrole", |main| {
- assert_eq!(
- main.attribute("version").unwrap().value(),
- v3.to_string().as_str()
- );
- Ok(())
- })
- .expect("Failed to get rootasrole element");
-
- //we add v3 features on document
- do_in_main_child(&doc, "rootasrole", |element| {
- let element = element.element().unwrap();
- element.set_attribute_value("timestamp-timeout", "10");
- return foreach_inner_elements_names(&element, &mut vec!["roles", "role"], |role| {
- role.set_attribute_value("parents", "role1");
- role.set_attribute_value("denied-capabilities", "CAP_CHOWN");
- role.set_attribute_value("incompatible-with", "role2");
- Ok(())
- });
- })
- .expect("Failed to add v3 features on document");
- assert_eq!(
- _migrate(v3, v1, &doc).expect(format!("Failed to migrate to {}", v2).as_str()),
- true
- );
- do_in_main_element(&doc, "rootasrole", |main| {
- assert_eq!(
- main.attribute("version").unwrap().value(),
- v1.to_string().as_str()
- );
- assert_eq!(main.attribute("timestamp-timeout"), None);
- foreach_inner_elements_names(&main, &mut vec!["roles", "role"], |role| {
- assert_eq!(role.attribute("parents"), None);
- assert_eq!(role.attribute("denied-capabilities"), None);
- assert_eq!(role.attribute("incompatible-with"), None);
- Ok(())
- })?;
- Ok(())
- })
- .expect("Failed to get rootasrole element");
- assert_eq!(
- _migrate(v1, v3, &doc).expect(format!("Failed to migrate to {}", v3).as_str()),
- true
- );
- do_in_main_element(&doc, "rootasrole", |main| {
- assert_eq!(
- main.attribute("version").unwrap().value(),
- v3.to_string().as_str()
- );
- Ok(())
- })
- .expect("Failed to get rootasrole element");
- let config = load_config_from_doc(&doc, false).expect("Failed to load config");
- save_config("/tmp/migrate_config.xml", &config.as_ref().borrow(), false)
- .expect("Failed to save config");
- let config = load_config(&"/tmp/migrate_config.xml").expect("Failed to load config");
- assert_eq!(
- config.as_ref().borrow().version.parse::().unwrap(),
- *v3
- );
- }
-}
diff --git a/src/database/migration.rs b/src/database/migration.rs
new file mode 100644
index 00000000..d14d3989
--- /dev/null
+++ b/src/database/migration.rs
@@ -0,0 +1,130 @@
+use std::error::Error;
+
+use semver::Version;
+use tracing::debug;
+
+use crate::common::version::PACKAGE_VERSION;
+
+
+pub struct Migration {
+ pub from: fn() -> Version,
+ pub to: fn() -> Version,
+ pub up: fn(&Self, &mut T) -> Result<(), Box>,
+ pub down: fn(&Self, &mut T) -> Result<(), Box>,
+}
+
+#[derive(PartialEq, Eq, Debug)]
+pub enum ChangeResult {
+ UpgradeDirect,
+ DowngradeDirect,
+ UpgradeIndirect,
+ DowngradeIndirect,
+ None,
+}
+
+impl Migration {
+ pub fn from(&self) -> Version {
+ (self.from)()
+ }
+ pub fn to(&self) -> Version {
+ (self.to)()
+ }
+ pub fn change(
+ &self,
+ doc: &mut T,
+ from: &Version,
+ to: &Version,
+ ) -> Result> {
+ debug!("Checking migration from {} to {} :", self.from(), self.to());
+ debug!(
+ "
+\tself.from() == *from -> {}\tself.from() == *to -> {}
+\tself.to() == *to -> {}\tself.to() == *from -> {}
+\t*from < *to -> {}\tself.to() < *to -> {}\tself.to() > *from -> {}
+\t*from > *to -> {}\tself.from() < *to -> {}\tself.from() > *from -> {}",
+ self.from() == *from,
+ self.to() == *from,
+ self.to() == *to,
+ self.to() == *from,
+ *from < *to,
+ self.to() < *to,
+ self.to() > *from,
+ *from > *to,
+ self.from() < *to,
+ self.from() > *from
+ );
+ if self.from() == *from && self.to() == *to {
+ debug!("Direct Upgrading from {} to {}", self.from(), self.to());
+ (self.up)(self, doc)?;
+ Ok(ChangeResult::UpgradeDirect)
+ } else if self.to() == *from && self.from() == *to {
+ debug!("Direct Downgrading from {} to {}", self.to(), self.from());
+ (self.down)(self, doc)?;
+ Ok(ChangeResult::DowngradeDirect)
+ } else if *from < *to && self.from() == *from && self.to() < *to && self.to() > *from {
+ debug!("Step Upgrading from {} to {}", self.from(), self.to());
+ // 1.0.0 -> 2.0.0 -> 3.0.0
+ (self.up)(self, doc)?;
+ Ok(ChangeResult::UpgradeIndirect)
+ } else if *from > *to && self.to() == *from && self.from() > *to && self.from() < *from {
+ debug!("Step Downgrading from {} to {}", self.to(), self.from());
+ // 3.0.0 -> 2.0.0 -> 1.0.0
+ (self.down)(self, doc)?;
+ Ok(ChangeResult::DowngradeIndirect)
+ } else {
+ Ok(ChangeResult::None)
+ }
+ }
+
+ pub fn migrate_from(from: &Version, to: &Version, doc: &mut T, migrations : &[Self]) -> Result> {
+ let mut from = from.clone();
+ let to = to.clone();
+ debug!("===== Migrating from {} to {} =====", from, to);
+ if from != to {
+ let mut migrated = ChangeResult::UpgradeIndirect;
+ while migrated == ChangeResult::UpgradeIndirect
+ || migrated == ChangeResult::DowngradeIndirect
+ {
+ for migration in migrations {
+ match migration.change(doc, &from, &to)? {
+ ChangeResult::UpgradeDirect | ChangeResult::DowngradeDirect => {
+ return Ok(true);
+ }
+ ChangeResult::UpgradeIndirect => {
+ from = migration.to();
+ migrated = ChangeResult::UpgradeIndirect;
+ break;
+ }
+ ChangeResult::DowngradeIndirect => {
+ from = migration.from();
+ migrated = ChangeResult::DowngradeIndirect;
+ break;
+ }
+ ChangeResult::None => {
+ migrated = ChangeResult::None;
+ }
+ }
+ }
+ if migrated == ChangeResult::None {
+ return Err(format!("No migration from {} to {} found", from, to).into());
+ }
+ }
+ }
+ Ok(false)
+ }
+
+ /// Migrate the database schema to the current version.
+ /// If the version is already the current version, nothing is done.
+ /// If the version is older, the database is upgraded.
+ /// If the version is newer, the database is downgraded.
+ pub fn migrate(version: &Version, doc: &mut T, migrations : &[Self]) -> Result>
+ where
+ {
+ Self::migrate_from(
+ &version,
+ &Version::parse(PACKAGE_VERSION).unwrap(),
+ doc,
+ migrations,
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/database/mod.rs b/src/database/mod.rs
new file mode 100644
index 00000000..d432e84c
--- /dev/null
+++ b/src/database/mod.rs
@@ -0,0 +1,77 @@
+
+
+use chrono::Duration;
+use linked_hash_set::LinkedHashSet;
+use serde::{de, Deserialize, Serialize};
+use self::options::EnvKey;
+
+mod migration;
+pub mod structs;
+mod version;
+pub mod options;
+pub mod wrapper;
+
+// deserialize the linked hash set
+fn lhs_deserialize_envkey<'de, D>(deserializer: D) -> Result, D::Error>
+where
+ D: de::Deserializer<'de>,
+{
+ let v: Vec = Vec::deserialize(deserializer)?;
+ Ok(v.into_iter().collect())
+}
+
+// serialize the linked hash set
+fn lhs_serialize_envkey(value: &LinkedHashSet, serializer: S) -> Result
+where
+ S: serde::Serializer,
+{
+ let v: Vec = value.iter().cloned().collect();
+ v.serialize(serializer)
+}
+
+// deserialize the linked hash set
+fn lhs_deserialize<'de, D>(deserializer: D) -> Result, D::Error>
+where
+ D: de::Deserializer<'de>,
+{
+ let v: Vec = Vec::deserialize(deserializer)?;
+ Ok(v.into_iter().collect())
+}
+
+// serialize the linked hash set
+fn lhs_serialize(value: &LinkedHashSet, serializer: S) -> Result
+where
+ S: serde::Serializer,
+{
+ let v: Vec = value.iter().cloned().collect();
+ v.serialize(serializer)
+}
+
+fn is_default(t: &T) -> bool {
+ t == &T::default()
+}
+
+fn serialize_duration(value: &Duration, serializer: S) -> Result
+where
+ S: serde::Serializer,
+{
+ // hh:mm:ss format
+ serializer.serialize_str(&format!("{}:{}:{}", value.num_hours(), value.num_minutes() % 60, value.num_seconds() % 60))
+
+}
+
+fn deserialize_duration<'de, D>(deserializer: D) -> Result
+where
+ D: de::Deserializer<'de>,
+{
+ let s = String::deserialize(deserializer)?;
+ 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().map_err(de::Error::custom)?;
+ let minutes: i64 = minutes.parse().map_err(de::Error::custom)?;
+ let seconds: i64 = seconds.parse().map_err(de::Error::custom)?;
+ return Ok(Duration::hours(hours) + Duration::minutes(minutes) + Duration::seconds(seconds))
+ }
+ Err(de::Error::custom("Invalid duration format"))
+}
\ No newline at end of file
diff --git a/src/database/options.rs b/src/database/options.rs
new file mode 100644
index 00000000..b94aa2a8
--- /dev/null
+++ b/src/database/options.rs
@@ -0,0 +1,922 @@
+use std::{
+ borrow::Borrow, cell::RefCell, path::PathBuf, rc::Rc
+};
+
+use linked_hash_set::LinkedHashSet;
+use pcre2::bytes::Regex;
+use serde::{Deserialize, Deserializer, Serialize};
+use serde_json::{Map, Value};
+use strum::{Display, EnumIs, FromRepr};
+use tracing::debug;
+
+use crate::rc_refcell;
+
+use super::is_default;
+
+use super::{
+ lhs_deserialize, lhs_deserialize_envkey, lhs_serialize, lhs_serialize_envkey,
+ structs::{SConfig, SRole, STask},
+};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Level {
+ None,
+ Default,
+ Global,
+ Role,
+ Task,
+}
+
+
+#[derive(Debug, Clone, Copy, FromRepr)]
+pub enum OptType {
+ Path,
+ Env,
+ Root,
+ Bounding,
+ Wildcard,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)]
+#[serde(rename_all = "lowercase")]
+pub enum PathBehavior {
+ Delete,
+ KeepSafe,
+ KeepUnsafe,
+ Inherit,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
+pub struct SPathOptions {
+ #[serde(rename = "default", default, skip_serializing_if = "is_default")]
+ pub default_behavior: PathBehavior,
+ #[serde(
+ default,
+ skip_serializing_if = "LinkedHashSet::is_empty",
+ deserialize_with = "lhs_deserialize",
+ serialize_with = "lhs_serialize"
+ )]
+ pub add: LinkedHashSet,
+ #[serde(
+ default,
+ skip_serializing_if = "LinkedHashSet::is_empty",
+ deserialize_with = "lhs_deserialize",
+ serialize_with = "lhs_serialize"
+ )]
+ pub sub: LinkedHashSet,
+ #[serde(default)]
+ #[serde(flatten)]
+ pub _extra_fields: Map,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)]
+#[serde(rename_all = "lowercase")]
+pub enum EnvBehavior {
+ Delete,
+ Keep,
+ Inherit,
+}
+
+#[derive(Serialize, Hash, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)]
+enum EnvKeyType {
+ Wildcarded,
+ Normal,
+}
+
+#[derive(Eq, Hash, PartialEq, Serialize, Debug, Clone)]
+pub struct EnvKey {
+ env_type: EnvKeyType,
+ value: String,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
+pub struct SEnvOptions {
+ #[serde(rename = "default", default, skip_serializing_if = "is_default")]
+ pub default_behavior: EnvBehavior,
+ #[serde(
+ default,
+ skip_serializing_if = "LinkedHashSet::is_empty",
+ deserialize_with = "lhs_deserialize_envkey",
+ serialize_with = "lhs_serialize_envkey"
+ )]
+ pub keep: LinkedHashSet,
+ #[serde(
+ default,
+ skip_serializing_if = "LinkedHashSet::is_empty",
+ deserialize_with = "lhs_deserialize_envkey",
+ serialize_with = "lhs_serialize_envkey"
+ )]
+ pub check: LinkedHashSet,
+ #[serde(
+ default,
+ skip_serializing_if = "LinkedHashSet::is_empty",
+ deserialize_with = "lhs_deserialize_envkey",
+ serialize_with = "lhs_serialize_envkey"
+ )]
+ pub delete: LinkedHashSet,
+ #[serde(default, flatten)]
+ pub _extra_fields: Map,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)]
+#[serde(rename_all = "lowercase")]
+pub enum SBounding {
+ Strict,
+ Ignore,
+ Inherit,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)]
+#[serde(rename_all = "kebab-case")]
+pub enum SPrivileged {
+ Privileged,
+ User,
+ Inherit,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
+#[serde(rename_all = "kebab-case")]
+pub struct Opt {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub path: Option,
+ #[serde(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(skip_serializing_if = "Option::is_none")]
+ pub wildcard_denied: Option,
+ #[serde(default)]
+ #[serde(flatten)]
+ pub _extra_fields: Map,
+ #[serde(skip)]
+ pub level: Level,
+}
+
+
+impl Default for Level {
+ fn default() -> Self {
+ Level::Default
+ }
+}
+
+impl Default for EnvBehavior {
+ fn default() -> Self {
+ EnvBehavior::Inherit
+ }
+}
+
+impl Default for SBounding {
+ fn default() -> Self {
+ SBounding::Inherit
+ }
+}
+
+impl Default for SPrivileged {
+ fn default() -> Self {
+ SPrivileged::User
+ }
+}
+
+impl Default for PathBehavior {
+ fn default() -> Self {
+ PathBehavior::Inherit
+ }
+}
+
+impl Opt {
+ pub fn new(level: Level) -> Self {
+ let mut opt = Self::default();
+ opt.level = level;
+ opt
+ }
+}
+
+
+
+impl Default for Opt {
+ fn default() -> Self {
+ Opt {
+ path: Some(SPathOptions::default()),
+ env: Some(SEnvOptions::default()),
+ root: Some(SPrivileged::default()),
+ bounding: Some(SBounding::default()),
+ wildcard_denied: Some(";&|".to_string()),
+ _extra_fields: Map::default(),
+ level: Level::Default,
+ }
+ }
+}
+
+impl Default for SPathOptions {
+ fn default() -> Self {
+ SPathOptions {
+ default_behavior: PathBehavior::Inherit,
+ add: LinkedHashSet::new(),
+ sub: LinkedHashSet::new(),
+ _extra_fields: Map::default(),
+ }
+ }
+}
+
+impl EnvKey {
+ pub fn new(s: String) -> Result {
+ if Regex::new("^[a-zA-Z_]+[a-zA-Z0-9_]*$") // check if it is a valid env name
+ .unwrap()
+ .is_match(s.as_bytes())
+ .is_ok_and(|m| m)
+ {
+ Ok(EnvKey {
+ env_type: EnvKeyType::Normal,
+ value: s,
+ })
+ } else if Regex::new("^[a-zA-Z_*?]+[a-zA-Z0-9_*?]*$") // check if it is a valid env name
+ .unwrap()
+ .is_match(s.as_bytes())
+ .is_ok_and(|m| m)
+ {
+ Ok(EnvKey {
+ env_type: EnvKeyType::Wildcarded,
+ value: s,
+ })
+ } else {
+ Err(format!("Invalid env key {}, must start with letter or underscore following by letters, numbers or underscores. Wildcard accepts only '?' and '*'", s))
+ }
+ }
+}
+
+impl PartialEq for EnvKey {
+ fn eq(&self, other: &str) -> bool {
+ self.value == *other
+ }
+}
+
+impl Into for EnvKey {
+ fn into(self) -> String {
+ self.value
+ }
+}
+
+impl From for EnvKey {
+ fn from(s: String) -> Self {
+ EnvKey::new(s).expect("Invalid env key")
+ }
+}
+
+impl From<&str> for EnvKey {
+ fn from(s: &str) -> Self {
+ EnvKey::new(s.into()).expect("Invalid env key")
+ }
+}
+
+impl<'de> Deserialize<'de> for EnvKey {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: Deserializer<'de>,
+ {
+ let s = String::deserialize(deserializer)?;
+ EnvKey::new(s).map_err(serde::de::Error::custom)
+ }
+}
+
+impl SPathOptions {
+ fn new(behavior : PathBehavior ) -> Self {
+ let mut res = SPathOptions::default();
+ res.default_behavior = behavior;
+ res
+ }
+ pub fn get_description(&self) -> String {
+ let mut description = String::new();
+ description.push_str(format!("Default behavior: {}\n", self.default_behavior).as_str());
+ if !self.add.is_empty() {
+ description.push_str("Add:\n");
+ for s in self.add.iter() {
+ description.push_str(format!("{}\n", s).as_str());
+ }
+ }
+ if !self.sub.is_empty() {
+ description.push_str("Sub:\n");
+ for s in self.sub.iter() {
+ description.push_str(format!("{}\n", s).as_str());
+ }
+ }
+ description
+ }
+
+}
+
+impl Default for SEnvOptions {
+ fn default() -> Self {
+ SEnvOptions {
+ default_behavior: EnvBehavior::default(),
+ keep: LinkedHashSet::new(),
+ check: LinkedHashSet::new(),
+ delete: LinkedHashSet::new(),
+ _extra_fields: Map::default(),
+ }
+ }
+}
+
+impl SEnvOptions {
+ pub fn new(behavior: EnvBehavior) -> Self {
+ let mut res = SEnvOptions::default();
+ res.default_behavior = behavior;
+ res
+ }
+ pub fn get_description(&self) -> String {
+ let mut description = String::new();
+ description.push_str(format!("Default behavior: {}\n", self.default_behavior).as_str());
+ if !self.keep.is_empty() {
+ description.push_str("Keep:\n");
+ for s in self.keep.iter() {
+ description.push_str(format!("{}\n", s.value).as_str());
+ }
+ }
+ if !self.check.is_empty() {
+ description.push_str("Check:\n");
+ for s in self.check.iter() {
+ description.push_str(format!("{}\n", s.value).as_str());
+ }
+ }
+ if !self.delete.is_empty() {
+ description.push_str("Delete:\n");
+ for s in self.delete.iter() {
+ description.push_str(format!("{}\n", s.value).as_str());
+ }
+ }
+ description
+ }
+}
+
+impl Opt {
+ pub fn get_description(&self) -> String {
+ let mut description = String::new();
+ if let Some(path) = &self.path {
+ description.push_str(format!("{}\n", path.get_description()).as_str());
+ }
+ if let Some(env) = &self.env {
+ description.push_str(format!("{}\n", env.get_description()).as_str());
+ }
+ if let Some(no_root) = &self.root {
+ description.push_str(format!("Root type: {}\n", no_root).as_str());
+ }
+ if let Some(bounding) = &self.bounding {
+ description.push_str(format!("Bounding enforcement: {}\n", bounding).as_str());
+ }
+ description
+ }
+}
+
+trait EnvSet {
+ fn env_matches(&self, wildcarded: &EnvKey) -> bool;
+}
+
+impl EnvSet for LinkedHashSet {
+ fn env_matches(&self, wildcarded: &EnvKey) -> bool {
+ match wildcarded.env_type {
+ EnvKeyType::Normal => self.contains(wildcarded),
+ EnvKeyType::Wildcarded => {
+ self.iter().any(|s| {
+ Regex::new(&format!(
+ "^{}$",
+ wildcarded.value.replace("*", ".*").replace("?", ".")
+ )) // convert to regex
+ .unwrap()
+ .is_match(s.value.as_bytes())
+ .is_ok_and(|m| m)
+ })
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct OptStack {
+ pub(crate) stack: [Option>>; 5],
+ roles: Rc>,
+ role: Option>>,
+ task: Option>>,
+}
+
+impl OptStack {
+ pub fn from_task(task: Rc>) -> Self {
+ let mut stack =
+ OptStack::from_role(task.as_ref().borrow()._role.as_ref().unwrap().upgrade().unwrap());
+ stack.task = Some(task.to_owned());
+ stack.set_opt(Level::Task, task.as_ref().borrow().options.to_owned());
+ stack
+ }
+ pub fn from_role(role: Rc>) -> Self {
+ let mut stack =
+ OptStack::from_roles(role.as_ref().borrow()._config.as_ref().unwrap().upgrade().unwrap());
+ stack.role = Some(role.to_owned());
+ stack.set_opt(Level::Role, role.as_ref().borrow().options.to_owned());
+ stack
+ }
+ pub fn from_roles(roles: Rc>) -> Self {
+ let mut stack = OptStack::new(roles);
+ stack.set_opt(
+ Level::Global,
+ stack.get_roles().as_ref().borrow().options.to_owned(),
+ );
+ stack
+ }
+
+ fn new(roles: Rc>) -> OptStack {
+ let mut opt = Opt::default();
+ opt.level = Level::Global;
+ opt.root = Some(SPrivileged::User);
+ opt.bounding = Some(SBounding::Strict);
+ let mut env = SEnvOptions::new(EnvBehavior::Delete);
+ env.check =
+ vec!["TZ".into(), "LOGNAME".into(), "LOGIN".into(), "USER".into()]
+ .iter()
+ .cloned()
+ .collect();
+ opt.env = Some(env);
+ opt.path.as_mut().unwrap().default_behavior = PathBehavior::Delete;
+ OptStack {
+ stack: [
+ None,
+ Some(Rc::new(opt.into())
+ ,
+ ),
+ None,
+ None,
+ None,
+ ],
+ roles,
+ role: None,
+ task: None,
+ }
+ }
+
+ fn get_roles(&self) -> Rc> {
+ self.roles.to_owned()
+ }
+
+ fn save(&mut self) {
+ let level = self.get_level();
+ let opt = self.get_opt(level);
+ match level {
+ Level::Global => {
+ self.get_roles().as_ref().borrow_mut().options = opt;
+ }
+ Level::Role => {
+ self.role.to_owned().unwrap().as_ref().borrow_mut().options = opt;
+ }
+ Level::Task => {
+ self.task.to_owned().unwrap().as_ref().borrow_mut().options = opt;
+ }
+ Level::None | Level::Default => {
+ panic!("Cannot save None/default options");
+ }
+ }
+ }
+
+ pub fn get_level(&self) -> Level {
+ self.stack
+ .iter()
+ .rev()
+ .find(|opt| opt.is_some())
+ .unwrap()
+ .as_ref()
+ .unwrap()
+ .as_ref()
+ .borrow()
+ .level
+ }
+ fn set_opt(&mut self, level: Level, opt: Option>>) {
+ if let Some(opt) = opt {
+ self.stack[level as usize] = Some(opt);
+ } else {
+ self.stack[level as usize] = Some(Rc::new(Opt::new(level).into()));
+ }
+ }
+
+ fn get_opt(&self, level: Level) -> Option>> {
+ self.stack[level as usize].to_owned()
+ }
+
+ fn find_in_options Option<(Level, V)>, V>(&self, f: F) -> Option<(Level, V)> {
+ for opt in self.stack.iter().rev() {
+ if let Some(opt) = opt.to_owned() {
+ let res = f(&opt.as_ref().borrow());
+ if res.is_some() {
+ debug!("res: {:?}", res.as_ref().unwrap().0);
+ return res;
+ }
+ }
+ }
+ None
+ }
+
+ fn iter_in_options(&self, mut f: F) {
+ for opt in self.stack.iter() {
+ if let Some(opt) = opt.to_owned() {
+ f(&opt.as_ref().borrow());
+ }
+ }
+ }
+
+ fn calculate_path(&self) -> String {
+ let mut final_behavior = PathBehavior::Delete;
+ 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 => {
+
+ // policy is to delete, so we add whitelist and remove blacklist
+ final_add_clone.as_ref().replace(final_add_clone.as_ref().borrow()
+ .union(&p.add)
+ .filter(|e| !p.sub.contains(*e))
+ .cloned()
+ .collect());
+ }
+ PathBehavior::KeepSafe | PathBehavior::KeepUnsafe => {
+ //policy is to keep, so we remove blacklist and add whitelist
+ final_sub_clone.as_ref().replace(final_sub_clone.as_ref().borrow()
+ .union(&p.sub)
+ .filter(|e| !p.add.contains(*e))
+ .cloned()
+ .collect());
+ }
+ PathBehavior::Inherit => {
+ if final_behavior.is_delete() {
+ final_add_clone.as_ref().replace(final_add_clone.as_ref().borrow()
+ .union(&p.add)
+ .filter(|e| !p.sub.contains(*e))
+ .cloned()
+ .collect());
+ } else {
+ final_sub_clone.as_ref().replace(final_sub_clone.as_ref().borrow()
+ .union(&p.sub)
+ .filter(|e| !p.add.contains(*e))
+ .cloned()
+ .collect());
+ }
+ }
+ }
+ if !p.default_behavior.is_inherit() {
+ final_behavior = p.default_behavior;
+ }
+ }
+ });
+ let final_add = final_add.clone().as_ref().borrow()
+ .difference(&final_sub.as_ref().borrow())
+ .fold("".to_string(), |mut acc, s| {
+ if !acc.is_empty() {
+ acc.push(':');
+ }
+ acc.push_str(s);
+ acc
+ });
+ match final_behavior {
+ PathBehavior::Inherit | PathBehavior::Delete => final_add,
+ is_safe => std::env::vars()
+ .find_map(|(key, value)| if key == "PATH" { Some(value) } else { None })
+ .unwrap_or(String::new())
+ .split(":")
+ .filter(|s| {
+ !final_sub.as_ref().borrow().contains(*s)
+ && (!is_safe.is_keep_safe() || PathBuf::from(s).exists())
+ })
+ .fold(final_add, |mut acc, s| {
+ if !acc.is_empty() {
+ acc.push(':');
+ }
+ acc.push_str(s);
+ acc
+ }),
+ }
+ }
+
+ fn check_env(&self, value: &str) -> bool {
+ value.chars().any(|c| c == '/' || c == '%')
+ }
+
+ pub fn calculate_filtered_env(&self) -> Result, String> {
+ let final_env = std::env::vars();
+ 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)
+ .filter(|e| !p.check.env_matches(&e) || !p.delete.env_matches(e))
+ .cloned()
+ .collect();
+ final_check = final_check
+ .union(&p.check)
+ .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)
+ .filter(|e| !p.keep.env_matches(&e) || !p.check.env_matches(&e))
+ .cloned()
+ .collect();
+ final_check = final_check
+ .union(&p.check)
+ .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)
+ .filter(|e| {
+ !p.delete.env_matches(&e) || !p.check.env_matches(&e)
+ })
+ .cloned()
+ .collect();
+ final_check = final_check
+ .union(&p.check)
+ .filter(|e| !p.delete.env_matches(&e))
+ .cloned()
+ .collect();
+ } else {
+ final_delete = final_delete
+ .union(&p.delete)
+ .filter(|e| !p.keep.env_matches(&e) || !p.check.env_matches(&e))
+ .cloned()
+ .collect();
+ final_check = final_check
+ .union(&p.check)
+ .filter(|e| !p.keep.env_matches(&e))
+ .cloned()
+ .collect();
+ }
+ final_behavior
+ }
+ };
+ }
+
+ });
+ match final_behavior {
+ EnvBehavior::Inherit => {
+ Err("Internal Error with environment behavior".to_string())
+ }
+ EnvBehavior::Delete => {
+ Ok(final_env.filter_map(|(key, value)| {
+ let key = EnvKey::new(key).expect("Unexpected environment variable");
+ if final_keep.env_matches(&key) || (final_check.env_matches(&key) && self.check_env(&value)) {
+ Some((key.value, value))
+ }
+ else {
+ None
+ }
+ }).collect())
+ }
+ EnvBehavior::Keep => {
+ Ok(final_env.filter_map(|(key, value)| {
+ let key = EnvKey::new(key).expect("Unexpected environment variable");
+ if key.value == "PATH" {
+ Some((key.value, self.calculate_path()))
+ } else if final_delete.env_matches(&key) || (final_check.env_matches(&key) && self.check_env(&value)) {
+ Some((key.value, value))
+ } else {
+ None
+ }
+ }).collect())
+ }
+ }
+ }
+ 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_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()))
+ }
+
+ fn get_description_of(&self, opttype: OptType) -> (Level, String) {
+ self.find_in_options(|opt| {
+ match opttype {
+ OptType::Path => {
+ if let Some(p) = opt.path.borrow().as_ref() {
+ return Some((opt.level, format!("{}\n", p.get_description()).to_string()));
+ }
+ }
+ OptType::Env => {
+ if let Some(p) = opt.env.borrow().as_ref() {
+ return Some((opt.level, format!("{}\n", p.get_description()).to_string()));
+ }
+ }
+ OptType::Root => {
+ if let Some(p) = &opt.root {
+ return Some((opt.level, format!("Root type: {}\n", p).to_string()));
+ }
+ }
+ OptType::Bounding => {
+ if let Some(p) = &opt.bounding {
+ return Some((opt.level, format!("Bounding enforcement: {}\n", p).to_string()));
+ }
+ }
+ OptType::Wildcard => {
+ if let Some(p) = opt.wildcard_denied.borrow().as_ref() {
+ return Some((opt.level, format!("Wildcard denied: {}\n", p).to_string()));
+ }
+ }
+ }
+ None
+ }).unwrap_or((Level::None, "".to_string()))
+ }
+
+ pub fn get_description(&self, current_level: Level, opttype: OptType) -> String {
+ let (level, description) = self.get_description_of(opttype.to_owned());
+ let leveldesc = if level != current_level {
+ match level {
+ Level::Default => " (Inherited from Default)",
+ Level::Global => " (Inherited from Global)",
+ Level::Role => " (Inherited from Role)",
+ Level::Task => " (Inherited from Commands)",
+ Level::None => " (Inherited from None)",
+ }
+ } else {
+ " (setted at this level)"
+ };
+ format!("{}\n{}", leveldesc, description)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ use crate::as_borrow_mut;
+ use crate::common::database::wrapper::SConfigWrapper;
+ use crate::common::database::wrapper::SRoleWrapper;
+ use crate::common::database::wrapper::STaskWrapper;
+ use crate::rc_refcell;
+
+ use super::super::options::*;
+ use super::super::structs::*;
+
+ #[test]
+ fn test_find_in_options() {
+ let config = rc_refcell!(SConfig::default());
+ let role = rc_refcell!(SRole::new(
+ "test".to_string(),
+ Rc::downgrade(&config)
+ ));
+ let mut global_path = SPathOptions::default();
+ global_path.default_behavior = PathBehavior::Delete;
+ global_path.add.insert("path1".to_string());
+ let mut role_path = SPathOptions::default();
+ role_path.default_behavior = PathBehavior::Inherit;
+ role_path.add.insert("path2".to_string());
+ let mut config_global = Opt::new(Level::Global);
+ config_global.path = Some(global_path);
+ as_borrow_mut!(config).options = Some(rc_refcell!(config_global));
+ let mut config_role = Opt::new(Level::Role);
+ config_role.path = Some(role_path.clone());
+ as_borrow_mut!(role).options = Some(rc_refcell!(config_role));
+ as_borrow_mut!(config).roles.push(role);
+ let options = OptStack::from_role(config.as_ref().borrow().roles[0].clone());
+
+ let res: Option<(Level, SPathOptions)> = options.find_in_options(|opt| {
+ if let Some(value) = opt.path.clone() {
+ Some((opt.level, value))
+ } else {
+ None
+ }
+ });
+ assert_eq!(res, Some((Level::Role, role_path)));
+ }
+
+ #[test]
+ fn test_get_path() {
+ let config = rc_refcell!(SConfig::default());
+ let role = rc_refcell!(SRole::new(
+ "test".to_string(),
+ Rc::downgrade(&config)
+ ));
+ let mut global_path = SPathOptions::default();
+ global_path.default_behavior = PathBehavior::Delete;
+ global_path.add.insert("path1".to_string());
+ let mut role_path = SPathOptions::default();
+ role_path.default_behavior = PathBehavior::Inherit;
+ role_path.add.insert("path2".to_string());
+ let mut config_global = Opt::new(Level::Global);
+ config_global.path = Some(global_path);
+ as_borrow_mut!(config).options = Some(rc_refcell!(config_global));
+ let mut config_role = Opt::new(Level::Role);
+ config_role.path = Some(role_path);
+ as_borrow_mut!(role).options = Some(rc_refcell!(config_role));
+ as_borrow_mut!(config).roles.push(role);
+ let options = OptStack::from_role(config.as_ref().borrow().roles.get(0).unwrap().clone());
+ let res = options.calculate_path();
+ assert_eq!(res, "path2:path1");
+ }
+
+ #[test]
+ fn test_get_description_inherited() {
+ let role = SRoleWrapper::default();
+ as_borrow_mut!(role).name = "test".to_string();
+ let mut path_options = SPathOptions::new(PathBehavior::Delete);
+ path_options.add.insert("path2".to_string());
+ let mut opt_role = Opt::new(Level::Role);
+ opt_role.path = Some(path_options);
+ as_borrow_mut!(role).options = Some(rc_refcell!(opt_role));
+ let config = SConfigWrapper::default();
+ as_borrow_mut!(config).roles.push(role.clone());
+ let mut global_options = Opt::new(Level::Global);
+ global_options.path = Some(SPathOptions::new(PathBehavior::Delete));
+ global_options.path.as_mut().unwrap().add.insert("path1".to_string());
+ as_borrow_mut!(role)._config = Some(Rc::downgrade(&config));
+ let options = OptStack::from_role(role);
+ let res = options.get_description(Level::Role, OptType::Path);
+ assert_eq!(res, " (Inherited from Global)\npath2:path1");
+ }
+
+ #[test]
+ fn test_get_description() {
+ let role = SRoleWrapper::default();
+ as_borrow_mut!(role).name = "test".to_string();
+ let mut path_options = SPathOptions::new(PathBehavior::Delete);
+ path_options.sub.insert("path1".to_string());
+ let mut opt_role = Opt::new(Level::Role);
+ opt_role.path = Some(path_options);
+ as_borrow_mut!(role).options = Some(rc_refcell!(opt_role));
+ let mut path_options = SPathOptions::new(PathBehavior::Delete);
+ path_options.add.insert("path1".to_string());
+ let mut opt_global = Opt::new(Level::Global);
+ opt_global.path = Some(path_options);
+ let config = SConfigWrapper::default();
+ as_borrow_mut!(config).roles.push(role.clone());
+ as_borrow_mut!(config).options = Some(rc_refcell!(opt_global));
+ as_borrow_mut!(role)._config = Some(Rc::downgrade(&config));
+ let options = OptStack::from_role(role);
+ let res = options.get_description(Level::Global, OptType::Path);
+ assert_eq!(res, "");
+ }
+
+ #[test]
+ fn test_task_level() {
+ let mut env_options = SEnvOptions::new(EnvBehavior::Delete);
+ env_options.keep.insert("env1".into());
+ let mut opt = Opt::new(Level::Task);
+ opt.env = Some(env_options);
+ let task = STaskWrapper::default();
+ as_borrow_mut!(task).name = IdTask::Number(1);
+ as_borrow_mut!(task).options = Some(rc_refcell!(opt));
+ let role = SRoleWrapper::default();
+ as_borrow_mut!(role).name = "test".to_string();
+ let mut env_options = SEnvOptions::new(EnvBehavior::Delete);
+ env_options.keep.insert("env2".into());
+ let mut opt = Opt::new(Level::Role);
+ opt.env = Some(env_options);
+ as_borrow_mut!(role).options = Some(rc_refcell!(opt));
+ as_borrow_mut!(task)._role = Some(Rc::downgrade(&role));
+
+ let mut env_options = SEnvOptions::new(EnvBehavior::Delete);
+ env_options.keep.insert("env3".into());
+
+ let mut opt = Opt::new(Level::Global);
+ opt.env = Some(env_options);
+ let config = SConfigWrapper::default();
+ as_borrow_mut!(config).roles.push(role.clone());
+ as_borrow_mut!(config).options = Some(rc_refcell!(opt));
+ as_borrow_mut!(role)._config = Some(Rc::downgrade(&config));
+ let options = OptStack::from_task(task);
+ let res = options.get_description(Level::Task, OptType::Env);
+ assert_eq!(res, " (setted at this level)\nenv3,env2,env1");
+ }
+}
diff --git a/src/database/structs.rs b/src/database/structs.rs
new file mode 100644
index 00000000..9abb0e6e
--- /dev/null
+++ b/src/database/structs.rs
@@ -0,0 +1,874 @@
+use capctl::CapSet;
+use chrono::Duration;
+use derivative::Derivative;
+use nix::{errno::Errno, unistd::{Group, User}};
+use serde::{de::{self, Visitor}, Deserialize, Deserializer, Serialize};
+use serde_json::{Map, Value};
+use strum::{Display, EnumIs};
+
+use std::{cell::RefCell, cmp::Ordering, fmt, ops::{Index, Not}, rc::{Rc, Weak}};
+
+use crate::common::database::is_default;
+
+use super::{deserialize_duration, options::Opt, serialize_duration, wrapper::{OptWrapper, STaskWrapper}};
+
+
+
+#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)]
+pub struct SConfig
+{
+ #[serde(default, skip_serializing_if = "is_default")]
+ pub timeout: STimeout,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub options: OptWrapper,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub roles: Vec>>,
+ #[serde(default)]
+ #[serde(flatten, skip_serializing_if = "Map::is_empty")]
+ pub _extra_fields: Map,
+}
+
+impl SRole
+{
+ pub fn new(name : String, config: Weak>) -> Self {
+ let mut r = SRole::default();
+ r.name = name;
+ r._config = Some(config);
+ r
+ }
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy)]
+#[serde(rename_all = "lowercase")]
+pub enum TimestampType
+{
+ PPID,
+ TTY,
+ UID,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug,)]
+pub struct STimeout
+{
+ #[serde(default, rename = "type")]
+ pub type_field: TimestampType,
+ #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")]
+ pub duration: Duration,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub max_usage: Option,
+ #[serde(default)]
+ #[serde(flatten, skip_serializing_if = "Map::is_empty")]
+ pub _extra_fields: Map