From abd0767888d5ec66840746bdee14bc036dd85991 Mon Sep 17 00:00:00 2001 From: howenyap Date: Thu, 19 Feb 2026 21:21:37 +0800 Subject: [PATCH 1/4] fix: pause cluster if passthrough auth is enabled and user password is missing/empty --- pgdog/src/backend/databases.rs | 155 ++++++++++++++++++++++- pgdog/src/backend/pool/cluster.rs | 30 +++++ pgdog/src/backend/pool/connection/mod.rs | 7 + pgdog/src/backend/pool/lb/mod.rs | 10 ++ pgdog/src/backend/pool/shard/mod.rs | 10 ++ pgdog/src/frontend/client/mod.rs | 5 + 6 files changed, 215 insertions(+), 2 deletions(-) diff --git a/pgdog/src/backend/databases.rs b/pgdog/src/backend/databases.rs index d8028c353..fcc6e333c 100644 --- a/pgdog/src/backend/databases.rs +++ b/pgdog/src/backend/databases.rs @@ -460,12 +460,19 @@ fn new_pool(user: &crate::config::User, config: &crate::config::Config) -> Optio &config.rewrite, ); + let cluster = Cluster::new(cluster_config); + + // Passthrough users without configured passwords should not probe backend. + if config.general.passthrough_auth() && user.password().is_empty() { + cluster.pause(); + } + Some(( User { user: user.name.clone(), database: user.database.clone(), }, - Cluster::new(cluster_config), + cluster, )) } @@ -617,7 +624,7 @@ pub fn from_config(config: &ConfigAndUsers) -> Databases { #[cfg(test)] mod tests { use super::*; - use crate::config::{Config, ConfigAndUsers, Database, Role}; + use crate::config::{Config, ConfigAndUsers, Database, PassthoughAuth, Role}; #[test] fn test_mirror_user_isolation() { @@ -1570,4 +1577,148 @@ mod tests { assert_eq!(databases.all().len(), 1); } + + #[test] + fn test_passthrough_empty_password_starts_paused() { + let mut config = Config::default(); + config.general.passthrough_auth = PassthoughAuth::EnabledPlain; + config.databases = vec![Database { + name: "pgdog".to_string(), + host: "localhost".to_string(), + port: 5432, + role: Role::Primary, + ..Default::default() + }]; + + let users = crate::config::Users { + users: vec![crate::config::User { + name: "pgdog".to_string(), + database: "pgdog".to_string(), + password: None, + ..Default::default() + }], + ..Default::default() + }; + + let databases = from_config(&ConfigAndUsers { + config, + users, + config_path: std::path::PathBuf::new(), + users_path: std::path::PathBuf::new(), + }); + + let key = User { + user: "pgdog".to_string(), + database: "pgdog".to_string(), + }; + + let cluster = databases.all().get(&key).expect("cluster should exist"); + + for shard in cluster.shards() { + for pool in shard.pools() { + assert!(pool.state().paused); + } + } + } + + #[test] + fn test_user_with_password_not_paused() { + let mut config = Config::default(); + config.general.passthrough_auth = PassthoughAuth::EnabledPlain; + config.databases = vec![Database { + name: "pgdog".to_string(), + host: "localhost".to_string(), + port: 5432, + role: Role::Primary, + ..Default::default() + }]; + + let users = crate::config::Users { + users: vec![crate::config::User { + name: "pgdog".to_string(), + database: "pgdog".to_string(), + password: Some("pgdog".to_string()), + ..Default::default() + }], + ..Default::default() + }; + + let databases = from_config(&ConfigAndUsers { + config, + users, + config_path: std::path::PathBuf::new(), + users_path: std::path::PathBuf::new(), + }); + + let key = User { + user: "pgdog".to_string(), + database: "pgdog".to_string(), + }; + + let cluster = databases.all().get(&key).expect("cluster should exist"); + + for shard in cluster.shards() { + for pool in shard.pools() { + assert!(!pool.state().paused); + } + } + } + + #[test] + fn test_replace_empty_password_cluster_with_passthrough_password() { + let mut config = Config::default(); + config.general.passthrough_auth = PassthoughAuth::EnabledPlain; + config.databases = vec![Database { + name: "pgdog".to_string(), + host: "localhost".to_string(), + port: 5432, + role: Role::Primary, + ..Default::default() + }]; + + let users = crate::config::Users { + users: vec![crate::config::User { + name: "pgdog".to_string(), + database: "pgdog".to_string(), + password: None, + ..Default::default() + }], + ..Default::default() + }; + + let databases = from_config(&ConfigAndUsers { + config: config.clone(), + users, + config_path: std::path::PathBuf::new(), + users_path: std::path::PathBuf::new(), + }); + + let passthrough_user = crate::config::User { + name: "pgdog".to_string(), + database: "pgdog".to_string(), + password: Some("secret".to_string()), + ..Default::default() + }; + + let (user, cluster) = new_pool(&passthrough_user, &config).expect("cluster should exist"); + let (added, databases) = databases.add(user, cluster); + + assert!(added); + assert!(databases.exists(("pgdog", "pgdog"))); + + let key = User { + user: "pgdog".to_string(), + database: "pgdog".to_string(), + }; + + let cluster = databases.all().get(&key).expect("cluster should exist"); + + assert_eq!(cluster.password(), "secret"); + + for shard in cluster.shards() { + for pool in shard.pools() { + assert!(!pool.state().paused); + } + } + } } diff --git a/pgdog/src/backend/pool/cluster.rs b/pgdog/src/backend/pool/cluster.rs index 33c0798a9..1b62e20cb 100644 --- a/pgdog/src/backend/pool/cluster.rs +++ b/pgdog/src/backend/pool/cluster.rs @@ -570,6 +570,16 @@ impl Cluster { self.readiness.online.store(true, Ordering::Relaxed); } + /// Pause all pools in this cluster. + pub fn pause(&self) { + self.shards().iter().for_each(|shard| shard.pause()) + } + + /// Resume all pools in this cluster. + pub fn resume(&self) { + self.shards().iter().for_each(|shard| shard.resume()) + } + /// Shutdown the connection pools. pub(crate) fn shutdown(&self) { for shard in self.shards() { @@ -821,6 +831,26 @@ mod test { assert!(!cluster.online()); } + #[test] + fn test_pause_resume_toggles_all_pools() { + let config = ConfigAndUsers::default(); + let cluster = Cluster::new_test(&config); + + cluster.pause(); + for shard in cluster.shards() { + for pool in shard.pools() { + assert!(pool.state().paused); + } + } + + cluster.resume(); + for shard in cluster.shards() { + for pool in shard.pools() { + assert!(!pool.state().paused); + } + } + } + #[tokio::test] async fn test_launch_schema_loading_idempotent() { use std::sync::atomic::Ordering; diff --git a/pgdog/src/backend/pool/connection/mod.rs b/pgdog/src/backend/pool/connection/mod.rs index 7ea427941..1304d3045 100644 --- a/pgdog/src/backend/pool/connection/mod.rs +++ b/pgdog/src/backend/pool/connection/mod.rs @@ -360,6 +360,13 @@ impl Connection { Ok(()) } + /// Resume pools for the currently bound cluster. + pub(crate) fn resume_cluster_pools(&self) { + if let Some(cluster) = &self.cluster { + cluster.resume(); + } + } + pub(crate) fn bind(&mut self, bind: &Bind) -> Result<(), Error> { match self.binding { Binding::MultiShard(_, ref mut state) => { diff --git a/pgdog/src/backend/pool/lb/mod.rs b/pgdog/src/backend/pool/lb/mod.rs index 8e030384e..5ce957a71 100644 --- a/pgdog/src/backend/pool/lb/mod.rs +++ b/pgdog/src/backend/pool/lb/mod.rs @@ -190,6 +190,16 @@ impl LoadBalancer { Monitor::spawn(self); } + /// Pause all target pools. + pub fn pause(&self) { + self.targets.iter().for_each(|target| target.pool.pause()); + } + + /// Resume all target pools. + pub fn resume(&self) { + self.targets.iter().for_each(|target| target.pool.resume()); + } + /// Get a live connection from the pool. pub async fn get(&self, request: &Request) -> Result { match timeout(self.checkout_timeout, self.get_internal(request)).await { diff --git a/pgdog/src/backend/pool/shard/mod.rs b/pgdog/src/backend/pool/shard/mod.rs index fca067eb7..d66e432d4 100644 --- a/pgdog/src/backend/pool/shard/mod.rs +++ b/pgdog/src/backend/pool/shard/mod.rs @@ -148,6 +148,16 @@ impl Shard { } } + /// Pause every pool in this shard. + pub fn pause(&self) { + self.lb.pause(); + } + + /// Resume every pool in this shard. + pub fn resume(&self) { + self.lb.resume(); + } + /// Returns true if the shard has a primary database. pub fn has_primary(&self) -> bool { self.lb.primary().is_some() diff --git a/pgdog/src/frontend/client/mod.rs b/pgdog/src/frontend/client/mod.rs index 3a07451b0..1151e3028 100644 --- a/pgdog/src/frontend/client/mod.rs +++ b/pgdog/src/frontend/client/mod.rs @@ -251,6 +251,11 @@ impl Client { stream.send(&Authentication::Ok).await?; } + // Allow pools to connect if passthrough password exists + if passthrough_password.is_some() { + conn.resume_cluster_pools(); + } + // Check if the pooler is shutting down. if comms.offline() && !admin { stream.fatal(ErrorResponse::shutting_down()).await?; From 07b5307ef5faf0adaf91d8ec400feb5a649c6f6b Mon Sep 17 00:00:00 2001 From: howenyap Date: Thu, 19 Feb 2026 22:33:55 +0800 Subject: [PATCH 2/4] fix: do not pause cluster when auth type is trust --- pgdog/src/backend/databases.rs | 111 +++++++++++++++++++++-- pgdog/src/backend/pool/connection/mod.rs | 5 +- pgdog/src/frontend/client/mod.rs | 3 +- 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/pgdog/src/backend/databases.rs b/pgdog/src/backend/databases.rs index fcc6e333c..23d260581 100644 --- a/pgdog/src/backend/databases.rs +++ b/pgdog/src/backend/databases.rs @@ -18,7 +18,7 @@ use crate::frontend::router::sharding::Mapping; use crate::frontend::PreparedStatements; use crate::{ backend::pool::PoolConfig, - config::{config, load, ConfigAndUsers, ManualQuery, Role}, + config::{config, load, AuthType, ConfigAndUsers, ManualQuery, Role}, net::{messages::BackendKeyData, tls}, }; @@ -234,11 +234,24 @@ impl Databases { } /// Check if a cluster exists, quickly. - pub fn exists(&self, user: impl ToUser) -> bool { - if let Some(cluster) = self.databases.get(&user.to_user()) { - !cluster.password().is_empty() + pub(crate) fn exists(&self, user: impl ToUser) -> bool { + self.databases.contains_key(&user.to_user()) + } + + /// Check if a cluster exists, and has a non-empty password. + pub(crate) fn has_password(&self, user: impl ToUser) -> bool { + self.databases + .get(&user.to_user()) + .is_some_and(|cluster| !cluster.password().is_empty()) + } + + /// Check if backend authentication can work for this user. + pub fn is_backend_auth_ready(&self, user: impl ToUser, authtype: &AuthType) -> bool { + // Trust auth doesn't need a password, so the cluster merely has to exist. + if authtype.trust() { + self.exists(user) } else { - false + self.has_password(user) } } @@ -462,8 +475,10 @@ fn new_pool(user: &crate::config::User, config: &crate::config::Config) -> Optio let cluster = Cluster::new(cluster_config); - // Passthrough users without configured passwords should not probe backend. - if config.general.passthrough_auth() && user.password().is_empty() { + if config.general.passthrough_auth() + && user.password().is_empty() + && !config.general.auth_type.trust() + { cluster.pause(); } @@ -624,7 +639,7 @@ pub fn from_config(config: &ConfigAndUsers) -> Databases { #[cfg(test)] mod tests { use super::*; - use crate::config::{Config, ConfigAndUsers, Database, PassthoughAuth, Role}; + use crate::config::{AuthType, Config, ConfigAndUsers, Database, PassthoughAuth, Role}; #[test] fn test_mirror_user_isolation() { @@ -1664,6 +1679,49 @@ mod tests { } } + #[test] + fn test_passthrough_empty_password_trust_starts_unpaused() { + let mut config = Config::default(); + config.general.passthrough_auth = PassthoughAuth::EnabledPlain; + config.general.auth_type = AuthType::Trust; + config.databases = vec![Database { + name: "pgdog".to_string(), + host: "localhost".to_string(), + port: 5432, + role: Role::Primary, + ..Default::default() + }]; + + let users = crate::config::Users { + users: vec![crate::config::User { + name: "pgdog".to_string(), + database: "pgdog".to_string(), + password: None, + ..Default::default() + }], + ..Default::default() + }; + + let databases = from_config(&ConfigAndUsers { + config, + users, + config_path: std::path::PathBuf::new(), + users_path: std::path::PathBuf::new(), + }); + + let key = User { + user: "pgdog".to_string(), + database: "pgdog".to_string(), + }; + let cluster = databases.all().get(&key).expect("cluster should exist"); + + for shard in cluster.shards() { + for pool in shard.pools() { + assert!(!pool.state().paused); + } + } + } + #[test] fn test_replace_empty_password_cluster_with_passthrough_password() { let mut config = Config::default(); @@ -1704,7 +1762,7 @@ mod tests { let (added, databases) = databases.add(user, cluster); assert!(added); - assert!(databases.exists(("pgdog", "pgdog"))); + assert!(databases.has_password(("pgdog", "pgdog"))); let key = User { user: "pgdog".to_string(), @@ -1721,4 +1779,39 @@ mod tests { } } } + + #[test] + fn test_backend_auth_ready_trust_allows_empty_password_user() { + let mut config = Config::default(); + config.general.passthrough_auth = PassthoughAuth::EnabledPlain; + config.general.auth_type = AuthType::Trust; + config.databases = vec![Database { + name: "pgdog".to_string(), + host: "localhost".to_string(), + port: 5432, + role: Role::Primary, + ..Default::default() + }]; + + let users = crate::config::Users { + users: vec![crate::config::User { + name: "pgdog".to_string(), + database: "pgdog".to_string(), + password: None, + ..Default::default() + }], + ..Default::default() + }; + + let databases = from_config(&ConfigAndUsers { + config, + users, + config_path: std::path::PathBuf::new(), + users_path: std::path::PathBuf::new(), + }); + + assert!(databases.exists(("pgdog", "pgdog"))); + assert!(!databases.has_password(("pgdog", "pgdog"))); + assert!(databases.is_backend_auth_ready(("pgdog", "pgdog"), &AuthType::Trust)); + } } diff --git a/pgdog/src/backend/pool/connection/mod.rs b/pgdog/src/backend/pool/connection/mod.rs index 1304d3045..6af9a8c15 100644 --- a/pgdog/src/backend/pool/connection/mod.rs +++ b/pgdog/src/backend/pool/connection/mod.rs @@ -325,8 +325,11 @@ impl Connection { match self.binding { Binding::Direct(_) | Binding::MultiShard(_, _) => { let user = (self.user.as_str(), self.database.as_str()); + let config = config(); // Check passthrough auth. - if config().config.general.passthrough_auth() && !databases().exists(user) { + if config.config.general.passthrough_auth() + && !databases().is_backend_auth_ready(user, &config.config.general.auth_type) + { if let Some(ref passthrough_password) = self.passthrough_password { let new_user = User::new(&self.user, passthrough_password, &self.database); databases::add(new_user); diff --git a/pgdog/src/frontend/client/mod.rs b/pgdog/src/frontend/client/mod.rs index 1151e3028..35cc4615f 100644 --- a/pgdog/src/frontend/client/mod.rs +++ b/pgdog/src/frontend/client/mod.rs @@ -156,7 +156,6 @@ impl Client { let comms = ClientComms::new(&id); // Auto database. - let exists = databases::databases().exists((user, database)); let passthrough_password = if config.config.general.passthrough_auth() && !admin { let password = if auth_type.trust() { // Use empty password. @@ -172,7 +171,7 @@ impl Client { Password::from_bytes(password.to_bytes()?)? }; - if !exists { + if !databases::databases().is_backend_auth_ready((user, database), auth_type) { let user = user_from_params(¶ms, &password).ok(); if let Some(user) = user { databases::add(user); From 234cbd4bdc730473e741911d48d9ca4c704836f4 Mon Sep 17 00:00:00 2001 From: howenyap Date: Thu, 19 Feb 2026 23:00:20 +0800 Subject: [PATCH 3/4] remove redundant test, rename test --- pgdog/src/backend/databases.rs | 37 +--------------------------------- 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/pgdog/src/backend/databases.rs b/pgdog/src/backend/databases.rs index 23d260581..93655da22 100644 --- a/pgdog/src/backend/databases.rs +++ b/pgdog/src/backend/databases.rs @@ -1637,7 +1637,7 @@ mod tests { } #[test] - fn test_user_with_password_not_paused() { + fn test_passthrough_user_with_password_unpaused() { let mut config = Config::default(); config.general.passthrough_auth = PassthoughAuth::EnabledPlain; config.databases = vec![Database { @@ -1779,39 +1779,4 @@ mod tests { } } } - - #[test] - fn test_backend_auth_ready_trust_allows_empty_password_user() { - let mut config = Config::default(); - config.general.passthrough_auth = PassthoughAuth::EnabledPlain; - config.general.auth_type = AuthType::Trust; - config.databases = vec![Database { - name: "pgdog".to_string(), - host: "localhost".to_string(), - port: 5432, - role: Role::Primary, - ..Default::default() - }]; - - let users = crate::config::Users { - users: vec![crate::config::User { - name: "pgdog".to_string(), - database: "pgdog".to_string(), - password: None, - ..Default::default() - }], - ..Default::default() - }; - - let databases = from_config(&ConfigAndUsers { - config, - users, - config_path: std::path::PathBuf::new(), - users_path: std::path::PathBuf::new(), - }); - - assert!(databases.exists(("pgdog", "pgdog"))); - assert!(!databases.has_password(("pgdog", "pgdog"))); - assert!(databases.is_backend_auth_ready(("pgdog", "pgdog"), &AuthType::Trust)); - } } From 5bb0d0b6ffbd263d3550ad20815ace1eff056dc2 Mon Sep 17 00:00:00 2001 From: howenyap Date: Fri, 20 Feb 2026 15:42:51 +0800 Subject: [PATCH 4/4] add integration test, only resume connection if password is still empty --- integration/rust/tests/integration/auth.rs | 31 ++++++++++++++++++++++ integration/users.toml | 4 +++ pgdog/src/frontend/client/mod.rs | 3 +-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/integration/rust/tests/integration/auth.rs b/integration/rust/tests/integration/auth.rs index 8556137e8..c2edb74b7 100644 --- a/integration/rust/tests/integration/auth.rs +++ b/integration/rust/tests/integration/auth.rs @@ -91,3 +91,34 @@ async fn test_passthrough_auth() { user.execute("SELECT 1").await.unwrap(); original.execute("SELECT 1").await.unwrap(); } + +#[tokio::test] +#[serial] +async fn test_user_without_password_passthrough_auth() { + let admin = admin_sqlx().await; + + admin.execute("RELOAD").await.unwrap(); + admin.execute("SET auth_type TO 'scram'").await.unwrap(); + assert_setting_str("auth_type", "scram").await; + + let user = "postgres://pgdog2:pgdog@127.0.0.1:6432/pgdog"; + + let no_password_err = PgConnection::connect(user).await.err().unwrap(); + + assert!( + no_password_err + .to_string() + .contains("password for user \"pgdog2\" and database \"pgdog\" is wrong") + ); + + admin + .execute("SET passthrough_auth TO 'enabled_plain'") + .await + .unwrap(); + assert_setting_str("passthrough_auth", "enabled_plain").await; + + let mut user = PgConnection::connect(user).await.unwrap(); + + user.execute("SELECT 1").await.unwrap(); + user.close().await.unwrap(); +} diff --git a/integration/users.toml b/integration/users.toml index 87273f7c6..4f818edc2 100644 --- a/integration/users.toml +++ b/integration/users.toml @@ -3,6 +3,10 @@ name = "pgdog" database = "pgdog" password = "pgdog" +[[users]] +name = "pgdog2" +database = "pgdog" + [[users]] name = "pgdog_session" database = "pgdog" diff --git a/pgdog/src/frontend/client/mod.rs b/pgdog/src/frontend/client/mod.rs index 35cc4615f..5f56b217e 100644 --- a/pgdog/src/frontend/client/mod.rs +++ b/pgdog/src/frontend/client/mod.rs @@ -250,8 +250,7 @@ impl Client { stream.send(&Authentication::Ok).await?; } - // Allow pools to connect if passthrough password exists - if passthrough_password.is_some() { + if passthrough_password.is_some() && conn.cluster()?.password().is_empty() { conn.resume_cluster_pools(); }