use crate::error::{Error, Result};
use lmrc_ssh::SshClient;
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct UserInfo {
pub name: String,
pub is_superuser: bool,
pub can_create_db: bool,
pub can_create_role: bool,
pub connection_limit: i32,
}
#[derive(Debug, Clone)]
pub struct DatabaseInfo {
pub name: String,
pub owner: String,
pub encoding: String,
pub size: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Privilege {
Select,
Insert,
Update,
Delete,
Truncate,
References,
Trigger,
Create,
Connect,
Temporary,
Execute,
Usage,
All,
}
impl Privilege {
pub fn as_str(&self) -> &str {
match self {
Privilege::Select => "SELECT",
Privilege::Insert => "INSERT",
Privilege::Update => "UPDATE",
Privilege::Delete => "DELETE",
Privilege::Truncate => "TRUNCATE",
Privilege::References => "REFERENCES",
Privilege::Trigger => "TRIGGER",
Privilege::Create => "CREATE",
Privilege::Connect => "CONNECT",
Privilege::Temporary => "TEMPORARY",
Privilege::Execute => "EXECUTE",
Privilege::Usage => "USAGE",
Privilege::All => "ALL PRIVILEGES",
}
}
}
pub async fn list_users(ssh: &mut SshClient) -> Result<Vec<UserInfo>> {
debug!("Listing PostgreSQL users");
let query = r#"
SELECT
usename,
usesuper,
usecreatedb,
usecreaterole,
useconnlimit
FROM pg_user
ORDER BY usename;
"#;
let cmd = format!(
r#"sudo -u postgres psql -t -A -F '|' -c "{}""#,
query.replace('\n', " ")
);
let output = ssh
.execute(&cmd)
.map_err(|e| Error::Configuration(format!("Failed to list users: {}", e)))?;
let mut users = Vec::new();
for line in output.stdout.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let parts: Vec<&str> = line.split('|').collect();
if parts.len() >= 5 {
users.push(UserInfo {
name: parts[0].to_string(),
is_superuser: parts[1] == "t",
can_create_db: parts[2] == "t",
can_create_role: parts[3] == "t",
connection_limit: parts[4].parse().unwrap_or(-1),
});
}
}
info!("Found {} user(s)", users.len());
Ok(users)
}
pub async fn list_databases(ssh: &mut SshClient) -> Result<Vec<DatabaseInfo>> {
debug!("Listing PostgreSQL databases");
let query = r#"
SELECT
d.datname,
u.usename,
pg_encoding_to_char(d.encoding),
pg_size_pretty(pg_database_size(d.datname))
FROM pg_database d
JOIN pg_user u ON d.datdba = u.usesysid
WHERE d.datistemplate = false
ORDER BY d.datname;
"#;
let cmd = format!(
r#"sudo -u postgres psql -t -A -F '|' -c "{}""#,
query.replace('\n', " ")
);
let output = ssh
.execute(&cmd)
.map_err(|e| Error::Configuration(format!("Failed to list databases: {}", e)))?;
let mut databases = Vec::new();
for line in output.stdout.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let parts: Vec<&str> = line.split('|').collect();
if parts.len() >= 4 {
databases.push(DatabaseInfo {
name: parts[0].to_string(),
owner: parts[1].to_string(),
encoding: parts[2].to_string(),
size: Some(parts[3].to_string()),
});
}
}
info!("Found {} database(s)", databases.len());
Ok(databases)
}
pub async fn drop_database(ssh: &mut SshClient, database_name: &str) -> Result<()> {
info!("Dropping database: {}", database_name);
let terminate_cmd = format!(
r#"sudo -u postgres psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{}' AND pid <> pg_backend_pid();" || true"#,
database_name
);
ssh.execute(&terminate_cmd)
.map_err(|e| Error::Configuration(format!("Failed to terminate connections: {}", e)))?;
let drop_cmd = format!(
r#"sudo -u postgres psql -c "DROP DATABASE IF EXISTS {};" || true"#,
database_name
);
ssh.execute(&drop_cmd)
.map_err(|e| Error::Configuration(format!("Failed to drop database: {}", e)))?;
info!("✓ Database {} dropped successfully", database_name);
Ok(())
}
pub async fn drop_user(ssh: &mut SshClient, username: &str) -> Result<()> {
info!("Dropping user: {}", username);
let reassign_cmd = format!(
r#"sudo -u postgres psql -c "REASSIGN OWNED BY {} TO postgres;" || true"#,
username
);
ssh.execute(&reassign_cmd).ok();
let drop_owned_cmd = format!(
r#"sudo -u postgres psql -c "DROP OWNED BY {} CASCADE;" || true"#,
username
);
ssh.execute(&drop_owned_cmd).ok();
let drop_cmd = format!(
r#"sudo -u postgres psql -c "DROP USER IF EXISTS {};" || true"#,
username
);
ssh.execute(&drop_cmd)
.map_err(|e| Error::Configuration(format!("Failed to drop user: {}", e)))?;
info!("✓ User {} dropped successfully", username);
Ok(())
}
pub async fn update_user_password(
ssh: &mut SshClient,
username: &str,
new_password: &str,
) -> Result<()> {
info!("Updating password for user: {}", username);
let update_cmd = format!(
r#"sudo -u postgres psql -c "ALTER USER {} WITH PASSWORD '{}';" || true"#,
username, new_password
);
ssh.execute(&update_cmd)
.map_err(|e| Error::Configuration(format!("Failed to update password: {}", e)))?;
info!("✓ Password updated for user {}", username);
Ok(())
}
pub async fn grant_privileges(
ssh: &mut SshClient,
database: &str,
username: &str,
privileges: &[Privilege],
) -> Result<()> {
info!(
"Granting privileges on database {} to user {}",
database, username
);
let privs: Vec<&str> = privileges.iter().map(|p| p.as_str()).collect();
let priv_string = privs.join(", ");
let grant_cmd = format!(
r#"sudo -u postgres psql -c "GRANT {} ON DATABASE {} TO {};" || true"#,
priv_string, database, username
);
ssh.execute(&grant_cmd)
.map_err(|e| Error::Configuration(format!("Failed to grant database privileges: {}", e)))?;
if privileges.contains(&Privilege::All)
|| privileges.contains(&Privilege::Select)
|| privileges.contains(&Privilege::Insert)
|| privileges.contains(&Privilege::Update)
|| privileges.contains(&Privilege::Delete)
{
let grant_tables_cmd = format!(
r#"sudo -u postgres psql -d {} -c "GRANT {} ON ALL TABLES IN SCHEMA public TO {};" || true"#,
database, priv_string, username
);
ssh.execute(&grant_tables_cmd).ok();
let grant_sequences_cmd = format!(
r#"sudo -u postgres psql -d {} -c "GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO {};" || true"#,
database, username
);
ssh.execute(&grant_sequences_cmd).ok();
let grant_default_cmd = format!(
r#"sudo -u postgres psql -d {} -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT {} ON TABLES TO {};" || true"#,
database, priv_string, username
);
ssh.execute(&grant_default_cmd).ok();
}
info!("✓ Privileges granted successfully");
Ok(())
}
pub async fn revoke_privileges(
ssh: &mut SshClient,
database: &str,
username: &str,
privileges: &[Privilege],
) -> Result<()> {
info!(
"Revoking privileges on database {} from user {}",
database, username
);
let privs: Vec<&str> = privileges.iter().map(|p| p.as_str()).collect();
let priv_string = privs.join(", ");
let revoke_cmd = format!(
r#"sudo -u postgres psql -c "REVOKE {} ON DATABASE {} FROM {};" || true"#,
priv_string, database, username
);
ssh.execute(&revoke_cmd)
.map_err(|e| Error::Configuration(format!("Failed to revoke privileges: {}", e)))?;
info!("✓ Privileges revoked successfully");
Ok(())
}
pub async fn create_role(
ssh: &mut SshClient,
role_name: &str,
can_login: bool,
is_superuser: bool,
) -> Result<()> {
info!("Creating role: {}", role_name);
let mut options = Vec::new();
if can_login {
options.push("LOGIN");
} else {
options.push("NOLOGIN");
}
if is_superuser {
options.push("SUPERUSER");
}
let options_str = options.join(" ");
let create_cmd = format!(
r#"sudo -u postgres psql -c "CREATE ROLE {} {};" || true"#,
role_name, options_str
);
ssh.execute(&create_cmd)
.map_err(|e| Error::Configuration(format!("Failed to create role: {}", e)))?;
info!("✓ Role {} created successfully", role_name);
Ok(())
}
pub async fn grant_role(ssh: &mut SshClient, role_name: &str, username: &str) -> Result<()> {
info!("Granting role {} to user {}", role_name, username);
let grant_cmd = format!(
r#"sudo -u postgres psql -c "GRANT {} TO {};" || true"#,
role_name, username
);
ssh.execute(&grant_cmd)
.map_err(|e| Error::Configuration(format!("Failed to grant role: {}", e)))?;
info!("✓ Role granted successfully");
Ok(())
}
pub async fn revoke_role(ssh: &mut SshClient, role_name: &str, username: &str) -> Result<()> {
info!("Revoking role {} from user {}", role_name, username);
let revoke_cmd = format!(
r#"sudo -u postgres psql -c "REVOKE {} FROM {};" || true"#,
role_name, username
);
ssh.execute(&revoke_cmd)
.map_err(|e| Error::Configuration(format!("Failed to revoke role: {}", e)))?;
info!("✓ Role revoked successfully");
Ok(())
}
pub async fn user_exists(ssh: &mut SshClient, username: &str) -> Result<bool> {
debug!("Checking if user {} exists", username);
let check_cmd = format!(
r#"sudo -u postgres psql -t -c "SELECT 1 FROM pg_user WHERE usename = '{}';" | grep -q 1"#,
username
);
Ok(ssh.execute(&check_cmd).is_ok())
}
pub async fn database_exists(ssh: &mut SshClient, database: &str) -> Result<bool> {
debug!("Checking if database {} exists", database);
let check_cmd = format!(
r#"sudo -u postgres psql -t -c "SELECT 1 FROM pg_database WHERE datname = '{}';" | grep -q 1"#,
database
);
Ok(ssh.execute(&check_cmd).is_ok())
}
pub async fn create_database_with_options(
ssh: &mut SshClient,
database_name: &str,
owner: Option<&str>,
encoding: Option<&str>,
template: Option<&str>,
) -> Result<()> {
info!("Creating database: {}", database_name);
let mut options = Vec::new();
if let Some(owner) = owner {
options.push(format!("OWNER {}", owner));
}
if let Some(encoding) = encoding {
options.push(format!("ENCODING '{}'", encoding));
}
if let Some(template) = template {
options.push(format!("TEMPLATE {}", template));
}
let options_str = if options.is_empty() {
String::new()
} else {
format!(" WITH {}", options.join(" "))
};
let create_cmd = format!(
r#"sudo -u postgres psql -c "CREATE DATABASE {}{};" || true"#,
database_name, options_str
);
ssh.execute(&create_cmd)
.map_err(|e| Error::Configuration(format!("Failed to create database: {}", e)))?;
info!("✓ Database {} created successfully", database_name);
Ok(())
}
pub async fn create_user_with_options(
ssh: &mut SshClient,
username: &str,
password: &str,
is_superuser: bool,
can_create_db: bool,
can_create_role: bool,
connection_limit: Option<i32>,
) -> Result<()> {
info!("Creating user: {}", username);
let mut options = Vec::new();
options.push(format!("PASSWORD '{}'", password));
if is_superuser {
options.push("SUPERUSER".to_string());
} else {
options.push("NOSUPERUSER".to_string());
}
if can_create_db {
options.push("CREATEDB".to_string());
} else {
options.push("NOCREATEDB".to_string());
}
if can_create_role {
options.push("CREATEROLE".to_string());
} else {
options.push("NOCREATEROLE".to_string());
}
if let Some(limit) = connection_limit {
options.push(format!("CONNECTION LIMIT {}", limit));
}
let options_str = options.join(" ");
let create_cmd = format!(
r#"sudo -u postgres psql -c "CREATE USER {} WITH {};" || true"#,
username, options_str
);
ssh.execute(&create_cmd)
.map_err(|e| Error::Configuration(format!("Failed to create user: {}", e)))?;
info!("✓ User {} created successfully", username);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_privilege_as_str() {
assert_eq!(Privilege::Select.as_str(), "SELECT");
assert_eq!(Privilege::All.as_str(), "ALL PRIVILEGES");
assert_eq!(Privilege::Insert.as_str(), "INSERT");
}
#[test]
fn test_privilege_equality() {
assert_eq!(Privilege::Select, Privilege::Select);
assert_ne!(Privilege::Select, Privilege::Insert);
}
}