From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 8341D1FF0E1 for ; Thu, 27 Aug 2026 13:42:56 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id DE2FC214DE; Thu, 27 Aug 2026 13:42:53 +0200 (CEST) From: Lukas Wagner To: pdm-devel@lists.proxmox.com Subject: [PATCH proxmox v3 04/21] product-config: add ProductConfig type Date: Thu, 27 Aug 2026 13:42:27 +0200 Message-ID: <20260827114244.424784-5-l.wagner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260827114244.424784-1-l.wagner@proxmox.com> References: <20260827114244.424784-1-l.wagner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1787830959858 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.608 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 2GDG54DP2QXGBOV2QFTDXNIFOFMZKQ44 X-Message-ID-Hash: 2GDG54DP2QXGBOV2QFTDXNIFOFMZKQ44 X-MailFrom: l.wagner@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: This type bundles user information for priv/unpriv users and common runtime paths. This type should be instantiated once and then live in the application code base, either in a OnceLock or an application context handle. Some of the methods are based on the existing helpers from filesystem_helpers.rs, with some additional ones to account for permission differences between directories and files (execute bit is set for directories). Signed-off-by: Lukas Wagner --- proxmox-product-config/src/lib.rs | 3 + proxmox-product-config/src/product_config.rs | 205 +++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 proxmox-product-config/src/product_config.rs diff --git a/proxmox-product-config/src/lib.rs b/proxmox-product-config/src/lib.rs index 9576a08d..48a6ba56 100644 --- a/proxmox-product-config/src/lib.rs +++ b/proxmox-product-config/src/lib.rs @@ -5,3 +5,6 @@ pub use filesystem_helpers::*; mod init; pub use init::*; + +mod product_config; +pub use product_config::{ProductConfig, ProductConfigParams}; diff --git a/proxmox-product-config/src/product_config.rs b/proxmox-product-config/src/product_config.rs new file mode 100644 index 00000000..d38764ec --- /dev/null +++ b/proxmox-product-config/src/product_config.rs @@ -0,0 +1,205 @@ +use std::path::{Path, PathBuf}; + +use nix::sys::stat::Mode; +use nix::unistd::User; + +use proxmox_sys::fs::CreateOptions; + +/// Parameter type for [`ProductConfig::new`]. +/// +/// This type is used to avoid having long lists of parameters with the same type, while also +/// keeping the members of the final type, [`ProductConfig`] private. +#[derive(Clone, Debug)] +pub struct ProductConfigParams { + /// The user the unprivileged API daemon runs as. + pub api_user: User, + + /// The user the privileged API daemon runs as. + pub priv_user: User, + + /// Directory for configuration files. + /// + /// This is typically in `/etc/`. + pub config_dir: PathBuf, + + /// Directory for persistent state. + /// + /// This is typically in `/var/lib/`. + pub state_dir: PathBuf, + + /// Directory for runtime data, not persisted across reboots. + /// + /// This is typically in `/run/`. + pub run_dir: PathBuf, + + /// Directory for cached data, which can be regenerated if lost. + /// + /// This is typically in `/var/cache/`. + pub cache_dir: PathBuf, +} + +/// Product-specific configuration, such as the users the product runs as and the directories it +/// stores its files in. +/// +/// # Examples +/// +/// ```no_run +/// use nix::unistd::User; +/// +/// use proxmox_product_config::{ProductConfig, ProductConfigParams}; +/// +/// # fn main() -> Result<(), anyhow::Error> { +/// let config = ProductConfig::new(ProductConfigParams { +/// api_user: User::from_name("www-data")?.expect("www-data user exists"), +/// priv_user: User::from_name("root")?.expect("root user exists"), +/// config_dir: "/etc/proxmox-product".into(), +/// state_dir: "/var/lib/proxmox-product".into(), +/// run_dir: "/run/proxmox-product".into(), +/// cache_dir: "/var/cache/proxmox-product".into(), +/// }); +/// +/// // Write a config file as `www-data:www-data` with mode 0640. +/// let config_file = config.config_dir().join("product.conf"); +/// let options = config.default_file_create_options(); +/// proxmox_sys::fs::replace_file(&config_file, b"key: value\n", options, true)?; +/// +/// // Files holding secrets are only accessible to the privileged daemon: `root:root`, mode 0600. +/// let key_file = config.config_dir().join("auth.key"); +/// let options = config.secret_file_create_options(); +/// proxmox_sys::fs::replace_file(&key_file, b"secret\n", options, true)?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Clone, Debug)] +pub struct ProductConfig(ProductConfigParams); + +impl ProductConfig { + /// Create a new [`ProductConfig`] from the provided + /// [`ProductConfigParams`]. + pub fn new(params: ProductConfigParams) -> Self { + Self(params) + } + + /// The user the unprivileged API daemon runs as. + pub fn api_user(&self) -> &User { + &self.0.api_user + } + + /// The user the privileged API daemon runs as. + pub fn priv_user(&self) -> &User { + &self.0.priv_user + } + + /// Directory for configuration files. + /// + /// This is typically in `/etc/`. + pub fn config_dir(&self) -> &Path { + &self.0.config_dir + } + + /// Directory for persistent state. + /// + /// This is typically in `/var/lib/`. + pub fn state_dir(&self) -> &Path { + &self.0.state_dir + } + + /// Directory for runtime data, not persisted across reboots. + /// + /// This is typically in `/run/`. + pub fn run_dir(&self) -> &Path { + &self.0.run_dir + } + + /// Directory for cached data, which can be regenerated if lost. + /// + /// This is typically in `/var/cache/`. + pub fn cache_dir(&self) -> &Path { + &self.0.cache_dir + } + + /// Default options for creating files: mode 0640, owned by the API user. + pub fn default_file_create_options(&self) -> CreateOptions { + let api_user = self.api_user(); + let mode = Mode::from_bits_truncate(0o0640); + + CreateOptions::new() + .perm(mode) + .owner(api_user.uid) + .group(api_user.gid) + } + + /// Default options for creating directories: mode 0750, owned by the API user. + pub fn default_dir_create_options(&self) -> CreateOptions { + let api_user = self.api_user(); + let mode = Mode::from_bits_truncate(0o0750); + + CreateOptions::new() + .perm(mode) + .owner(api_user.uid) + .group(api_user.gid) + } + + /// Return [CreateOptions] for files owned by `priv_user.uid:api_user.gid` with permission `0640`. + /// + /// Only `priv_user` can write those files, but group `api_user.gid` can read them. + pub fn privileged_file_create_options(&self) -> CreateOptions { + let api_user = self.api_user(); + let priv_user = self.priv_user(); + let mode = Mode::from_bits_truncate(0o0640); + + CreateOptions::new() + .perm(mode) + .owner(priv_user.uid) + .group(api_user.gid) + } + + /// Return [CreateOptions] for files owned by `priv_user.uid:api_user.gid` with permission `0750`. + /// + /// Only `priv_user` can write those files, but group `api_user.gid` can read them. + pub fn privileged_dir_create_options(&self) -> CreateOptions { + let api_user = self.api_user(); + let priv_user = self.priv_user(); + let mode = Mode::from_bits_truncate(0o0750); + + CreateOptions::new() + .perm(mode) + .owner(priv_user.uid) + .group(api_user.gid) + } + + /// Return [CreateOptions] for files owned by `priv_user.uid:priv_user.gid` with permission `0600`. + /// + /// Only `priv_user` can read and write those files. + pub fn secret_file_create_options(&self) -> CreateOptions { + let priv_user = self.priv_user(); + let mode = Mode::from_bits_truncate(0o0600); + + CreateOptions::new() + .perm(mode) + .owner(priv_user.uid) + .group(priv_user.gid) + } + + /// Return [CreateOptions] for directories owned by `priv_user.uid:priv_user.gid` with permission `0600`. + /// + /// Only `priv_user` can read and write those files. + pub fn secret_dir_create_options(&self) -> CreateOptions { + let priv_user = self.priv_user(); + let mode = Mode::from_bits_truncate(0o0700); + + CreateOptions::new() + .perm(mode) + .owner(priv_user.uid) + .group(priv_user.gid) + } + + /// Return [CreateOptions] for lock files, owner `api_user.uid/api_user.gid` and mode `0660`. + pub fn lockfile_create_options(&self) -> CreateOptions { + let api_user = self.api_user(); + CreateOptions::new() + .perm(Mode::from_bits_truncate(0o660)) + .owner(api_user.uid) + .group(api_user.gid) + } +} -- 2.47.3