use pgroles_operator::context::build_set_role_stmt;
use sqlx::postgres::{PgConnectOptions, PgPool, PgPoolOptions};
use sqlx::{ConnectOptions, Executor, Row};
use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
fn database_url() -> String {
std::env::var("DATABASE_URL").expect("DATABASE_URL must be set for live DB tests")
}
fn unique_suffix() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX epoch")
.as_nanos()
}
async fn superuser_pool() -> PgPool {
PgPool::connect(&database_url())
.await
.expect("failed to connect to live test database as superuser")
}
fn low_priv_options(low_user: &str, low_password: &str) -> PgConnectOptions {
PgConnectOptions::from_str(&database_url())
.expect("DATABASE_URL must parse as Postgres connect options")
.username(low_user)
.password(low_password)
.log_statements(tracing::log::LevelFilter::Off)
}
#[tokio::test]
#[ignore]
async fn set_role_grants_create_role_privilege_to_low_priv_user() {
let suffix = unique_suffix();
let low_user = format!("pgr_setrole_low_{suffix}");
let high_role = format!("pgr_setrole_high_{suffix}");
let target_role = format!("pgr_setrole_target_{suffix}");
let low_password = "low_priv_password";
let admin = superuser_pool().await;
let setup_sql = format!(
r#"
DROP ROLE IF EXISTS "{target_role}";
DROP ROLE IF EXISTS "{low_user}";
DROP ROLE IF EXISTS "{high_role}";
CREATE ROLE "{high_role}" CREATEROLE NOINHERIT;
CREATE ROLE "{low_user}" LOGIN NOINHERIT PASSWORD '{low_password}';
GRANT "{high_role}" TO "{low_user}";
"#
);
admin
.execute(setup_sql.as_str())
.await
.expect("failed to set up test roles");
let plain_pool = PgPoolOptions::new()
.max_connections(2)
.acquire_timeout(Duration::from_secs(10))
.connect_with(low_priv_options(&low_user, low_password))
.await
.expect("low-priv pool without SET ROLE should still be able to connect");
let create_without_set_role = plain_pool
.execute(format!(r#"CREATE ROLE "{target_role}""#).as_str())
.await;
assert!(
create_without_set_role.is_err(),
"CREATE ROLE must fail when SET ROLE is NOT applied (got: {:?})",
create_without_set_role,
);
if let Err(err) = &create_without_set_role {
let msg = err.to_string();
assert!(
msg.contains("permission denied"),
"expected permission denied error, got: {msg}",
);
}
drop(plain_pool);
let high_role_for_hook = high_role.clone();
let set_role_pool = PgPoolOptions::new()
.max_connections(2)
.acquire_timeout(Duration::from_secs(10))
.after_connect(move |conn, _meta| {
let stmt = build_set_role_stmt(&high_role_for_hook);
Box::pin(async move {
sqlx::Executor::execute(&mut *conn, stmt.as_str()).await?;
Ok(())
})
})
.connect_with(low_priv_options(&low_user, low_password))
.await
.expect("low-priv pool with SET ROLE hook should connect");
let effective: String = sqlx::query("SELECT current_user::text AS u")
.fetch_one(&set_role_pool)
.await
.expect("should be able to read current_user")
.get("u");
assert_eq!(
effective, high_role,
"after_connect hook must make current_user = high_role"
);
set_role_pool
.execute(format!(r#"CREATE ROLE "{target_role}""#).as_str())
.await
.expect("CREATE ROLE must succeed once SET ROLE is applied");
let exists: bool = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = $1)")
.bind(&target_role)
.fetch_one(&admin)
.await
.expect("should be able to query pg_roles")
.get(0);
assert!(exists, "target role should have been created");
drop(set_role_pool);
let cleanup_sql = format!(
r#"
DROP ROLE IF EXISTS "{target_role}";
DROP ROLE IF EXISTS "{low_user}";
DROP ROLE IF EXISTS "{high_role}";
"#
);
admin
.execute(cleanup_sql.as_str())
.await
.expect("failed to clean up test roles");
}