Skip to content

Commit cdd03e7

Browse files
add auth_query_database config
This commit adds the new config auth_query_database to allow use a different database to get the hashes from the database instance. I updated the example folder with functional code to validate the new feature and updated the config docs. Also updated ruby code to use the new env var LOG_LEVEL. Signed-off-by: Sebastian Webber <[email protected]>
1 parent 19cb8a3 commit cdd03e7

14 files changed

+145
-108
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ lcov.info
1010
dev/.bash_history
1111
dev/cache
1212
!dev/cache/.keepme
13+
.bundle

CONFIG.md

+42
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,38 @@ Query to be sent to servers to obtain the hash used for md5 authentication. The
297297
established using the database configured in the pool. This parameter is inherited by every pool
298298
and can be redefined in pool configuration.
299299

300+
#### Configuration example
301+
302+
> This example uses the `postgres` database.
303+
304+
add on `pgcat.toml`:
305+
306+
```toml
307+
auth_query = "SELECT * FROM pgcat.user_lookup('$1');"
308+
auth_query_user = "connection_pooler"
309+
auth_query_password = "user-look-up-pass"
310+
auth_query_database = "postgres"
311+
```
312+
313+
setup in the `postgres` database:
314+
315+
```sql
316+
CREATE ROLE connection_pooler PASSWORD 'user-look-up-pass' LOGIN;
317+
CREATE SCHEMA IF NOT EXISTS pgcat;
318+
319+
CREATE OR REPLACE FUNCTION pgcat.user_lookup(i_username text)
320+
RETURNS table ("user" text, hash text) AS $$
321+
SELECT usename as user, passwd as hash FROM pg_catalog.pg_shadow
322+
WHERE usename = i_username;
323+
$$ LANGUAGE sql SECURITY DEFINER;
324+
325+
GRANT CONNECT ON DATABASE postgres TO connection_pooler;
326+
GRANT USAGE ON SCHEMA pgcat TO connection_pooler;
327+
328+
REVOKE ALL ON FUNCTION pgcat.user_lookup(text) FROM public, connection_pooler;
329+
GRANT EXECUTE ON FUNCTION pgcat.user_lookup(text) TO connection_pooler;
330+
```
331+
300332
### auth_query_user
301333
```
302334
path: pools.<pool_name>.auth_query_user
@@ -319,6 +351,16 @@ Password to be used for connecting to servers to obtain the hash used for md5 au
319351
specified in `auth_query_user`. The connection will be established using the database configured in the pool.
320352
This parameter is inherited by every pool and can be redefined in pool configuration.
321353

354+
### auth_query_database
355+
```
356+
path: pools.<pool_name>.auth_query_database
357+
default: <UNSET>
358+
example: "postgres"
359+
```
360+
361+
Database to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
362+
specified in `auth_query_query`. This parameter is inherited by every pool and can be redefined in pool configuration.
363+
322364
### automatic_sharding_key
323365
```
324366
path: pools.<pool_name>.automatic_sharding_key

docker-compose.yml

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ services:
55
environment:
66
POSTGRES_PASSWORD: postgres
77
POSTGRES_HOST_AUTH_METHOD: md5
8+
ports:
9+
- "15432:5432"
10+
volumes:
11+
- "${PWD}/examples/docker:/docker-entrypoint-initdb.d"
812
pgcat:
913
build: .
1014
command:

examples/docker/01-setup.sql

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER SYSTEM SET password_encryption to 'md5';
2+
SELECT pg_reload_conf();
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE ROLE myappadmin PASSWORD 'myappadmin' LOGIN;
2+
CREATE DATABASE myapp owner myappadmin;
3+
GRANT ALL PRIVILEGES ON DATABASE myapp TO myappadmin;

examples/docker/03-query-auth.sql

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
CREATE ROLE connection_pooler PASSWORD 'user-look-up-pass' LOGIN;
2+
CREATE SCHEMA IF NOT EXISTS pgcat;
3+
4+
CREATE OR REPLACE FUNCTION pgcat.user_lookup(i_username text)
5+
RETURNS table ("user" text, hash text) AS $$
6+
SELECT usename as user, passwd as hash FROM pg_catalog.pg_shadow
7+
WHERE usename = i_username;
8+
$$ LANGUAGE sql SECURITY DEFINER;
9+
10+
-- usage:
11+
-- SELECT * FROM pgcat.user_lookup('$1');
12+
13+
GRANT CONNECT ON DATABASE postgres TO connection_pooler;
14+
GRANT USAGE ON SCHEMA pgcat TO connection_pooler;
15+
16+
REVOKE ALL ON FUNCTION pgcat.user_lookup(text) FROM public, connection_pooler;
17+
GRANT EXECUTE ON FUNCTION pgcat.user_lookup(text) TO connection_pooler;

examples/docker/pgcat.toml

+22-103
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,42 @@
1-
#
2-
# PgCat config example.
3-
#
4-
5-
#
6-
# General pooler settings
1+
## pgcat.toml
72
[general]
8-
# What IP to run on, 0.0.0.0 means accessible from everywhere.
93
host = "0.0.0.0"
10-
11-
# Port to run on, same as PgBouncer used in this example.
124
port = 6432
13-
14-
# Whether to enable prometheus exporter or not.
155
enable_prometheus_exporter = true
16-
17-
# Port at which prometheus exporter listens on.
186
prometheus_exporter_port = 9930
197

20-
# How long to wait before aborting a server connection (ms).
21-
connect_timeout = 5000
22-
23-
# How much time to give `SELECT 1` health check query to return with a result (ms).
24-
healthcheck_timeout = 1000
25-
26-
# How long to keep connection available for immediate re-use, without running a healthcheck query on it
27-
healthcheck_delay = 30000
28-
29-
# How much time to give clients during shutdown before forcibly killing client connections (ms).
30-
shutdown_timeout = 60000
31-
32-
# For how long to ban a server if it fails a health check (seconds).
33-
ban_time = 60 # seconds
34-
35-
# If we should log client connections
36-
log_client_connections = false
8+
log_client_connections = true
9+
log_client_disconnections = true
3710

38-
# If we should log client disconnections
39-
log_client_disconnections = false
11+
server_tls = false
12+
verify_server_certificate = false
13+
#tls_certificate = "/etc/postgresql/certificate/server.crt"
14+
#tls_private_key = "/etc/postgresql/certificate/server.key"
4015

41-
# TLS
42-
# tls_certificate = "server.cert"
43-
# tls_private_key = "server.key"
44-
45-
# Credentials to access the virtual administrative database (pgbouncer or pgcat)
46-
# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc..
4716
admin_username = "postgres"
4817
admin_password = "postgres"
4918

50-
# pool
51-
# configs are structured as pool.<pool_name>
52-
# the pool_name is what clients use as database name when connecting
53-
# For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded"
54-
[pools.postgres]
55-
# Pool mode (see PgBouncer docs for more).
56-
# session: one server connection per connected client
57-
# transaction: one server connection per client transaction
58-
pool_mode = "transaction"
19+
auth_query = "SELECT * FROM pgcat.user_lookup('$1');"
20+
auth_query_user = "connection_pooler"
21+
auth_query_password = "user-look-up-pass"
22+
auth_query_database = "postgres"
5923

60-
# If the client doesn't specify, route traffic to
61-
# this role by default.
62-
#
63-
# any: round-robin between primary and replicas,
64-
# replica: round-robin between replicas only without touching the primary,
65-
# primary: all queries go to the primary unless otherwise specified.
24+
## session mode
25+
[pools.myapp]
26+
pool_mode = "session"
6627
default_role = "any"
6728

68-
# Query parser. If enabled, we'll attempt to parse
69-
# every incoming query to determine if it's a read or a write.
70-
# If it's a read query, we'll direct it to a replica. Otherwise, if it's a write,
71-
# we'll direct it to the primary.
72-
query_parser_enabled = true
73-
74-
# If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for
75-
# load balancing of read queries. Otherwise, the primary will only be used for write
76-
# queries. The primary can always be explicitly selected with our custom protocol.
77-
primary_reads_enabled = true
29+
# query_parser_enabled = true
7830

79-
# So what if you wanted to implement a different hashing function,
80-
# or you've already built one and you want this pooler to use it?
81-
#
82-
# Current options:
83-
#
84-
# pg_bigint_hash: PARTITION BY HASH (Postgres hashing function)
85-
# sha1: A hashing function based on SHA1
86-
#
8731
sharding_function = "pg_bigint_hash"
8832

89-
# Credentials for users that may connect to this cluster
90-
[pools.postgres.users.0]
91-
username = "postgres"
92-
password = "postgres"
93-
# Maximum number of server connections that can be established for this user
94-
# The maximum number of connection from a single Pgcat process to any database in the cluster
95-
# is the sum of pool_size across all users.
96-
pool_size = 9
97-
98-
# Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way.
99-
statement_timeout = 0
100-
101-
# Shard 0
102-
[pools.postgres.shards.0]
103-
# [ host, port, role ]
104-
servers = [
105-
[ "postgres", 5432, "primary" ],
106-
[ "postgres", 5432, "replica" ]
107-
]
108-
# Database name (e.g. "postgres")
109-
database = "postgres"
110-
111-
[pools.postgres.shards.1]
112-
servers = [
113-
[ "postgres", 5432, "primary" ],
114-
[ "postgres", 5432, "replica" ],
115-
]
116-
database = "postgres"
33+
[pools.myapp.users.0]
34+
username = "myappadmin"
35+
pool_size = 23
11736

118-
[pools.postgres.shards.2]
37+
[pools.myapp.shards.0]
11938
servers = [
120-
[ "postgres", 5432, "primary" ],
121-
[ "postgres", 5432, "replica" ],
39+
# [ "localhost", 15432, "primary" ], # to use without docker
40+
[ "postgres", 5432, "primary" ], # to use with docker
12241
]
123-
database = "postgres"
42+
database = "myapp"

pgcat.toml

+4
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ sharding_function = "pg_bigint_hash"
195195
# This parameter is inherited by every pool and can be redefined in pool configuration.
196196
# auth_query_password = "sharding_user"
197197

198+
# Database to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
199+
# specified in `auth_query_query`. This parameter is inherited by every pool and can be redefined in pool configuration.
200+
# auth_query_database = "shared_db"
201+
198202
# Automatically parse this from queries and route queries to the right shard!
199203
# automatic_sharding_key = "data.id"
200204

src/auth_passthrough.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ pub struct AuthPassthrough {
88
password: String,
99
query: String,
1010
user: String,
11+
database: Option<String>,
1112
}
1213

1314
impl AuthPassthrough {
1415
/// Initializes an AuthPassthrough.
15-
pub fn new(query: &str, user: &str, password: &str) -> Self {
16+
pub fn new(query: &str, user: &str, password: &str, database: Option<String>) -> Self {
1617
AuthPassthrough {
1718
password: password.to_string(),
1819
query: query.to_string(),
1920
user: user.to_string(),
21+
database: database,
2022
}
2123
}
2224

@@ -28,6 +30,7 @@ impl AuthPassthrough {
2830
pool_config.auth_query.as_ref().unwrap(),
2931
pool_config.auth_query_user.as_ref().unwrap(),
3032
pool_config.auth_query_password.as_ref().unwrap(),
33+
pool_config.auth_query_database.clone(),
3134
));
3235
}
3336

@@ -64,7 +67,7 @@ impl AuthPassthrough {
6467
/// ```
6568
/// use pgcat::auth_passthrough::AuthPassthrough;
6669
/// use pgcat::config::Address;
67-
/// let auth_passthrough = AuthPassthrough::new("SELECT * FROM public.user_lookup('$1');", "postgres", "postgres");
70+
/// let auth_passthrough = AuthPassthrough::new("SELECT * FROM public.user_lookup('$1');", "postgres", "postgres", None);
6871
/// auth_passthrough.fetch_hash(&Address::default());
6972
/// ```
7073
///
@@ -87,7 +90,11 @@ impl AuthPassthrough {
8790

8891
let auth_query = self.query.replace("$1", user);
8992

90-
match Server::exec_simple_query(address, &auth_user, &auth_query).await {
93+
let mut query_address = address.clone();
94+
if self.database.is_some() {
95+
query_address.database = self.database.clone().unwrap();
96+
}
97+
match Server::exec_simple_query(&query_address, &auth_user, &auth_query).await {
9198
Ok(password_data) => {
9299
if password_data.len() == 2 && password_data.first().unwrap() == user {
93100
if let Some(stripped_hash) = password_data

src/config.rs

+30
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ pub struct General {
322322
pub auth_query: Option<String>,
323323
pub auth_query_user: Option<String>,
324324
pub auth_query_password: Option<String>,
325+
pub auth_query_database: Option<String>,
325326

326327
#[serde(default)]
327328
pub prepared_statements: bool,
@@ -448,6 +449,7 @@ impl Default for General {
448449
auth_query: None,
449450
auth_query_user: None,
450451
auth_query_password: None,
452+
auth_query_database: None,
451453
server_lifetime: Self::default_server_lifetime(),
452454
server_round_robin: Self::default_server_round_robin(),
453455
validate_config: true,
@@ -538,6 +540,7 @@ pub struct Pool {
538540
pub auth_query: Option<String>,
539541
pub auth_query_user: Option<String>,
540542
pub auth_query_password: Option<String>,
543+
pub auth_query_database: Option<String>,
541544

542545
#[serde(default = "Pool::default_cleanup_server_connections")]
543546
pub cleanup_server_connections: bool,
@@ -674,6 +677,7 @@ impl Default for Pool {
674677
auth_query: None,
675678
auth_query_user: None,
676679
auth_query_password: None,
680+
auth_query_database: None,
677681
server_lifetime: None,
678682
plugins: None,
679683
cleanup_server_connections: true,
@@ -876,6 +880,10 @@ impl Config {
876880
if pool.auth_query_password.is_none() {
877881
pool.auth_query_password = self.general.auth_query_password.clone();
878882
}
883+
884+
if pool.auth_query_database.is_none() {
885+
pool.auth_query_database = self.general.auth_query_database.clone();
886+
}
879887
}
880888
}
881889
}
@@ -1011,6 +1019,28 @@ impl Config {
10111019
self.general.server_lifetime
10121020
);
10131021
info!("Sever round robin: {}", self.general.server_round_robin);
1022+
1023+
if self.general.auth_query.is_some() {
1024+
info!(
1025+
"Auth query configured: {}",
1026+
self.general.auth_query.clone().unwrap()
1027+
);
1028+
}
1029+
1030+
if self.general.auth_query_user.is_some() {
1031+
info!(
1032+
"Auth query user: {}",
1033+
self.general.auth_query_user.clone().unwrap()
1034+
);
1035+
}
1036+
1037+
if self.general.auth_query_database.is_some() {
1038+
info!(
1039+
"Auth query database: {}",
1040+
self.general.auth_query_database.clone().unwrap()
1041+
);
1042+
}
1043+
10141044
match self.general.tls_certificate.clone() {
10151045
Some(tls_certificate) => {
10161046
info!("TLS certificate: {}", tls_certificate);

src/pool.rs

+3
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ pub struct PoolSettings {
142142
pub auth_query: Option<String>,
143143
pub auth_query_user: Option<String>,
144144
pub auth_query_password: Option<String>,
145+
pub auth_query_database: Option<String>,
145146

146147
/// Plugins
147148
pub plugins: Option<Plugins>,
@@ -169,6 +170,7 @@ impl Default for PoolSettings {
169170
auth_query: None,
170171
auth_query_user: None,
171172
auth_query_password: None,
173+
auth_query_database: None,
172174
plugins: None,
173175
}
174176
}
@@ -474,6 +476,7 @@ impl ConnectionPool {
474476
auth_query: pool_config.auth_query.clone(),
475477
auth_query_user: pool_config.auth_query_user.clone(),
476478
auth_query_password: pool_config.auth_query_password.clone(),
479+
auth_query_database: pool_config.auth_query_database.clone(),
477480
plugins: match pool_config.plugins {
478481
Some(ref plugins) => Some(plugins.clone()),
479482
None => config.plugins.clone(),

0 commit comments

Comments
 (0)