From d6de3e4d0b38371eb8193c5afec0ced5b87c7e61 Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Sun, 6 Oct 2024 22:18:09 +1100 Subject: [PATCH 1/6] feat: initial work on supporting dbus-daemon XML configuration files --- Cargo.lock | 11 + Cargo.toml | 1 + src/configuration.rs | 588 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + tests/configuration.rs | 43 +++ 5 files changed, 644 insertions(+) create mode 100644 src/configuration.rs create mode 100644 tests/configuration.rs diff --git a/Cargo.lock b/Cargo.lock index 575c46a..359ba38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -362,6 +362,7 @@ dependencies = [ "hex", "nix", "ntest", + "quick-xml", "rand", "serde", "tokio", @@ -1284,6 +1285,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" diff --git a/Cargo.toml b/Cargo.toml index 05e128b..cc26029 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ tracing-subscriber = { version = "0.3.18", features = [ "ansi", ], default-features = false, optional = true } anyhow = "1.0.82" +quick-xml = { version = "0.36.2", features = ["overlapped-lists", "serialize"] } # Explicitly depend on serde to enable `rc` feature. serde = { version = "1.0.200", features = ["rc"] } futures-util = "0.3.30" diff --git a/src/configuration.rs b/src/configuration.rs new file mode 100644 index 0000000..e8c6a6f --- /dev/null +++ b/src/configuration.rs @@ -0,0 +1,588 @@ +//! implementation of "Configuration File" described at: +//! https://dbus.freedesktop.org/doc/dbus-daemon.1.html + +use std::{path::PathBuf, str::FromStr}; + +use serde::Deserialize; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct Apparmor { + #[serde(rename = "@mode")] + mode: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +enum ApparmorMode { + Disabled, + Enabled, + Required, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +struct Associate { + #[serde(rename = "@context")] + context: String, + #[serde(rename = "@own")] + own: String, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Configuration { + allow_anonymous: Option, + apparmor: Option, + auth: Vec, + fork: Option, + include: Vec, + includedir: Vec, + keep_umask: Option, + limit: Vec, + listen: Vec, + pidfile: Option, + policy: Vec, + selinux: Vec, + servicedir: Vec, + servicehelper: Option, + standard_session_servicedirs: Option, + standard_system_servicedirs: Option, + syslog: Option, + r#type: Option, + user: Option, +} +impl TryFrom for Configuration { + type Error = Error; + + fn try_from(value: RawConfiguration) -> Result { + let mut policy = Vec::with_capacity(value.policy.len()); + for rp in value.policy { + match Policy::try_from(rp) { + Ok(p) => policy.push(p), + Err(err) => { + return Err(err); + } + } + } + + let mut bc = Self { + allow_anonymous: value.allow_anonymous.map(|_| true), + apparmor: match value.apparmor { + Some(a) => a.mode, + None => None, + }, + auth: value.auth, + fork: value.fork.map(|_| true), + include: value.include, + includedir: value.includedir, + keep_umask: value.keep_umask.map(|_| true), + limit: value.limit, + listen: value.listen, + pidfile: value.pidfile, + policy, + // TODO: SELinux could probably more-conveniently be represented as a HashMap + // TODO: last one wins for SELinux associates with the same name + selinux: match value.selinux { + Some(s) => s.associate, + None => vec![], + }, + servicedir: value.servicedir, + servicehelper: value.servicehelper, + standard_session_servicedirs: value.standard_session_servicedirs.map(|_| true), + standard_system_servicedirs: value.standard_system_servicedirs.map(|_| true), + syslog: value.syslog.map(|_| true), + ..Default::default() + }; + + // > The last element "wins" + if let Some(te) = value.r#type.into_iter().last() { + bc.r#type = Some(te.text); + } + if let Some(ue) = value.user.into_iter().last() { + bc.user = Some(ue.text); + } + + Ok(bc) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Error { + PolicyHasMultipleAttributes, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +enum IgnoreMissing { + No, + Yes, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +enum Include { + Session, + System, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct IncludeElement { + #[serde(rename = "@ignore_missing")] + ignore_missing: Option, + #[serde(rename = "$text")] + text: PathBuf, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct LimitElement { + #[serde(rename = "@name")] + name: LimitName, + #[serde(rename = "$text")] + text: i32, // semantically should be u32, but i32 for compatibility +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +enum LimitName { + AuthTimeout, + MaxCompletedConnections, + MaxConnectionsPerUser, + MaxIncomingBytes, + MaxIncomingUnixFds, + MaxIncompleteConnections, + MaxMatchRulesPerConnection, + MaxMessageSize, + MaxMessageUnixFds, + MaxNamesPerConnection, + MaxOutgoingBytes, + MaxOutgoingUnixFds, + MaxPendingServiceStarts, + MaxRepliesPerConnection, + PendingFdTimeout, + ServiceStartTimeout, + ReplyTimeout, +} + +// reuse this between Vec fields, +// except those with field-specific attributes +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct PathBufElement { + #[serde(rename = "$text")] + text: PathBuf, +} + +#[derive(Clone, Debug, PartialEq)] +enum Policy { + Console { rules: Vec }, + DefaultContext { rules: Vec }, + Group { group: Principal, rules: Vec }, + MandatoryContext { rules: Vec }, + NoConsole { rules: Vec }, + User { user: Principal, rules: Vec }, +} +impl TryFrom for Policy { + type Error = Error; + fn try_from(value: RawPolicy) -> Result { + // TODO: more validations and conversions as documented against dbus-daemon + match value { + RawPolicy { + at_console: Some(b), + context: None, + group: None, + rules, + user: None, + } => Ok(match b { + true => Self::Console { rules }, + false => Self::NoConsole { rules }, + }), + RawPolicy { + at_console: None, + context: Some(pc), + group: None, + rules, + user: None, + } => Ok(match pc { + PolicyContext::Default => Self::DefaultContext { rules }, + PolicyContext::Mandatory => Self::MandatoryContext { rules }, + }), + RawPolicy { + at_console: None, + context: None, + group: Some(p), + rules, + user: None, + } => Ok(Self::Group { group: p, rules }), + RawPolicy { + at_console: None, + context: None, + group: None, + rules, + user: Some(p), + } => Ok(Self::User { user: p, rules }), + _ => Err(Error::PolicyHasMultipleAttributes), + } + } +} +// TODO: impl PartialOrd/Ord for Policy, for order in which policies are applied to a connection + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +enum PolicyContext { + Default, + Mandatory, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(default)] +struct PolicyList { + policy: Vec, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase", untagged)] +enum Principal { + Id(u32), + Name(String), +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(default)] +pub struct RawConfiguration { + allow_anonymous: Option<()>, + apparmor: Option, + auth: Vec, + fork: Option<()>, + include: Vec, + includedir: Vec, + keep_umask: Option<()>, + limit: Vec, + listen: Vec, + pidfile: Option, + policy: Vec, + selinux: Option, + servicedir: Vec, + servicehelper: Option, + standard_session_servicedirs: Option<()>, + standard_system_servicedirs: Option<()>, + syslog: Option<()>, + r#type: Vec, + user: Vec, +} +impl FromStr for RawConfiguration { + type Err = quick_xml::DeError; + + fn from_str(s: &str) -> Result { + // TODO: validate expected DOCTYPE + // TODO: validate expected root element (busconfig) + quick_xml::de::from_str(s) + } +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct RawPolicy { + #[serde(rename = "@at_console")] + at_console: Option, + #[serde(rename = "@context")] + context: Option, + #[serde(rename = "@group")] + group: Option, + #[serde(default, rename = "$value")] + rules: Vec, + #[serde(rename = "@user")] + user: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +enum Rule { + Allow(RuleAttributes), + Deny(RuleAttributes), +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(default, rename_all = "lowercase")] +struct RuleAttributes { + #[serde(rename = "@send_interface")] + send_interface: Option, + #[serde(rename = "@send_member")] + send_member: Option, + #[serde(rename = "@send_error")] + send_error: Option, + #[serde(rename = "@send_broadcast")] + send_broadcast: Option, + #[serde(rename = "@send_destination")] + send_destination: Option, + #[serde(rename = "@send_destination_prefix")] + send_destination_prefix: Option, + #[serde(rename = "@send_type")] + send_type: Option, + #[serde(rename = "@send_path")] + send_path: Option, + #[serde(rename = "@receive_interface")] + receive_interface: Option, + #[serde(rename = "@receive_member")] + receive_member: Option, + #[serde(rename = "@receive_error")] + receive_error: Option, + #[serde(rename = "@receive_sender")] + receive_sender: Option, + #[serde(rename = "@receive_type")] + receive_type: Option, + #[serde(rename = "@receive_path")] + receive_path: Option, + #[serde(rename = "@send_requested_reply")] + send_requested_reply: Option, + #[serde(rename = "@receive_requested_reply")] + receive_requested_reply: Option, + #[serde(rename = "@eavesdrop")] + eavesdrop: Option, + #[serde(rename = "@own")] + own: Option, + #[serde(rename = "@own_prefix")] + own_prefix: Option, + #[serde(rename = "@user")] + user: Option, + #[serde(rename = "@group")] + group: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case", untagged)] +enum RuleMatch { + #[serde(rename = "*")] + Any, + One(String), +} +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +enum RuleMatchType { + #[serde(rename = "*")] + Any, + Error, + MethodCall, + MethodReturn, + Signal, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(default)] +struct Selinux { + associate: Vec, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +enum Type { + Session, + System, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct TypeElement { + #[serde(rename = "$text")] + text: Type, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct UserElement { + #[serde(rename = "$text")] + text: Principal, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn busconfig_fromstr_last_type_wins_ok() { + let input = r#" + + + system + session + + "#; + + let got = RawConfiguration::from_str(input).expect("should parse input XML"); + let got = Configuration::try_from(got).expect("should validate and convert"); + + assert_eq!(got.r#type, Some(Type::Session)); + } + + #[test] + fn busconfig_fromstr_last_user_wins_ok() { + let input = r#" + + + 1234 + nobody + + "#; + + let got = RawConfiguration::from_str(input).expect("should parse input XML"); + let got = Configuration::try_from(got).expect("should validate and convert"); + + assert_eq!(got.user, Some(Principal::Name(String::from("nobody")))); + } + + #[test] + fn busconfig_fromstr_allow_deny_allow_ok() { + // from https://github.com/OpenPrinting/system-config-printer/blob/caa1ba33da20fd2a82cee0bcc97589fede512cc8/dbus/com.redhat.PrinterDriversInstaller.conf + // selected because it has a in the middle of a list of s + let input = r#" + + + + + + + + + + + + + + + "#; + + let got = RawConfiguration::from_str(input).expect("should parse input XML"); + let got = Configuration::try_from(got).expect("should validate and convert"); + + assert_eq!( + got, + Configuration { + policy: vec![ + Policy::User { + rules: vec![Rule::Allow(RuleAttributes { + send_destination: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + send_interface: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + ..Default::default() + })], + user: Principal::Name(String::from("root")), + }, + Policy::DefaultContext { + rules: vec![ + Rule::Allow(RuleAttributes { + own: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + ..Default::default() + }), + Rule::Deny(RuleAttributes { + send_destination: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + send_interface: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + ..Default::default() + }), + Rule::Allow(RuleAttributes { + send_destination: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + send_interface: Some(RuleMatch::One(String::from( + "org.freedesktop.DBus.Introspectable" + ))), + ..Default::default() + }), + Rule::Allow(RuleAttributes { + send_destination: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + send_interface: Some(RuleMatch::One(String::from( + "org.freedesktop.DBus.Properties" + ))), + ..Default::default() + }), + ] + } + ], + ..Default::default() + } + ); + } + + #[test] + fn busconfig_fromstr_limit_ok() { + let input = r#" + + + 133169152 + 64 + + "#; + + let got = RawConfiguration::from_str(input).expect("should parse input XML"); + let got = Configuration::try_from(got).expect("should validate and convert"); + + assert_eq!( + got, + Configuration { + limit: vec![ + LimitElement { + name: LimitName::MaxIncomingBytes, + text: 133169152 + }, + LimitElement { + name: LimitName::MaxIncomingUnixFds, + text: 64 + }, + ], + ..Default::default() + } + ); + } + + #[test] + fn busconfig_fromstr_apparmor_and_selinux_ok() { + let input = r#" + + + + + + + + "#; + + let got = RawConfiguration::from_str(input).expect("should parse input XML"); + let got = Configuration::try_from(got).expect("should validate and convert"); + + assert_eq!( + got, + Configuration { + apparmor: Some(ApparmorMode::Enabled), + selinux: vec![Associate { + context: String::from("foo_t"), + own: String::from("org.freedesktop.Foobar") + },], + ..Default::default() + } + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 07a5f33..67f0bd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod bus; +pub mod configuration; pub mod fdo; pub mod match_rules; pub mod name_registry; diff --git a/tests/configuration.rs b/tests/configuration.rs new file mode 100644 index 0000000..57c2358 --- /dev/null +++ b/tests/configuration.rs @@ -0,0 +1,43 @@ +use std::{ + ffi::OsStr, + fs::{read_dir, read_to_string, DirEntry}, + path::PathBuf, + str::FromStr, +}; + +use busd::configuration::{Configuration, RawConfiguration}; + +#[test] +fn find_and_parse_real_configuration_files() { + let mut file_paths = vec![ + PathBuf::from("/usr/share/dbus-1/session.conf"), + PathBuf::from("/usr/share/dbus-1/system.conf"), + ]; + + for dir_path in ["/usr/share/dbus-1/session.d", "/usr/share/dbus-1/system.d"] { + if let Ok(rd) = read_dir(dir_path) { + file_paths.extend( + rd.flatten() + .map(|fp| DirEntry::path(&fp)) + .filter(|fp| fp.extension() == Some(OsStr::new("conf"))), + ); + } + } + + for file_path in file_paths { + let configuration_text = match read_to_string(&file_path) { + Ok(ok) => ok, + Err(_) => continue, + }; + + let got = RawConfiguration::from_str(&configuration_text).unwrap_or_else(|err| { + panic!("should correctly parse {}: {err:?}", file_path.display()) + }); + Configuration::try_from(got).unwrap_or_else(|err| { + panic!( + "should validate and convert {}: {err:?}", + file_path.display() + ) + }); + } +} From 95a39b45ddea634c58370ec13f704ad6eef8d32b Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Mon, 7 Oct 2024 11:02:02 +1100 Subject: [PATCH 2/6] fix(xml): `Raw...` types internal-only, make other types `pub` --- src/configuration.rs | 152 +++++++++++++++++++---------------------- tests/configuration.rs | 10 +-- 2 files changed, 74 insertions(+), 88 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index e8c6a6f..1425d95 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -7,21 +7,14 @@ use serde::Deserialize; #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] -struct Apparmor { - #[serde(rename = "@mode")] - mode: Option, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -enum ApparmorMode { +pub enum ApparmorMode { Disabled, Enabled, Required, } #[derive(Clone, Debug, Default, Deserialize, PartialEq)] -struct Associate { +pub struct Associate { #[serde(rename = "@context")] context: String, #[serde(rename = "@own")] @@ -50,6 +43,15 @@ pub struct Configuration { r#type: Option, user: Option, } +impl FromStr for Configuration { + type Err = Error; + + fn from_str(s: &str) -> Result { + RawConfiguration::from_str(s) + .map_err(Error::DeserializeXml) + .and_then(Self::try_from) + } +} impl TryFrom for Configuration { type Error = Error; @@ -105,28 +107,22 @@ impl TryFrom for Configuration { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub enum Error { + DeserializeXml(quick_xml::DeError), PolicyHasMultipleAttributes, } #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] -enum IgnoreMissing { +pub enum IgnoreMissing { No, Yes, } #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] -enum Include { - Session, - System, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -struct IncludeElement { +pub struct IncludeElement { #[serde(rename = "@ignore_missing")] ignore_missing: Option, #[serde(rename = "$text")] @@ -135,7 +131,7 @@ struct IncludeElement { #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] -struct LimitElement { +pub struct LimitElement { #[serde(rename = "@name")] name: LimitName, #[serde(rename = "$text")] @@ -144,7 +140,7 @@ struct LimitElement { #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] -enum LimitName { +pub enum LimitName { AuthTimeout, MaxCompletedConnections, MaxConnectionsPerUser, @@ -168,13 +164,13 @@ enum LimitName { // except those with field-specific attributes #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] -struct PathBufElement { +pub struct PathBufElement { #[serde(rename = "$text")] text: PathBuf, } #[derive(Clone, Debug, PartialEq)] -enum Policy { +pub enum Policy { Console { rules: Vec }, DefaultContext { rules: Vec }, Group { group: Principal, rules: Vec }, @@ -204,8 +200,8 @@ impl TryFrom for Policy { rules, user: None, } => Ok(match pc { - PolicyContext::Default => Self::DefaultContext { rules }, - PolicyContext::Mandatory => Self::MandatoryContext { rules }, + RawPolicyContext::Default => Self::DefaultContext { rules }, + RawPolicyContext::Mandatory => Self::MandatoryContext { rules }, }), RawPolicy { at_console: None, @@ -227,31 +223,25 @@ impl TryFrom for Policy { } // TODO: impl PartialOrd/Ord for Policy, for order in which policies are applied to a connection -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -enum PolicyContext { - Default, - Mandatory, -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -#[serde(default)] -struct PolicyList { - policy: Vec, -} - #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase", untagged)] -enum Principal { +pub enum Principal { Id(u32), Name(String), } +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct RawApparmor { + #[serde(rename = "@mode")] + mode: Option, +} + #[derive(Clone, Debug, Default, Deserialize, PartialEq)] #[serde(default)] -pub struct RawConfiguration { +struct RawConfiguration { allow_anonymous: Option<()>, - apparmor: Option, + apparmor: Option, auth: Vec, fork: Option<()>, include: Vec, @@ -261,14 +251,14 @@ pub struct RawConfiguration { listen: Vec, pidfile: Option, policy: Vec, - selinux: Option, + selinux: Option, servicedir: Vec, servicehelper: Option, standard_session_servicedirs: Option<()>, standard_system_servicedirs: Option<()>, syslog: Option<()>, - r#type: Vec, - user: Vec, + r#type: Vec, + user: Vec, } impl FromStr for RawConfiguration { type Err = quick_xml::DeError; @@ -286,7 +276,7 @@ struct RawPolicy { #[serde(rename = "@at_console")] at_console: Option, #[serde(rename = "@context")] - context: Option, + context: Option, #[serde(rename = "@group")] group: Option, #[serde(default, rename = "$value")] @@ -297,14 +287,41 @@ struct RawPolicy { #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] -enum Rule { +enum RawPolicyContext { + Default, + Mandatory, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(default)] +struct RawSelinux { + associate: Vec, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct RawTypeElement { + #[serde(rename = "$text")] + text: Type, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct RawUserElement { + #[serde(rename = "$text")] + text: Principal, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Rule { Allow(RuleAttributes), Deny(RuleAttributes), } #[derive(Clone, Debug, Default, Deserialize, PartialEq)] #[serde(default, rename_all = "lowercase")] -struct RuleAttributes { +pub struct RuleAttributes { #[serde(rename = "@send_interface")] send_interface: Option, #[serde(rename = "@send_member")] @@ -351,14 +368,14 @@ struct RuleAttributes { #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "snake_case", untagged)] -enum RuleMatch { +pub enum RuleMatch { #[serde(rename = "*")] Any, One(String), } #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] -enum RuleMatchType { +pub enum RuleMatchType { #[serde(rename = "*")] Any, Error, @@ -367,33 +384,13 @@ enum RuleMatchType { Signal, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -#[serde(default)] -struct Selinux { - associate: Vec, -} - #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] -enum Type { +pub enum Type { Session, System, } -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -struct TypeElement { - #[serde(rename = "$text")] - text: Type, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -struct UserElement { - #[serde(rename = "$text")] - text: Principal, -} - #[cfg(test)] mod tests { use super::*; @@ -410,8 +407,7 @@ mod tests { "#; - let got = RawConfiguration::from_str(input).expect("should parse input XML"); - let got = Configuration::try_from(got).expect("should validate and convert"); + let got = Configuration::from_str(input).expect("should parse input XML"); assert_eq!(got.r#type, Some(Type::Session)); } @@ -428,8 +424,7 @@ mod tests { "#; - let got = RawConfiguration::from_str(input).expect("should parse input XML"); - let got = Configuration::try_from(got).expect("should validate and convert"); + let got = Configuration::from_str(input).expect("should parse input XML"); assert_eq!(got.user, Some(Principal::Name(String::from("nobody")))); } @@ -461,8 +456,7 @@ mod tests { "#; - let got = RawConfiguration::from_str(input).expect("should parse input XML"); - let got = Configuration::try_from(got).expect("should validate and convert"); + let got = Configuration::from_str(input).expect("should parse input XML"); assert_eq!( got, @@ -535,8 +529,7 @@ mod tests { "#; - let got = RawConfiguration::from_str(input).expect("should parse input XML"); - let got = Configuration::try_from(got).expect("should validate and convert"); + let got = Configuration::from_str(input).expect("should parse input XML"); assert_eq!( got, @@ -570,8 +563,7 @@ mod tests { "#; - let got = RawConfiguration::from_str(input).expect("should parse input XML"); - let got = Configuration::try_from(got).expect("should validate and convert"); + let got = Configuration::from_str(input).expect("should parse input XML"); assert_eq!( got, diff --git a/tests/configuration.rs b/tests/configuration.rs index 57c2358..2751997 100644 --- a/tests/configuration.rs +++ b/tests/configuration.rs @@ -5,7 +5,7 @@ use std::{ str::FromStr, }; -use busd::configuration::{Configuration, RawConfiguration}; +use busd::configuration::Configuration; #[test] fn find_and_parse_real_configuration_files() { @@ -30,14 +30,8 @@ fn find_and_parse_real_configuration_files() { Err(_) => continue, }; - let got = RawConfiguration::from_str(&configuration_text).unwrap_or_else(|err| { + Configuration::from_str(&configuration_text).unwrap_or_else(|err| { panic!("should correctly parse {}: {err:?}", file_path.display()) }); - Configuration::try_from(got).unwrap_or_else(|err| { - panic!( - "should validate and convert {}: {err:?}", - file_path.display() - ) - }); } } From 7adfdb565cda6cd1f15c2c4c786be5a8d48a86c7 Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Mon, 7 Oct 2024 12:38:08 +1100 Subject: [PATCH 3/6] fix(xml): validate and convert `RawRule...` into useful `Rule...` --- src/configuration.rs | 412 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 343 insertions(+), 69 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index 1425d95..559071e 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -27,14 +27,18 @@ pub struct Configuration { apparmor: Option, auth: Vec, fork: Option, + // TODO: consider processing `include` more to remove XML-specific structure include: Vec, + // TODO: consider processing `include` more to remove XML-specific structure includedir: Vec, keep_umask: Option, + // TODO: consider processing `include` more to remove XML-specific structure limit: Vec, listen: Vec, pidfile: Option, policy: Vec, selinux: Vec, + // TODO: consider processing `include` more to remove XML-specific structure servicedir: Vec, servicehelper: Option, standard_session_servicedirs: Option, @@ -107,10 +111,17 @@ impl TryFrom for Configuration { } } +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ConnectRule { + group: Option, + user: Option, +} + #[derive(Clone, Debug)] pub enum Error { DeserializeXml(quick_xml::DeError), PolicyHasMultipleAttributes, + RuleHasInvalidCombinationOfAttributes, } #[derive(Clone, Debug, Deserialize, PartialEq)] @@ -160,6 +171,12 @@ pub enum LimitName { ReplyTimeout, } +#[derive(Clone, Debug, Default, PartialEq)] +pub struct OwnRule { + own: Option, + own_prefix: Option, +} + // reuse this between Vec fields, // except those with field-specific attributes #[derive(Clone, Debug, Deserialize, PartialEq)] @@ -181,14 +198,21 @@ pub enum Policy { impl TryFrom for Policy { type Error = Error; fn try_from(value: RawPolicy) -> Result { - // TODO: more validations and conversions as documented against dbus-daemon + let mut rules: Vec = Vec::with_capacity(value.rules.len()); + for rule in value.rules { + match Rule::try_from(rule) { + Ok(ok) => rules.push(ok), + Err(err) => return Err(err), + } + } + match value { RawPolicy { at_console: Some(b), context: None, group: None, - rules, user: None, + .. } => Ok(match b { true => Self::Console { rules }, false => Self::NoConsole { rules }, @@ -197,8 +221,8 @@ impl TryFrom for Policy { at_console: None, context: Some(pc), group: None, - rules, user: None, + .. } => Ok(match pc { RawPolicyContext::Default => Self::DefaultContext { rules }, RawPolicyContext::Mandatory => Self::MandatoryContext { rules }, @@ -207,15 +231,15 @@ impl TryFrom for Policy { at_console: None, context: None, group: Some(p), - rules, user: None, + .. } => Ok(Self::Group { group: p, rules }), RawPolicy { at_console: None, context: None, group: None, - rules, user: Some(p), + .. } => Ok(Self::User { user: p, rules }), _ => Err(Error::PolicyHasMultipleAttributes), } @@ -280,7 +304,7 @@ struct RawPolicy { #[serde(rename = "@group")] group: Option, #[serde(default, rename = "$value")] - rules: Vec, + rules: Vec, #[serde(rename = "@user")] user: Option, } @@ -292,36 +316,16 @@ enum RawPolicyContext { Mandatory, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -#[serde(default)] -struct RawSelinux { - associate: Vec, -} - #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] -struct RawTypeElement { - #[serde(rename = "$text")] - text: Type, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -struct RawUserElement { - #[serde(rename = "$text")] - text: Principal, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum Rule { - Allow(RuleAttributes), - Deny(RuleAttributes), +enum RawRule { + Allow(RawRuleAttributes), + Deny(RawRuleAttributes), } #[derive(Clone, Debug, Default, Deserialize, PartialEq)] #[serde(default, rename_all = "lowercase")] -pub struct RuleAttributes { +struct RawRuleAttributes { #[serde(rename = "@send_interface")] send_interface: Option, #[serde(rename = "@send_member")] @@ -366,6 +370,185 @@ pub struct RuleAttributes { group: Option, } +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(default)] +struct RawSelinux { + associate: Vec, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct RawTypeElement { + #[serde(rename = "$text")] + text: Type, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +struct RawUserElement { + #[serde(rename = "$text")] + text: Principal, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ReceiveRule { + eavesdrop: Option, + receive_error: Option, + receive_interface: Option, + receive_member: Option, + receive_path: Option, + receive_requested_reply: Option, + receive_sender: Option, + receive_type: Option, +} + +pub type Rule = (RuleEffect, RulePhase); +impl TryFrom for Rule { + type Error = Error; + + fn try_from(value: RawRule) -> Result { + let (effect, attributes) = match value { + RawRule::Allow(attributes) => (RuleEffect::Allow, attributes), + RawRule::Deny(attributes) => (RuleEffect::Deny, attributes), + }; + match attributes { + RawRuleAttributes { + eavesdrop, + group: None, + own: None, + own_prefix: None, + receive_error, + receive_interface, + receive_member, + receive_path, + receive_requested_reply, + receive_sender, + receive_type, + 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, + } => Ok(( + effect, + RulePhase::Receive(ReceiveRule { + eavesdrop, + receive_error, + receive_interface, + receive_member, + receive_path, + receive_requested_reply, + receive_sender, + receive_type, + }), + )), + RawRuleAttributes { + eavesdrop, + group: None, + own: None, + own_prefix: None, + receive_error: None, + receive_interface: None, + receive_member: None, + receive_path: None, + receive_requested_reply: None, + receive_sender: None, + receive_type: None, + send_broadcast, + send_destination, + send_destination_prefix, + send_error, + send_interface, + send_member, + send_path, + send_requested_reply, + send_type, + user: None, + } => Ok(( + effect, + RulePhase::Send(SendRule { + eavesdrop, + send_broadcast, + send_destination, + send_destination_prefix, + send_error, + send_interface, + send_member, + send_path, + send_requested_reply, + send_type, + }), + )), + RawRuleAttributes { + eavesdrop: None, + group: None, + own, + own_prefix, + receive_error: None, + receive_interface: None, + receive_member: None, + receive_path: None, + receive_requested_reply: None, + receive_sender: None, + receive_type: 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, + } => Ok((effect, RulePhase::Own(OwnRule { own, own_prefix }))), + RawRuleAttributes { + eavesdrop: None, + group, + own: None, + own_prefix: None, + receive_error: None, + receive_interface: None, + receive_member: None, + receive_path: None, + receive_requested_reply: None, + receive_sender: None, + receive_type: 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, + } => Ok((effect, RulePhase::Connect(ConnectRule { group, user }))), + _ => Err(Error::RuleHasInvalidCombinationOfAttributes), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum RuleEffect { + Allow, + Deny, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum RulePhase { + Connect(ConnectRule), + Own(OwnRule), + Receive(ReceiveRule), + Send(SendRule), +} + #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "snake_case", untagged)] pub enum RuleMatch { @@ -384,6 +567,20 @@ pub enum RuleMatchType { Signal, } +#[derive(Clone, Debug, Default, PartialEq)] +pub struct SendRule { + eavesdrop: Option, + send_broadcast: Option, + send_destination: Option, + send_destination_prefix: Option, + send_error: Option, + send_interface: Option, + send_member: Option, + send_path: Option, + send_requested_reply: Option, + send_type: Option, +} + #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Type { @@ -463,26 +660,9 @@ mod tests { Configuration { policy: vec![ Policy::User { - rules: vec![Rule::Allow(RuleAttributes { - send_destination: Some(RuleMatch::One(String::from( - "com.redhat.PrinterDriversInstaller" - ))), - send_interface: Some(RuleMatch::One(String::from( - "com.redhat.PrinterDriversInstaller" - ))), - ..Default::default() - })], - user: Principal::Name(String::from("root")), - }, - Policy::DefaultContext { - rules: vec![ - Rule::Allow(RuleAttributes { - own: Some(RuleMatch::One(String::from( - "com.redhat.PrinterDriversInstaller" - ))), - ..Default::default() - }), - Rule::Deny(RuleAttributes { + rules: vec![( + RuleEffect::Allow, + RulePhase::Send(SendRule { send_destination: Some(RuleMatch::One(String::from( "com.redhat.PrinterDriversInstaller" ))), @@ -490,25 +670,57 @@ mod tests { "com.redhat.PrinterDriversInstaller" ))), ..Default::default() - }), - Rule::Allow(RuleAttributes { - send_destination: Some(RuleMatch::One(String::from( - "com.redhat.PrinterDriversInstaller" - ))), - send_interface: Some(RuleMatch::One(String::from( - "org.freedesktop.DBus.Introspectable" - ))), - ..Default::default() - }), - Rule::Allow(RuleAttributes { - send_destination: Some(RuleMatch::One(String::from( - "com.redhat.PrinterDriversInstaller" - ))), - send_interface: Some(RuleMatch::One(String::from( - "org.freedesktop.DBus.Properties" - ))), - ..Default::default() - }), + }) + )], + user: Principal::Name(String::from("root")), + }, + Policy::DefaultContext { + rules: vec![ + ( + RuleEffect::Allow, + RulePhase::Own(OwnRule { + own: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + ..Default::default() + }) + ), + ( + RuleEffect::Deny, + RulePhase::Send(SendRule { + send_destination: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + send_interface: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + ..Default::default() + }) + ), + ( + RuleEffect::Allow, + RulePhase::Send(SendRule { + send_destination: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + send_interface: Some(RuleMatch::One(String::from( + "org.freedesktop.DBus.Introspectable" + ))), + ..Default::default() + }) + ), + ( + RuleEffect::Allow, + RulePhase::Send(SendRule { + send_destination: Some(RuleMatch::One(String::from( + "com.redhat.PrinterDriversInstaller" + ))), + send_interface: Some(RuleMatch::One(String::from( + "org.freedesktop.DBus.Properties" + ))), + ..Default::default() + }) + ), ] } ], @@ -577,4 +789,66 @@ mod tests { } ); } + + #[test] + fn busconfig_fromstr_receiverule_ok() { + // from https://github.com/OpenPrinting/system-config-printer/blob/caa1ba33da20fd2a82cee0bcc97589fede512cc8/dbus/com.redhat.PrinterDriversInstaller.conf + // selected because it has a in the middle of a list of s + let input = r#" + + + + + + + + + + "#; + + let got = Configuration::from_str(input).expect("should parse input XML"); + + assert_eq!( + got, + Configuration { + policy: vec![Policy::DefaultContext { + rules: vec![ + ( + RuleEffect::Allow, + RulePhase::Receive(ReceiveRule { + eavesdrop: Some(false), + ..Default::default() + }) + ), + ( + RuleEffect::Allow, + RulePhase::Receive(ReceiveRule { + eavesdrop: Some(true), + ..Default::default() + }) + ), + ( + RuleEffect::Deny, + RulePhase::Receive(ReceiveRule { + eavesdrop: Some(false), + receive_requested_reply: Some(true), + ..Default::default() + }) + ), + ( + RuleEffect::Deny, + RulePhase::Receive(ReceiveRule { + eavesdrop: Some(true), + receive_requested_reply: Some(true), + ..Default::default() + }) + ), + ] + }], + ..Default::default() + } + ); + } } From ae79bf054393d9114129cdcff32f0e3764ed9271 Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Mon, 7 Oct 2024 12:58:19 +1100 Subject: [PATCH 4/6] chore(xml): refactor `Raw...` out into separate file --- src/configuration.rs | 139 +------------------------------------ src/configuration/raw.rs | 146 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 136 deletions(-) create mode 100644 src/configuration/raw.rs diff --git a/src/configuration.rs b/src/configuration.rs index 559071e..595f78d 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -3,8 +3,11 @@ use std::{path::PathBuf, str::FromStr}; +use raw::{RawConfiguration, RawPolicy, RawPolicyContext, RawRule, RawRuleAttributes}; use serde::Deserialize; +mod raw; + #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum ApparmorMode { @@ -254,142 +257,6 @@ pub enum Principal { Name(String), } -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -struct RawApparmor { - #[serde(rename = "@mode")] - mode: Option, -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -#[serde(default)] -struct RawConfiguration { - allow_anonymous: Option<()>, - apparmor: Option, - auth: Vec, - fork: Option<()>, - include: Vec, - includedir: Vec, - keep_umask: Option<()>, - limit: Vec, - listen: Vec, - pidfile: Option, - policy: Vec, - selinux: Option, - servicedir: Vec, - servicehelper: Option, - standard_session_servicedirs: Option<()>, - standard_system_servicedirs: Option<()>, - syslog: Option<()>, - r#type: Vec, - user: Vec, -} -impl FromStr for RawConfiguration { - type Err = quick_xml::DeError; - - fn from_str(s: &str) -> Result { - // TODO: validate expected DOCTYPE - // TODO: validate expected root element (busconfig) - quick_xml::de::from_str(s) - } -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -struct RawPolicy { - #[serde(rename = "@at_console")] - at_console: Option, - #[serde(rename = "@context")] - context: Option, - #[serde(rename = "@group")] - group: Option, - #[serde(default, rename = "$value")] - rules: Vec, - #[serde(rename = "@user")] - user: Option, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -enum RawPolicyContext { - Default, - Mandatory, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -enum RawRule { - Allow(RawRuleAttributes), - Deny(RawRuleAttributes), -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -#[serde(default, rename_all = "lowercase")] -struct RawRuleAttributes { - #[serde(rename = "@send_interface")] - send_interface: Option, - #[serde(rename = "@send_member")] - send_member: Option, - #[serde(rename = "@send_error")] - send_error: Option, - #[serde(rename = "@send_broadcast")] - send_broadcast: Option, - #[serde(rename = "@send_destination")] - send_destination: Option, - #[serde(rename = "@send_destination_prefix")] - send_destination_prefix: Option, - #[serde(rename = "@send_type")] - send_type: Option, - #[serde(rename = "@send_path")] - send_path: Option, - #[serde(rename = "@receive_interface")] - receive_interface: Option, - #[serde(rename = "@receive_member")] - receive_member: Option, - #[serde(rename = "@receive_error")] - receive_error: Option, - #[serde(rename = "@receive_sender")] - receive_sender: Option, - #[serde(rename = "@receive_type")] - receive_type: Option, - #[serde(rename = "@receive_path")] - receive_path: Option, - #[serde(rename = "@send_requested_reply")] - send_requested_reply: Option, - #[serde(rename = "@receive_requested_reply")] - receive_requested_reply: Option, - #[serde(rename = "@eavesdrop")] - eavesdrop: Option, - #[serde(rename = "@own")] - own: Option, - #[serde(rename = "@own_prefix")] - own_prefix: Option, - #[serde(rename = "@user")] - user: Option, - #[serde(rename = "@group")] - group: Option, -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -#[serde(default)] -struct RawSelinux { - associate: Vec, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -struct RawTypeElement { - #[serde(rename = "$text")] - text: Type, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -struct RawUserElement { - #[serde(rename = "$text")] - text: Principal, -} - #[derive(Clone, Debug, Default, PartialEq)] pub struct ReceiveRule { eavesdrop: Option, diff --git a/src/configuration/raw.rs b/src/configuration/raw.rs new file mode 100644 index 0000000..c003c31 --- /dev/null +++ b/src/configuration/raw.rs @@ -0,0 +1,146 @@ +//! internal implementation details for handling configuration XML + +use std::{path::PathBuf, str::FromStr}; + +use serde::Deserialize; + +use super::{ + ApparmorMode, Associate, IncludeElement, LimitElement, PathBufElement, Principal, RuleMatch, + RuleMatchType, Type, +}; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub(super) struct RawApparmor { + #[serde(rename = "@mode")] + pub mode: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(default)] +pub(super) struct RawConfiguration { + pub allow_anonymous: Option<()>, + pub apparmor: Option, + pub auth: Vec, + pub fork: Option<()>, + pub include: Vec, + pub includedir: Vec, + pub keep_umask: Option<()>, + pub limit: Vec, + pub listen: Vec, + pub pidfile: Option, + pub policy: Vec, + pub selinux: Option, + pub servicedir: Vec, + pub servicehelper: Option, + pub standard_session_servicedirs: Option<()>, + pub standard_system_servicedirs: Option<()>, + pub syslog: Option<()>, + pub r#type: Vec, + pub user: Vec, +} +impl FromStr for RawConfiguration { + type Err = quick_xml::DeError; + + fn from_str(s: &str) -> Result { + // TODO: validate expected DOCTYPE + // TODO: validate expected root element (busconfig) + quick_xml::de::from_str(s) + } +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub(super) struct RawPolicy { + #[serde(rename = "@at_console")] + pub at_console: Option, + #[serde(rename = "@context")] + pub context: Option, + #[serde(rename = "@group")] + pub group: Option, + #[serde(default, rename = "$value")] + pub rules: Vec, + #[serde(rename = "@user")] + pub user: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub(super) enum RawPolicyContext { + Default, + Mandatory, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub(super) enum RawRule { + Allow(RawRuleAttributes), + Deny(RawRuleAttributes), +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(default, rename_all = "lowercase")] +pub(super) struct RawRuleAttributes { + #[serde(rename = "@send_interface")] + pub send_interface: Option, + #[serde(rename = "@send_member")] + pub send_member: Option, + #[serde(rename = "@send_error")] + pub send_error: 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_type")] + pub send_type: Option, + #[serde(rename = "@send_path")] + pub send_path: Option, + #[serde(rename = "@receive_interface")] + pub receive_interface: Option, + #[serde(rename = "@receive_member")] + pub receive_member: Option, + #[serde(rename = "@receive_error")] + pub receive_error: Option, + #[serde(rename = "@receive_sender")] + pub receive_sender: Option, + #[serde(rename = "@receive_type")] + pub receive_type: Option, + #[serde(rename = "@receive_path")] + pub receive_path: Option, + #[serde(rename = "@send_requested_reply")] + pub send_requested_reply: Option, + #[serde(rename = "@receive_requested_reply")] + pub receive_requested_reply: Option, + #[serde(rename = "@eavesdrop")] + pub eavesdrop: Option, + #[serde(rename = "@own")] + pub own: Option, + #[serde(rename = "@own_prefix")] + pub own_prefix: Option, + #[serde(rename = "@user")] + pub user: Option, + #[serde(rename = "@group")] + pub group: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(default)] +pub(super) struct RawSelinux { + pub associate: Vec, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub(super) struct RawTypeElement { + #[serde(rename = "$text")] + pub text: Type, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub(super) struct RawUserElement { + #[serde(rename = "$text")] + pub text: Principal, +} From f88031f30f9514eff1731d920dc5ae5e6173846d Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Mon, 7 Oct 2024 17:25:17 +1100 Subject: [PATCH 5/6] fix(xml): simplify `PathBuf` fields on `Configuration` --- src/configuration.rs | 31 +++++++++++-------------------- src/configuration/raw.rs | 17 +++++++++++++---- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index 595f78d..de23c34 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -8,10 +8,11 @@ use serde::Deserialize; mod raw; -#[derive(Clone, Debug, Deserialize, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum ApparmorMode { Disabled, + #[default] Enabled, Required, } @@ -27,13 +28,12 @@ pub struct Associate { #[derive(Clone, Debug, Default, PartialEq)] pub struct Configuration { allow_anonymous: Option, - apparmor: Option, + apparmor: ApparmorMode, auth: Vec, fork: Option, // TODO: consider processing `include` more to remove XML-specific structure include: Vec, - // TODO: consider processing `include` more to remove XML-specific structure - includedir: Vec, + includedir: Vec, keep_umask: Option, // TODO: consider processing `include` more to remove XML-specific structure limit: Vec, @@ -41,8 +41,7 @@ pub struct Configuration { pidfile: Option, policy: Vec, selinux: Vec, - // TODO: consider processing `include` more to remove XML-specific structure - servicedir: Vec, + servicedir: Vec, servicehelper: Option, standard_session_servicedirs: Option, standard_system_servicedirs: Option, @@ -78,11 +77,12 @@ impl TryFrom for Configuration { apparmor: match value.apparmor { Some(a) => a.mode, None => None, - }, + } + .unwrap_or_default(), auth: value.auth, fork: value.fork.map(|_| true), include: value.include, - includedir: value.includedir, + includedir: value.includedir.into_iter().map(|pb| pb.text).collect(), keep_umask: value.keep_umask.map(|_| true), limit: value.limit, listen: value.listen, @@ -94,7 +94,7 @@ impl TryFrom for Configuration { Some(s) => s.associate, None => vec![], }, - servicedir: value.servicedir, + servicedir: value.servicedir.into_iter().map(|pb| pb.text).collect(), servicehelper: value.servicehelper, standard_session_servicedirs: value.standard_session_servicedirs.map(|_| true), standard_system_servicedirs: value.standard_system_servicedirs.map(|_| true), @@ -180,15 +180,6 @@ pub struct OwnRule { own_prefix: Option, } -// reuse this between Vec fields, -// except those with field-specific attributes -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub struct PathBufElement { - #[serde(rename = "$text")] - text: PathBuf, -} - #[derive(Clone, Debug, PartialEq)] pub enum Policy { Console { rules: Vec }, @@ -635,7 +626,7 @@ mod tests { "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> - + @@ -647,7 +638,7 @@ mod tests { assert_eq!( got, Configuration { - apparmor: Some(ApparmorMode::Enabled), + apparmor: ApparmorMode::Required, selinux: vec![Associate { context: String::from("foo_t"), own: String::from("org.freedesktop.Foobar") diff --git a/src/configuration/raw.rs b/src/configuration/raw.rs index c003c31..8ab7407 100644 --- a/src/configuration/raw.rs +++ b/src/configuration/raw.rs @@ -5,8 +5,8 @@ use std::{path::PathBuf, str::FromStr}; use serde::Deserialize; use super::{ - ApparmorMode, Associate, IncludeElement, LimitElement, PathBufElement, Principal, RuleMatch, - RuleMatchType, Type, + ApparmorMode, Associate, IncludeElement, LimitElement, Principal, RuleMatch, RuleMatchType, + Type, }; #[derive(Clone, Debug, Deserialize, PartialEq)] @@ -24,14 +24,14 @@ pub(super) struct RawConfiguration { pub auth: Vec, pub fork: Option<()>, pub include: Vec, - pub includedir: Vec, + pub includedir: Vec, pub keep_umask: Option<()>, pub limit: Vec, pub listen: Vec, pub pidfile: Option, pub policy: Vec, pub selinux: Option, - pub servicedir: Vec, + pub servicedir: Vec, pub servicehelper: Option, pub standard_session_servicedirs: Option<()>, pub standard_system_servicedirs: Option<()>, @@ -144,3 +144,12 @@ pub(super) struct RawUserElement { #[serde(rename = "$text")] pub text: Principal, } + +// reuse this between Vec fields, +// except those with field-specific attributes +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub(super) struct RawPathBufElement { + #[serde(rename = "$text")] + pub text: PathBuf, +} From 7ec0995d17012c4ed70731b3f500ccf614178397 Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Mon, 7 Oct 2024 17:37:25 +1100 Subject: [PATCH 6/6] chore(xml): extract `Policy...` and `Rule...` out into separate module --- src/configuration.rs | 272 +----------------------------------- src/configuration/policy.rs | 270 +++++++++++++++++++++++++++++++++++ src/configuration/raw.rs | 4 +- 3 files changed, 279 insertions(+), 267 deletions(-) create mode 100644 src/configuration/policy.rs diff --git a/src/configuration.rs b/src/configuration.rs index de23c34..95756c7 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -3,11 +3,13 @@ use std::{path::PathBuf, str::FromStr}; -use raw::{RawConfiguration, RawPolicy, RawPolicyContext, RawRule, RawRuleAttributes}; use serde::Deserialize; +mod policy; mod raw; +use crate::configuration::{policy::Policy, raw::RawConfiguration}; + #[derive(Clone, Debug, Default, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum ApparmorMode { @@ -114,12 +116,6 @@ impl TryFrom for Configuration { } } -#[derive(Clone, Debug, Default, PartialEq)] -pub struct ConnectRule { - group: Option, - user: Option, -} - #[derive(Clone, Debug)] pub enum Error { DeserializeXml(quick_xml::DeError), @@ -174,73 +170,6 @@ pub enum LimitName { ReplyTimeout, } -#[derive(Clone, Debug, Default, PartialEq)] -pub struct OwnRule { - own: Option, - own_prefix: Option, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Policy { - Console { rules: Vec }, - DefaultContext { rules: Vec }, - Group { group: Principal, rules: Vec }, - MandatoryContext { rules: Vec }, - NoConsole { rules: Vec }, - User { user: Principal, rules: Vec }, -} -impl TryFrom for Policy { - type Error = Error; - fn try_from(value: RawPolicy) -> Result { - let mut rules: Vec = Vec::with_capacity(value.rules.len()); - for rule in value.rules { - match Rule::try_from(rule) { - Ok(ok) => rules.push(ok), - Err(err) => return Err(err), - } - } - - match value { - RawPolicy { - at_console: Some(b), - context: None, - group: None, - user: None, - .. - } => Ok(match b { - true => Self::Console { rules }, - false => Self::NoConsole { rules }, - }), - RawPolicy { - at_console: None, - context: Some(pc), - group: None, - user: None, - .. - } => Ok(match pc { - RawPolicyContext::Default => Self::DefaultContext { rules }, - RawPolicyContext::Mandatory => Self::MandatoryContext { rules }, - }), - RawPolicy { - at_console: None, - context: None, - group: Some(p), - user: None, - .. - } => Ok(Self::Group { group: p, rules }), - RawPolicy { - at_console: None, - context: None, - group: None, - user: Some(p), - .. - } => Ok(Self::User { user: p, rules }), - _ => Err(Error::PolicyHasMultipleAttributes), - } - } -} -// TODO: impl PartialOrd/Ord for Policy, for order in which policies are applied to a connection - #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase", untagged)] pub enum Principal { @@ -248,197 +177,6 @@ pub enum Principal { Name(String), } -#[derive(Clone, Debug, Default, PartialEq)] -pub struct ReceiveRule { - eavesdrop: Option, - receive_error: Option, - receive_interface: Option, - receive_member: Option, - receive_path: Option, - receive_requested_reply: Option, - receive_sender: Option, - receive_type: Option, -} - -pub type Rule = (RuleEffect, RulePhase); -impl TryFrom for Rule { - type Error = Error; - - fn try_from(value: RawRule) -> Result { - let (effect, attributes) = match value { - RawRule::Allow(attributes) => (RuleEffect::Allow, attributes), - RawRule::Deny(attributes) => (RuleEffect::Deny, attributes), - }; - match attributes { - RawRuleAttributes { - eavesdrop, - group: None, - own: None, - own_prefix: None, - receive_error, - receive_interface, - receive_member, - receive_path, - receive_requested_reply, - receive_sender, - receive_type, - 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, - } => Ok(( - effect, - RulePhase::Receive(ReceiveRule { - eavesdrop, - receive_error, - receive_interface, - receive_member, - receive_path, - receive_requested_reply, - receive_sender, - receive_type, - }), - )), - RawRuleAttributes { - eavesdrop, - group: None, - own: None, - own_prefix: None, - receive_error: None, - receive_interface: None, - receive_member: None, - receive_path: None, - receive_requested_reply: None, - receive_sender: None, - receive_type: None, - send_broadcast, - send_destination, - send_destination_prefix, - send_error, - send_interface, - send_member, - send_path, - send_requested_reply, - send_type, - user: None, - } => Ok(( - effect, - RulePhase::Send(SendRule { - eavesdrop, - send_broadcast, - send_destination, - send_destination_prefix, - send_error, - send_interface, - send_member, - send_path, - send_requested_reply, - send_type, - }), - )), - RawRuleAttributes { - eavesdrop: None, - group: None, - own, - own_prefix, - receive_error: None, - receive_interface: None, - receive_member: None, - receive_path: None, - receive_requested_reply: None, - receive_sender: None, - receive_type: 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, - } => Ok((effect, RulePhase::Own(OwnRule { own, own_prefix }))), - RawRuleAttributes { - eavesdrop: None, - group, - own: None, - own_prefix: None, - receive_error: None, - receive_interface: None, - receive_member: None, - receive_path: None, - receive_requested_reply: None, - receive_sender: None, - receive_type: 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, - } => Ok((effect, RulePhase::Connect(ConnectRule { group, user }))), - _ => Err(Error::RuleHasInvalidCombinationOfAttributes), - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum RuleEffect { - Allow, - Deny, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum RulePhase { - Connect(ConnectRule), - Own(OwnRule), - Receive(ReceiveRule), - Send(SendRule), -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case", untagged)] -pub enum RuleMatch { - #[serde(rename = "*")] - Any, - One(String), -} -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum RuleMatchType { - #[serde(rename = "*")] - Any, - Error, - MethodCall, - MethodReturn, - Signal, -} - -#[derive(Clone, Debug, Default, PartialEq)] -pub struct SendRule { - eavesdrop: Option, - send_broadcast: Option, - send_destination: Option, - send_destination_prefix: Option, - send_error: Option, - send_interface: Option, - send_member: Option, - send_path: Option, - send_requested_reply: Option, - send_type: Option, -} - #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Type { @@ -448,6 +186,10 @@ pub enum Type { #[cfg(test)] mod tests { + use crate::configuration::policy::{ + OwnRule, ReceiveRule, RuleEffect, RuleMatch, RulePhase, SendRule, + }; + use super::*; #[test] diff --git a/src/configuration/policy.rs b/src/configuration/policy.rs new file mode 100644 index 0000000..0a68323 --- /dev/null +++ b/src/configuration/policy.rs @@ -0,0 +1,270 @@ +use serde::Deserialize; + +use super::{ + raw::{RawPolicy, RawPolicyContext, RawRule, RawRuleAttributes}, + Error, Principal, +}; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ConnectRule { + pub group: Option, + pub user: Option, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct OwnRule { + pub own: Option, + pub own_prefix: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Policy { + Console { rules: Vec }, + DefaultContext { rules: Vec }, + Group { group: Principal, rules: Vec }, + MandatoryContext { rules: Vec }, + NoConsole { rules: Vec }, + User { user: Principal, rules: Vec }, +} +impl TryFrom for Policy { + type Error = Error; + fn try_from(value: RawPolicy) -> Result { + let mut rules: Vec = Vec::with_capacity(value.rules.len()); + for rule in value.rules { + match Rule::try_from(rule) { + Ok(ok) => rules.push(ok), + Err(err) => return Err(err), + } + } + + match value { + RawPolicy { + at_console: Some(b), + context: None, + group: None, + user: None, + .. + } => Ok(match b { + true => Self::Console { rules }, + false => Self::NoConsole { rules }, + }), + RawPolicy { + at_console: None, + context: Some(pc), + group: None, + user: None, + .. + } => Ok(match pc { + RawPolicyContext::Default => Self::DefaultContext { rules }, + RawPolicyContext::Mandatory => Self::MandatoryContext { rules }, + }), + RawPolicy { + at_console: None, + context: None, + group: Some(p), + user: None, + .. + } => Ok(Self::Group { group: p, rules }), + RawPolicy { + at_console: None, + context: None, + group: None, + user: Some(p), + .. + } => Ok(Self::User { user: p, rules }), + _ => Err(Error::PolicyHasMultipleAttributes), + } + } +} +// TODO: impl PartialOrd/Ord for Policy, for order in which policies are applied to a connection + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ReceiveRule { + pub eavesdrop: Option, + pub receive_error: Option, + pub receive_interface: Option, + pub receive_member: Option, + pub receive_path: Option, + pub receive_requested_reply: Option, + pub receive_sender: Option, + pub receive_type: Option, +} + +pub type Rule = (RuleEffect, RulePhase); +impl TryFrom for Rule { + type Error = Error; + + fn try_from(value: RawRule) -> Result { + let (effect, attributes) = match value { + RawRule::Allow(attributes) => (RuleEffect::Allow, attributes), + RawRule::Deny(attributes) => (RuleEffect::Deny, attributes), + }; + match attributes { + RawRuleAttributes { + eavesdrop, + group: None, + own: None, + own_prefix: None, + receive_error, + receive_interface, + receive_member, + receive_path, + receive_requested_reply, + receive_sender, + receive_type, + 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, + } => Ok(( + effect, + RulePhase::Receive(ReceiveRule { + eavesdrop, + receive_error, + receive_interface, + receive_member, + receive_path, + receive_requested_reply, + receive_sender, + receive_type, + }), + )), + RawRuleAttributes { + eavesdrop, + group: None, + own: None, + own_prefix: None, + receive_error: None, + receive_interface: None, + receive_member: None, + receive_path: None, + receive_requested_reply: None, + receive_sender: None, + receive_type: None, + send_broadcast, + send_destination, + send_destination_prefix, + send_error, + send_interface, + send_member, + send_path, + send_requested_reply, + send_type, + user: None, + } => Ok(( + effect, + RulePhase::Send(SendRule { + eavesdrop, + send_broadcast, + send_destination, + send_destination_prefix, + send_error, + send_interface, + send_member, + send_path, + send_requested_reply, + send_type, + }), + )), + RawRuleAttributes { + eavesdrop: None, + group: None, + own, + own_prefix, + receive_error: None, + receive_interface: None, + receive_member: None, + receive_path: None, + receive_requested_reply: None, + receive_sender: None, + receive_type: 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, + } => Ok((effect, RulePhase::Own(OwnRule { own, own_prefix }))), + RawRuleAttributes { + eavesdrop: None, + group, + own: None, + own_prefix: None, + receive_error: None, + receive_interface: None, + receive_member: None, + receive_path: None, + receive_requested_reply: None, + receive_sender: None, + receive_type: 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, + } => Ok((effect, RulePhase::Connect(ConnectRule { group, user }))), + _ => Err(Error::RuleHasInvalidCombinationOfAttributes), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum RuleEffect { + Allow, + Deny, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum RulePhase { + Connect(ConnectRule), + Own(OwnRule), + Receive(ReceiveRule), + Send(SendRule), +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case", untagged)] +pub enum RuleMatch { + #[serde(rename = "*")] + Any, + One(String), +} +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum RuleMatchType { + #[serde(rename = "*")] + Any, + Error, + MethodCall, + MethodReturn, + Signal, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct SendRule { + pub eavesdrop: Option, + pub send_broadcast: Option, + pub send_destination: Option, + pub send_destination_prefix: Option, + pub send_error: Option, + pub send_interface: Option, + pub send_member: Option, + pub send_path: Option, + pub send_requested_reply: Option, + pub send_type: Option, +} diff --git a/src/configuration/raw.rs b/src/configuration/raw.rs index 8ab7407..243c597 100644 --- a/src/configuration/raw.rs +++ b/src/configuration/raw.rs @@ -5,8 +5,8 @@ use std::{path::PathBuf, str::FromStr}; use serde::Deserialize; use super::{ - ApparmorMode, Associate, IncludeElement, LimitElement, Principal, RuleMatch, RuleMatchType, - Type, + policy::{RuleMatch, RuleMatchType}, + ApparmorMode, Associate, IncludeElement, LimitElement, Principal, Type, }; #[derive(Clone, Debug, Deserialize, PartialEq)]