Skip to content

Commit 75fe0af

Browse files
authored
feat: allow passthrough user to change password (#842)
- fix #830 - fix #373 - fix: we were sending unnecessary `Terminate` message after `FATAL` error
1 parent faf931a commit 75fe0af

29 files changed

Lines changed: 1067 additions & 299 deletions

File tree

.schema/pgdog.schema.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,16 @@
12491249
"description": "Enabled without TLS requirement; network traffic may expose plaintext passwords.",
12501250
"type": "string",
12511251
"const": "enabled_plain"
1252+
},
1253+
{
1254+
"description": "Enabled and allows password changes.",
1255+
"type": "string",
1256+
"const": "enabled_allow_change"
1257+
},
1258+
{
1259+
"description": "Enabled without TLS requirement and allows password changes.",
1260+
"type": "string",
1261+
"const": "enabled_plain_allow_change"
12521262
}
12531263
]
12541264
},

integration/ruby/auth_spec.rb

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'rspec_helper'
4+
5+
describe 'authentication' do
6+
before(:all) do
7+
# Reset state so passthrough auth from previous runs is disabled.
8+
adm = PG.connect('postgres://admin:pgdog@127.0.0.1:6432/admin')
9+
adm.exec 'RELOAD'
10+
adm.close
11+
end
12+
13+
it 'rejects connections with bad password' do
14+
expect do
15+
PG.connect(dbname: 'pgdog', user: 'pgdog', password: 'wrong_password', port: 6432, host: '127.0.0.1')
16+
end.to raise_error(PG::ConnectionBad, /password for user "pgdog" and database "pgdog" is wrong/)
17+
end
18+
19+
it 'rejects connections with bad user' do
20+
expect do
21+
PG.connect(dbname: 'pgdog', user: 'nonexistent_user', password: 'pgdog', port: 6432, host: '127.0.0.1')
22+
end.to raise_error(PG::ConnectionBad, /password for user "nonexistent_user" and database "pgdog" is wrong/)
23+
end
24+
25+
shared_examples 'passthrough authentication' do
26+
it 'authenticates pgdog_pass user via passthrough' do
27+
adm = admin
28+
29+
# Reload to reset state, then enable passthrough auth.
30+
adm.exec 'RELOAD'
31+
adm.exec "SET passthrough_auth TO 'enabled_plain'"
32+
33+
# Verify pool starts as offline.
34+
pools = adm.exec 'SHOW POOLS'
35+
pass_pool = pools.select { |p| p['user'] == 'pgdog_pass' && p['database'] == 'pgdog' }.first
36+
expect(pass_pool).not_to be_nil
37+
expect(pass_pool['online']).to eq('f')
38+
39+
# Connect with passthrough user and run queries.
40+
conn = PG.connect(dbname: 'pgdog', user: 'pgdog_pass', password: 'pgdog', port: 6432, host: '127.0.0.1')
41+
res = conn.exec 'SELECT 1 AS one'
42+
expect(res[0]['one']).to eq('1')
43+
res = conn.exec 'SELECT 2 AS two'
44+
expect(res[0]['two']).to eq('2')
45+
46+
# Verify statement_timeout is carried over from user config.
47+
res = conn.exec 'SHOW statement_timeout'
48+
expect(res[0]['statement_timeout']).to eq('45999ms')
49+
conn.close
50+
51+
# Verify pool is now online.
52+
pools = adm.exec 'SHOW POOLS'
53+
pass_pool = pools.select { |p| p['user'] == 'pgdog_pass' && p['database'] == 'pgdog' }.first
54+
expect(pass_pool).not_to be_nil
55+
expect(pass_pool['online']).to eq('t')
56+
57+
adm.close
58+
end
59+
end
60+
61+
10.times do |i|
62+
context "passthrough attempt #{i + 1}" do
63+
include_examples 'passthrough authentication'
64+
end
65+
end
66+
end

integration/rust/tests/integration/auth.rs

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,48 @@ use sqlx::{Connection, Executor, PgConnection};
55

66
#[tokio::test]
77
#[serial]
8-
async fn test_auth() {
8+
async fn test_auth_types() {
99
let admin = admin_sqlx().await;
10-
let bad_password = "postgres://pgdog:skjfhjk23h4234@127.0.0.1:6432/pgdog";
11-
12-
admin.execute("SET auth_type TO 'trust'").await.unwrap();
13-
assert_setting_str("auth_type", "trust").await;
14-
15-
let mut any_password = PgConnection::connect(bad_password).await.unwrap();
16-
any_password.execute("SELECT 1").await.unwrap();
17-
18-
let mut empty_password = PgConnection::connect("postgres://pgdog@127.0.0.1:6432/pgdog")
19-
.await
20-
.unwrap();
21-
empty_password.execute("SELECT 1").await.unwrap();
22-
23-
admin.execute("SET auth_type TO 'scram'").await.unwrap();
24-
assert_setting_str("auth_type", "scram").await;
10+
let good = "postgres://pgdog:pgdog@127.0.0.1:6432/pgdog";
11+
let bad = "postgres://pgdog:wrong@127.0.0.1:6432/pgdog";
12+
let none = "postgres://pgdog@127.0.0.1:6432/pgdog";
13+
14+
for auth_type in ["md5", "scram", "plain", "trust"] {
15+
admin
16+
.execute(format!("SET auth_type TO '{auth_type}'").as_str())
17+
.await
18+
.unwrap();
19+
assert_setting_str("auth_type", auth_type).await;
20+
21+
let mut conn = PgConnection::connect(good).await.unwrap();
22+
conn.execute("SELECT 1").await.unwrap();
23+
24+
if auth_type == "trust" {
25+
let mut conn = PgConnection::connect(bad).await.unwrap();
26+
conn.execute("SELECT 1").await.unwrap();
27+
28+
let mut conn = PgConnection::connect(none).await.unwrap();
29+
conn.execute("SELECT 1").await.unwrap();
30+
} else {
31+
let bad_err = PgConnection::connect(bad).await.err().unwrap();
32+
assert!(
33+
bad_err
34+
.to_string()
35+
.contains("password for user \"pgdog\" and database \"pgdog\" is wrong"),
36+
"{auth_type}: bad password error: {bad_err}"
37+
);
38+
let none_err = PgConnection::connect(none).await.err().unwrap();
39+
assert!(
40+
none_err
41+
.to_string()
42+
.contains("password for user \"pgdog\" and database \"pgdog\" is wrong"),
43+
"{auth_type}: no password error: {none_err}"
44+
);
45+
}
46+
}
2547

26-
assert!(PgConnection::connect(bad_password).await.is_err());
48+
// Reset config.
49+
admin.execute("RELOAD").await.unwrap();
2750
}
2851

2952
#[tokio::test]
@@ -91,3 +114,55 @@ async fn test_passthrough_auth() {
91114
user.execute("SELECT 1").await.unwrap();
92115
original.execute("SELECT 1").await.unwrap();
93116
}
117+
118+
#[tokio::test]
119+
async fn test_passthrough_password_change() {
120+
let admin = admin_sqlx().await;
121+
let mut direct =
122+
PgConnection::connect("postgres://pgdog:pgdog@127.0.0.1:5432/pgdog?sslmode=disable")
123+
.await
124+
.unwrap();
125+
126+
// Ensure clean state.
127+
admin.execute("RELOAD").await.unwrap();
128+
admin
129+
.execute("SET passthrough_auth TO 'enabled_plain_allow_change'")
130+
.await
131+
.unwrap();
132+
assert_setting_str("passthrough_auth", "enabled_plain_allow_change").await;
133+
134+
// Make sure pgdog1 has the original password.
135+
direct
136+
.execute("ALTER USER pgdog1 PASSWORD 'pgdog'")
137+
.await
138+
.unwrap();
139+
140+
// Connect with original password and keep connection alive.
141+
let mut existing = PgConnection::connect("postgres://pgdog1:pgdog@127.0.0.1:6432/pgdog")
142+
.await
143+
.unwrap();
144+
existing.execute("SELECT 1").await.unwrap();
145+
146+
// Change password in PostgreSQL directly.
147+
direct
148+
.execute("ALTER USER pgdog1 PASSWORD 'new_password'")
149+
.await
150+
.unwrap();
151+
152+
// New connection with new password should work.
153+
let mut new_conn = PgConnection::connect("postgres://pgdog1:new_password@127.0.0.1:6432/pgdog")
154+
.await
155+
.unwrap();
156+
new_conn.execute("SELECT 1").await.unwrap();
157+
158+
// Existing connection should still work.
159+
existing.execute("SELECT 1").await.unwrap();
160+
161+
// Cleanup: restore original password.
162+
direct
163+
.execute("ALTER USER pgdog1 PASSWORD 'pgdog'")
164+
.await
165+
.unwrap();
166+
167+
admin.execute("RELOAD").await.unwrap();
168+
}

integration/rust/tests/integration/reload.rs

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::time::Duration;
22

3-
use rust::setup::{admin_tokio, backends, connection_failover};
3+
use rust::setup::{admin_tokio, backends, connection_failover, connection_sqlx_direct};
44
use serial_test::serial;
5-
use sqlx::Executor;
5+
use sqlx::{Executor, Row, postgres::PgPoolOptions};
66
use tokio::time::sleep;
77

88
#[tokio::test]
@@ -34,6 +34,103 @@ async fn test_reload() {
3434
conn.close().await;
3535
}
3636

37+
#[tokio::test]
38+
#[serial]
39+
async fn test_reload_connection_count_stable() {
40+
sleep(Duration::from_secs(1)).await;
41+
let admin = admin_tokio().await;
42+
let direct = connection_sqlx_direct().await;
43+
44+
// Count PgDog connections on Postgres before reload.
45+
let before: i64 = direct
46+
.fetch_one("SELECT COUNT(*)::BIGINT FROM pg_stat_activity WHERE application_name = 'PgDog'")
47+
.await
48+
.unwrap()
49+
.get(0);
50+
51+
assert!(before > 0, "expected pgdog connections before reload");
52+
eprintln!("connections before RELOAD: {before}");
53+
54+
admin.simple_query("RELOAD").await.unwrap();
55+
sleep(Duration::from_millis(500)).await;
56+
57+
let after: i64 = direct
58+
.fetch_one("SELECT COUNT(*)::BIGINT FROM pg_stat_activity WHERE application_name = 'PgDog'")
59+
.await
60+
.unwrap()
61+
.get(0);
62+
63+
eprintln!("connections after RELOAD: {after}");
64+
65+
assert_eq!(
66+
before, after,
67+
"connection count changed after RELOAD: before={before}, after={after}"
68+
);
69+
70+
direct.close().await;
71+
}
72+
73+
#[tokio::test]
74+
#[serial]
75+
async fn test_reload_pool_size_not_exceeded() {
76+
let pool_size: i64 = 50; // default_pool_size
77+
let num_clients = 20;
78+
let app_name = "test_reload_pool_limit";
79+
80+
sleep(Duration::from_secs(1)).await;
81+
let admin = admin_tokio().await;
82+
let direct = connection_sqlx_direct().await;
83+
84+
// Spin up clients that continuously run queries.
85+
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
86+
let mut handles = vec![];
87+
88+
for _ in 0..num_clients {
89+
let stop = stop.clone();
90+
handles.push(tokio::spawn(async move {
91+
let pool = PgPoolOptions::new()
92+
.max_connections(1)
93+
.connect(&format!(
94+
"postgres://pgdog:pgdog@127.0.0.1:6432/pgdog?application_name={app_name}"
95+
))
96+
.await
97+
.unwrap();
98+
while !stop.load(std::sync::atomic::Ordering::Relaxed) {
99+
let _ = pool.execute("SELECT 1").await;
100+
sleep(Duration::from_millis(10)).await;
101+
}
102+
pool.close().await;
103+
}));
104+
}
105+
106+
// Let clients warm up.
107+
sleep(Duration::from_millis(500)).await;
108+
109+
// Issue multiple reloads while clients are active and check pool size each time.
110+
for i in 0..10 {
111+
admin.simple_query("RELOAD").await.unwrap();
112+
sleep(Duration::from_millis(100)).await;
113+
114+
let query = format!(
115+
"SELECT COUNT(*)::BIGINT FROM pg_stat_activity WHERE application_name = '{app_name}'"
116+
);
117+
let count: i64 = direct.fetch_one(query.as_str()).await.unwrap().get(0);
118+
119+
eprintln!("reload {i}: {count} connections (pool_size={pool_size})");
120+
assert!(
121+
count <= pool_size,
122+
"pool size exceeded after reload {i}: {count} > {pool_size}"
123+
);
124+
}
125+
126+
stop.store(true, std::sync::atomic::Ordering::Relaxed);
127+
for h in handles {
128+
h.await.unwrap();
129+
}
130+
131+
direct.close().await;
132+
}
133+
37134
#[tokio::test]
38135
#[serial]
39136
async fn test_reconnect() {

integration/toxi/toxi_spec.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def warm_up
5757
end
5858
25.times do
5959
c.exec 'SELECT 1'
60-
rescue PG::SystemError
60+
rescue PG::SystemError, PG::ConnectionBad
6161
c = conn # reconnect
6262
errors.increment
6363
end
@@ -84,11 +84,12 @@ def warm_up
8484
25.times do
8585
Sharded.where(id: 1).first
8686
ok += 1
87-
rescue StandardError
87+
rescue StandardError => e
88+
puts "Error: #{e.class}: #{e.message}"
8889
errors += 1
8990
end
9091
end
91-
expect(errors).to be <= 1
92+
expect(errors).to be <= 2
9293
expect(25 - ok).to eq(errors)
9394
end
9495
end

integration/users.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,11 @@ database = "pgdog_schema_no_cross"
8383
server_user = "pgdog"
8484
cross_shard_disabled = true
8585
pooler_mode = "session"
86+
87+
[[users]]
88+
name = "pgdog_pass"
89+
server_user = "pgdog"
90+
pool_size = 5
91+
database = "pgdog"
92+
min_pool_size = 1
93+
statement_timeout = 45_999

pgdog-config/src/auth.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ pub enum PassthroughAuth {
1717
Enabled,
1818
/// Enabled without TLS requirement; network traffic may expose plaintext passwords.
1919
EnabledPlain,
20+
/// Enabled and allows password changes.
21+
EnabledAllowChange,
22+
/// Enabled without TLS requirement and allows password changes.
23+
EnabledPlainAllowChange,
24+
}
25+
26+
impl PassthroughAuth {
27+
pub fn allows_change(&self) -> bool {
28+
matches!(
29+
self,
30+
Self::EnabledPlainAllowChange | Self::EnabledAllowChange
31+
)
32+
}
2033
}
2134

2235
/// authentication mechanism for client connections.

pgdog-config/src/general.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,8 +1129,15 @@ impl General {
11291129
}
11301130

11311131
pub fn passthrough_auth(&self) -> bool {
1132-
self.tls().is_some() && self.passthrough_auth == PassthroughAuth::Enabled
1133-
|| self.passthrough_auth == PassthroughAuth::EnabledPlain
1132+
self.tls().is_some()
1133+
&& matches!(
1134+
self.passthrough_auth,
1135+
PassthroughAuth::Enabled | PassthroughAuth::EnabledAllowChange
1136+
)
1137+
|| matches!(
1138+
self.passthrough_auth,
1139+
PassthroughAuth::EnabledPlain | PassthroughAuth::EnabledPlainAllowChange
1140+
)
11341141
}
11351142

11361143
/// Support for LISTEN/NOTIFY.

0 commit comments

Comments
 (0)