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 B01A0A050B for ; Wed, 8 Nov 2023 16:40:19 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 1F062A504 for ; Wed, 8 Nov 2023 16:40:19 +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 ; Wed, 8 Nov 2023 16:40:10 +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 5788047490 for ; Wed, 8 Nov 2023 16:40:10 +0100 (CET) From: Lukas Wagner To: pve-devel@lists.proxmox.com Date: Wed, 8 Nov 2023 16:40:00 +0100 Message-Id: <20231108154005.895814-7-l.wagner@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20231108154005.895814-1-l.wagner@proxmox.com> References: <20231108154005.895814-1-l.wagner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.014 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy 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 T_SCC_BODY_TEXT_LINE -0.01 - Subject: [pve-devel] [PATCH v4 proxmox 06/11] notify: add api for smtp endpoints X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 08 Nov 2023 15:40:19 -0000 Signed-off-by: Lukas Wagner --- proxmox-notify/src/api/mod.rs | 33 +++ proxmox-notify/src/api/smtp.rs | 356 +++++++++++++++++++++++++++ proxmox-notify/src/endpoints/smtp.rs | 8 - 3 files changed, 389 insertions(+), 8 deletions(-) create mode 100644 proxmox-notify/src/api/smtp.rs diff --git a/proxmox-notify/src/api/mod.rs b/proxmox-notify/src/api/mod.rs index 8042157..762d448 100644 --- a/proxmox-notify/src/api/mod.rs +++ b/proxmox-notify/src/api/mod.rs @@ -1,3 +1,4 @@ +use serde::Serialize; use std::collections::HashSet; use proxmox_http_error::HttpError; @@ -10,6 +11,8 @@ pub mod gotify; pub mod matcher; #[cfg(feature = "sendmail")] pub mod sendmail; +#[cfg(feature = "smtp")] +pub mod smtp; // We have our own, local versions of http_err and http_bail, because // we don't want to wrap the error in anyhow::Error. If we were to do that, @@ -60,6 +63,10 @@ fn ensure_endpoint_exists(#[allow(unused)] config: &Config, name: &str) -> Resul { exists = exists || gotify::get_endpoint(config, name).is_ok(); } + #[cfg(feature = "smtp")] + { + exists = exists || smtp::get_endpoint(config, name).is_ok(); + } if !exists { http_bail!(NOT_FOUND, "endpoint '{name}' does not exist") @@ -100,6 +107,7 @@ fn get_referrers(config: &Config, entity: &str) -> Result, HttpE } } } + Ok(referrers) } @@ -148,6 +156,31 @@ fn get_referenced_entities(config: &Config, entity: &str) -> HashSet { expanded } +#[allow(unused)] +fn set_private_config_entry( + config: &mut Config, + private_config: &T, + typename: &str, + name: &str, +) -> Result<(), HttpError> { + config + .private_config + .set_data(name, typename, private_config) + .map_err(|e| { + http_err!( + INTERNAL_SERVER_ERROR, + "could not save private config for endpoint '{}': {e}", + name + ) + }) +} + +#[allow(unused)] +fn remove_private_config_entry(config: &mut Config, name: &str) -> Result<(), HttpError> { + config.private_config.sections.remove(name); + Ok(()) +} + #[cfg(test)] mod test_helpers { use crate::Config; diff --git a/proxmox-notify/src/api/smtp.rs b/proxmox-notify/src/api/smtp.rs new file mode 100644 index 0000000..bd9d7bb --- /dev/null +++ b/proxmox-notify/src/api/smtp.rs @@ -0,0 +1,356 @@ +use proxmox_http_error::HttpError; + +use crate::api::{http_bail, http_err}; +use crate::endpoints::smtp::{ + DeleteableSmtpProperty, SmtpConfig, SmtpConfigUpdater, SmtpPrivateConfig, + SmtpPrivateConfigUpdater, SMTP_TYPENAME, +}; +use crate::Config; + +/// Get a list of all smtp endpoints. +/// +/// The caller is responsible for any needed permission checks. +/// Returns a list of all smtp endpoints or a `HttpError` if the config is +/// erroneous (`500 Internal server error`). +pub fn get_endpoints(config: &Config) -> Result, HttpError> { + config + .config + .convert_to_typed_array(SMTP_TYPENAME) + .map_err(|e| http_err!(NOT_FOUND, "Could not fetch endpoints: {e}")) +} + +/// Get smtp endpoint with given `name`. +/// +/// The caller is responsible for any needed permission checks. +/// Returns the endpoint or a `HttpError` if the endpoint was not found (`404 Not found`). +pub fn get_endpoint(config: &Config, name: &str) -> Result { + config + .config + .lookup(SMTP_TYPENAME, name) + .map_err(|_| http_err!(NOT_FOUND, "endpoint '{name}' not found")) +} + +/// Add a new smtp endpoint. +/// +/// The caller is responsible for any needed permission checks. +/// The caller also responsible for locking the configuration files. +/// Returns a `HttpError` if: +/// - an entity with the same name already exists (`400 Bad request`) +/// - the configuration could not be saved (`500 Internal server error`) +/// - mailto *and* mailto_user are both set to `None` +pub fn add_endpoint( + config: &mut Config, + endpoint_config: &SmtpConfig, + private_endpoint_config: &SmtpPrivateConfig, +) -> Result<(), HttpError> { + if endpoint_config.name != private_endpoint_config.name { + // Programming error by the user of the crate, thus we panic + panic!("name for endpoint config and private config must be identical"); + } + + super::ensure_unique(config, &endpoint_config.name)?; + + if endpoint_config.mailto.is_none() && endpoint_config.mailto_user.is_none() { + http_bail!( + BAD_REQUEST, + "must at least provide one recipient, either in mailto or in mailto-user" + ); + } + + super::set_private_config_entry( + config, + private_endpoint_config, + SMTP_TYPENAME, + &endpoint_config.name, + )?; + + config + .config + .set_data(&endpoint_config.name, SMTP_TYPENAME, endpoint_config) + .map_err(|e| { + http_err!( + INTERNAL_SERVER_ERROR, + "could not save endpoint '{}': {e}", + endpoint_config.name + ) + }) +} + +/// Update existing smtp endpoint +/// +/// The caller is responsible for any needed permission checks. +/// The caller also responsible for locking the configuration files. +/// Returns a `HttpError` if: +/// - the configuration could not be saved (`500 Internal server error`) +/// - mailto *and* mailto_user are both set to `None` +pub fn update_endpoint( + config: &mut Config, + name: &str, + updater: &SmtpConfigUpdater, + private_endpoint_config_updater: &SmtpPrivateConfigUpdater, + delete: Option<&[DeleteableSmtpProperty]>, + digest: Option<&[u8]>, +) -> Result<(), HttpError> { + super::verify_digest(config, digest)?; + + let mut endpoint = get_endpoint(config, name)?; + + if let Some(delete) = delete { + for deleteable_property in delete { + match deleteable_property { + DeleteableSmtpProperty::Author => endpoint.author = None, + DeleteableSmtpProperty::Comment => endpoint.comment = None, + DeleteableSmtpProperty::Mailto => endpoint.mailto = None, + DeleteableSmtpProperty::MailtoUser => endpoint.mailto_user = None, + DeleteableSmtpProperty::Password => super::set_private_config_entry( + config, + &SmtpPrivateConfig { + name: name.to_string(), + password: None, + }, + SMTP_TYPENAME, + name, + )?, + DeleteableSmtpProperty::Port => endpoint.port = None, + DeleteableSmtpProperty::Username => endpoint.username = None, + } + } + } + + if let Some(mailto) = &updater.mailto { + endpoint.mailto = Some(mailto.iter().map(String::from).collect()); + } + if let Some(mailto_user) = &updater.mailto_user { + endpoint.mailto_user = Some(mailto_user.iter().map(String::from).collect()); + } + if let Some(from_address) = &updater.from_address { + endpoint.from_address = from_address.into(); + } + if let Some(server) = &updater.server { + endpoint.server = server.into(); + } + if let Some(port) = &updater.port { + endpoint.port = Some(*port); + } + if let Some(username) = &updater.username { + endpoint.username = Some(username.into()); + } + if let Some(mode) = &updater.mode { + endpoint.mode = Some(*mode); + } + if let Some(password) = &private_endpoint_config_updater.password { + super::set_private_config_entry( + config, + &SmtpPrivateConfig { + name: name.into(), + password: Some(password.into()), + }, + SMTP_TYPENAME, + name, + )?; + } + + if let Some(author) = &updater.author { + endpoint.author = Some(author.into()); + } + + if let Some(comment) = &updater.comment { + endpoint.comment = Some(comment.into()); + } + + if endpoint.mailto.is_none() && endpoint.mailto_user.is_none() { + http_bail!( + BAD_REQUEST, + "must at least provide one recipient, either in mailto or in mailto-user" + ); + } + + config + .config + .set_data(name, SMTP_TYPENAME, &endpoint) + .map_err(|e| { + http_err!( + INTERNAL_SERVER_ERROR, + "could not save endpoint '{}': {e}", + endpoint.name + ) + }) +} + +/// Delete existing smtp endpoint +/// +/// The caller is responsible for any needed permission checks. +/// The caller also responsible for locking the configuration files. +/// Returns a `HttpError` if: +/// - an entity with the same name already exists (`400 Bad request`) +/// - the configuration could not be saved (`500 Internal server error`) +pub fn delete_endpoint(config: &mut Config, name: &str) -> Result<(), HttpError> { + // Check if the endpoint exists + let _ = get_endpoint(config, name)?; + super::ensure_unused(config, name)?; + + super::remove_private_config_entry(config, name)?; + config.config.sections.remove(name); + + Ok(()) +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::api::test_helpers::*; + use crate::endpoints::smtp::SmtpMode; + + pub fn add_smtp_endpoint_for_test(config: &mut Config, name: &str) -> Result<(), HttpError> { + add_endpoint( + config, + &SmtpConfig { + name: name.into(), + mailto: Some(vec!["user1@example.com".into()]), + mailto_user: None, + from_address: "from@example.com".into(), + author: Some("root".into()), + comment: Some("Comment".into()), + mode: Some(SmtpMode::StartTls), + server: "localhost".into(), + port: Some(555), + username: Some("username".into()), + }, + &SmtpPrivateConfig { + name: name.into(), + password: Some("password".into()), + }, + )?; + + assert!(get_endpoint(config, name).is_ok()); + Ok(()) + } + + #[test] + fn test_smtp_create() -> Result<(), HttpError> { + let mut config = empty_config(); + + assert_eq!(get_endpoints(&config)?.len(), 0); + add_smtp_endpoint_for_test(&mut config, "smtp-endpoint")?; + + // Endpoints must have a unique name + assert!(add_smtp_endpoint_for_test(&mut config, "smtp-endpoint").is_err()); + assert_eq!(get_endpoints(&config)?.len(), 1); + Ok(()) + } + + #[test] + fn test_update_not_existing_returns_error() -> Result<(), HttpError> { + let mut config = empty_config(); + + assert!(update_endpoint( + &mut config, + "test", + &Default::default(), + &Default::default(), + None, + None, + ) + .is_err()); + + Ok(()) + } + + #[test] + fn test_update_invalid_digest_returns_error() -> Result<(), HttpError> { + let mut config = empty_config(); + add_smtp_endpoint_for_test(&mut config, "sendmail-endpoint")?; + + assert!(update_endpoint( + &mut config, + "sendmail-endpoint", + &Default::default(), + &Default::default(), + None, + Some(&[0; 32]), + ) + .is_err()); + + Ok(()) + } + + #[test] + fn test_update() -> Result<(), HttpError> { + let mut config = empty_config(); + add_smtp_endpoint_for_test(&mut config, "smtp-endpoint")?; + + let digest = config.digest; + + update_endpoint( + &mut config, + "smtp-endpoint", + &SmtpConfigUpdater { + mailto: Some(vec!["user2@example.com".into(), "user3@example.com".into()]), + mailto_user: Some(vec!["root@pam".into()]), + from_address: Some("root@example.com".into()), + author: Some("newauthor".into()), + comment: Some("new comment".into()), + mode: Some(SmtpMode::Insecure), + server: Some("pali".into()), + port: Some(444), + username: Some("newusername".into()), + ..Default::default() + }, + &Default::default(), + None, + Some(&digest), + )?; + + let endpoint = get_endpoint(&config, "smtp-endpoint")?; + + assert_eq!( + endpoint.mailto, + Some(vec![ + "user2@example.com".to_string(), + "user3@example.com".to_string() + ]) + ); + assert_eq!(endpoint.mailto_user, Some(vec!["root@pam".to_string(),])); + assert_eq!(endpoint.from_address, "root@example.com".to_string()); + assert_eq!(endpoint.author, Some("newauthor".to_string())); + assert_eq!(endpoint.comment, Some("new comment".to_string())); + + // Test property deletion + update_endpoint( + &mut config, + "smtp-endpoint", + &Default::default(), + &Default::default(), + Some(&[ + DeleteableSmtpProperty::Author, + DeleteableSmtpProperty::MailtoUser, + DeleteableSmtpProperty::Port, + DeleteableSmtpProperty::Username, + DeleteableSmtpProperty::Comment, + ]), + None, + )?; + + let endpoint = get_endpoint(&config, "smtp-endpoint")?; + + assert_eq!(endpoint.author, None); + assert_eq!(endpoint.comment, None); + assert_eq!(endpoint.port, None); + assert_eq!(endpoint.username, None); + assert_eq!(endpoint.mailto_user, None); + + Ok(()) + } + + #[test] + fn test_delete() -> Result<(), HttpError> { + let mut config = empty_config(); + add_smtp_endpoint_for_test(&mut config, "smtp-endpoint")?; + + delete_endpoint(&mut config, "smtp-endpoint")?; + assert!(delete_endpoint(&mut config, "smtp-endpoint").is_err()); + assert_eq!(get_endpoints(&config)?.len(), 0); + + Ok(()) + } +} diff --git a/proxmox-notify/src/endpoints/smtp.rs b/proxmox-notify/src/endpoints/smtp.rs index 9c92da0..a6899b4 100644 --- a/proxmox-notify/src/endpoints/smtp.rs +++ b/proxmox-notify/src/endpoints/smtp.rs @@ -58,10 +58,6 @@ pub enum SmtpMode { optional: true, schema: COMMENT_SCHEMA, }, - filter: { - optional: true, - schema: ENTITY_NAME_SCHEMA, - }, }, )] #[derive(Debug, Serialize, Deserialize, Updater, Default)] @@ -95,9 +91,6 @@ pub struct SmtpConfig { /// Comment #[serde(skip_serializing_if = "Option::is_none")] pub comment: Option, - /// Filter to apply - #[serde(skip_serializing_if = "Option::is_none")] - pub filter: Option, } #[derive(Serialize, Deserialize)] @@ -105,7 +98,6 @@ pub struct SmtpConfig { pub enum DeleteableSmtpProperty { Author, Comment, - Filter, Mailto, MailtoUser, Password, -- 2.39.2