Skip to content

Commit beabd7d

Browse files
authored
feat: log which auth error the user experienced to stderr (#1009)
1 parent da0524c commit beabd7d

4 files changed

Lines changed: 107 additions & 27 deletions

File tree

pgdog/src/auth/auth_result.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use std::fmt::Display;
2+
3+
#[derive(Default, PartialEq, Debug, Clone, Copy)]
4+
pub enum AuthResult {
5+
/// No problems.
6+
#[default]
7+
Ok,
8+
/// Password provided by user doesn't match config.
9+
NoPasswordMatch,
10+
/// Passwords not configured.
11+
NoPasswordConfig,
12+
/// User identity (TLS cert) doesn't match configured identity.
13+
NoIdentity,
14+
/// Passthrough auth says user doesn't exist.
15+
NoPassthroughNoUser,
16+
/// Passthrough auth doesn't allow password changes.
17+
NoPassthroughPasswordChange,
18+
/// No user or database in config.
19+
NoUserOrDatabase,
20+
/// Client didn't provide password message.
21+
NoPasswordMessage,
22+
}
23+
24+
impl AuthResult {
25+
pub fn is_ok(&self) -> bool {
26+
matches!(self, Self::Ok)
27+
}
28+
}
29+
30+
impl PartialEq<bool> for AuthResult {
31+
fn eq(&self, other: &bool) -> bool {
32+
self.is_ok() == *other
33+
}
34+
}
35+
36+
impl Display for AuthResult {
37+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38+
match self {
39+
Self::Ok => write!(f, "auth ok"),
40+
Self::NoPasswordMatch => write!(f, "wrong password"),
41+
Self::NoPasswordConfig => write!(f, "user has no passwords in config"),
42+
Self::NoIdentity => write!(f, "user identity does not match certificate"),
43+
Self::NoPassthroughNoUser => write!(f, "no user in config (passthrough auth)"),
44+
Self::NoPassthroughPasswordChange => {
45+
write!(f, "passthrough auth does not allow password change")
46+
}
47+
Self::NoUserOrDatabase => write!(f, "no user or database in config"),
48+
Self::NoPasswordMessage => write!(f, "client did not send password message"),
49+
}
50+
}
51+
}

pgdog/src/auth/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
//! PostgreSQL authentication mechanisms.
22
3+
pub mod auth_result;
34
pub mod error;
45
pub mod md5;
56
pub mod scram;
67

8+
pub use auth_result::AuthResult;
79
pub use error::Error;
810
pub use md5::Client;

pgdog/src/backend/databases.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use parking_lot::{Mutex, RawMutex};
1212
use pgdog_config::users::PasswordKind;
1313
use tracing::{debug, error, info, warn};
1414

15+
use crate::auth::AuthResult;
1516
use crate::backend::replication::ShardedSchemas;
1617
use crate::config::PoolerMode;
1718
use crate::frontend::client::query_engine::two_pc::Manager;
@@ -154,7 +155,7 @@ pub fn reload() -> Result<(), Error> {
154155
///
155156
/// Return true if user can login, false otherwise.
156157
///
157-
pub(crate) fn add(user: ConfigUser) -> Result<bool, Error> {
158+
pub(crate) fn add(user: ConfigUser) -> Result<AuthResult, Error> {
158159
fn add_user(user: ConfigUser) -> Result<(), Error> {
159160
debug!(
160161
r#"adding user "{}" to database "{}" via passthrough auth"#,
@@ -179,23 +180,23 @@ pub(crate) fn add(user: ConfigUser) -> Result<bool, Error> {
179180
existing.password = user.password.clone();
180181
add_user(existing)?;
181182
reload_from_existing()?;
182-
Ok(true)
183+
Ok(AuthResult::Ok)
183184
} else if existing.password == user.password {
184185
// Passwords match.
185-
Ok(true)
186+
Ok(AuthResult::Ok)
186187
} else if config.config.general.passthrough_auth.allows_change() {
187188
// Passwords don't match but we can change it.
188189
existing.password = user.password.clone();
189190
add_user(user)?;
190191
reload_from_existing()?;
191-
Ok(true)
192+
Ok(AuthResult::Ok)
192193
} else {
193-
Ok(false)
194+
Ok(AuthResult::NoPassthroughPasswordChange)
194195
}
195196
} else {
196197
add_user(user)?;
197198
reload_from_existing()?;
198-
Ok(true)
199+
Ok(AuthResult::Ok)
199200
}
200201
}
201202

@@ -773,7 +774,7 @@ mod tests {
773774

774775
let result = add(make_user("new_user", Some("secret")));
775776
assert!(result.is_ok());
776-
assert!(result.unwrap());
777+
assert!(result.unwrap().is_ok());
777778

778779
let config = crate::config::config();
779780
let found = config.users.find(&make_user("new_user", None));
@@ -790,7 +791,7 @@ mod tests {
790791

791792
let result = add(make_user("alice", Some("pass123")));
792793
assert!(result.is_ok());
793-
assert!(result.unwrap());
794+
assert!(result.unwrap().is_ok());
794795
}
795796

796797
#[tokio::test]
@@ -802,7 +803,7 @@ mod tests {
802803

803804
let result = add(make_user("bob", Some("new_pass")));
804805
assert!(result.is_ok());
805-
assert!(result.unwrap());
806+
assert!(result.unwrap().is_ok());
806807

807808
let config = crate::config::config();
808809
let found = config.users.find(&make_user("bob", None));
@@ -818,7 +819,7 @@ mod tests {
818819

819820
let result = add(make_user("charlie", Some("wrong_pass")));
820821
assert!(result.is_ok());
821-
assert!(!result.unwrap());
822+
assert!(!result.unwrap().is_ok());
822823
}
823824

824825
#[tokio::test]
@@ -830,7 +831,7 @@ mod tests {
830831

831832
let result = add(make_user("dave", Some("new_pass")));
832833
assert!(result.is_ok());
833-
assert!(result.unwrap());
834+
assert!(result.unwrap().is_ok());
834835

835836
let config = crate::config::config();
836837
let found = config.users.find(&make_user("dave", None));

pgdog/src/frontend/client/mod.rs

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ use std::time::{Duration, Instant};
77
use pgdog_config::users::PasswordKind;
88
use timeouts::Timeouts;
99
use tokio::{select, spawn, time::timeout};
10-
use tracing::{debug, enabled, error, info, trace, Level as LogLevel};
10+
use tracing::{debug, enabled, error, info, trace, warn, Level as LogLevel};
1111

1212
use super::{ClientRequest, Error, PreparedStatements};
13+
use crate::auth::AuthResult;
1314
use crate::auth::{md5, scram::Server};
1415
use crate::backend::maintenance_mode;
1516
use crate::backend::pool::stats::MemoryStats;
@@ -144,12 +145,12 @@ impl Client {
144145
user: &str,
145146
auth_type: &AuthType,
146147
passwords: &[PasswordKind],
147-
) -> Result<bool, Error> {
148+
) -> Result<AuthResult, Error> {
148149
if passwords.is_empty() {
149-
return Ok(false);
150+
return Ok(AuthResult::NoPasswordConfig);
150151
}
151152

152-
let ok = match auth_type {
153+
let result = match auth_type {
153154
AuthType::Md5 => {
154155
let md5 = md5::Client::new(
155156
user,
@@ -158,9 +159,13 @@ impl Client {
158159
stream.send_flush(&md5.challenge()).await?;
159160
let password = Password::from_bytes(stream.read().await?.to_bytes()?)?;
160161
if let Password::PasswordMessage { response } = password {
161-
md5.check(&response)
162+
if md5.check(&response) {
163+
AuthResult::Ok
164+
} else {
165+
AuthResult::NoPasswordMatch
166+
}
162167
} else {
163-
false
168+
AuthResult::NoPasswordMessage
164169
}
165170
}
166171

@@ -169,7 +174,11 @@ impl Client {
169174

170175
let scram = Server::new(passwords);
171176
let res = scram.handle(stream).await;
172-
matches!(res, Ok(true))
177+
if matches!(res, Ok(true)) {
178+
AuthResult::Ok
179+
} else {
180+
AuthResult::NoPasswordMatch
181+
}
173182
}
174183

175184
AuthType::Plain => {
@@ -178,15 +187,21 @@ impl Client {
178187
.await?;
179188
let response = stream.read().await?;
180189
let response = Password::from_bytes(response.to_bytes()?)?;
181-
passwords
190+
let is_match = passwords
182191
.iter()
183-
.any(|p| Some(p.as_str()) == response.password())
192+
.any(|p| Some(p.as_str()) == response.password());
193+
194+
if is_match {
195+
AuthResult::Ok
196+
} else {
197+
AuthResult::NoPasswordMatch
198+
}
184199
}
185200

186-
AuthType::Trust => true,
201+
AuthType::Trust => AuthResult::Ok,
187202
};
188203

189-
Ok(ok)
204+
Ok(result)
190205
}
191206

192207
/// Create new frontend client from the given TCP stream.
@@ -210,13 +225,14 @@ impl Client {
210225
let passthrough = config.config.general.passthrough_auth();
211226
let id = BackendKeyData::new_client(protocol_version);
212227
let comms = ClientComms::new(&id);
228+
let log_connections = config.config.general.log_connections;
213229

214230
// Check if we need to ask the client for its password in plaintext
215231
// because we don't actually have it configured.
216232
//
217233
// This is likely because passthrough authentication is enabled.
218234
//
219-
let auth_ok = if passthrough {
235+
let auth_result = if passthrough {
220236
// Get the password. We always need it because we need to check if
221237
// it's current and hasn't been changed.
222238
stream
@@ -232,7 +248,7 @@ impl Client {
232248
if let Some(user) = user {
233249
databases::add(user)?
234250
} else {
235-
false
251+
AuthResult::NoPassthroughNoUser
236252
}
237253
} else if admin {
238254
// The admin database is virtual and never present in the cluster
@@ -245,19 +261,29 @@ impl Client {
245261
if let Some(identity) = cluster.identity() {
246262
// mTLS authentication: the client certificate identity
247263
// must match the configured user identity.
248-
stream.tls_identity() == Some(identity)
264+
if stream.tls_identity() == Some(identity) {
265+
AuthResult::Ok
266+
} else {
267+
AuthResult::NoIdentity
268+
}
249269
} else {
250270
// Password authentication.
251271
Self::check_password(&mut stream, user, auth_type, cluster.passwords())
252272
.await?
253273
}
254274
}
255275

256-
Err(_) => false,
276+
Err(_) => AuthResult::NoUserOrDatabase,
257277
}
258278
};
259279

260-
if !auth_ok {
280+
if !auth_result.is_ok() {
281+
if log_connections {
282+
warn!(
283+
r#"user "{}" and database "{}" auth error: {}"#,
284+
user, database, auth_result
285+
);
286+
}
261287
stream.fatal(ErrorResponse::auth(user, database)).await?;
262288
return Ok(None);
263289
} else {

0 commit comments

Comments
 (0)