From d097111bdc83f187263d052564bdc4e7317ae7f7 Mon Sep 17 00:00:00 2001 From: "Kai A. Hiller" Date: Mon, 25 Aug 2025 19:12:13 +0200 Subject: [PATCH 1/2] Add get_password method --- sqlx-postgres/src/connection/establish.rs | 4 ++-- sqlx-postgres/src/connection/sasl.rs | 2 +- sqlx-postgres/src/options/mod.rs | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/sqlx-postgres/src/connection/establish.rs b/sqlx-postgres/src/connection/establish.rs index 634b71de4b..6da98b3c66 100644 --- a/sqlx-postgres/src/connection/establish.rs +++ b/sqlx-postgres/src/connection/establish.rs @@ -77,7 +77,7 @@ impl PgConnection { stream .send(Password::Cleartext( - options.password.as_deref().unwrap_or_default(), + &options.get_password().unwrap_or_default(), )) .await?; } @@ -91,7 +91,7 @@ impl PgConnection { stream .send(Password::Md5 { username: &options.username, - password: options.password.as_deref().unwrap_or_default(), + password: &options.get_password().unwrap_or_default(), salt: body.salt, }) .await?; diff --git a/sqlx-postgres/src/connection/sasl.rs b/sqlx-postgres/src/connection/sasl.rs index 729cc1fcc5..288b5028a8 100644 --- a/sqlx-postgres/src/connection/sasl.rs +++ b/sqlx-postgres/src/connection/sasl.rs @@ -87,7 +87,7 @@ pub(crate) async fn authenticate( // SaltedPassword := Hi(Normalize(password), salt, i) let salted_password = hi( - options.password.as_deref().unwrap_or_default(), + &options.get_password().unwrap_or_default(), &cont.salt, cont.iterations, )?; diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index efbc43989b..83040babca 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -519,6 +519,21 @@ impl PgConnectOptions { &self.username } + /// Get the password. + /// + /// ```rust + /// # use sqlx_postgres::PgConnectOptions; + /// let options = PgConnectOptions::new() + /// .password("53C237"); + /// assert_eq!(options.get_password().as_deref(), Some("53C237")); + /// ``` + pub fn get_password(&self) -> Option> { + if self.password.is_some() { + return self.password.as_deref().map(Cow::Borrowed); + } + None + } + /// Get the current database name. /// /// # Example From a14e4eb54851fd05c8007ec6a7b39e318ed20aae Mon Sep 17 00:00:00 2001 From: "Kai A. Hiller" Date: Mon, 25 Aug 2025 20:04:18 +0200 Subject: [PATCH 2/2] Load passfile content on apply_pgpass --- sqlx-postgres/src/options/mod.rs | 21 ++-- sqlx-postgres/src/options/pgpass.rs | 158 +++++++++++++++------------- 2 files changed, 98 insertions(+), 81 deletions(-) diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index 83040babca..d3c9106eb6 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; pub use ssl_mode::PgSslMode; +use crate::options::pgpass::PGPassFile; use crate::{connection::LogSettings, net::tls::CertificateInput}; mod connect; @@ -20,6 +21,7 @@ pub struct PgConnectOptions { pub(crate) socket: Option, pub(crate) username: String, pub(crate) password: Option, + pub(crate) passfile: PGPassFile, pub(crate) database: Option, pub(crate) ssl_mode: PgSslMode, pub(crate) ssl_root_cert: Option, @@ -74,6 +76,7 @@ impl PgConnectOptions { socket: None, username, password: var("PGPASSWORD").ok(), + passfile: PGPassFile::default(), database, ssl_root_cert: var("PGSSLROOTCERT").ok().map(CertificateInput::from), ssl_client_cert: var("PGSSLCERT").ok().map(CertificateInput::from), @@ -94,14 +97,7 @@ impl PgConnectOptions { } pub(crate) fn apply_pgpass(mut self) -> Self { - if self.password.is_none() { - self.password = pgpass::load_password( - &self.host, - self.port, - &self.username, - self.database.as_deref(), - ); - } + self.passfile = PGPassFile::load().unwrap_or_default(); self } @@ -531,7 +527,14 @@ impl PgConnectOptions { if self.password.is_some() { return self.password.as_deref().map(Cow::Borrowed); } - None + self.passfile + .password_if_matching( + &self.host, + self.port, + self.database.as_deref(), + &self.username, + ) + .map(Cow::Owned) } /// Get the current database name. diff --git a/sqlx-postgres/src/options/pgpass.rs b/sqlx-postgres/src/options/pgpass.rs index bf16559548..f9a81a4453 100644 --- a/sqlx-postgres/src/options/pgpass.rs +++ b/sqlx-postgres/src/options/pgpass.rs @@ -1,86 +1,100 @@ use std::borrow::Cow; use std::env::var_os; use std::fs::File; -use std::io::{BufRead, BufReader}; +use std::io::{BufRead, Read}; use std::path::PathBuf; -/// try to load a password from the various pgpass file locations -pub fn load_password( - host: &str, - port: u16, - username: &str, - database: Option<&str>, -) -> Option { - let custom_file = var_os("PGPASSFILE"); - if let Some(file) = custom_file { - if let Some(password) = - load_password_from_file(PathBuf::from(file), host, port, username, database) - { - return Some(password); +/// PostgreSQL passfile content. +#[derive(Clone, Debug, Default)] +pub struct PGPassFile(String); + +impl PGPassFile { + /// Loads the first valid passfile discovered. + /// + /// Loading is attempted in the following order: + /// 1. Path given via the `PGPASSFILE` environment variable. + /// 2. Default path (`~/.pgpass` on Linux and + /// `%APPDATA%/postgres/pgpass.conf` on Windows) + /// + /// If loading of any file fails, the function proceeds to the next. + /// Returns `None` in case no file can be loaded. + pub fn load() -> Option { + let custom_file = var_os("PGPASSFILE"); + if let Some(file) = custom_file { + if let Some(password) = Self::load_from_file(PathBuf::from(file)) { + return Some(password); + } } - } - #[cfg(not(target_os = "windows"))] - let default_file = home::home_dir().map(|path| path.join(".pgpass")); - #[cfg(target_os = "windows")] - let default_file = { - use etcetera::BaseStrategy; - - etcetera::base_strategy::Windows::new() - .ok() - .map(|basedirs| basedirs.data_dir().join("postgres").join("pgpass.conf")) - }; - load_password_from_file(default_file?, host, port, username, database) -} + #[cfg(not(target_os = "windows"))] + let default_file = home::home_dir().map(|path| path.join(".pgpass")); + #[cfg(target_os = "windows")] + let default_file = { + use etcetera::BaseStrategy; + + etcetera::base_strategy::Windows::new() + .ok() + .map(|basedirs| basedirs.data_dir().join("postgres").join("pgpass.conf")) + }; + Self::load_from_file(default_file?) + } -/// try to extract a password from a pgpass file -fn load_password_from_file( - path: PathBuf, - host: &str, - port: u16, - username: &str, - database: Option<&str>, -) -> Option { - let file = File::open(&path) - .map_err(|e| { - match e.kind() { - std::io::ErrorKind::NotFound => { - tracing::debug!( - path = %path.display(), - "`.pgpass` file not found", - ); - } - _ => { - tracing::warn!( - path = %path.display(), - "Failed to open `.pgpass` file: {e:?}", - ); - } - }; - }) - .ok()?; - - #[cfg(target_os = "linux")] - { - use std::os::unix::fs::PermissionsExt; - - // check file permissions on linux - - let metadata = file.metadata().ok()?; - let permissions = metadata.permissions(); - let mode = permissions.mode(); - if mode & 0o77 != 0 { - tracing::warn!( - path = %path.display(), - permissions = format!("{mode:o}"), - "Ignoring path. Permissions are not strict enough", - ); - return None; + /// Returns the PostgreSQL passfile loaded from the given path. + fn load_from_file(path: PathBuf) -> Option { + let mut file = File::open(&path) + .map_err(|e| { + match e.kind() { + std::io::ErrorKind::NotFound => { + tracing::debug!( + path = %path.display(), + "`.pgpass` file not found", + ); + } + _ => { + tracing::warn!( + path = %path.display(), + "Failed to open `.pgpass` file: {e:?}", + ); + } + }; + }) + .ok()?; + + #[cfg(target_os = "linux")] + { + use std::os::unix::fs::PermissionsExt; + + // check file permissions on linux + + let metadata = file.metadata().ok()?; + let permissions = metadata.permissions(); + let mode = permissions.mode(); + if mode & 0o77 != 0 { + tracing::warn!( + path = %path.display(), + permissions = format!("{mode:o}"), + "Ignoring path. Permissions are not strict enough", + ); + return None; + } } + + let mut passfile = Self::default(); + file.read_to_string(&mut passfile.0).ok()?; + + Some(passfile) } - let reader = BufReader::new(file); - load_password_from_reader(reader, host, port, username, database) + /// Returns the password matched by the given parameters. + pub fn password_if_matching( + &self, + hostname: &str, + port: u16, + database: Option<&str>, + username: &str, + ) -> Option { + load_password_from_reader(self.0.as_bytes(), hostname, port, username, database) + } } fn load_password_from_reader(