Skip to content

Auth passthrough (auth_query) #266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ jobs:
POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256 --auth=scram-sha-256

- image: postgres:14
command: ["postgres", "-p", "10432", "-c", "shared_preload_libraries=pg_stat_statements"]
environment:
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5

# Add steps to the job
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
steps:
Expand Down
1 change: 1 addition & 0 deletions .circleci/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 5432 -U postgres -f tests/sharding/q
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 7432 -U postgres -f tests/sharding/query_routing_setup.sql
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 8432 -U postgres -f tests/sharding/query_routing_setup.sql
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 9432 -U postgres -f tests/sharding/query_routing_setup.sql
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 10432 -U postgres -f tests/sharding/query_routing_setup.sql

PGPASSWORD=sharding_user pgbench -h 127.0.0.1 -U sharding_user shard0 -i
PGPASSWORD=sharding_user pgbench -h 127.0.0.1 -U sharding_user shard1 -i
Expand Down
2 changes: 2 additions & 0 deletions .rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
edition = "2021"
hard_tabs = false
56 changes: 55 additions & 1 deletion CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,41 @@ Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DAT
### admin_password
```
path: general.admin_password
default: "admin_pass"
default: <UNSET>
```

Password to access the virtual administrative database

### auth_query (experimental)
```
path: general.auth_query
default: <UNSET>
```

Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be
established using the database configured in the pool. This parameter is inherited by every pool
and can be redefined in pool configuration.

### auth_query_user (experimental)
```
path: general.auth_query_user
default: <UNSET>
```

User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
specified in `auth_query_user`. The connection will be established using the database configured in the pool.
This parameter is inherited by every pool and can be redefined in pool configuration.

### auth_query_password (experimental)
```
path: general.auth_query_password
default: <UNSET>
```

Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
specified in `auth_query_user`. The connection will be established using the database configured in the pool.
This parameter is inherited by every pool and can be redefined in pool configuration.

## `pools.<pool_name>` Section

### pool_mode
Expand Down Expand Up @@ -281,6 +311,30 @@ default: 3000

Connect timeout can be overwritten in the pool

### auth_query (experimental)
```
path: general.auth_query
default: <UNSET>
```

Auth query can be overwritten in the pool

### auth_query_user (experimental)
```
path: general.auth_query_user
default: <UNSET>
```

Auth query user can be overwritten in the pool

### auth_query_password (experimental)
```
path: general.auth_query_password
default: <UNSET>
```

Auth query password can be overwritten in the pool

## `pools.<pool_name>.users.<user_index>` Section

### username
Expand Down
44 changes: 41 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ version = "1.0.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version = "1", features = ["full"] }
bytes = "1"
Expand Down Expand Up @@ -38,6 +37,8 @@ futures = "0.3"
socket2 = { version = "0.4.7", features = ["all"] }
nix = "0.26.2"
atomic_enum = "0.2.0"
postgres-protocol = "0.6.4"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great find!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@magec I created this library a while ago with intention to create a PR to use this in PgCat. Wondering if you'd take a look, https://github.com/zain-kabani/postgres-proto-rs

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took this one because was the one used in postgres crate. Also, I found it pretty easy to use and is widely imused. Havent seen yours yet. Will take a look.

fallible-iterator = "0.2"

[target.'cfg(not(target_env = "msvc"))'.dependencies]
jemallocator = "0.5.0"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load bal
| Sharding using comments parsing/Regex | **Experimental** | Clients can include shard information (sharding key, shard ID) in the query comments. |
| Automatic sharding | **Experimental** | PgCat can parse queries, detect sharding keys automatically, and route queries to the correct shard. |
| Mirroring | **Experimental** | Mirror queries between multiple databases in order to test servers with realistic production traffic. |
| Auth passthrough | **Experimental** | MD5 password authentication can be configured to use an `auth_query` so no cleartext passwords are needed in the config file. |


## Status
Expand Down
8 changes: 8 additions & 0 deletions dev/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ services:
POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256 --auth=scram-sha-256
PGPORT: 9432
command: ["postgres", "-p", "9432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"]
pg5:
<<: *common-definition-pg
environment:
<<: *common-env-pg
POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5
PGPORT: 10432
command: ["postgres", "-p", "5432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"]

toxiproxy:
build: .
Expand All @@ -71,6 +78,7 @@ services:
- pg2
- pg3
- pg4
- pg5

pgcat-shell:
stdin_open: true
Expand Down
107 changes: 107 additions & 0 deletions src/auth_passthrough.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use crate::errors::Error;
use crate::server::Server;
use log::debug;

#[derive(Clone, Debug)]
pub struct AuthPassthrough {
password: String,
query: String,
user: String,
}

impl AuthPassthrough {
/// Initializes an AuthPassthrough.
pub fn new(query: &str, user: &str, password: &str) -> Self {
AuthPassthrough {
password: password.to_string(),
query: query.to_string(),
user: user.to_string(),
}
}

/// Returns an AuthPassthrough given the pool configuration.
/// If any of required values is not set, None is returned.
pub fn from_pool_config(pool_config: &crate::config::Pool) -> Option<Self> {
if pool_config.is_auth_query_configured() {
return Some(AuthPassthrough::new(
pool_config.auth_query.as_ref().unwrap(),
pool_config.auth_query_user.as_ref().unwrap(),
pool_config.auth_query_password.as_ref().unwrap(),
));
}

None
}

/// Returns an AuthPassthrough given the pool settings.
/// If any of required values is not set, None is returned.
pub fn from_pool_settings(pool_settings: &crate::pool::PoolSettings) -> Option<Self> {
let pool_config = crate::config::Pool {
auth_query: pool_settings.auth_query.clone(),
auth_query_password: pool_settings.auth_query_password.clone(),
auth_query_user: pool_settings.auth_query_user.clone(),
..Default::default()
};

AuthPassthrough::from_pool_config(&pool_config)
}

/// Connects to server and executes auth_query for the specified address.
/// If the response is a row with two columns containing the username set in the address.
/// and its MD5 hash, the MD5 hash returned.
///
/// Note that the query is executed, changing $1 with the name of the user
/// this is so we only hold in memory (and transfer) the least amount of 'sensitive' data.
/// Also, it is compatible with pgbouncer.
///
/// # Arguments
///
/// * `address` - An Address of the server we want to connect to. The username for the hash will be obtained from this value.
///
/// # Examples
///
/// ```
/// use pgcat::auth_passthrough::AuthPassthrough;
/// use pgcat::config::Address;
/// let auth_passthrough = AuthPassthrough::new("SELECT * FROM public.user_lookup('$1');", "postgres", "postgres");
/// auth_passthrough.fetch_hash(&Address::default());
/// ```
///
pub async fn fetch_hash(&self, address: &crate::config::Address) -> Result<String, Error> {
let auth_user = crate::config::User {
username: self.user.clone(),
password: Some(self.password.clone()),
pool_size: 1,
statement_timeout: 0,
};

let user = &address.username;

debug!("Connecting to server to obtain auth hashes.");
let auth_query = self.query.replace("$1", user);
match Server::exec_simple_query(address, &auth_user, &auth_query).await {
Ok(password_data) => {
if password_data.len() == 2 && password_data.first().unwrap() == user {
if let Some(stripped_hash) = password_data.last().unwrap().to_string().strip_prefix("md5") {
Ok(stripped_hash.to_string())
}
else {
Err(Error::AuthPassthroughError(
"Obtained hash from auth_query does not seem to be in md5 format.".to_string(),
))
}
} else {
Err(Error::AuthPassthroughError(
"Data obtained from query does not follow the scheme 'user','hash'."
.to_string(),
))
}
}
Err(err) => {
Err(Error::AuthPassthroughError(
format!("Error trying to obtain password from auth_query, ignoring hash for user '{}'. Error: {:?}",
user, err)))
}
}
}
}
Loading