Auth passthrough (auth_query) (#266)

* Add a new exec_simple_query method

This adds a new `exec_simple_query` method so we can make 'out of band'
queries to servers that don't interfere with pools at all.
In order to reuse startup code for making these simple queries,
we need to set the stats (`Reporter`) optional, so using these
simple queries wont interfere with stats.

* Add auth passthough (auth_query)

Adds a feature that allows setting auth passthrough for md5 auth.

It adds 3 new (general and pool) 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.

The configuration can be done either on the general config (so pools share them) or in a per-pool basis.

The behavior is, at boot time, when validating server connections, a hash is fetched per server
and stored in the pool. When new server connections are created, and no cleartext password is specified,
the obtained hash is used for creating them, if the hash could not be obtained for whatever reason, it retries
it.

When client authentication is tried, it uses cleartext passwords if specified, it not, it checks whether
we have query_auth set up, if so, it tries to use the obtained hash for making client auth. If there is no
hash (we could not obtain one when validating the connection), a new fetch is tried.

Once we have a hash, we authenticate using it against whathever the client has sent us, if there is a failure
we refetch the hash and retry auth (so password changes can be done).

The idea with this 'retrial' mechanism is to make it fault tolerant, so if for whatever reason hash could not be
obtained during connection validation, or the password has change, we can still connect later.

* Add documentation for Auth passthrough
This commit is contained in:
Jose Fernández
2023-03-30 22:29:23 +02:00
committed by GitHub
parent 0757d7f3a0
commit 6f768a84ce
24 changed files with 1026 additions and 31 deletions

View File

@@ -177,7 +177,7 @@ impl Address {
#[derive(Clone, PartialEq, Hash, Eq, Serialize, Deserialize, Debug)]
pub struct User {
pub username: String,
pub password: String,
pub password: Option<String>,
pub pool_size: u32,
#[serde(default)] // 0
pub statement_timeout: u64,
@@ -187,7 +187,7 @@ impl Default for User {
fn default() -> User {
User {
username: String::from("postgres"),
password: String::new(),
password: None,
pool_size: 15,
statement_timeout: 0,
}
@@ -250,6 +250,10 @@ pub struct General {
pub tls_private_key: Option<String>,
pub admin_username: String,
pub admin_password: String,
pub auth_query: Option<String>,
pub auth_query_user: Option<String>,
pub auth_query_password: Option<String>,
}
impl General {
@@ -334,6 +338,9 @@ impl Default for General {
tls_private_key: None,
admin_username: String::from("admin"),
admin_password: String::from("admin"),
auth_query: None,
auth_query_user: None,
auth_query_password: None,
}
}
}
@@ -406,6 +413,10 @@ pub struct Pool {
pub shard_id_regex: Option<String>,
pub regex_search_limit: Option<usize>,
pub auth_query: Option<String>,
pub auth_query_user: Option<String>,
pub auth_query_password: Option<String>,
pub shards: BTreeMap<String, Shard>,
pub users: BTreeMap<String, User>,
// Note, don't put simple fields below these configs. There's a compatability issue with TOML that makes it
@@ -420,6 +431,12 @@ impl Pool {
s.finish()
}
pub fn is_auth_query_configured(&self) -> bool {
self.auth_query_password.is_some()
&& self.auth_query_user.is_some()
&& self.auth_query_password.is_some()
}
pub fn default_pool_mode() -> PoolMode {
PoolMode::Transaction
}
@@ -512,6 +529,9 @@ impl Default for Pool {
sharding_key_regex: None,
shard_id_regex: None,
regex_search_limit: Some(1000),
auth_query: None,
auth_query_user: None,
auth_query_password: None,
}
}
}
@@ -612,9 +632,31 @@ pub struct Config {
}
impl Config {
pub fn is_auth_query_configured(&self) -> bool {
self.pools
.iter()
.any(|(_name, pool)| pool.is_auth_query_configured())
}
pub fn default_path() -> String {
String::from("pgcat.toml")
}
pub fn fill_up_auth_query_config(&mut self) {
for (_name, pool) in self.pools.iter_mut() {
if pool.auth_query.is_none() {
pool.auth_query = self.general.auth_query.clone();
}
if pool.auth_query_user.is_none() {
pool.auth_query_user = self.general.auth_query_user.clone();
}
if pool.auth_query_password.is_none() {
pool.auth_query_password = self.general.auth_query_password.clone();
}
}
}
}
impl Default for Config {
@@ -832,6 +874,35 @@ impl Config {
}
pub fn validate(&mut self) -> Result<(), Error> {
// Validation for auth_query feature
if self.general.auth_query.is_some()
&& (self.general.auth_query_user.is_none()
|| self.general.auth_query_password.is_none())
{
error!("If auth_query is specified, you need to provide a value for `auth_query_user`, `auth_query_password`");
return Err(Error::BadConfig);
}
for (name, pool) in self.pools.iter() {
if pool.auth_query.is_some()
&& (pool.auth_query_user.is_none() || pool.auth_query_password.is_none())
{
error!("Error in pool {{ {} }}. If auth_query is specified, you need to provide a value for `auth_query_user`, `auth_query_password`", name);
return Err(Error::BadConfig);
}
for (_name, user_data) in pool.users.iter() {
if (pool.auth_query.is_none()
|| pool.auth_query_password.is_none()
|| pool.auth_query_user.is_none())
&& user_data.password.is_none()
{
error!("Error in pool {{ {} }}. You have to specify a user password for every pool if auth_query is not specified", name);
return Err(Error::BadConfig);
}
}
}
// Validate TLS!
match self.general.tls_certificate.clone() {
Some(tls_certificate) => {
@@ -911,6 +982,7 @@ pub async fn parse(path: &str) -> Result<(), Error> {
}
};
config.fill_up_auth_query_config();
config.validate()?;
config.path = path.to_string();
@@ -980,7 +1052,10 @@ mod test {
"sharding_user"
);
assert_eq!(
get_config().pools["sharded_db"].users["1"].password,
get_config().pools["sharded_db"].users["1"]
.password
.as_ref()
.unwrap(),
"other_user"
);
assert_eq!(get_config().pools["sharded_db"].users["1"].pool_size, 21);
@@ -1005,10 +1080,16 @@ mod test {
"simple_user"
);
assert_eq!(
get_config().pools["simple_db"].users["0"].password,
get_config().pools["simple_db"].users["0"]
.password
.as_ref()
.unwrap(),
"simple_user"
);
assert_eq!(get_config().pools["simple_db"].users["0"].pool_size, 5);
assert_eq!(get_config().general.auth_query, None);
assert_eq!(get_config().general.auth_query_user, None);
assert_eq!(get_config().general.auth_query_password, None);
}
#[tokio::test]