use anyhow::Result;
use log::debug;
use postgres::row::Row;
use postgres::types::ToSql;
use postgres::{Client, NoTls, ToStatement};
pub struct DbConnection {
connection_info: String,
client: Client,
}
#[derive(Debug)]
pub struct User {
user_name: String,
user_createdb: bool,
user_super: bool,
passwd: String,
}
#[derive(Debug)]
pub struct UserSchemaRole {
user_name: String,
schema_name: String,
has_create: bool,
has_usage: bool,
}
impl DbConnection {
pub fn connect(conn: &str) -> Self {
let connection_info = conn.to_string();
let client = Client::connect(conn, NoTls)
.unwrap_or_else(|err| panic!("could not connect to {}: {:?}", conn, err));
Self {
connection_info,
client,
}
}
pub fn connection_info(self) -> String {
self.connection_info
}
pub fn ping(&mut self) -> Result<bool> {
let stmt = self.client.prepare("SELECT 1").unwrap();
let rows = self.client.execute(&stmt, &[]).unwrap();
assert_eq!(rows, 1, "should be 1");
Ok(true)
}
pub fn drop_user(&mut self, user: &User) {
let sql: String = format!("DROP USER IF EXISTS {}", user.user_name).to_owned();
debug!("drop_user: {}", sql);
self.client.execute(&sql, &[]).expect("could not drop user");
}
pub fn try_drop_user(&mut self, user: &User) {
let sql: String = format!("DROP USER IF EXISTS {}", user.user_name).to_owned();
debug!("try_drop_user: {}", sql);
self.client.execute(&sql, &[]).unwrap_or_else(|_| 1);
}
pub fn create_user(&mut self, user: &User) {
let mut sql: String = format!("CREATE USER {} ", user.user_name).to_owned();
if user.user_createdb {
sql += "CREATEDB"
}
if !user.passwd.is_empty() {
sql += &format!(" PASSWORD '{}'", user.passwd).to_string()
}
debug!("create user: {}", sql);
let stmt = self.client.prepare(&sql).unwrap();
self.client
.execute(&stmt, &[])
.expect("could not create user");
}
pub fn get_users(&mut self) -> Result<Vec<User>> {
let mut users = vec![];
let sql = "SELECT usename, usecreatedb, usesuper FROM pg_user";
for row in self.client.query(sql, &[])? {
match (row.get(0), row.get(1), row.get(2)) {
(Some(user_name), Some(user_createdb), Some(user_super)) => users.push(User {
user_name,
user_createdb,
user_super,
passwd: String::from(""),
}),
(Some(user_name), _, _) => users.push(User {
user_name,
user_createdb: false,
user_super: false,
passwd: String::from(""),
}),
(_, _, _) => (),
}
}
debug!("get_users: {:#?}", users);
Ok(users)
}
pub fn get_schema_roles(&mut self) -> Result<Vec<UserSchemaRole>> {
let sql = "
SELECT
u.usename AS user_name,
s.schemaname AS schema_name,
has_schema_privilege(u.usename, s.schemaname, 'create') AS has_create,
has_schema_privilege(u.usename, s.schemaname, 'usage') AS has_usage
FROM
pg_user u
CROSS JOIN (SELECT DISTINCT schemaname FROM pg_tables) s
WHERE
1 = 1
AND s.schemaname != 'pg_catalog'
AND s.schemaname != 'information_schema';
";
let mut user_schema_roles = vec![];
for row in self.client.query(sql, &[])? {
match (row.get(0), row.get(1), row.get(2), row.get(3)) {
(Some(user_name), Some(schema_name), Some(has_create), Some(has_usage)) => {
user_schema_roles.push(UserSchemaRole {
user_name,
schema_name,
has_create,
has_usage,
})
}
(_, _, _, _) => (),
}
}
Ok(user_schema_roles)
}
pub fn query<T>(&mut self, query: &T, params: &[&(dyn ToSql + Sync)]) -> Result<Vec<Row>>
where
T: ?Sized + ToStatement,
{
let ri = self.client.query(query, params)?;
Ok(ri)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{thread_rng, Rng};
#[test]
fn test_connect() {
let url = "postgresql://postgres:postgres@localhost:5432/postgres";
let mut db = DbConnection::connect(url);
db.ping().expect("cannot ping");
}
#[test]
fn test_drop_user() {
let url = "postgresql://postgres:postgres@localhost:5432/postgres";
let mut db = DbConnection::connect(url);
let user_name = random_username();
let user = User {
user_name: user_name.to_owned(),
user_createdb: false,
user_super: false,
passwd: "duyet".to_string(),
};
db.drop_user(&user);
db.create_user(&user);
db.drop_user(&user);
let users = db.get_users().unwrap();
assert_eq!(users.iter().any(|u| u.user_name == user_name), false);
db.drop_user(&user);
}
#[test]
fn test_drop_create_user() {
let url = "postgresql://postgres:postgres@localhost:5432/postgres";
let mut db = DbConnection::connect(url);
let user_name = random_username();
let user = User {
user_name: user_name.to_owned(),
user_createdb: false,
user_super: false,
passwd: "duyet".to_string(),
};
db.drop_user(&user);
db.create_user(&user);
let users = db.get_users().unwrap();
assert_eq!(users.iter().any(|u| u.user_name == user_name), true);
db.drop_user(&user);
}
#[test]
fn test_get_schema_roles() {
let url = "postgresql://postgres:postgres@localhost:5432/postgres";
let mut db = DbConnection::connect(url);
let user_name = random_username();
let user = User {
user_name: user_name.to_owned(),
user_createdb: false,
user_super: false,
passwd: "duyet".to_string(),
};
db.drop_user(&user);
db.create_user(&user);
let user_schema_roles = db.get_schema_roles().unwrap();
println!("xxx {:#?}", user_schema_roles);
if user_schema_roles.len() > 0 {
assert_eq!(
user_schema_roles.iter().any(|u| u.user_name == user_name
&& u.has_usage == false
&& u.has_create == false),
true
);
}
db.drop_user(&user);
}
#[test]
fn test_query() {
let url = "postgresql://postgres:postgres@localhost:5432/postgres";
let mut db = DbConnection::connect(url);
let rows = db.query("SELECT 1 as t", &[]).unwrap();
debug!("test_query: {:?}", rows);
assert_eq!(rows.len(), 1);
assert_eq!(rows.get(0).unwrap().len(), 1);
let t: i32 = rows.get(0).unwrap().get("t");
assert_eq!(t, 1);
}
fn random_username() -> String {
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
let mut rng = thread_rng();
let user_name: String = (0..10)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect();
user_name
}
}