From a1ddba05c186e397de1d186815ec585383c217d0 Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Sat, 30 Nov 2024 12:07:53 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Read=20`Vec`=20into=20c?= =?UTF-8?q?onsumer-ready=20`Config`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #78 Fixes #79 Fixes #148 --- src/config/mod.rs | 1198 +++++++++++++++++ tests/config.rs | 490 +++++++ tests/data/example-session-disable-stats.conf | 17 + tests/data/example-system-enable-stats.conf | 17 + tests/data/includedir/a.conf | 5 + tests/data/includedir/not_included.xml | 5 + tests/data/missing_include.conf | 5 + tests/data/session.conf | 83 ++ tests/data/system.conf | 145 ++ tests/data/transitive_missing_include.conf | 5 + tests/data/valid.conf | 13 + tests/data/valid_included.conf | 10 + 12 files changed, 1993 insertions(+) create mode 100644 tests/config.rs create mode 100644 tests/data/example-session-disable-stats.conf create mode 100644 tests/data/example-system-enable-stats.conf create mode 100644 tests/data/includedir/a.conf create mode 100644 tests/data/includedir/not_included.xml create mode 100644 tests/data/missing_include.conf create mode 100644 tests/data/session.conf create mode 100644 tests/data/system.conf create mode 100644 tests/data/transitive_missing_include.conf create mode 100644 tests/data/valid.conf create mode 100644 tests/data/valid_included.conf diff --git a/src/config/mod.rs b/src/config/mod.rs index d2a73a7..7c72c88 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,8 +1,191 @@ +use std::{ + env::var, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::{Error, Result}; use serde::Deserialize; +use zbus::{Address, AuthMechanism}; pub mod limits; mod xml; +use xml::{ + Document, Element, PolicyContext, PolicyElement, RuleAttributes, RuleElement, TypeElement, +}; + +/// The bus configuration. +/// +/// This is currently only loaded from the [XML configuration files] defined by the specification. +/// We plan to add support for other formats (e.g JSON) in the future. +/// +/// [XML configuration files]: https://dbus.freedesktop.org/doc/dbus-daemon.1.html#configuration_file +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct Config { + /// If `true`, connections that authenticated using the ANONYMOUS mechanism will be authorized + /// to connect. This option has no practical effect unless the ANONYMOUS mechanism has also + /// been enabled using the `auth` option. + pub allow_anonymous: bool, + + /// Lists permitted authorization mechanisms. + /// If this element doesn't exist, then all known mechanisms are allowed. + // TODO: warn when multiple `` elements are defined, as we only support one + // TODO: consider implementing `Deserialize` over in zbus crate, then removing this "skip..." + #[serde(default, skip_deserializing)] + pub auth: Option, + + /// If `true`, the bus daemon becomes a real daemon (forks into the background, etc.). + pub fork: bool, + + /// If `true`, the bus daemon keeps its original umask when forking. + /// This may be useful to avoid affecting the behavior of child processes. + pub keep_umask: bool, + + /// Address that the bus should listen on. + /// The address is in the standard D-Bus format that contains a transport name plus possible + /// parameters/options. + // TODO: warn when multiple `` elements are defined, as we only support one + // TODO: consider implementing `Deserialize` over in zbus crate, then removing this "skip..." + #[serde(default, skip_deserializing)] + pub listen: Option
, + + /// The bus daemon will write its pid to the specified file. + pub pidfile: Option, + + pub policies: Vec, + + /// Adds a directory to search for .service files, + /// which tell the dbus-daemon how to start a program to provide a particular well-known bus + /// name. + #[serde(default)] + pub servicedirs: Vec, + + /// Specifies the setuid helper that is used to launch system daemons with an alternate user. + pub servicehelper: Option, + + /// If `true`, the bus daemon will log to syslog. + pub syslog: bool, + + /// This element only controls which message bus specific environment variables are set in + /// activated clients. + pub r#type: Option, + + /// The user account the daemon should run as, as either a username or a UID. + /// If the daemon cannot change to this UID on startup, it will exit. + /// If this element is not present, the daemon will not change or care about its UID. + pub user: Option, +} + +impl TryFrom for Config { + type Error = Error; + + fn try_from(value: Document) -> std::result::Result { + let mut config = Config::default(); + + for element in value.busconfig { + match element { + Element::AllowAnonymous => config.allow_anonymous = true, + Element::Auth(auth) => { + config.auth = Some(AuthMechanism::from_str(&auth)?); + } + Element::Fork => config.fork = true, + Element::Include(_) => { + // NO-OP: removed during `Document::resolve_includes` + } + Element::Includedir(_) => { + // NO-OP: removed during `Document::resolve_includedirs` + } + Element::KeepUmask => config.keep_umask = true, + Element::Limit => { + // NO-OP: deprecated and ignored + } + Element::Listen(listen) => { + config.listen = Some(Address::from_str(&listen)?); + } + Element::Pidfile(p) => config.pidfile = Some(p), + Element::Policy(pe) => { + if let Some(p) = OptionalPolicy::try_from(pe)? { + config.policies.push(p); + } + } + Element::Servicedir(p) => { + config.servicedirs.push(p); + } + Element::Servicehelper(p) => { + // NOTE: we're assuming this has the same "last one wins" behaviour as `` + + // TODO: warn and then ignore if we aren't reading: + // /usr/share/dbus-1/system.conf + config.servicehelper = Some(p); + } + Element::StandardSessionServicedirs => { + // TODO: warn and then ignore if we aren't reading: /etc/dbus-1/session.conf + if let Ok(runtime_dir) = var("XDG_RUNTIME_DIR") { + config + .servicedirs + .push(PathBuf::from(runtime_dir).join("dbus-1/services")); + } + if let Ok(data_dir) = var("XDG_DATA_HOME") { + config + .servicedirs + .push(PathBuf::from(data_dir).join("dbus-1/services")); + } + let mut servicedirs_in_data_dirs = xdg_data_dirs() + .iter() + .map(|p| p.join("dbus-1/services")) + .map(PathBuf::from) + .collect(); + config.servicedirs.append(&mut servicedirs_in_data_dirs); + config + .servicedirs + .push(PathBuf::from("/usr/share/dbus-1/services")); + // TODO: add Windows-specific session directories + } + Element::StandardSystemServicedirs => { + // TODO: warn and then ignore if we aren't reading: + // /usr/share/dbus-1/system.conf + config + .servicedirs + .extend(STANDARD_SYSTEM_SERVICEDIRS.iter().map(PathBuf::from)); + } + Element::Syslog => config.syslog = true, + Element::Type(TypeElement { r#type: value }) => config.r#type = Some(value), + Element::User(s) => config.user = Some(s), + } + } + + Ok(config) + } +} + +impl Config { + pub fn parse(s: &str) -> Result { + // TODO: validate that our DOCTYPE and root element are correct + quick_xml::de::from_str::(s)?.try_into() + } + + pub fn read_file(file_path: impl AsRef) -> Result { + // TODO: error message should contain file path to missing `` + Document::read_file(&file_path)?.try_into() + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct ConnectOperation { + pub group: Option, + pub user: Option, +} + +impl From for ConnectOperation { + fn from(value: RuleAttributes) -> Self { + Self { + group: value.group, + user: value.user, + } + } +} + #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum BusType { @@ -21,3 +204,1018 @@ pub enum MessageType { Signal, Error, } + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Name { + #[serde(rename = "*")] + Any, + Exact(String), + Prefix(String), +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum Operation { + /// rules checked when a new connection to the message bus is established + Connect(ConnectOperation), + /// rules checked when a connection attempts to own a well-known bus names + Own(NameOwnership), + /// rules that are checked for each recipient of a message + Receive(ReceiveOperation), + /// rules that are checked when a connection attempts to send a message + Send(SendOperation), +} + +type OptionalOperation = Option; + +impl TryFrom for OptionalOperation { + type Error = Error; + + fn try_from(value: RuleAttributes) -> std::result::Result { + let has_connect = value.group.is_some() || value.user.is_some(); + let has_own = value.own.is_some() || value.own_prefix.is_some(); + let has_send = value.send_broadcast.is_some() + || value.send_destination.is_some() + || value.send_destination_prefix.is_some() + || value.send_error.is_some() + || value.send_interface.is_some() + || value.send_member.is_some() + || value.send_path.is_some() + || value.send_requested_reply.is_some() + || value.send_type.is_some(); + let has_receive = value.receive_error.is_some() + || value.receive_interface.is_some() + || value.receive_member.is_some() + || value.receive_path.is_some() + || value.receive_sender.is_some() + || value.receive_requested_reply.is_some() + || value.receive_type.is_some(); + + let operations_count: i8 = vec![has_connect, has_own, has_receive, has_send] + .into_iter() + .map(i8::from) + .sum(); + + if operations_count > 1 { + return Err(Error::msg(format!("do not mix rule attributes for connect, own, receive, and/or send attributes in the same rule: {value:?}"))); + } + + if has_connect { + Ok(Some(Operation::Connect(ConnectOperation::from(value)))) + } else if has_own { + Ok(Some(Operation::Own(NameOwnership::from(value)))) + } else if has_receive { + Ok(Some(Operation::Receive(ReceiveOperation::from(value)))) + } else if has_send { + Ok(Some(Operation::Send(SendOperation::from(value)))) + } else { + Err(Error::msg(format!("rule must specify supported attributes for connect, own, receive, or send operations: {value:?}"))) + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct NameOwnership { + pub own: Option, +} + +impl From for NameOwnership { + fn from(value: RuleAttributes) -> Self { + let own = match value { + RuleAttributes { + own: Some(some), + own_prefix: None, + .. + } if some == "*" => Some(Name::Any), + RuleAttributes { + own: Some(some), + own_prefix: None, + .. + } => Some(Name::Exact(some)), + RuleAttributes { + own: None, + own_prefix: Some(some), + .. + } => Some(Name::Prefix(some)), + _ => None, + }; + Self { own } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum Policy { + DefaultContext(Vec), + Group(Vec, String), + MandatoryContext(Vec), + User(Vec, String), +} +// TODO: implement Cmp/Ord to help stable-sort Policy values: +// DefaultContext < Group < User < MandatoryContext + +type OptionalPolicy = Option; + +impl TryFrom for OptionalPolicy { + type Error = Error; + + fn try_from(value: PolicyElement) -> std::result::Result { + match value { + PolicyElement { + at_console: Some(_), + context: None, + group: None, + user: None, + .. + } => Ok(None), + PolicyElement { + at_console: None, + context: Some(c), + group: None, + rules, + user: None, + } => Ok(Some(match c { + PolicyContext::Default => { + Policy::DefaultContext(rules_try_from_rule_elements(rules)?) + } + PolicyContext::Mandatory => { + Policy::MandatoryContext(rules_try_from_rule_elements(rules)?) + } + })), + PolicyElement { + at_console: None, + context: None, + group: Some(group), + rules, + user: None, + } => Ok(Some(Policy::Group( + rules_try_from_rule_elements(rules)?, + group, + ))), + PolicyElement { + at_console: None, + context: None, + group: None, + rules, + user: Some(user), + } => Ok(Some(Policy::User( + rules_try_from_rule_elements(rules)?, + user, + ))), + _ => Err(Error::msg(format!( + "policy contains conflicting attributes: {value:?}" + ))), + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct ReceiveOperation { + pub error: Option, + pub interface: Option, + pub max_fds: Option, + pub member: Option, + pub min_fds: Option, + pub path: Option, + pub sender: Option, + pub r#type: Option, +} + +impl From for ReceiveOperation { + fn from(value: RuleAttributes) -> Self { + Self { + error: value.receive_error, + interface: value.receive_interface, + max_fds: value.max_fds, + member: value.receive_member, + min_fds: value.min_fds, + path: value.receive_path, + sender: value.receive_sender, + r#type: value.receive_type, + } + } +} + +type OptionalRule = Option; + +impl TryFrom for OptionalRule { + type Error = Error; + + fn try_from(value: RuleElement) -> std::result::Result { + match value { + RuleElement::Allow(RuleAttributes { + group: Some(_), + user: Some(_), + .. + }) + | RuleElement::Deny(RuleAttributes { + group: Some(_), + user: Some(_), + .. + }) => Err(Error::msg(format!( + "`group` cannot be combined with `user` in the same rule: {value:?}" + ))), + RuleElement::Allow(RuleAttributes { + own: Some(_), + own_prefix: Some(_), + .. + }) + | RuleElement::Deny(RuleAttributes { + own: Some(_), + own_prefix: Some(_), + .. + }) => Err(Error::msg(format!( + "`own_prefix` cannot be combined with `own` in the same rule: {value:?}" + ))), + RuleElement::Allow(RuleAttributes { + send_destination: Some(_), + send_destination_prefix: Some(_), + .. + }) + | RuleElement::Deny(RuleAttributes { + send_destination: Some(_), + send_destination_prefix: Some(_), + .. + }) => Err(Error::msg(format!( + "`send_destination_prefix` cannot be combined with `send_destination` in the same rule: {value:?}" + ))), + RuleElement::Allow(RuleAttributes { + eavesdrop: Some(true), + group: None, + own: None, + receive_requested_reply: None, + receive_sender: None, + send_broadcast: None, + send_destination: None, + send_destination_prefix: None, + send_error: None, + send_interface: None, + send_member: None, + send_path: None, + send_requested_reply: None, + send_type: None, + user: None, + .. + }) => { + // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 + Ok(None) + } + RuleElement::Allow( + RuleAttributes { + receive_requested_reply: Some(false), + .. + } + | RuleAttributes { + send_requested_reply: Some(false), + .. + }, + ) => { + // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 + Ok(None) + } + RuleElement::Allow(attrs) => { + // if attrs.eavesdrop == Some(true) { + // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 + // } + match OptionalOperation::try_from(attrs)? { + Some(some) => Ok(Some((Access::Allow, some))), + None => Ok(None), + } + } + RuleElement::Deny(RuleAttributes { + eavesdrop: Some(true), + .. + }) => { + // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 + Ok(None) + } + RuleElement::Deny( + RuleAttributes { + receive_requested_reply: Some(true), + .. + } + | RuleAttributes { + send_requested_reply: Some(true), + .. + }, + ) => { + // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 + Ok(None) + } + RuleElement::Deny(attrs) => match OptionalOperation::try_from(attrs)? { + Some(some) => Ok(Some((Access::Deny, some))), + None => Ok(None), + }, + } + } +} + +pub type Rule = (Access, Operation); + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum Access { + Allow, + Deny, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct SendOperation { + pub broadcast: Option, + pub destination: Option, + pub error: Option, + pub interface: Option, + pub max_fds: Option, + pub member: Option, + pub min_fds: Option, + pub path: Option, + pub r#type: Option, +} + +impl From for SendOperation { + fn from(value: RuleAttributes) -> Self { + let destination = match value { + RuleAttributes { + send_destination: Some(some), + send_destination_prefix: None, + .. + } if some == "*" => Some(Name::Any), + RuleAttributes { + send_destination: Some(some), + send_destination_prefix: None, + .. + } => Some(Name::Exact(some)), + RuleAttributes { + send_destination: None, + send_destination_prefix: Some(some), + .. + } => Some(Name::Prefix(some)), + _ => None, + }; + Self { + broadcast: value.send_broadcast, + destination, + error: value.send_error, + interface: value.send_interface, + max_fds: value.max_fds, + member: value.send_member, + min_fds: value.min_fds, + path: value.send_path, + r#type: value.send_type, + } + } +} + +const DEFAULT_DATA_DIRS: &[&str] = &["/usr/local/share", "/usr/share"]; + +const STANDARD_SYSTEM_SERVICEDIRS: &[&str] = &[ + "/usr/local/share/dbus-1/system-services", + "/usr/share/dbus-1/system-services", + "/lib/dbus-1/system-services", +]; + +fn rules_try_from_rule_elements(value: Vec) -> Result> { + let mut rules = vec![]; + for rule in value { + let rule = OptionalRule::try_from(rule)?; + if let Some(some) = rule { + rules.push(some); + } + } + Ok(rules) +} + +fn xdg_data_dirs() -> Vec { + if let Ok(ok) = var("XDG_DATA_DIRS") { + return ok.split(":").map(PathBuf::from).collect(); + } + DEFAULT_DATA_DIRS.iter().map(PathBuf::from).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn config_parse_with_dtd_and_root_element_ok() { + let input = r#" + + "#; + Config::parse(input).expect("should parse XML input"); + } + + #[test] + #[should_panic] + fn config_parse_with_type_error() { + let input = r#" + + not-a-valid-message-bus-type + + "#; + Config::parse(input).expect("should parse XML input"); + } + + #[test] + fn config_parse_with_allow_anonymous_and_fork_and_keep_umask_and_syslog_ok() { + let input = r#" + + + + + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + allow_anonymous: true, + fork: true, + keep_umask: true, + syslog: true, + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_auth_ok() { + let input = r#" + + ANONYMOUS + EXTERNAL + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + auth: Some(AuthMechanism::External), + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_limit_ok() { + let input = r#" + + 1000000000 + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[test] + fn config_parse_with_listen_ok() { + let input = r#" + + unix:path=/tmp/foo + tcp:host=localhost,port=1234 + tcp:host=localhost,port=0,family=ipv4 + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + listen: Some( + Address::from_str("tcp:host=localhost,port=0,family=ipv4") + .expect("should parse address") + ), + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_overlapped_lists_ok() { + // confirm this works with/without quick-xml's [`overlapped-lists`] feature + // [`overlapped-lists`]: https://docs.rs/quick-xml/latest/quick_xml/#overlapped-lists + let input = r#" + + ANONYMOUS + unix:path=/tmp/foo + + + + + + EXTERNAL + tcp:host=localhost,port=1234 + + + + + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + auth: Some(AuthMechanism::External), + listen: Some( + Address::from_str("tcp:host=localhost,port=1234") + .expect("should parse address") + ), + policies: vec![ + Policy::DefaultContext(vec![ + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ]), + Policy::DefaultContext(vec![ + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ]), + ], + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_pidfile_ok() { + let input = r#" + + /var/run/busd.pid + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + pidfile: Some(PathBuf::from("/var/run/busd.pid")), + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_policies_ok() { + let input = r#" + + + + + + + + + + + + + + + + + + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + policies: vec![ + Policy::DefaultContext(vec![ + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Exact(String::from("org.freedesktop.DBus"))) + }) + ), + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Prefix(String::from("org.freedesktop"))) + }) + ), + ( + Access::Allow, + Operation::Connect(ConnectOperation { + group: Some(String::from("wheel")), + user: None, + }) + ), + ( + Access::Allow, + Operation::Connect(ConnectOperation { + group: None, + user: Some(String::from("root")), + }) + ), + ]), + Policy::User( + vec![ + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: Some(true), + destination: Some(Name::Exact(String::from( + "org.freedesktop.DBus" + ))), + error: Some(String::from("something bad")), + interface: Some(String::from( + "org.freedesktop.systemd1.Activator" + )), + max_fds: Some(128), + member: Some(String::from("DoSomething")), + min_fds: Some(12), + path: Some(String::from("/org/freedesktop")), + r#type: Some(MessageType::Signal), + }) + ), + ( + Access::Allow, + Operation::Receive(ReceiveOperation { + error: Some(String::from("something bad")), + interface: Some(String::from( + "org.freedesktop.systemd1.Activator" + )), + max_fds: Some(128), + member: Some(String::from("DoSomething")), + min_fds: Some(12), + path: Some(String::from("/org/freedesktop")), + sender: Some(String::from("org.freedesktop.DBus")), + r#type: Some(MessageType::Signal), + }) + ) + ], + String::from("root") + ), + Policy::Group( + vec![ + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Prefix(String::from( + "org.freedesktop" + ))), + error: None, + interface: None, + max_fds: None, + member: Some(String::from("DoSomething")), + min_fds: None, + path: None, + r#type: None + }) + ), + // ` + + + + + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[test] + fn config_parse_with_policies_with_ignored_rules_and_rule_attributes_ok() { + let input = r#" + + + + + + + + + + + + + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + policies: vec![ + Policy::DefaultContext(vec![ + ( + Access::Allow, + // `eavesdrop="true"` is dropped, keep other attributes + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Any), + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None + }) + ), + // `` has nothing left after dropping eavesdrop + // `` is completely ignored + ], + ..Default::default() + } + ); + } + + #[should_panic] + #[test] + fn config_parse_with_policies_with_own_and_own_prefix_error() { + let input = r#" + + + + + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[should_panic] + #[test] + fn config_parse_with_policies_with_send_destination_and_send_destination_prefix_error() { + let input = r#" + + + + + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[should_panic] + #[test] + fn config_parse_with_policies_with_send_and_receive_attributes_error() { + let input = r#" + + + + + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[should_panic] + #[test] + fn config_parse_with_policies_without_attributes_error() { + let input = r#" + + + + + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[test] + fn config_parse_with_servicedir_and_standard_session_servicedirs_ok() { + let input = r#" + + /example + + /anotherexample + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + // TODO: improve test: contents are dynamic depending upon environment variables + assert_eq!(config.servicedirs.first(), Some(&PathBuf::from("/example"))); + assert_eq!( + config.servicedirs.last(), + Some(&PathBuf::from("/usr/share/dbus-1/services")) + ); + } + + #[test] + fn config_parse_with_servicedir_and_standard_system_servicedirs_ok() { + let input = r#" + + /example + + /anotherexample + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + servicedirs: vec![ + PathBuf::from("/example"), + PathBuf::from("/usr/local/share/dbus-1/system-services"), + PathBuf::from("/usr/share/dbus-1/system-services"), + PathBuf::from("/lib/dbus-1/system-services"), + PathBuf::from("/anotherexample"), + PathBuf::from("/usr/local/share/dbus-1/system-services"), + PathBuf::from("/usr/share/dbus-1/system-services"), + PathBuf::from("/lib/dbus-1/system-services"), + ], + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_servicehelper_ok() { + let input = r#" + + /example + /anotherexample + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + servicehelper: Some(PathBuf::from("/anotherexample")), + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_type_ok() { + let input = r#" + + session + system + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + r#type: Some(BusType::System), + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_user_ok() { + let input = r#" + + 1000 + alice + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + user: Some(String::from("alice")), + ..Default::default() + } + ); + } +} diff --git a/tests/config.rs b/tests/config.rs new file mode 100644 index 0000000..eb336c2 --- /dev/null +++ b/tests/config.rs @@ -0,0 +1,490 @@ +use std::{path::PathBuf, str::FromStr}; + +use busd::config::{ + Access, BusType, Config, ConnectOperation, MessageType, Name, NameOwnership, Operation, Policy, + ReceiveOperation, SendOperation, +}; +use zbus::{Address, AuthMechanism}; + +#[test] +fn config_read_file_with_includes_ok() { + let got = + Config::read_file("./tests/data/valid.conf").expect("should read and parse XML input"); + + assert_eq!( + got, + Config { + auth: Some(AuthMechanism::External), + listen: Some(Address::from_str("unix:path=/tmp/a").expect("should parse address")), + policies: vec![ + Policy::DefaultContext(vec![ + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ]), + Policy::MandatoryContext(vec![ + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ],), + ], + ..Default::default() + } + ); +} + +#[test] +fn config_read_file_example_session_disable_stats_conf_ok() { + let got = Config::read_file("./tests/data/example-session-disable-stats.conf") + .expect("should read and parse XML input"); + + assert_eq!( + got, + Config { + policies: vec![Policy::DefaultContext(vec![( + Access::Deny, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None + }), + ),]),], + ..Default::default() + } + ); +} + +#[test] +fn config_read_file_example_system_enable_stats_conf_ok() { + let got = Config::read_file("./tests/data/example-system-enable-stats.conf") + .expect("should read and parse XML input"); + + assert_eq!( + got, + Config { + policies: vec![Policy::User( + vec![( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None + }), + )], + String::from("USERNAME"), + ),], + ..Default::default() + } + ); +} + +#[test] +fn config_read_file_session_conf_ok() { + let mut got = + Config::read_file("./tests/data/session.conf").expect("should read and parse XML input"); + + assert!(!got.servicedirs.is_empty()); + + // nuking this to make it easier to `assert_eq!()` + got.servicedirs = vec![]; + + assert_eq!( + got, + Config { + listen: Some( + Address::from_str("unix:path=/run/user/1000/bus").expect("should parse address") + ), + keep_umask: true, + policies: vec![Policy::DefaultContext(vec![ + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Any), + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any), + }), + ), + ]),], + r#type: Some(BusType::Session), + ..Default::default() + } + ); +} + +#[test] +fn config_read_file_system_conf_ok() { + let want = Config { + auth: Some(AuthMechanism::External), + fork: true, + listen: Some( + Address::from_str("unix:path=/var/run/dbus/system_bus_socket") + .expect("should parse address"), + ), + pidfile: Some(PathBuf::from("@DBUS_SYSTEM_PID_FILE@")), + policies: vec![ + Policy::DefaultContext(vec![ + ( + Access::Allow, + Operation::Connect(ConnectOperation { + group: None, + user: Some(String::from("*")), + }), + ), + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any), + }), + ), + ( + Access::Deny, + Operation::Send(SendOperation { + broadcast: None, + destination: None, + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: Some(MessageType::MethodCall), + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: None, + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: Some(MessageType::Signal), + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: None, + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: Some(MessageType::MethodReturn), + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: None, + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: Some(MessageType::Error), + }), + ), + ( + Access::Allow, + Operation::Receive(ReceiveOperation { + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + sender: None, + r#type: Some(MessageType::MethodCall), + }), + ), + ( + Access::Allow, + Operation::Receive(ReceiveOperation { + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + sender: None, + r#type: Some(MessageType::MethodReturn), + }), + ), + ( + Access::Allow, + Operation::Receive(ReceiveOperation { + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + sender: None, + r#type: Some(MessageType::Error), + }), + ), + ( + Access::Allow, + Operation::Receive(ReceiveOperation { + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + sender: None, + r#type: Some(MessageType::Signal), + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Introspectable")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Properties")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Containers1")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Deny, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus")), + max_fds: None, + member: Some(String::from("UpdateActivationEnvironment")), + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Deny, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Deny, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.systemd1.Activator")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ]), + Policy::User( + vec![( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.systemd1.Activator")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + )], + String::from("root"), + ), + Policy::User( + vec![( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Monitoring")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + )], + String::from("root"), + ), + Policy::User( + vec![( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + )], + String::from("root"), + ), + ], + servicehelper: Some(PathBuf::from("@DBUS_LIBEXECDIR@/dbus-daemon-launch-helper")), + syslog: true, + r#type: Some(BusType::System), + user: Some(String::from("@DBUS_USER@")), + ..Default::default() + }; + + let mut got = + Config::read_file("./tests/data/system.conf").expect("should read and parse XML input"); + + assert!(!got.servicedirs.is_empty()); + + // nuking this to make it easier to `assert_eq!()` + got.servicedirs = vec![]; + + assert_eq!(got, want,); +} + +#[cfg(unix)] +#[test] +fn config_read_file_real_usr_share_dbus1_session_conf_ok() { + let config_path = PathBuf::from("/usr/share/dbus-1/session.conf"); + if !config_path.exists() { + return; + } + Config::read_file(config_path).expect("should read and parse XML input"); +} + +#[cfg(unix)] +#[test] +fn config_read_file_real_usr_share_dbus1_system_conf_ok() { + let config_path = PathBuf::from("/usr/share/dbus-1/system.conf"); + if !config_path.exists() { + return; + } + Config::read_file(config_path).expect("should read and parse XML input"); +} + +#[should_panic] +#[test] +fn config_read_file_with_missing_include_err() { + Config::read_file("./tests/data/missing_include.conf") + .expect("should read and parse XML input"); +} + +#[should_panic] +#[test] +fn config_read_file_with_transitive_missing_include_err() { + Config::read_file("./tests/data/transitive_missing_include.conf") + .expect("should read and parse XML input"); +} diff --git a/tests/data/example-session-disable-stats.conf b/tests/data/example-session-disable-stats.conf new file mode 100644 index 0000000..baafb2d --- /dev/null +++ b/tests/data/example-session-disable-stats.conf @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/tests/data/example-system-enable-stats.conf b/tests/data/example-system-enable-stats.conf new file mode 100644 index 0000000..677f923 --- /dev/null +++ b/tests/data/example-system-enable-stats.conf @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/tests/data/includedir/a.conf b/tests/data/includedir/a.conf new file mode 100644 index 0000000..d602268 --- /dev/null +++ b/tests/data/includedir/a.conf @@ -0,0 +1,5 @@ + + + unix:path=/tmp/a + diff --git a/tests/data/includedir/not_included.xml b/tests/data/includedir/not_included.xml new file mode 100644 index 0000000..5af9363 --- /dev/null +++ b/tests/data/includedir/not_included.xml @@ -0,0 +1,5 @@ + + + unix:path=/tmp/not_included + diff --git a/tests/data/missing_include.conf b/tests/data/missing_include.conf new file mode 100644 index 0000000..67632af --- /dev/null +++ b/tests/data/missing_include.conf @@ -0,0 +1,5 @@ + + + ./missing.conf + diff --git a/tests/data/session.conf b/tests/data/session.conf new file mode 100644 index 0000000..aa4ddf9 --- /dev/null +++ b/tests/data/session.conf @@ -0,0 +1,83 @@ + + + + + + + + + session + + + + + unix:path=/run/user/1000/bus + + + + + + + + + + + + + + + + + @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/session.conf + + + + + + + + @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/session-local.conf + + + + + + + 1000000000 + 250000000 + 1000000000 + 250000000 + 1000000000 + + 120000 + 240000 + 150000 + 100000 + 10000 + 100000 + 10000 + 50000 + 50000 + 50000 + + diff --git a/tests/data/system.conf b/tests/data/system.conf new file mode 100644 index 0000000..a68b8fb --- /dev/null +++ b/tests/data/system.conf @@ -0,0 +1,145 @@ + + + + + + + + + + + + system + + + @DBUS_USER@ + + + + + + + + + @DBUS_LIBEXECDIR@/dbus-daemon-launch-helper + + + @DBUS_SYSTEM_PID_FILE@ + + + + + + EXTERNAL + + + unix:path=/var/run/dbus/system_bus_socket + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/system.conf + + + + + + + + + + + + + + + + + + + + + + + + + + @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/system-local.conf + + + + diff --git a/tests/data/transitive_missing_include.conf b/tests/data/transitive_missing_include.conf new file mode 100644 index 0000000..a317999 --- /dev/null +++ b/tests/data/transitive_missing_include.conf @@ -0,0 +1,5 @@ + + + ./missing_include.conf + diff --git a/tests/data/valid.conf b/tests/data/valid.conf new file mode 100644 index 0000000..f8e2a00 --- /dev/null +++ b/tests/data/valid.conf @@ -0,0 +1,13 @@ + + + ANONYMOUS + unix:path=/tmp/foo + + + + + ./valid_included.conf + ./valid_missing.conf + ./includedir + diff --git a/tests/data/valid_included.conf b/tests/data/valid_included.conf new file mode 100644 index 0000000..89ed48b --- /dev/null +++ b/tests/data/valid_included.conf @@ -0,0 +1,10 @@ + + + EXTERNAL + tcp:host=localhost,port=1234 + + + + +