public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Fiona Ebner <f.ebner@proxmox.com>
To: pve-devel@lists.proxmox.com, pbs-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox-mail-forward 1/3] initial commit
Date: Fri, 21 Oct 2022 15:02:45 +0200	[thread overview]
Message-ID: <20221021130252.176316-3-f.ebner@proxmox.com> (raw)
In-Reply-To: <20221021130252.176316-1-f.ebner@proxmox.com>

It is intended to replace the current pvemailforward binary+script in
PVE and also be used in PBS. The implemenation is largely based on the
pvemailforward script to try and keep behavior mostly the same in PVE.

To read the config in PBS, the binary would need to belong to
backup:backup with setuid and setgid bits (proxmox-backup is 700 owned
by backup:backup and user.cfg is 640 owned by root:backup). To read
the configs in PVE the setgid bit for www-data would need to be set.

To avoid this issue, the helper will be a root-owned setuid binary and
set the effective UID to the real UID, after reading in the config
files.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---

Dependency bump for proxmox-section-config needed!

 .cargo/config |   5 ++
 .gitignore    |   2 +
 Cargo.toml    |  25 ++++++++
 rustfmt.toml  |   1 +
 src/main.rs   | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 207 insertions(+)
 create mode 100644 .cargo/config
 create mode 100644 .gitignore
 create mode 100644 Cargo.toml
 create mode 100644 rustfmt.toml
 create mode 100644 src/main.rs

diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 0000000..3b5b6e4
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,5 @@
+[source]
+[source.debian-packages]
+directory = "/usr/share/cargo/registry"
+[source.crates-io]
+replace-with = "debian-packages"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1e7caa9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+Cargo.lock
+target/
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..4dd0681
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "proxmox-mail-forward"
+version = "0.1.0"
+authors = [
+    "Fiona Ebner <f.ebner@proxmox.com>",
+    "Proxmox Support Team <support@proxmox.com>",
+]
+edition = "2021"
+license = "AGPL-3"
+description = "Proxmox mail forward helper"
+homepage = "https://www.proxmox.com"
+
+exclude = [ "debian" ]
+
+[dependencies]
+anyhow = "1.0"
+log = "0.4.17"
+nix = "0.24"
+serde = { version = "1.0", features = ["derive"] }
+#serde_json = "1.0"
+syslog = "4.0"
+
+proxmox-schema = "1.3.4"
+proxmox-section-config = "1.0"
+proxmox-sys = "0.4"
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..3a26366
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1 @@
+edition = "2021"
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..e0cacb2
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,174 @@
+use std::path::Path;
+use std::process::Command;
+
+use anyhow::{bail, format_err, Error};
+use serde::Deserialize;
+
+use proxmox_schema::{ObjectSchema, Schema, StringSchema};
+use proxmox_section_config::{SectionConfig, SectionConfigPlugin};
+use proxmox_sys::fs;
+
+const PBS_USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg";
+const PBS_ROOT_USER: &str = "root@pam";
+
+// FIXME: Switch to the actual schema when possible in terms of dependency.
+// It's safe to assume that the config was written with the actual schema restrictions, so parsing
+// it with the less restrictive schema should be enough for the purpose of getting the mail address.
+const DUMMY_ID_SCHEMA: Schema = StringSchema::new("dummy ID").min_length(3).schema();
+const DUMMY_EMAIL_SCHEMA: Schema = StringSchema::new("dummy email").schema();
+const DUMMY_USER_SCHEMA: ObjectSchema = ObjectSchema {
+    description: "minimal PBS user",
+    properties: &[
+        ("userid", false, &DUMMY_ID_SCHEMA),
+        ("email", true, &DUMMY_EMAIL_SCHEMA),
+    ],
+    additional_properties: true,
+    default_key: None,
+};
+
+#[derive(Deserialize)]
+struct DummyPbsUser {
+    pub email: Option<String>,
+}
+
+const PVE_USER_CFG_FILENAME: &str = "/etc/pve/user.cfg";
+const PVE_DATACENTER_CFG_FILENAME: &str = "/etc/pve/datacenter.cfg";
+const PVE_ROOT_USER: &str = "root@pam";
+
+/// Convenience helper to get the trimmed contents of an optional &str, mapping blank ones to `None`
+/// and creating a String from it for returning.
+fn normalize_for_return(s: Option<&str>) -> Option<String> {
+    match s?.trim() {
+        "" => None,
+        s => Some(s.to_string()),
+    }
+}
+
+/// Extract the root user's email address from the PBS user config.
+fn get_pbs_mail_to(content: &str) -> Option<String> {
+    let mut config = SectionConfig::new(&DUMMY_ID_SCHEMA);
+    let user_plugin = SectionConfigPlugin::new(
+        "user".to_string(),
+        Some("userid".to_string()),
+        &DUMMY_USER_SCHEMA,
+    );
+    config.register_plugin(user_plugin);
+
+    match config.parse(PBS_USER_CFG_FILENAME, content) {
+        Ok(parsed) => {
+            parsed.sections.get(PBS_ROOT_USER)?;
+            match parsed.lookup::<DummyPbsUser>("user", PBS_ROOT_USER) {
+                Ok(user) => normalize_for_return(user.email.as_deref()),
+                Err(err) => {
+                    log::error!("unable to parse {} - {}", PBS_USER_CFG_FILENAME, err);
+                    None
+                }
+            }
+        }
+        Err(err) => {
+            log::error!("unable to parse {} - {}", PBS_USER_CFG_FILENAME, err);
+            None
+        }
+    }
+}
+
+/// Extract the root user's email address from the PVE user config.
+fn get_pve_mail_to(content: &str) -> Option<String> {
+    normalize_for_return(content.lines().find_map(|line| {
+        let fields: Vec<&str> = line.split(':').collect();
+        #[allow(clippy::get_first)] // to keep expression style consistent
+        match fields.get(0)?.trim() == "user" && fields.get(1)?.trim() == PVE_ROOT_USER {
+            true => fields.get(6).copied(),
+            false => None,
+        }
+    }))
+}
+
+/// Extract the From-address configured in the PVE datacenter config.
+fn get_pve_mail_from(content: &str) -> Option<String> {
+    normalize_for_return(
+        content
+            .lines()
+            .find_map(|line| line.strip_prefix("email_from:")),
+    )
+}
+
+/// Executes sendmail as a child process with the specified From/To-addresses, expecting the mail
+/// contents to be passed via stdin inherited from this program.
+fn forward_mail(mail_from: String, mail_to: Vec<String>) -> Result<(), Error> {
+    if mail_to.is_empty() {
+        bail!("user 'root@pam' does not have an email address");
+    }
+
+    log::info!("forward mail to <{}>", mail_to.join(","));
+
+    let mut cmd = Command::new("sendmail");
+    cmd.args([
+        "-bm", "-N", "never", // never send DSN (avoid mail loops)
+        "-f", &mail_from, "--",
+    ]);
+    cmd.args(mail_to);
+    cmd.env("PATH", "/sbin:/bin:/usr/sbin:/usr/bin");
+
+    // with status(), child inherits stdin
+    cmd.status()
+        .map_err(|err| format_err!("command {:?} failed - {}", cmd, err))?;
+
+    Ok(())
+}
+
+/// Wrapper around `proxmox_sys::fs::file_read_optional_string` which also returns `None` upon error
+/// after logging it.
+fn attempt_file_read<P: AsRef<Path>>(path: P) -> Option<String> {
+    match fs::file_read_optional_string(path) {
+        Ok(contents) => contents,
+        Err(err) => {
+            log::error!("{}", err);
+            None
+        }
+    }
+}
+
+fn main() {
+    if let Err(err) = syslog::init(
+        syslog::Facility::LOG_DAEMON,
+        log::LevelFilter::Info,
+        Some("proxmox-mail-forward"),
+    ) {
+        eprintln!("unable to inititialize syslog - {}", err);
+    }
+
+    let pbs_user_cfg_content = attempt_file_read(PBS_USER_CFG_FILENAME);
+    let pve_user_cfg_content = attempt_file_read(PVE_USER_CFG_FILENAME);
+    let pve_datacenter_cfg_content = attempt_file_read(PVE_DATACENTER_CFG_FILENAME);
+
+    let real_uid = nix::unistd::getuid();
+    if let Err(err) = nix::unistd::seteuid(real_uid) {
+        log::error!(
+            "mail forward failed: unable to set effective uid to {}: {}",
+            real_uid,
+            err
+        );
+        return;
+    }
+
+    let pbs_mail_to = pbs_user_cfg_content.and_then(|content| get_pbs_mail_to(&content));
+    let pve_mail_to = pve_user_cfg_content.and_then(|content| get_pve_mail_to(&content));
+    let pve_mail_from = pve_datacenter_cfg_content.and_then(|content| get_pve_mail_from(&content));
+
+    let mail_from = pve_mail_from.unwrap_or_else(|| "root".to_string());
+
+    let mut mail_to = vec![];
+    if let Some(pve_mail_to) = pve_mail_to {
+        mail_to.push(pve_mail_to);
+    }
+    if let Some(pbs_mail_to) = pbs_mail_to {
+        if !mail_to.contains(&pbs_mail_to) {
+            mail_to.push(pbs_mail_to);
+        }
+    }
+
+    if let Err(err) = forward_mail(mail_from, mail_to) {
+        log::error!("mail forward failed: {}", err);
+    }
+}
-- 
2.30.2





  parent reply	other threads:[~2022-10-21 13:03 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-10-21 13:02 [pve-devel] [PATCH-SERIES proxmox{, -mail-forward, -backup}/pve-manager] add proxmox-mail-forward helper binary Fiona Ebner
2022-10-21 13:02 ` [pve-devel] [PATCH proxmox 1/1] section config: parse additional properties when schema allows it Fiona Ebner
2022-10-24 11:47   ` [pve-devel] applied: " Wolfgang Bumiller
2022-10-21 13:02 ` Fiona Ebner [this message]
2022-11-10 10:46   ` [pve-devel] applied: [PATCH proxmox-mail-forward 1/3] initial commit Wolfgang Bumiller
2022-10-21 13:02 ` [pve-devel] [PATCH proxmox-mail-forward 2/3] add Debian packaging Fiona Ebner
2022-10-21 13:02 ` [pve-devel] [PATCH proxmox-mail-forward 3/3] d/postinst: register binary in .forward Fiona Ebner
2022-10-21 13:02 ` [pve-devel] [PATCH proxmox-backup 1/1] fix #4287: d/control: recommend proxmox-mail-forward Fiona Ebner
2022-11-10 10:49   ` [pve-devel] applied: " Wolfgang Bumiller
2022-10-21 13:02 ` [pve-devel] [PATCH manager 1/4] d/control: depend on proxmox-mail-forward Fiona Ebner
2022-10-21 13:02 ` [pve-devel] [PATCH manager 2/4] d/postinst: replace pvemailforward with proxmox-mail-forward Fiona Ebner
2022-10-21 13:02 ` [pve-devel] [PATCH manager 3/4] remove pvemailforward binary Fiona Ebner
2022-10-21 13:02 ` [pve-devel] [PATCH manager 4/4] d/control: drop ${shlibs:Depends} for pve-manager Fiona Ebner
2022-11-10 11:11   ` Thomas Lamprecht
2022-11-10 10:58 ` [pve-devel] applied-series: [PATCH-SERIES proxmox{, -mail-forward, -backup}/pve-manager] add proxmox-mail-forward helper binary Wolfgang Bumiller

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=20221021130252.176316-3-f.ebner@proxmox.com \
    --to=f.ebner@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal