Skip to content

Commit 6381136

Browse files
committed
Add auth_query config and make pass in pools optional
Add `auth_query` feature Adds a feature that allows setting auth passthrough for md5 auth. It adds 4 new general config parameters: - `auth_query`: An string containing a query that will be executed on boot to obtain the hash of a given user. This query have to use a placeholder `$1`, so pgcat can replace it with the user its trying to fetch the hash from. - `auth_query_user`: The user to use for connecting to the server and executing the auth_query. - `auth_query_password`: The password to use for connecting to the server and executing the auth_query. - `auth_query_database`: The database to use for connecting to the server and executing the auth_query. The behavior is, at boot time, when validating server connections, a hash is fetched per server and stored there. When new server connections are created, that hash is used for creating them, if the hash could not be obtained for whatever reason, it falls back to the password set. Client connections are also authenticated using the obtained hash. Fix error message info parameters (username <-> pool_name) Make reporter optional in server definition We use server connections for pools but also for fetching auth hashes if query_auth is set up. As we do not want to mess with stats this change allows reporter to be optional. Change AuthPassthrough::fetch_hah to use Address username Allow query_auth configuration reload This adds support for reloading query_auth configuration. Current implementation: - Checks whether `auth_query` configuration is active, if so, it fetches (or refetches) hashes from servers and sets up pools. When it detects a password change, the pool is dropped and a new one is created, current established connections will be left connected. - When we go from 'auth_query' configured to unconfigured the reload logic detects it and drops Md5 hashes stored in connection pools, so cleartext passwords are used instead. Add tests for `query_auth` reload functionality This change adds tests for query_auth configuration reload and also does some refactoring. - Now postgres containers in `docker-compose` are set to use md5 auth. Note that this still uses scram if the password is stored using scram. - Current tests are: - Test that activating the functionality can be added without restarting. - Test that with a failing `query_auth`, clear text passwords are used. - Test that with a correct `query_auth` and without clear text passwords, auth works as expected. - Test that when we change a password in postgres and reload, the new password is obtained, and the pool rotated. - Test that we can use and ENV var to set `auth_query_password`. Change auth_query config to be per pool instead of global. Given that we can connect to different username/databases/servers using connection pools, it makes sense that `auth_query` should be configured in a per pool basis. This change implements that and also leaves the global parameters so pool configuration can inherit global ones. Note that now, `auth_query_database` is dropped in favor of the pool configured database.
1 parent eb8cfdb commit 6381136

23 files changed

+1047
-40
lines changed

.circleci/pgcat.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ tls_private_key = ".circleci/server.key"
5050
admin_username = "admin_user"
5151
admin_password = "admin_pass"
5252

53+
# auth_query = "SELECT * FROM public.user_lookup('$1');"
54+
# auth_query_user = "md5_auth_user"
55+
# auth_query_password = "secret"
56+
5357
# pool
5458
# configs are structured as pool.<pool_name>
5559
# the pool_name is what clients use as database name when connecting

.circleci/query_auth_test.sh

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/bin/bash
2+
# Query auth test
3+
4+
set -e
5+
set -o xtrace
6+
7+
export LOCAL_IP=$(hostname -i)
8+
9+
# In config file we have this commented:
10+
# [general]
11+
# ...
12+
# auth_query = "SELECT * FROM public.user_lookup('$1');"
13+
# auth_query_user = "md5_auth_user"
14+
# auth_query_password = "secret"
15+
# ...
16+
17+
# Before (sets up auth_query in postgres and pgcat)
18+
PGDATABASE=shard0 PGPASSWORD=postgres exec_in_servers postgres tests/sharding/query_auth_setup.sql
19+
PGDATABASE=shard0 PGPASSWORD=postgres exec_in_servers postgres tests/sharding/query_auth_setup_function.sql
20+
21+
sed -i 's/^# auth_query/auth_query/' .circleci/pgcat.toml
22+
23+
# TEST_WRONG_AUTH_QUERY BEGIN
24+
# When auth_query fails...
25+
PGDATABASE=shard0 \
26+
PGPASSWORD=postgres \
27+
psql -e -h 127.0.0.1 -p 5432 -U postgres -c "REVOKE ALL ON FUNCTION public.user_lookup(text) FROM public, md5_auth_user;"
28+
29+
kill -SIGHUP $(pgrep pgcat) # Reload config
30+
sleep 0.2
31+
32+
# ... we can still connect.
33+
echo "When query_auth_config is wrong, we fall back to passwords set in cleartext."
34+
psql -U sharding_user -h 127.0.0.1 -p 6432 -c 'SELECT 1'
35+
36+
# After
37+
PGDATABASE=shard0 \
38+
PGPASSWORD=postgres \
39+
psql -e -h 127.0.0.1 -p 5432 -U postgres -c "GRANT EXECUTE ON FUNCTION public.user_lookup(text) TO md5_auth_user;"
40+
# TEST_WRONG_AUTH_QUERY END
41+
42+
# TEST_AUTH_QUERY BEGIN
43+
# When no passwords are specified in config file...
44+
sed -i 's/^password =/# password =/' .circleci/pgcat.toml
45+
kill -SIGHUP $(pgrep pgcat) # Reload config
46+
sleep 0.2
47+
48+
# ... we can still connect
49+
echo "When no passwords are specified in config file, and query_auth is set, we can still connect"
50+
psql -U sharding_user -h 127.0.0.1 -p 6432 -c 'SELECT 1'
51+
# TEST_AUTH_QUERY END
52+
53+
# TEST_PASSWORD_CHANGE BEGIN
54+
# When we change the password of a user in postgres...
55+
PGDATABASE=shard0 \
56+
PGPASSWORD=postgres \
57+
psql -e -h 127.0.0.1 -p 5432 -U postgres \
58+
-c "ALTER USER sharding_user WITH ENCRYPTED PASSWORD 'md5b47a59331e93a520d20e90fc8a3355a4'; --- another_sharding_password"
59+
60+
# ... we can connect using the new password
61+
echo "When we change pass in postgres the new hash is fetched after a connection error."
62+
PGPASSWORD=another_sharding_password psql -U sharding_user -h "${LOCAL_IP}" -p 6432 -c 'SELECT 1'
63+
# TEST_PASSWORD_CHANGE END
64+
65+
# After
66+
PGDATABASE=shard0 PGPASSWORD=postgres exec_in_servers postgres tests/sharding/query_auth_teardown_function.sql
67+
PGDATABASE=shard0 PGPASSWORD=postgres exec_in_servers postgres tests/sharding/query_auth_teardown.sql
68+
sed -i 's/^auth_query/# auth_query/' .circleci/pgcat.toml
69+
sed -i 's/^# password =/password =/' .circleci/pgcat.toml
70+
71+
kill -SIGHUP $(pgrep pgcat)
72+
sleep 0.2

.circleci/run_tests.sh

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@ function start_pgcat() {
1414
sleep 1
1515
}
1616

17+
# Executes an sql file in every postgres server set up in tests
18+
# exec_in_servers user file
19+
function exec_in_servers() {
20+
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 5432 -U $1 -f $2
21+
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 7432 -U $1 -f $2
22+
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 8432 -U $1 -f $2
23+
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 9432 -U $1 -f $2
24+
}
25+
1726
# Setup the database with shards and user
18-
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 5432 -U postgres -f tests/sharding/query_routing_setup.sql
19-
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 7432 -U postgres -f tests/sharding/query_routing_setup.sql
20-
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 8432 -U postgres -f tests/sharding/query_routing_setup.sql
21-
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 9432 -U postgres -f tests/sharding/query_routing_setup.sql
27+
PGPASSWORD=postgres exec_in_servers postgres tests/sharding/query_routing_setup.sql
2228

2329
PGPASSWORD=sharding_user pgbench -h 127.0.0.1 -U sharding_user shard0 -i
2430
PGPASSWORD=sharding_user pgbench -h 127.0.0.1 -U sharding_user shard1 -i
@@ -34,11 +40,15 @@ toxiproxy-cli create -l 127.0.0.1:5433 -u 127.0.0.1:5432 postgres_replica
3440
start_pgcat "info"
3541

3642
# Check that prometheus is running
37-
curl --fail localhost:9930/metrics
43+
#curl --fail localhost:9930/metrics
3844

3945
export PGPASSWORD=sharding_user
4046
export PGDATABASE=sharded_db
4147

48+
# Query auth test
49+
echo 'THIS IS A TEST'
50+
source .circleci/query_auth_test.sh
51+
4252
# pgbench test
4353
pgbench -U sharding_user -i -h 127.0.0.1 -p 6432
4454
pgbench -U sharding_user -h 127.0.0.1 -p 6432 -t 500 -c 2 --protocol simple -f tests/pgbench/simple.sql

.rustfmt.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
edition = "2021"

Cargo.lock

Lines changed: 40 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ version = "0.6.0-alpha1"
44
edition = "2021"
55

66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7-
87
[dependencies]
98
tokio = { version = "1", features = ["full"] }
109
bytes = "1"
@@ -37,6 +36,8 @@ exitcode = "1.1.2"
3736
futures = "0.3"
3837
socket2 = { version = "0.4.7", features = ["all"] }
3938
nix = "0.26.2"
39+
postgres-protocol = "0.6.4"
40+
fallible-iterator = "0.2"
4041

4142
[target.'cfg(not(target_env = "msvc"))'.dependencies]
4243
jemallocator = "0.5.0"
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#
2+
# PgCat config example.
3+
#
4+
5+
#
6+
# General pooler settings
7+
[general]
8+
# What IP to run on, 0.0.0.0 means accessible from everywhere.
9+
host = "0.0.0.0"
10+
11+
# Port to run on, same as PgBouncer used in this example.
12+
port = 6432
13+
14+
# Whether to enable prometheus exporter or not.
15+
enable_prometheus_exporter = true
16+
17+
# Port at which prometheus exporter listens on.
18+
prometheus_exporter_port = 9930
19+
20+
# How long to wait before aborting a server connection (ms).
21+
connect_timeout = 5000
22+
23+
# How long an idle connection with a server is left open (ms).
24+
idle_timeout = 30000
25+
26+
# How much time to give the health check query to return with a result (ms).
27+
healthcheck_timeout = 1000
28+
29+
# How long to keep connection available for immediate re-use, without running a healthcheck query on it
30+
healthcheck_delay = 30000
31+
32+
# How much time to give clients during shutdown before forcibly killing client connections (ms).
33+
shutdown_timeout = 60000
34+
35+
# For how long to ban a server if it fails a health check (seconds).
36+
ban_time = 60 # seconds
37+
38+
# If we should log client connections
39+
log_client_connections = false
40+
41+
# If we should log client disconnections
42+
log_client_disconnections = false
43+
44+
# Reload config automatically if it changes.
45+
autoreload = false
46+
47+
# Number of worker threads the Runtime will use (4 by default).
48+
worker_threads = 5
49+
50+
# TLS
51+
# tls_certificate = "server.cert"
52+
# tls_private_key = "server.key"
53+
54+
# Credentials to access the virtual administrative database (pgbouncer or pgcat)
55+
# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc..
56+
admin_username = "admin_user"
57+
admin_password = "admin_pass"
58+
59+
auth_query = "SELECT 1"
60+
auth_query_user = "testuser"
61+
62+
# pool
63+
# configs are structured as pool.<pool_name>
64+
# the pool_name is what clients use as database name when connecting
65+
# For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded_db"
66+
[pools.sharded_db]
67+
# Pool mode (see PgBouncer docs for more).
68+
# session: one server connection per connected client
69+
# transaction: one server connection per client transaction
70+
pool_mode = "transaction"
71+
72+
# If the client doesn't specify, route traffic to
73+
# this role by default.
74+
#
75+
# any: round-robin between primary and replicas,
76+
# replica: round-robin between replicas only without touching the primary,
77+
# primary: all queries go to the primary unless otherwise specified.
78+
default_role = "any"
79+
80+
# Query parser. If enabled, we'll attempt to parse
81+
# every incoming query to determine if it's a read or a write.
82+
# If it's a read query, we'll direct it to a replica. Otherwise, if it's a write,
83+
# we'll direct it to the primary.
84+
query_parser_enabled = true
85+
86+
# If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for
87+
# load balancing of read queries. Otherwise, the primary will only be used for write
88+
# queries. The primary can always be explicitly selected with our custom protocol.
89+
primary_reads_enabled = true
90+
91+
# So what if you wanted to implement a different hashing function,
92+
# or you've already built one and you want this pooler to use it?
93+
#
94+
# Current options:
95+
#
96+
# pg_bigint_hash: PARTITION BY HASH (Postgres hashing function)
97+
# sha1: A hashing function based on SHA1
98+
#
99+
sharding_function = "pg_bigint_hash"
100+
101+
# Automatically parse this from queries and route queries to the right shard!
102+
automatic_sharding_key = "id"
103+
104+
# Idle timeout can be overwritten in the pool
105+
idle_timeout = 40000
106+
107+
# Credentials for users that may connect to this cluster
108+
[pools.sharded_db.users.0]
109+
username = "sharding_user"
110+
# Maximum number of server connections that can be established for this user
111+
# The maximum number of connection from a single Pgcat process to any database in the cluster
112+
# is the sum of pool_size across all users.
113+
pool_size = 9
114+
115+
# Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way.
116+
statement_timeout = 0
117+
118+
[pools.sharded_db.users.1]
119+
username = "other_user"
120+
password = "other_user"
121+
pool_size = 21
122+
statement_timeout = 15000
123+
124+
# Shard 0
125+
[pools.sharded_db.shards.0]
126+
# [ host, port, role ]
127+
servers = [
128+
[ "127.0.0.1", 5432, "primary" ],
129+
[ "localhost", 5432, "replica" ]
130+
]
131+
# Database name (e.g. "postgres")
132+
database = "shard0"

0 commit comments

Comments
 (0)