From: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
To: Lukas Wagner <l.wagner@proxmox.com>
Cc: pve-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox v2 02/16] notify: smtp: Introduce state management
Date: Thu, 9 Apr 2026 13:35:26 +0200 [thread overview]
Message-ID: <zntknpftbvchzpi6hb5ndz4klybb3gq2rcaod2df7ym2v3m2mu@xqo5j6houp7l> (raw)
In-Reply-To: <DHOJ5CVS1CUS.2FV2YHDLFX32I@proxmox.com>
On Thu, Apr 09, 2026 at 11:51:43AM +0200, Lukas Wagner wrote:
Thanks for the review :)
> Looking good!
>
> With the trivial suggestions implemented:
>
> Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
>
[...]
> > +use crate::endpoints::smtp::State;
>
> I think this import must be feature gated, otherwise a
>
> cargo build --no-default-features
>
> does not compile
>
[...]
> > +use crate::endpoints::smtp::State;
>
> This one here as well with
>
> cargo build --no-default-features --features=pbs-context
>
>
[...]
> > +use crate::endpoints::smtp::State;
>
> Also this one with
>
> cargo build --no-default-features --features=pve-context
>
>
Argh, should have tested these again sorry about that
> > use crate::renderer::TemplateSource;
> > use crate::Error;
> >
> > @@ -125,6 +126,19 @@ impl Context for PBSContext {
> > .map_err(|err| Error::Generic(format!("could not load template: {err}")))?;
> > Ok(template_string)
> > }
> > +
> > + fn load_oauth_state(&self, endpoint_name: &str) -> Result<State, Error> {
> > + let path = format!("/var/lib/proxmox-backup-priv/notifications/state-{endpoint_name}.json");
> > + State::load(path)
> > + }
> > +
> > + fn save_oauth_state(&self, endpoint_name: &str, state: Option<State>) -> Result<(), Error> {
> > + let path = format!("/var/lib/proxmox-backup-priv/notifications/state-{endpoint_name}.json");
>
> Sorry for the back and forth with the path. As discussed off-list, we
> now agreed on the following paths for the oauth state files:
>
> PBS: /var/lib/proxmox-backup/notifications/oauth-state/<endpoint>.json
> PDM: /var/lib/proxmox-datacenter-manager/notifications/oauth-state/<endpoint>.json
> PVE: /etc/pve/priv/notifications/oauth-state/<endpoint>.json
>
ACK
[...]
> > +#[derive(Serialize, Deserialize, Clone, Debug, Default)]
> > +#[serde(rename_all = "kebab-case")]
>
> No big deal, but this struct could use a doc comment.
>
You're right, will add one, thanks!
> > +pub struct State {
> > + #[serde(skip_serializing_if = "Option::is_none")]
> > + pub oauth2_refresh_token: Option<String>,
> > + pub last_refreshed: i64,
> > +}
> > +
> > +impl From<Option<String>> for State {
> > + fn from(value: Option<String>) -> Self {
> > + Self {
> > + oauth2_refresh_token: value,
> > + last_refreshed: proxmox_time::epoch_i64(),
> > + }
> > + }
> > +}
>
> This `impl From` feels a bit off to me, due to it being not 100%
> functional (due to the timestamp). Also `From<Option<...>>`
> feels a bit odd to me. Maybe just have a `State::new` instead?
>
> In the end-result of this series, you only ever call this From
> in the api handler, and there only if the refresh-token is provided.
> I guess you could have a State::new(refresh_token: String, timestamp:
> i64) then?
>
Yea you're right that a `new` method would be cleaner here, will update
this.
> > +
> > +/// Attempt to create a directory at `path` with `mode`, returning `Ok(())` if the directory either already
> > +/// exists, or was successfully created.
> > +///
> > +/// `pmxcfs` automatically sets the x-bit in directory permissions, however it does not allow the user
> > +/// to set it, the `fchmod` fails with `EPERM` on directories with `0o700`.
> > +///
> > +/// The `proxmox_sys` version of this function unconditionally logs mode mismatches even with
> > +/// `enforce_permissions == false` AND calls `fchmod`. This means that if using that version, we would
> > +/// always get a permission mismatch warning in the `pvedaemon` logs, even though we do not need the
> > +/// `fchmod` call.
> > +fn ensure_dir_exists<P: AsRef<Path>>(path: P, mode: nix::sys::stat::Mode) -> Result<(), Error> {
> > + match nix::unistd::mkdir(path.as_ref(), mode) {
> > + Ok(()) | Err(nix::errno::Errno::EEXIST) => Ok(()),
> > + Err(e) => Err(Error::StatePersistence(
> > + path.as_ref().to_string_lossy().into(),
> > + e.into(),
> > + )),
> > + }
> > +}
> > +
> > +impl State {
> > + /// Load the state for the endpoint identified by `endpoint_name`, instantiating a default object
> > + /// if yes state exists.
>
> the 'yes' should not be here, I think? :)
>
It should be 'no'...
> > + ///
> > + /// # Errors
> > + /// An [`Error`] is returned if deserialization of the state object fails.
> > + pub(crate) fn load<P: AsRef<Path>>(path: P) -> Result<State, Error> {
>
> This one should be `pub`, since it is supposed to be called from the
> context implementation (which should be moved out of this crate at some
> point).
>
[...]
> > + /// Persist the state for the endpoint identified by `endpoint_name`.
> > + ///
> > + /// # Errors
> > + /// An [`Error`] is returned if serialization of the state object, or the final write, fail.
> > + pub(crate) fn save<P: AsRef<Path>>(
>
> Same here.
>
Yes, I meant to change that for v2 but this somehow slipped through my
TODOs, thanks!
> > + self,
> > + path: P,
> > + mode: nix::sys::stat::Mode,
>
> I'd rather use CreateOptions here and move the CreateOptions::new to the
> caller
>
Yes, the reason I did not do that was because ensure_dir_exists expects
a Mode, which is a private field in CreateOptions. Since the directories we
need to create for the state files are now deeper though, we need to use
create_path instead, which does take CreateOptions structs directly, so
that is solved :)
> > + ) -> Result<(), Error> {
> > + let path_str = path.as_ref().to_string_lossy();
> > + let parent = path.as_ref().parent().unwrap();
> > +
> > + debug!("attempting to persist state at {path_str}");
> > +
> > + ensure_dir_exists(parent, mode)?;
> > +
> > + let s = serde_json::to_string_pretty(&self)
> > + .map_err(|e| Error::StatePersistence(path_str.to_string(), e.into()))?;
> > +
> > + proxmox_sys::fs::replace_file(
> > + &path,
> > + s.as_bytes(),
> > + proxmox_sys::fs::CreateOptions::new().perm(mode),
> > + false,
> > + )
> > + .map_err(|e| Error::StatePersistence(path_str.to_string(), e.into()))
> > + }
> > +
> > + /// Delete the state for the endpoint identitied by `endpoint_name`.
> > + ///
> > + /// # Errors
> > + /// An [`Error`] is returned if the state file cannot be deleted.
>
> This seems to be wrong?
>
Yes, copy-paste error, will be fixed
> > + pub(crate) fn delete<P: AsRef<Path>>(path: P) {
> > + let _ = std::fs::remove_file(&path);
>
> Might make sense to the following here (untested):
>
> if let Err(e) = std::fs::remove_file(&path) {
> if e.kind() != std::io::ErrorKind::NotFound {
> // log error
> }
> }
>
ACK, will do that
[...]
next prev parent reply other threads:[~2026-04-09 11:35 UTC|newest]
Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-25 13:14 [PATCH docs/manager/proxmox{,-perl-rs,-widget-toolkit} v2 00/16] fix #7238: Add XOAUTH2 authentication support for SMTP notification targets Arthur Bied-Charreton
2026-03-25 13:14 ` [PATCH proxmox v2 01/16] notify: smtp: Introduce xoauth2 module Arthur Bied-Charreton
2026-04-09 9:51 ` Lukas Wagner
2026-03-25 13:14 ` [PATCH proxmox v2 02/16] notify: smtp: Introduce state management Arthur Bied-Charreton
2026-04-09 9:51 ` Lukas Wagner
2026-04-09 11:35 ` Arthur Bied-Charreton [this message]
2026-04-09 12:30 ` Arthur Bied-Charreton
2026-03-25 13:14 ` [PATCH proxmox v2 03/16] notify: smtp: Factor out transport building logic Arthur Bied-Charreton
2026-04-09 9:51 ` Lukas Wagner
2026-03-25 13:14 ` [PATCH proxmox v2 04/16] notify: smtp: Update API with OAuth2 parameters Arthur Bied-Charreton
2026-04-09 9:51 ` Lukas Wagner
2026-03-25 13:14 ` [PATCH proxmox v2 05/16] notify: smtp: Add state handling logic Arthur Bied-Charreton
2026-04-09 9:51 ` Lukas Wagner
2026-03-25 13:14 ` [PATCH proxmox v2 06/16] notify: smtp: Add XOAUTH2 authentication support Arthur Bied-Charreton
2026-04-09 9:51 ` Lukas Wagner
2026-03-25 13:14 ` [PATCH proxmox-perl-rs v2 07/16] pve-rs: notify: smtp: add OAuth2 parameters to bindings Arthur Bied-Charreton
2026-04-09 9:51 ` Lukas Wagner
2026-03-25 13:14 ` [PATCH proxmox-perl-rs v2 08/16] pve-rs: notify: Add binding for triggering state refresh Arthur Bied-Charreton
2026-04-09 9:52 ` Lukas Wagner
2026-03-25 13:14 ` [PATCH proxmox-widget-toolkit v2 09/16] utils: Add OAuth2 flow handlers Arthur Bied-Charreton
2026-03-25 13:14 ` [PATCH proxmox-widget-toolkit v2 10/16] notifications: Add opt-in OAuth2 support for SMTP targets Arthur Bied-Charreton
2026-03-25 13:14 ` [PATCH pve-manager v2 11/16] notifications: Add OAuth2 parameters to schema and add/update endpoints Arthur Bied-Charreton
2026-03-25 13:14 ` [PATCH pve-manager v2 12/16] notifications: Add trigger-state-refresh endpoint Arthur Bied-Charreton
2026-03-25 13:14 ` [PATCH pve-manager v2 13/16] notifications: Trigger notification target refresh in pveupdate Arthur Bied-Charreton
2026-03-25 13:14 ` [PATCH pve-manager v2 14/16] notifications: Handle OAuth2 callback in login handler Arthur Bied-Charreton
2026-03-25 13:14 ` [PATCH pve-manager v2 15/16] fix #7238: notifications: Opt into OAuth2 authentication Arthur Bied-Charreton
2026-03-25 13:14 ` [PATCH pve-docs v2 16/16] notifications: Add section about OAuth2 to SMTP targets docs 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=zntknpftbvchzpi6hb5ndz4klybb3gq2rcaod2df7ym2v3m2mu@xqo5j6houp7l \
--to=a.bied-charreton@proxmox.com \
--cc=l.wagner@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox