From e5c8d70324512e28d977d381dc52a790c81052bc Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 12 Jun 2024 15:12:29 +0200 Subject: [PATCH] auth-api: add PasswordAuthenticator This is the PbsAuthenticator with the hardcoded shadow.json/lock configurable. Signed-off-by: Wolfgang Bumiller --- proxmox-auth-api/Cargo.toml | 11 +++ proxmox-auth-api/src/lib.rs | 5 ++ .../src/password_authenticator.rs | 90 +++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 proxmox-auth-api/src/password_authenticator.rs diff --git a/proxmox-auth-api/Cargo.toml b/proxmox-auth-api/Cargo.toml index 789967ee..8ceec37a 100644 --- a/proxmox-auth-api/Cargo.toml +++ b/proxmox-auth-api/Cargo.toml @@ -20,6 +20,7 @@ lazy_static = { workspace = true, optional = true } libc = { workspace = true, optional = true } log = { workspace = true, optional = true } http = { workspace = true, optional = true } +nix = { workspace = true, optional = true } openssl = { workspace = true, optional = true } pam-sys = { workspace = true, optional = true } percent-encoding = { workspace = true, optional = true } @@ -28,9 +29,11 @@ serde = { workspace = true, optional = true, features = [ "derive" ] } serde_json = { workspace = true, optional = true } serde_plain = { workspace = true, optional = true } +proxmox-product-config = { workspace = true, optional = true } proxmox-rest-server = { workspace = true, optional = true } proxmox-router = { workspace = true, optional = true } proxmox-schema = { workspace = true, optional = true, features = [ "api-macro", "api-types" ] } +proxmox-sys = { workspace = true, optional = true } proxmox-tfa = { workspace = true, optional = true, features = [ "api" ] } [features] @@ -50,3 +53,11 @@ api = [ "dep:proxmox-tfa", ] pam-authenticator = [ "api", "dep:libc", "dep:log", "dep:pam-sys" ] +password-authenticator = [ + "api", + "dep:log", + "dep:nix", + "dep:proxmox-product-config", + "dep:proxmox-sys", + "proxmox-sys?/crypt", +] diff --git a/proxmox-auth-api/src/lib.rs b/proxmox-auth-api/src/lib.rs index dc4add3f..c312aa45 100644 --- a/proxmox-auth-api/src/lib.rs +++ b/proxmox-auth-api/src/lib.rs @@ -34,3 +34,8 @@ pub mod types; mod pam_authenticator; #[cfg(feature = "pam-authenticator")] pub use pam_authenticator::Pam; + +#[cfg(feature = "password-authenticator")] +mod password_authenticator; +#[cfg(feature = "password-authenticator")] +pub use password_authenticator::PasswordAuthenticator; diff --git a/proxmox-auth-api/src/password_authenticator.rs b/proxmox-auth-api/src/password_authenticator.rs new file mode 100644 index 00000000..06b6551a --- /dev/null +++ b/proxmox-auth-api/src/password_authenticator.rs @@ -0,0 +1,90 @@ +use std::future::Future; +use std::net::IpAddr; +use std::pin::Pin; + +use anyhow::{bail, Error}; +use serde_json::json; + +use proxmox_product_config::open_secret_lockfile; + +use crate::types::UsernameRef; + +/// A simple password authenticator with a configurable path for a shadow json and lock file. +pub struct PasswordAuthenticator { + pub config_filename: &'static str, + pub lock_filename: &'static str, +} + +impl crate::api::Authenticator for PasswordAuthenticator { + fn authenticate_user<'a>( + &'a self, + username: &'a UsernameRef, + password: &'a str, + client_ip: Option<&'a IpAddr>, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let data = proxmox_sys::fs::file_get_json(self.config_filename, Some(json!({})))?; + match data[username.as_str()].as_str() { + None => bail!("no password set"), + Some(enc_password) => { + proxmox_sys::crypt::verify_crypt_pw(password, enc_password)?; + + // if the password hash is not based on the current hashing function (as + // identified by its prefix), rehash the password. + if !enc_password.starts_with(proxmox_sys::crypt::HASH_PREFIX) { + // only log that we could not upgrade a password, we already know that the + // user has a valid password, no reason the deny to log in attempt. + if let Err(e) = self.store_password(username, password, client_ip) { + log::warn!("could not upgrade a users password! - {e}"); + } + } + } + } + Ok(()) + }) + } + + fn store_password( + &self, + username: &UsernameRef, + password: &str, + _client_ip: Option<&IpAddr>, + ) -> Result<(), Error> { + let enc_password = proxmox_sys::crypt::encrypt_pw(password)?; + + let _guard = open_secret_lockfile(self.lock_filename, None, true); + let mut data = proxmox_sys::fs::file_get_json(self.config_filename, Some(json!({})))?; + data[username.as_str()] = enc_password.into(); + + let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); + let options = proxmox_sys::fs::CreateOptions::new() + .perm(mode) + .owner(nix::unistd::ROOT) + .group(nix::unistd::Gid::from_raw(0)); + + let data = serde_json::to_vec_pretty(&data)?; + proxmox_sys::fs::replace_file(self.config_filename, &data, options, true)?; + + Ok(()) + } + + fn remove_password(&self, username: &UsernameRef) -> Result<(), Error> { + let _guard = open_secret_lockfile(self.lock_filename, None, true); + + let mut data = proxmox_sys::fs::file_get_json(self.config_filename, Some(json!({})))?; + if let Some(map) = data.as_object_mut() { + map.remove(username.as_str()); + } + + let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); + let options = proxmox_sys::fs::CreateOptions::new() + .perm(mode) + .owner(nix::unistd::ROOT) + .group(nix::unistd::Gid::from_raw(0)); + + let data = serde_json::to_vec_pretty(&data)?; + proxmox_sys::fs::replace_file(self.config_filename, &data, options, true)?; + + Ok(()) + } +}