From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 8FB80BE3F for ; Fri, 25 Nov 2022 16:16:12 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 367711E434 for ; Fri, 25 Nov 2022 16:15:42 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Fri, 25 Nov 2022 16:15:40 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 96A7D44EA8 for ; Fri, 25 Nov 2022 16:15:40 +0100 (CET) From: Fiona Ebner To: pbs-devel@lists.proxmox.com Date: Fri, 25 Nov 2022 16:15:35 +0100 Message-Id: <20221125151536.190947-1-f.ebner@proxmox.com> X-Mailer: git-send-email 2.30.2 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.027 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pbs-devel] [PATCH proxmox] section config: support allowing unknown section types X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 25 Nov 2022 15:16:12 -0000 Similar to commit c9ede1c ("support unknown types in section config") in pve-common. Unknown sections are parsed as String-JSON String key-value pairs without any additional checks and also written as-is. Suggested-by: Wolfgang Bumiller Signed-off-by: Fiona Ebner --- proxmox-section-config/src/lib.rs | 202 +++++++++++++++++++++++++----- 1 file changed, 174 insertions(+), 28 deletions(-) diff --git a/proxmox-section-config/src/lib.rs b/proxmox-section-config/src/lib.rs index d9978d1..f5bc315 100644 --- a/proxmox-section-config/src/lib.rs +++ b/proxmox-section-config/src/lib.rs @@ -90,11 +90,14 @@ pub struct SectionConfig { fn(type_name: &str, section_id: &str, data: &Value) -> Result, format_section_content: fn(type_name: &str, section_id: &str, key: &str, value: &Value) -> Result, + + allow_unknown_sections: bool, } enum ParseState<'a> { BeforeHeader, InsideSection(&'a SectionConfigPlugin, String, Value), + InsideUnknownSection(String, String, Value), } /// Interface to manipulate configuration data @@ -238,6 +241,7 @@ impl SectionConfig { parse_section_content: Self::default_parse_section_content, format_section_header: Self::default_format_section_header, format_section_content: Self::default_format_section_content, + allow_unknown_sections: false, } } @@ -250,6 +254,7 @@ impl SectionConfig { parse_section_content: Self::systemd_parse_section_content, format_section_header: Self::systemd_format_section_header, format_section_content: Self::systemd_format_section_content, + allow_unknown_sections: false, } } @@ -277,9 +282,15 @@ impl SectionConfig { parse_section_content, format_section_header, format_section_content, + allow_unknown_sections: false, } } + pub const fn allow_unknown_sections(mut self, allow_unknown_sections: bool) -> Self { + self.allow_unknown_sections = allow_unknown_sections; + self + } + /// Register a plugin, which defines the `Schema` for a section type. pub fn register_plugin(&mut self, plugin: SectionConfigPlugin) { self.plugins.insert(plugin.type_name.clone(), plugin); @@ -324,32 +335,53 @@ impl SectionConfig { for section_id in list { let (type_name, section_config) = config.sections.get(section_id).unwrap(); - let plugin = self.plugins.get(type_name).unwrap(); - let id_schema = plugin.get_id_schema().unwrap_or(self.id_schema); - if let Err(err) = id_schema.parse_simple_value(section_id) { - bail!("syntax error in section identifier: {}", err.to_string()); - } - if section_id.chars().any(|c| c.is_control()) { - bail!("detected unexpected control character in section ID."); - } - if let Err(err) = plugin.properties.verify_json(section_config) { - bail!("verify section '{}' failed - {}", section_id, err); - } + match self.plugins.get(type_name) { + Some(plugin) => { + let id_schema = plugin.get_id_schema().unwrap_or(self.id_schema); + if let Err(err) = id_schema.parse_simple_value(section_id) { + bail!("syntax error in section identifier: {}", err.to_string()); + } + if section_id.chars().any(|c| c.is_control()) { + bail!("detected unexpected control character in section ID."); + } + if let Err(err) = plugin.properties.verify_json(section_config) { + bail!("verify section '{}' failed - {}", section_id, err); + } - if !raw.is_empty() { - raw += "\n" - } + if !raw.is_empty() { + raw += "\n" + } - raw += &(self.format_section_header)(type_name, section_id, section_config)?; + raw += &(self.format_section_header)(type_name, section_id, section_config)?; - for (key, value) in section_config.as_object().unwrap() { - if let Some(id_property) = &plugin.id_property { - if id_property == key { - continue; // skip writing out id properties, they are in the section header + for (key, value) in section_config.as_object().unwrap() { + if let Some(id_property) = &plugin.id_property { + if id_property == key { + continue; // skip writing out id properties, they are in the section header + } + } + raw += &(self.format_section_content)(type_name, section_id, key, value)?; } } - raw += &(self.format_section_content)(type_name, section_id, key, value)?; + None if self.allow_unknown_sections => { + if section_id.chars().any(|c| c.is_control()) { + bail!("detected unexpected control character in section ID."); + } + + if !raw.is_empty() { + raw += "\n" + } + + raw += &(self.format_section_header)(type_name, section_id, section_config)?; + + for (key, value) in section_config.as_object().unwrap() { + raw += &(self.format_section_content)(type_name, section_id, key, value)?; + } + } + None => { + bail!("unknown section type '{type_name}'"); + } } } @@ -415,6 +447,12 @@ impl SectionConfig { } state = ParseState::InsideSection(plugin, section_id, json!({})); + } else if self.allow_unknown_sections { + state = ParseState::InsideUnknownSection( + section_type, + section_id, + json!({}), + ); } else { bail!("unknown section type '{}'", section_type); } @@ -477,18 +515,48 @@ impl SectionConfig { bail!("syntax error (expected section properties)"); } } + ParseState::InsideUnknownSection( + ref section_type, + ref mut section_id, + ref mut config, + ) => { + if line.trim().is_empty() { + // finish section + result.set_data(section_id, section_type, config.take())?; + result.record_order(section_id); + + state = ParseState::BeforeHeader; + continue; + } + if let Some((key, value)) = (self.parse_section_content)(line) { + config[key] = json!(value); + } else { + bail!("syntax error (expected section properties)"); + } + } } } - if let ParseState::InsideSection(plugin, ref mut section_id, ref mut config) = state - { - // finish section - test_required_properties(config, plugin.properties, &plugin.id_property)?; - if let Some(id_property) = &plugin.id_property { - config[id_property] = Value::from(section_id.clone()); + match state { + ParseState::BeforeHeader => {} + ParseState::InsideSection(plugin, ref mut section_id, ref mut config) => { + // finish section + test_required_properties(config, plugin.properties, &plugin.id_property)?; + if let Some(id_property) = &plugin.id_property { + config[id_property] = Value::from(section_id.clone()); + } + result.set_data(section_id, &plugin.type_name, config)?; + result.record_order(section_id); + } + ParseState::InsideUnknownSection( + ref section_type, + ref mut section_id, + ref mut config, + ) => { + // finish section + result.set_data(section_id, section_type, config)?; + result.record_order(section_id); } - result.set_data(section_id, &plugin.type_name, config)?; - result.record_order(section_id); } Ok(()) @@ -960,6 +1028,84 @@ user: root@pam assert!(config.write(filename, &res.unwrap()).is_err()); } +#[test] +fn test_section_config_with_unknown_section_types() { + let filename = "user.cfg"; + + const ID_SCHEMA: Schema = StringSchema::new("default id schema.") + .min_length(3) + .schema(); + let mut config = SectionConfig::new(&ID_SCHEMA).allow_unknown_sections(true); + + const PROPERTIES: [(&str, bool, &proxmox_schema::Schema); 2] = [ + ( + "email", + false, + &StringSchema::new("The e-mail of the user").schema(), + ), + ( + "userid", + true, + &StringSchema::new("The id of the user (name@realm).") + .min_length(3) + .schema(), + ), + ]; + + const USER_PROPERTIES: ObjectSchema = ObjectSchema { + description: "user properties", + properties: &PROPERTIES, + additional_properties: false, + default_key: None, + }; + + let plugin = SectionConfigPlugin::new( + "user".to_string(), + Some("userid".to_string()), + &USER_PROPERTIES, + ); + config.register_plugin(plugin); + + let raw = r" + +user: root@pam + email root@example.com + +token: asdf@pbs!asdftoken + enable true + expire 0 +"; + + let check = |res: SectionConfigData| { + let (_, token_config) = res.sections.get("root@pam").unwrap(); + assert_eq!( + *token_config.get("email").unwrap(), + json!("root@example.com") + ); + + let (token_id, token_config) = res.sections.get("asdf@pbs!asdftoken").unwrap(); + assert_eq!(token_id, "token"); + assert_eq!(*token_config.get("enable").unwrap(), json!("true")); + assert_eq!(*token_config.get("expire").unwrap(), json!("0")); + }; + + let res = config.parse(filename, raw).unwrap(); + println!("RES: {:?}", res); + let written = config.write(filename, &res); + println!("CONFIG:\n{}", written.as_ref().unwrap()); + + check(res); + + let res = config.parse(filename, &written.unwrap()).unwrap(); + println!("RES second time: {:?}", res); + + check(res); + + let config = config.allow_unknown_sections(false); + + assert!(config.parse(filename, raw).is_err()); +} + /// Generate ReST Documentaion for ``SectionConfig`` pub fn dump_section_config(config: &SectionConfig) -> String { let mut res = String::new(); -- 2.30.2