all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
To: pve-devel@lists.proxmox.com, pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox v5 05/27] notify: smtp: update API with OAuth2 parameters
Date: Tue,  5 May 2026 10:32:26 +0200	[thread overview]
Message-ID: <20260505083248.36450-6-a.bied-charreton@proxmox.com> (raw)
In-Reply-To: <20260505083248.36450-1-a.bied-charreton@proxmox.com>

Add the OAuth2 client & tenant IDs to the public config, and the client
secret to the private config.

The refresh token is more dynamic and will be updated on the fly by
proxmox-notify. In order to avoid the config file changing without user
interaction, it is passed as its own parameter and will be managed
separately in per-endpoint state files.

Signed-off-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 proxmox-notify/src/api/smtp.rs       | 101 ++++++++++++++++++++-------
 proxmox-notify/src/endpoints/smtp.rs |  45 +++++++++++-
 2 files changed, 118 insertions(+), 28 deletions(-)

diff --git a/proxmox-notify/src/api/smtp.rs b/proxmox-notify/src/api/smtp.rs
index 470701bf..d1482047 100644
--- a/proxmox-notify/src/api/smtp.rs
+++ b/proxmox-notify/src/api/smtp.rs
@@ -1,9 +1,10 @@
 use proxmox_http_error::HttpError;
 
 use crate::api::{http_bail, http_err};
+use crate::context::context;
 use crate::endpoints::smtp::{
-    DeleteableSmtpProperty, SmtpConfig, SmtpConfigUpdater, SmtpPrivateConfig,
-    SmtpPrivateConfigUpdater, SMTP_TYPENAME,
+    DeleteableSmtpProperty, SmtpAuthMethod, SmtpConfig, SmtpConfigUpdater, SmtpPrivateConfig,
+    SmtpPrivateConfigUpdater, State, SMTP_TYPENAME,
 };
 use crate::Config;
 
@@ -30,6 +31,31 @@ pub fn get_endpoint(config: &Config, name: &str) -> Result<SmtpConfig, HttpError
         .map_err(|_| http_err!(NOT_FOUND, "endpoint '{name}' not found"))
 }
 
+/// Apply `updater` to the private config identified by `name`, and set
+/// the private config entry afterwards.
+fn update_private_config(
+    config: &mut Config,
+    name: &str,
+    updater: impl FnOnce(&mut SmtpPrivateConfig),
+) -> Result<(), HttpError> {
+    let mut private_config: SmtpPrivateConfig = get_private_config(config, name)?;
+    updater(&mut private_config);
+
+    super::set_private_config_entry(config, private_config, SMTP_TYPENAME, name)
+}
+
+fn get_private_config(config: &Config, name: &str) -> Result<SmtpPrivateConfig, HttpError> {
+    config
+        .private_config
+        .lookup(SMTP_TYPENAME, name)
+        .map_err(|e| {
+            http_err!(
+                NOT_FOUND,
+                "no private config found for SMTP endpoint : '{e}'"
+            )
+        })
+}
+
 /// Add a new smtp endpoint.
 ///
 /// The caller is responsible for any needed permission checks.
@@ -38,10 +64,16 @@ pub fn get_endpoint(config: &Config, name: &str) -> Result<SmtpConfig, HttpError
 ///   - 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`
+///
+/// The OAuth2 refresh token lives in a per-endpoint state file rather than
+/// the notifications config, so it is passed through as a separate parameter.
+/// Callers pass `Some(_)` only when seeding the state at create time or after
+/// a re-authorization.
 pub fn add_endpoint(
     config: &mut Config,
     endpoint_config: SmtpConfig,
     private_endpoint_config: SmtpPrivateConfig,
+    oauth2_refresh_token: Option<String>,
 ) -> Result<(), HttpError> {
     if endpoint_config.name != private_endpoint_config.name {
         // Programming error by the user of the crate, thus we panic
@@ -83,11 +115,17 @@ pub fn add_endpoint(
 /// Returns a `HttpError` if:
 ///   - the configuration could not be saved (`500 Internal server error`)
 ///   - mailto *and* mailto_user are both set to `None`
+///
+/// The OAuth2 refresh token lives in a per-endpoint state file rather than
+/// the notifications config, so it is passed through as a separate parameter.
+/// Callers pass `Some(_)` only when seeding the state at create time or after
+/// a re-authorization.
 pub fn update_endpoint(
     config: &mut Config,
     name: &str,
     updater: SmtpConfigUpdater,
     private_endpoint_config_updater: SmtpPrivateConfigUpdater,
+    oauth2_refresh_token: Option<String>,
     delete: Option<&[DeleteableSmtpProperty]>,
     digest: Option<&[u8]>,
 ) -> Result<(), HttpError> {
@@ -103,20 +141,20 @@ pub fn update_endpoint(
                 DeleteableSmtpProperty::Disable => endpoint.disable = None,
                 DeleteableSmtpProperty::Mailto => endpoint.mailto.clear(),
                 DeleteableSmtpProperty::MailtoUser => endpoint.mailto_user.clear(),
-                DeleteableSmtpProperty::Password => super::set_private_config_entry(
-                    config,
-                    SmtpPrivateConfig {
-                        name: name.to_string(),
-                        password: None,
-                    },
-                    SMTP_TYPENAME,
-                    name,
-                )?,
+                DeleteableSmtpProperty::Password => {
+                    update_private_config(config, name, |c| c.password = None)?
+                }
+                DeleteableSmtpProperty::AuthMethod => endpoint.auth_method = None,
+                DeleteableSmtpProperty::OAuth2ClientId => endpoint.oauth2_client_id = None,
+                DeleteableSmtpProperty::OAuth2ClientSecret => {
+                    update_private_config(config, name, |c| c.oauth2_client_secret = None)?
+                }
+                DeleteableSmtpProperty::OAuth2TenantId => endpoint.oauth2_tenant_id = None,
                 DeleteableSmtpProperty::Port => endpoint.port = None,
                 DeleteableSmtpProperty::Username => endpoint.username = None,
             }
         }
-    }
+    };
 
     if let Some(mailto) = updater.mailto {
         endpoint.mailto = mailto;
@@ -139,29 +177,24 @@ pub fn update_endpoint(
     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),
-            },
-            SMTP_TYPENAME,
-            name,
-        )?;
+    if let Some(auth_method) = updater.auth_method {
+        endpoint.auth_method = Some(auth_method);
     }
-
     if let Some(author) = updater.author {
         endpoint.author = Some(author);
     }
-
     if let Some(comment) = updater.comment {
         endpoint.comment = Some(comment);
     }
-
     if let Some(disable) = updater.disable {
         endpoint.disable = Some(disable);
     }
+    if let Some(oauth2_client_id) = updater.oauth2_client_id {
+        endpoint.oauth2_client_id = Some(oauth2_client_id);
+    }
+    if let Some(oauth2_tenant_id) = updater.oauth2_tenant_id {
+        endpoint.oauth2_tenant_id = Some(oauth2_tenant_id);
+    }
 
     if endpoint.mailto.is_empty() && endpoint.mailto_user.is_empty() {
         http_bail!(
@@ -169,6 +202,14 @@ pub fn update_endpoint(
             "must at least provide one recipient, either in mailto or in mailto-user"
         );
     }
+    update_private_config(config, name, |c| {
+        if let Some(password) = private_endpoint_config_updater.password {
+            c.password = Some(password);
+        }
+        if let Some(oauth2_client_secret) = private_endpoint_config_updater.oauth2_client_secret {
+            c.oauth2_client_secret = Some(oauth2_client_secret);
+        }
+    })?;
 
     config
         .config
@@ -195,6 +236,7 @@ pub fn delete_endpoint(config: &mut Config, name: &str) -> Result<(), HttpError>
     super::ensure_safe_to_delete(config, name)?;
 
     super::remove_private_config_entry(config, name)?;
+
     config.config.sections.remove(name);
 
     Ok(())
@@ -204,7 +246,7 @@ pub fn delete_endpoint(config: &mut Config, name: &str) -> Result<(), HttpError>
 pub mod tests {
     use super::*;
     use crate::api::test_helpers::*;
-    use crate::endpoints::smtp::SmtpMode;
+    use crate::endpoints::smtp::{SmtpAuthMethod, SmtpMode};
 
     pub fn add_smtp_endpoint_for_test(config: &mut Config, name: &str) -> Result<(), HttpError> {
         add_endpoint(
@@ -217,6 +259,7 @@ pub mod tests {
                 author: Some("root".into()),
                 comment: Some("Comment".into()),
                 mode: Some(SmtpMode::StartTls),
+                auth_method: Some(SmtpAuthMethod::Plain),
                 server: "localhost".into(),
                 port: Some(555),
                 username: Some("username".into()),
@@ -225,7 +268,9 @@ pub mod tests {
             SmtpPrivateConfig {
                 name: name.into(),
                 password: Some("password".into()),
+                oauth2_client_secret: None,
             },
+            None,
         )?;
 
         assert!(get_endpoint(config, name).is_ok());
@@ -256,6 +301,7 @@ pub mod tests {
             Default::default(),
             None,
             None,
+            None,
         )
         .is_err());
 
@@ -273,6 +319,7 @@ pub mod tests {
             Default::default(),
             Default::default(),
             None,
+            None,
             Some(&[0; 32]),
         )
         .is_err());
@@ -304,6 +351,7 @@ pub mod tests {
             },
             Default::default(),
             None,
+            None,
             Some(&digest),
         )?;
 
@@ -327,6 +375,7 @@ pub mod tests {
             "smtp-endpoint",
             Default::default(),
             Default::default(),
+            None,
             Some(&[
                 DeleteableSmtpProperty::Author,
                 DeleteableSmtpProperty::MailtoUser,
diff --git a/proxmox-notify/src/endpoints/smtp.rs b/proxmox-notify/src/endpoints/smtp.rs
index 48fdbd8f..19b97113 100644
--- a/proxmox-notify/src/endpoints/smtp.rs
+++ b/proxmox-notify/src/endpoints/smtp.rs
@@ -83,11 +83,20 @@ pub struct SmtpConfig {
     pub port: Option<u16>,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub mode: Option<SmtpMode>,
+    /// Method to be used for authentication.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub auth_method: Option<SmtpAuthMethod>,
     /// Username to use during authentication.
-    /// If no username is set, no authentication will be performed.
-    /// The PLAIN and LOGIN authentication methods are supported
     #[serde(skip_serializing_if = "Option::is_none")]
     pub username: Option<String>,
+    /// Client ID for XOAUTH2 authentication method.
+    /// If set to `None`, no authentication will be performed.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub oauth2_client_id: Option<String>,
+    /// Tenant ID for XOAUTH2 authentication method. Only required for
+    /// Microsoft Exchange Online OAuth2.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub oauth2_tenant_id: Option<String>,
     /// Mail address to send a mail to.
     #[serde(default, skip_serializing_if = "Vec::is_empty")]
     #[updater(serde(skip_serializing_if = "Option::is_none"))]
@@ -134,12 +143,39 @@ pub enum DeleteableSmtpProperty {
     MailtoUser,
     /// Delete `password`
     Password,
+    /// Delete `auth_method`
+    AuthMethod,
+    /// Delete `oauth2_client_id`
+    #[serde(rename = "oauth2-client-id")]
+    OAuth2ClientId,
+    /// Delete `oauth2_client_secret`
+    #[serde(rename = "oauth2-client-secret")]
+    OAuth2ClientSecret,
+    /// Delete `oauth2_tenant_id`
+    #[serde(rename = "oauth2-tenant-id")]
+    OAuth2TenantId,
     /// Delete `port`
     Port,
     /// Delete `username`
     Username,
 }
 
+/// Authentication mode to use for SMTP.
+#[api]
+#[derive(Serialize, Deserialize, Clone, Debug, Default, Copy)]
+#[serde(rename_all = "kebab-case")]
+pub enum SmtpAuthMethod {
+    /// Username + password
+    #[default]
+    Plain,
+    /// Google OAuth2
+    #[serde(rename = "google-oauth2")]
+    GoogleOAuth2,
+    /// Microsoft OAuth2
+    #[serde(rename = "microsoft-oauth2")]
+    MicrosoftOAuth2,
+}
+
 #[api]
 #[derive(Serialize, Deserialize, Clone, Updater, Debug)]
 #[serde(rename_all = "kebab-case")]
@@ -150,9 +186,14 @@ pub struct SmtpPrivateConfig {
     /// Name of the endpoint
     #[updater(skip)]
     pub name: String,
+
     /// The password to use during authentication.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub password: Option<String>,
+
+    /// OAuth2 client secret
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub oauth2_client_secret: Option<String>,
 }
 
 /// A sendmail notification endpoint.
-- 
2.47.3




  parent reply	other threads:[~2026-05-05  8:35 UTC|newest]

Thread overview: 28+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-05  8:32 [PATCH docs/manager/proxmox{,-perl-rs,-widget-toolkit,-backup} v5 00/27] fix #7238: Add XOAUTH2 authentication support for SMTP notification targets Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox v5 01/27] add oauth2 and ureq to workspace dependencies Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox v5 02/27] notify: smtp: introduce xoauth2 module Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox v5 03/27] notify: smtp: introduce state management Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox v5 04/27] notify: smtp: factor out transport building logic Arthur Bied-Charreton
2026-05-05  8:32 ` Arthur Bied-Charreton [this message]
2026-05-05  8:32 ` [PATCH proxmox v5 06/27] notify: smtp: add API to exchange authorization code for refresh token Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox v5 07/27] notify: smtp: infer auth method for backwards compatibility Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox v5 08/27] notify: smtp: add state handling logic Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox v5 09/27] notify: smtp: add XOAUTH2 authentication support Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-perl-rs v5 10/27] pve-rs: notify: smtp: add OAuth2 parameters to bindings Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-perl-rs v5 11/27] pve-rs: notify: add binding for triggering state refresh Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-perl-rs v5 12/27] pve-rs: notify: add binding for initial OAuth2 refresh token exchange Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-widget-toolkit v5 13/27] utils: add OAuth2 flow handlers Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-widget-toolkit v5 14/27] utils: oauth2: add callback handler Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-widget-toolkit v5 15/27] notifications: add opt-in OAuth2 support for SMTP targets Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH pve-manager v5 16/27] notifications: smtp: api: add XOAUTH2 parameters Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH pve-manager v5 17/27] notifications: add endpoint for initial OAuth2 refresh token exchange Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH pve-manager v5 18/27] pveupdate: refresh notification targets' OAuth2 state Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH pve-manager v5 19/27] login: handle OAuth2 callback Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH pve-manager v5 20/27] fix #7238: notifications: smtp: add XOAUTH2 support Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-backup v5 21/27] notifications: add XOAUTH2 parameters to endpoints Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-backup v5 22/27] notifications: add endpoint for initial OAuth2 refresh token exchange Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-backup v5 23/27] login: handle OAuth2 callback Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-backup v5 24/27] fix #7238: notifications: smtp: add XOAUTH2 support Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-backup v5 25/27] daily-update: refresh OAuth2 state for SMTP notification endpoints Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH proxmox-backup v5 26/27] notifications: add OAuth2 section to SMTP targets docs Arthur Bied-Charreton
2026-05-05  8:32 ` [PATCH pve-docs v5 27/27] " Arthur Bied-Charreton

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260505083248.36450-6-a.bied-charreton@proxmox.com \
    --to=a.bied-charreton@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal