From 9d9da8307dcef524bb5ee2bd56531f746d677eb8 Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Sat, 30 Nov 2024 11:49:31 +1100 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9E=95=20Add=20the=20"quick-xml"=20crate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This crate offers a serde-compatible Deserializer for XML data. --- Cargo.lock | 15 +++++++++++++-- Cargo.toml | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 28a0769..c1fac8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,7 @@ dependencies = [ "futures-util", "nix", "ntest", + "quick-xml", "rand", "serde", "tokio", @@ -991,6 +992,16 @@ dependencies = [ "prost", ] +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.36" @@ -1419,9 +1430,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", diff --git a/Cargo.toml b/Cargo.toml index ca5bf6f..82f214d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ console-subscriber = { version = "0.4.0", optional = true } xdg-home = "1.1.0" event-listener = "5.3.0" fastrand = "2.2.0" +quick-xml = { version = "0.36.2", features = ["serialize"] } [target.'cfg(unix)'.dependencies] nix = { version = "0.29.0", features = ["user"] } From 8c78440f6e9d31e89bf604dfedd950c8f738925e Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Sat, 30 Nov 2024 11:58:28 +1100 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=94=A7=20Read=20XML=20file(s)=20into?= =?UTF-8?q?=20consumer-ready=20`Config`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can deserialize an XML file using the "serde" and "quick-xml" crates. Creating a struct type with nested structs within is a direct approach that works well with `#[derive(Deserialize)]`. However, the result isn't optimal for consumption, and also makes it very difficult to implement aspects of the business logic that are sensitive to the order of certain XML elements, e.g. ``, ``, etc. So our approach is to first deserialize into a `Vec` (inspired by @elmarco 's work over in #23 ). Importantly, by starting with this intermediate representation, we can preserve the XML author's intention regarding the order of elements. Then we replace any `` and `` elements with the parsed contents the XML files to which they refer, further replacing any `` and `` elements within those. Finally, we can make one final iteration over the `Vec` to produce the final optimally-structured `Config`. Fixes #78 --- src/config/mod.rs | 1220 +++++++++++++++++ src/config/xml.rs | 343 +++++ src/lib.rs | 1 + 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 + 14 files changed, 2359 insertions(+) create mode 100644 src/config/mod.rs create mode 100644 src/config/xml.rs 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 new file mode 100644 index 0000000..0859c31 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,1220 @@ +use std::{ + env::var, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::{Error, Result}; +use serde::Deserialize; +use zbus::{Address, AuthMechanism}; + +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 { + Session, + System, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum MessageType { + #[default] + #[serde(rename = "*")] + Any, + MethodCall, + MethodReturn, + 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/src/config/xml.rs b/src/config/xml.rs new file mode 100644 index 0000000..89e7fd6 --- /dev/null +++ b/src/config/xml.rs @@ -0,0 +1,343 @@ +use std::{ + env::current_dir, + ffi::OsString, + fs::{read_dir, read_to_string}, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::{Error, Result}; +use serde::Deserialize; +use tracing::{error, warn}; + +use super::{BusType, MessageType}; + +/// 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 Document { + #[serde(rename = "$value", default)] + pub busconfig: Vec, + file_path: Option, +} + +impl FromStr for Document { + type Err = Error; + + fn from_str(s: &str) -> Result { + quick_xml::de::from_str(s).map_err(Error::msg) + } +} + +impl Document { + pub fn read_file(file_path: impl AsRef) -> Result { + let text = read_to_string(file_path.as_ref())?; + + let mut doc = Document::from_str(&text)?; + doc.file_path = Some(file_path.as_ref().to_path_buf()); + doc.resolve_includedirs()?.resolve_includes() + } + + fn resolve_includedirs(self) -> Result { + let base_path = self.base_path()?; + let Document { + busconfig, + file_path, + } = self; + + let mut doc = Document { + busconfig: vec![], + file_path: None, + }; + + for el in busconfig { + match el { + Element::Includedir(dir_path) => { + let dir_path = resolve_include_path(&base_path, &dir_path); + let dir_path = match dir_path.canonicalize() { + Ok(ok) => ok, + // we treat `` as though it has `ignore_missing="yes"` + Err(err) => { + warn!( + "should canonicalize directory path '{}': {:?}", + &dir_path.display(), + err + ); + continue; + } + }; + match read_dir(&dir_path) { + Ok(ok) => { + for entry in ok { + let path = entry?.path(); + if path.extension() == Some(&OsString::from("conf")) + && path.is_file() + { + doc.busconfig.push(Element::Include(IncludeElement { + file_path: path, + ..Default::default() + })); + } + } + } + // we treat `` as though it has `ignore_missing="yes"` + Err(err) => { + warn!("should read directory '{}': {:?}", &dir_path.display(), err); + continue; + } + } + } + _ => doc.busconfig.push(el), + } + } + + doc.file_path = file_path; + Ok(doc) + } + + fn resolve_includes(self) -> Result { + // TODO: implement protection against circular `` references + let base_path = self.base_path()?; + let Document { + busconfig, + file_path, + } = self; + + let mut doc = Document { + busconfig: vec![], + file_path: None, + }; + + for el in busconfig { + match el { + Element::Include(include) => { + if include.if_selinux_enable == IncludeOption::Yes + || include.selinux_root_relative == IncludeOption::Yes + { + // TODO: implement SELinux support + continue; + } + + let ignore_missing = include.ignore_missing == IncludeOption::Yes; + let file_path = resolve_include_path(&base_path, &include.file_path); + let file_path = match file_path.canonicalize().map_err(Error::msg) { + Ok(ok) => ok, + Err(err) => { + let msg = format!( + "should canonicalize file path '{}': {:?}", + &file_path.display(), + err + ); + if ignore_missing { + warn!(msg); + continue; + } + error!(msg); + return Err(err); + } + }; + let mut included = match Document::read_file(&file_path) { + Ok(ok) => ok, + Err(err) => { + let msg = format!( + "'{}' should contain valid XML", + include.file_path.display() + ); + if ignore_missing { + warn!(msg); + continue; + } + error!(msg); + return Err(err); + } + }; + doc.busconfig.append(&mut included.busconfig); + } + _ => doc.busconfig.push(el), + } + } + + doc.file_path = file_path; + Ok(doc) + } + + fn base_path(&self) -> Result { + match &self.file_path { + Some(some) => Ok(some + .parent() + .ok_or_else(|| Error::msg("`` path should contain a file name"))? + .to_path_buf()), + None => { + warn!("ad-hoc document with unknown file path, using current working directory"); + current_dir().map_err(Error::msg) + } + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum Element { + AllowAnonymous, + Auth(String), + Fork, + /// Include a file at this point. If the filename is relative, it is located relative to the + /// configuration file doing the including. + Include(IncludeElement), + /// Files in the directory are included in undefined order. + /// Only files ending in ".conf" are included. + Includedir(PathBuf), + KeepUmask, + Listen(String), + Limit, + Pidfile(PathBuf), + Policy(PolicyElement), + Servicedir(PathBuf), + Servicehelper(PathBuf), + /// Requests a standard set of session service directories. + /// Its effect is similar to specifying a series of elements for each of the data + /// directories, in the order given here. + StandardSessionServicedirs, + /// Specifies the standard system-wide activation directories that should be searched for + /// service files. + StandardSystemServicedirs, + Syslog, + Type(TypeElement), + User(String), +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct IncludeElement { + #[serde(default, rename = "@ignore_missing")] + ignore_missing: IncludeOption, + + // TODO: implement SELinux + #[serde(default, rename = "@if_selinux_enabled")] + if_selinux_enable: IncludeOption, + #[serde(default, rename = "@selinux_root_relative")] + selinux_root_relative: IncludeOption, + + #[serde(rename = "$value")] + file_path: PathBuf, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum IncludeOption { + #[default] + No, + Yes, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum PolicyContext { + Default, + Mandatory, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct PolicyElement { + #[serde(rename = "@at_console")] + pub at_console: Option, + #[serde(rename = "@context")] + pub context: Option, + #[serde(rename = "@group")] + pub group: Option, + #[serde(rename = "$value", default)] + pub rules: Vec, + #[serde(rename = "@user")] + pub user: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct RuleAttributes { + #[serde(rename = "@max_fds")] + pub max_fds: Option, + #[serde(rename = "@min_fds")] + pub min_fds: Option, + + #[serde(rename = "@receive_error")] + pub receive_error: Option, + #[serde(rename = "@receive_interface")] + pub receive_interface: Option, + /// deprecated and ignored + #[serde(rename = "@receive_member")] + pub receive_member: Option, + #[serde(rename = "@receive_path")] + pub receive_path: Option, + #[serde(rename = "@receive_sender")] + pub receive_sender: Option, + #[serde(rename = "@receive_type")] + pub receive_type: Option, + + #[serde(rename = "@send_broadcast")] + pub send_broadcast: Option, + #[serde(rename = "@send_destination")] + pub send_destination: Option, + #[serde(rename = "@send_destination_prefix")] + pub send_destination_prefix: Option, + #[serde(rename = "@send_error")] + pub send_error: Option, + #[serde(rename = "@send_interface")] + pub send_interface: Option, + #[serde(rename = "@send_member")] + pub send_member: Option, + #[serde(rename = "@send_path")] + pub send_path: Option, + #[serde(rename = "@send_type")] + pub send_type: Option, + + /// deprecated and ignored + #[serde(rename = "@receive_requested_reply")] + pub receive_requested_reply: Option, + /// deprecated and ignored + #[serde(rename = "@send_requested_reply")] + pub send_requested_reply: Option, + + /// deprecated and ignored + #[serde(rename = "@eavesdrop")] + pub eavesdrop: Option, + + #[serde(rename = "@own")] + pub own: Option, + #[serde(rename = "@own_prefix")] + pub own_prefix: Option, + + #[serde(rename = "@group")] + pub group: Option, + #[serde(rename = "@user")] + pub user: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum RuleElement { + Allow(RuleAttributes), + Deny(RuleAttributes), +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct TypeElement { + #[serde(rename = "$text")] + pub r#type: BusType, +} + +fn resolve_include_path(base_path: impl AsRef, include_path: impl AsRef) -> PathBuf { + let p = include_path.as_ref(); + if p.is_absolute() { + return p.to_path_buf(); + } + + error!( + "resolve_include_path: {} {}", + &base_path.as_ref().display(), + &include_path.as_ref().display() + ); + + base_path.as_ref().join(p) +} diff --git a/src/lib.rs b/src/lib.rs index 07a5f33..be211b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod bus; +pub mod config; pub mod fdo; pub mod match_rules; pub mod name_registry; 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 + + + + + From 90046a85865387c795333653d31827ba43a19014 Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Sat, 30 Nov 2024 12:09:35 +1100 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=94=A7=20Support=20`--config`,=20`--s?= =?UTF-8?q?ession`=20(default),=20and=20`--system`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bin/busd.rs | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/bin/busd.rs b/src/bin/busd.rs index fdd1424..bcc593f 100644 --- a/src/bin/busd.rs +++ b/src/bin/busd.rs @@ -1,9 +1,10 @@ extern crate busd; +use std::path::PathBuf; #[cfg(unix)] use std::{fs::File, io::Write, os::fd::FromRawFd}; -use busd::bus; +use busd::{bus, config::Config}; use anyhow::Result; use clap::Parser; @@ -18,9 +19,14 @@ use tracing::{info, warn}; #[clap(author, version, about, long_about = None)] struct Args { /// The address to listen on. + /// Takes precedence over any `` element in the configuration file. #[clap(short = 'a', long, value_parser)] address: Option, + /// Use the given configuration file. + #[clap(long)] + config: Option, + /// Print the address of the message bus to standard output. #[clap(long)] print_address: bool, @@ -36,6 +42,15 @@ struct Args { #[cfg(unix)] #[clap(long)] ready_fd: Option, + + /// Equivalent to `--config /usr/share/dbus-1/session.conf`. + /// This is the default if `--config` and `--system` are unspecified. + #[clap(long)] + session: bool, + + /// Equivalent to `--config /usr/share/dbus-1/system.conf`. + #[clap(long)] + system: bool, } #[tokio::main] @@ -44,7 +59,23 @@ async fn main() -> Result<()> { let args = Args::parse(); - let mut bus = bus::Bus::for_address(args.address.as_deref()).await?; + let config_path = if args.system { + PathBuf::from("/usr/share/dbus-1/system.conf") + } else if let Some(config_path) = args.config { + config_path + } else { + PathBuf::from("/usr/share/dbus-1/session.conf") + }; + eprintln!("reading configuration file {} ...", config_path.display()); + let config = Config::read_file(&config_path)?; + + let address = if let Some(address) = args.address { + Some(address) + } else { + config.listen.map(|address| format!("{address}")) + }; + + let mut bus = bus::Bus::for_address(address.as_deref()).await?; #[cfg(unix)] if let Some(fd) = args.ready_fd { From 502df1cbaf9487479cd874216e0fc98702fa60ee Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Sat, 30 Nov 2024 12:20:05 +1100 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=94=8A=20Make=20errors=20and=20warnin?= =?UTF-8?q?gs=20a=20little=20bit=20more=20descriptive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See: https://github.com/dbus2/busd/pull/159#discussion_r1863490751 --- src/config/xml.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/config/xml.rs b/src/config/xml.rs index 89e7fd6..2cda981 100644 --- a/src/config/xml.rs +++ b/src/config/xml.rs @@ -63,7 +63,7 @@ impl Document { // we treat `` as though it has `ignore_missing="yes"` Err(err) => { warn!( - "should canonicalize directory path '{}': {:?}", + "cannot resolve '{}' to an absolute path: {}", &dir_path.display(), err ); @@ -86,7 +86,11 @@ impl Document { } // we treat `` as though it has `ignore_missing="yes"` Err(err) => { - warn!("should read directory '{}': {:?}", &dir_path.display(), err); + warn!( + "cannot read '{}': {}", + &dir_path.display(), + err + ); continue; } } @@ -128,7 +132,7 @@ impl Document { Ok(ok) => ok, Err(err) => { let msg = format!( - "should canonicalize file path '{}': {:?}", + "cannot resolve '{}' to an absolute path: {}", &file_path.display(), err ); @@ -172,7 +176,7 @@ impl Document { .ok_or_else(|| Error::msg("`` path should contain a file name"))? .to_path_buf()), None => { - warn!("ad-hoc document with unknown file path, using current working directory"); + warn!("cannot determine file path for this XML document, using current working directory"); current_dir().map_err(Error::msg) } } @@ -333,11 +337,5 @@ fn resolve_include_path(base_path: impl AsRef, include_path: impl AsRef Date: Mon, 9 Dec 2024 21:23:48 +1100 Subject: [PATCH 5/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20`Rule`=20and?= =?UTF-8?q?=20`Policy`=20into=20their=20own=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/mod.rs | 389 ++----------------------------------------- src/config/policy.rs | 72 ++++++++ src/config/rule.rs | 318 +++++++++++++++++++++++++++++++++++ 3 files changed, 401 insertions(+), 378 deletions(-) create mode 100644 src/config/policy.rs create mode 100644 src/config/rule.rs diff --git a/src/config/mod.rs b/src/config/mod.rs index 0859c31..60f713e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -5,14 +5,19 @@ use std::{ }; use anyhow::{Error, Result}; +use policy::OptionalPolicy; use serde::Deserialize; use zbus::{Address, AuthMechanism}; +pub mod policy; +pub mod rule; mod xml; -use xml::{ - Document, Element, PolicyContext, PolicyElement, RuleAttributes, RuleElement, TypeElement, +pub use policy::Policy; +pub use rule::{ + Access, ConnectOperation, NameOwnership, Operation, ReceiveOperation, SendOperation, }; +use xml::{Document, Element, TypeElement}; /// The bus configuration. /// @@ -170,21 +175,6 @@ impl Config { } } -#[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 { @@ -213,356 +203,6 @@ pub enum Name { 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] = &[ @@ -571,17 +211,6 @@ const STANDARD_SYSTEM_SERVICEDIRS: &[&str] = &[ "/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(); @@ -591,6 +220,10 @@ fn xdg_data_dirs() -> Vec { #[cfg(test)] mod tests { + use rule::{ + Access, ConnectOperation, NameOwnership, Operation, ReceiveOperation, SendOperation, + }; + use super::*; #[test] diff --git a/src/config/policy.rs b/src/config/policy.rs new file mode 100644 index 0000000..c8cbd27 --- /dev/null +++ b/src/config/policy.rs @@ -0,0 +1,72 @@ +use anyhow::Error; +use serde::Deserialize; + +use super::{ + rule::{rules_try_from_rule_elements, Rule}, + xml::{PolicyContext, PolicyElement}, +}; + +#[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 + +pub 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:?}" + ))), + } + } +} diff --git a/src/config/rule.rs b/src/config/rule.rs new file mode 100644 index 0000000..d2d3c8e --- /dev/null +++ b/src/config/rule.rs @@ -0,0 +1,318 @@ +use anyhow::{Error, Result}; +use serde::Deserialize; + +use super::{ + xml::{RuleAttributes, RuleElement}, + MessageType, Name, +}; + +#[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)] +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 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, + } + } +} + +pub 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) +} From 2c8b75cdd086c5b5c96ee10fc1660df59924d706 Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Mon, 9 Dec 2024 21:29:18 +1100 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=94=8A=20=20Use=20the=20"tracing"=20c?= =?UTF-8?q?rate=20for=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bin/busd.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bin/busd.rs b/src/bin/busd.rs index bcc593f..b7fb942 100644 --- a/src/bin/busd.rs +++ b/src/bin/busd.rs @@ -10,9 +10,9 @@ use anyhow::Result; use clap::Parser; #[cfg(unix)] use tokio::{select, signal::unix::SignalKind}; -use tracing::error; #[cfg(unix)] -use tracing::{info, warn}; +use tracing::warn; +use tracing::{error, info}; /// A simple D-Bus broker. #[derive(Parser, Debug)] @@ -66,7 +66,7 @@ async fn main() -> Result<()> { } else { PathBuf::from("/usr/share/dbus-1/session.conf") }; - eprintln!("reading configuration file {} ...", config_path.display()); + info!("reading configuration file {} ...", config_path.display()); let config = Config::read_file(&config_path)?; let address = if let Some(address) = args.address {