lmrc_postgres/
user_db_management.rs

1//! User and Database Management
2//!
3//! This module provides comprehensive user and database management operations including:
4//! - List users and databases
5//! - Create/drop users and databases
6//! - Update user passwords
7//! - Role management
8//! - Granular permission management
9
10use crate::error::{Error, Result};
11use lmrc_ssh::SshClient;
12use tracing::{debug, info};
13
14/// PostgreSQL user information
15#[derive(Debug, Clone)]
16pub struct UserInfo {
17    /// Username
18    pub name: String,
19    /// Is superuser
20    pub is_superuser: bool,
21    /// Can create databases
22    pub can_create_db: bool,
23    /// Can create roles
24    pub can_create_role: bool,
25    /// Connection limit (-1 for unlimited)
26    pub connection_limit: i32,
27}
28
29/// PostgreSQL database information
30#[derive(Debug, Clone)]
31pub struct DatabaseInfo {
32    /// Database name
33    pub name: String,
34    /// Owner username
35    pub owner: String,
36    /// Encoding (e.g., UTF8)
37    pub encoding: String,
38    /// Database size (human-readable)
39    pub size: Option<String>,
40}
41
42/// PostgreSQL privilege types
43#[derive(Debug, Clone, PartialEq)]
44pub enum Privilege {
45    /// SELECT privilege
46    Select,
47    /// INSERT privilege
48    Insert,
49    /// UPDATE privilege
50    Update,
51    /// DELETE privilege
52    Delete,
53    /// TRUNCATE privilege
54    Truncate,
55    /// REFERENCES privilege
56    References,
57    /// TRIGGER privilege
58    Trigger,
59    /// CREATE privilege
60    Create,
61    /// CONNECT privilege
62    Connect,
63    /// TEMPORARY privilege
64    Temporary,
65    /// EXECUTE privilege
66    Execute,
67    /// USAGE privilege
68    Usage,
69    /// ALL privileges
70    All,
71}
72
73impl Privilege {
74    /// Convert privilege to PostgreSQL keyword
75    pub fn as_str(&self) -> &str {
76        match self {
77            Privilege::Select => "SELECT",
78            Privilege::Insert => "INSERT",
79            Privilege::Update => "UPDATE",
80            Privilege::Delete => "DELETE",
81            Privilege::Truncate => "TRUNCATE",
82            Privilege::References => "REFERENCES",
83            Privilege::Trigger => "TRIGGER",
84            Privilege::Create => "CREATE",
85            Privilege::Connect => "CONNECT",
86            Privilege::Temporary => "TEMPORARY",
87            Privilege::Execute => "EXECUTE",
88            Privilege::Usage => "USAGE",
89            Privilege::All => "ALL PRIVILEGES",
90        }
91    }
92}
93
94/// List all users in the PostgreSQL instance
95pub async fn list_users(ssh: &mut SshClient) -> Result<Vec<UserInfo>> {
96    debug!("Listing PostgreSQL users");
97
98    let query = r#"
99        SELECT
100            usename,
101            usesuper,
102            usecreatedb,
103            usecreaterole,
104            useconnlimit
105        FROM pg_user
106        ORDER BY usename;
107    "#;
108
109    let cmd = format!(
110        r#"sudo -u postgres psql -t -A -F '|' -c "{}""#,
111        query.replace('\n', " ")
112    );
113
114    let output = ssh
115        .execute(&cmd)
116        .map_err(|e| Error::Configuration(format!("Failed to list users: {}", e)))?;
117
118    let mut users = Vec::new();
119
120    for line in output.stdout.lines() {
121        let line = line.trim();
122        if line.is_empty() {
123            continue;
124        }
125
126        let parts: Vec<&str> = line.split('|').collect();
127        if parts.len() >= 5 {
128            users.push(UserInfo {
129                name: parts[0].to_string(),
130                is_superuser: parts[1] == "t",
131                can_create_db: parts[2] == "t",
132                can_create_role: parts[3] == "t",
133                connection_limit: parts[4].parse().unwrap_or(-1),
134            });
135        }
136    }
137
138    info!("Found {} user(s)", users.len());
139    Ok(users)
140}
141
142/// List all databases in the PostgreSQL instance
143pub async fn list_databases(ssh: &mut SshClient) -> Result<Vec<DatabaseInfo>> {
144    debug!("Listing PostgreSQL databases");
145
146    let query = r#"
147        SELECT
148            d.datname,
149            u.usename,
150            pg_encoding_to_char(d.encoding),
151            pg_size_pretty(pg_database_size(d.datname))
152        FROM pg_database d
153        JOIN pg_user u ON d.datdba = u.usesysid
154        WHERE d.datistemplate = false
155        ORDER BY d.datname;
156    "#;
157
158    let cmd = format!(
159        r#"sudo -u postgres psql -t -A -F '|' -c "{}""#,
160        query.replace('\n', " ")
161    );
162
163    let output = ssh
164        .execute(&cmd)
165        .map_err(|e| Error::Configuration(format!("Failed to list databases: {}", e)))?;
166
167    let mut databases = Vec::new();
168
169    for line in output.stdout.lines() {
170        let line = line.trim();
171        if line.is_empty() {
172            continue;
173        }
174
175        let parts: Vec<&str> = line.split('|').collect();
176        if parts.len() >= 4 {
177            databases.push(DatabaseInfo {
178                name: parts[0].to_string(),
179                owner: parts[1].to_string(),
180                encoding: parts[2].to_string(),
181                size: Some(parts[3].to_string()),
182            });
183        }
184    }
185
186    info!("Found {} database(s)", databases.len());
187    Ok(databases)
188}
189
190/// Drop a database
191pub async fn drop_database(ssh: &mut SshClient, database_name: &str) -> Result<()> {
192    info!("Dropping database: {}", database_name);
193
194    // Terminate connections to the database first
195    let terminate_cmd = format!(
196        r#"sudo -u postgres psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{}' AND pid <> pg_backend_pid();" || true"#,
197        database_name
198    );
199
200    ssh.execute(&terminate_cmd)
201        .map_err(|e| Error::Configuration(format!("Failed to terminate connections: {}", e)))?;
202
203    // Drop the database
204    let drop_cmd = format!(
205        r#"sudo -u postgres psql -c "DROP DATABASE IF EXISTS {};" || true"#,
206        database_name
207    );
208
209    ssh.execute(&drop_cmd)
210        .map_err(|e| Error::Configuration(format!("Failed to drop database: {}", e)))?;
211
212    info!("✓ Database {} dropped successfully", database_name);
213    Ok(())
214}
215
216/// Drop a user
217pub async fn drop_user(ssh: &mut SshClient, username: &str) -> Result<()> {
218    info!("Dropping user: {}", username);
219
220    // Reassign owned objects first (to avoid dependency errors)
221    let reassign_cmd = format!(
222        r#"sudo -u postgres psql -c "REASSIGN OWNED BY {} TO postgres;" || true"#,
223        username
224    );
225
226    ssh.execute(&reassign_cmd).ok(); // Don't fail if user owns nothing
227
228    // Drop owned objects
229    let drop_owned_cmd = format!(
230        r#"sudo -u postgres psql -c "DROP OWNED BY {} CASCADE;" || true"#,
231        username
232    );
233
234    ssh.execute(&drop_owned_cmd).ok(); // Don't fail if user owns nothing
235
236    // Drop the user
237    let drop_cmd = format!(
238        r#"sudo -u postgres psql -c "DROP USER IF EXISTS {};" || true"#,
239        username
240    );
241
242    ssh.execute(&drop_cmd)
243        .map_err(|e| Error::Configuration(format!("Failed to drop user: {}", e)))?;
244
245    info!("✓ User {} dropped successfully", username);
246    Ok(())
247}
248
249/// Update user password
250pub async fn update_user_password(
251    ssh: &mut SshClient,
252    username: &str,
253    new_password: &str,
254) -> Result<()> {
255    info!("Updating password for user: {}", username);
256
257    let update_cmd = format!(
258        r#"sudo -u postgres psql -c "ALTER USER {} WITH PASSWORD '{}';" || true"#,
259        username, new_password
260    );
261
262    ssh.execute(&update_cmd)
263        .map_err(|e| Error::Configuration(format!("Failed to update password: {}", e)))?;
264
265    info!("✓ Password updated for user {}", username);
266    Ok(())
267}
268
269/// Grant specific privileges on a database to a user
270pub async fn grant_privileges(
271    ssh: &mut SshClient,
272    database: &str,
273    username: &str,
274    privileges: &[Privilege],
275) -> Result<()> {
276    info!(
277        "Granting privileges on database {} to user {}",
278        database, username
279    );
280
281    // Grant database-level privileges
282    let privs: Vec<&str> = privileges.iter().map(|p| p.as_str()).collect();
283    let priv_string = privs.join(", ");
284
285    let grant_cmd = format!(
286        r#"sudo -u postgres psql -c "GRANT {} ON DATABASE {} TO {};" || true"#,
287        priv_string, database, username
288    );
289
290    ssh.execute(&grant_cmd)
291        .map_err(|e| Error::Configuration(format!("Failed to grant database privileges: {}", e)))?;
292
293    // For schema-level privileges (tables, sequences, functions)
294    if privileges.contains(&Privilege::All)
295        || privileges.contains(&Privilege::Select)
296        || privileges.contains(&Privilege::Insert)
297        || privileges.contains(&Privilege::Update)
298        || privileges.contains(&Privilege::Delete)
299    {
300        // Grant on all tables in public schema
301        let grant_tables_cmd = format!(
302            r#"sudo -u postgres psql -d {} -c "GRANT {} ON ALL TABLES IN SCHEMA public TO {};" || true"#,
303            database, priv_string, username
304        );
305
306        ssh.execute(&grant_tables_cmd).ok();
307
308        // Grant on all sequences
309        let grant_sequences_cmd = format!(
310            r#"sudo -u postgres psql -d {} -c "GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO {};" || true"#,
311            database, username
312        );
313
314        ssh.execute(&grant_sequences_cmd).ok();
315
316        // Grant default privileges for future tables
317        let grant_default_cmd = format!(
318            r#"sudo -u postgres psql -d {} -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT {} ON TABLES TO {};" || true"#,
319            database, priv_string, username
320        );
321
322        ssh.execute(&grant_default_cmd).ok();
323    }
324
325    info!("✓ Privileges granted successfully");
326    Ok(())
327}
328
329/// Revoke privileges on a database from a user
330pub async fn revoke_privileges(
331    ssh: &mut SshClient,
332    database: &str,
333    username: &str,
334    privileges: &[Privilege],
335) -> Result<()> {
336    info!(
337        "Revoking privileges on database {} from user {}",
338        database, username
339    );
340
341    let privs: Vec<&str> = privileges.iter().map(|p| p.as_str()).collect();
342    let priv_string = privs.join(", ");
343
344    let revoke_cmd = format!(
345        r#"sudo -u postgres psql -c "REVOKE {} ON DATABASE {} FROM {};" || true"#,
346        priv_string, database, username
347    );
348
349    ssh.execute(&revoke_cmd)
350        .map_err(|e| Error::Configuration(format!("Failed to revoke privileges: {}", e)))?;
351
352    info!("✓ Privileges revoked successfully");
353    Ok(())
354}
355
356/// Create a role (user without login)
357pub async fn create_role(
358    ssh: &mut SshClient,
359    role_name: &str,
360    can_login: bool,
361    is_superuser: bool,
362) -> Result<()> {
363    info!("Creating role: {}", role_name);
364
365    let mut options = Vec::new();
366
367    if can_login {
368        options.push("LOGIN");
369    } else {
370        options.push("NOLOGIN");
371    }
372
373    if is_superuser {
374        options.push("SUPERUSER");
375    }
376
377    let options_str = options.join(" ");
378
379    let create_cmd = format!(
380        r#"sudo -u postgres psql -c "CREATE ROLE {} {};" || true"#,
381        role_name, options_str
382    );
383
384    ssh.execute(&create_cmd)
385        .map_err(|e| Error::Configuration(format!("Failed to create role: {}", e)))?;
386
387    info!("✓ Role {} created successfully", role_name);
388    Ok(())
389}
390
391/// Grant a role to a user
392pub async fn grant_role(ssh: &mut SshClient, role_name: &str, username: &str) -> Result<()> {
393    info!("Granting role {} to user {}", role_name, username);
394
395    let grant_cmd = format!(
396        r#"sudo -u postgres psql -c "GRANT {} TO {};" || true"#,
397        role_name, username
398    );
399
400    ssh.execute(&grant_cmd)
401        .map_err(|e| Error::Configuration(format!("Failed to grant role: {}", e)))?;
402
403    info!("✓ Role granted successfully");
404    Ok(())
405}
406
407/// Revoke a role from a user
408pub async fn revoke_role(ssh: &mut SshClient, role_name: &str, username: &str) -> Result<()> {
409    info!("Revoking role {} from user {}", role_name, username);
410
411    let revoke_cmd = format!(
412        r#"sudo -u postgres psql -c "REVOKE {} FROM {};" || true"#,
413        role_name, username
414    );
415
416    ssh.execute(&revoke_cmd)
417        .map_err(|e| Error::Configuration(format!("Failed to revoke role: {}", e)))?;
418
419    info!("✓ Role revoked successfully");
420    Ok(())
421}
422
423/// Check if a user exists
424pub async fn user_exists(ssh: &mut SshClient, username: &str) -> Result<bool> {
425    debug!("Checking if user {} exists", username);
426
427    let check_cmd = format!(
428        r#"sudo -u postgres psql -t -c "SELECT 1 FROM pg_user WHERE usename = '{}';" | grep -q 1"#,
429        username
430    );
431
432    Ok(ssh.execute(&check_cmd).is_ok())
433}
434
435/// Check if a database exists
436pub async fn database_exists(ssh: &mut SshClient, database: &str) -> Result<bool> {
437    debug!("Checking if database {} exists", database);
438
439    let check_cmd = format!(
440        r#"sudo -u postgres psql -t -c "SELECT 1 FROM pg_database WHERE datname = '{}';" | grep -q 1"#,
441        database
442    );
443
444    Ok(ssh.execute(&check_cmd).is_ok())
445}
446
447/// Create a database with options
448pub async fn create_database_with_options(
449    ssh: &mut SshClient,
450    database_name: &str,
451    owner: Option<&str>,
452    encoding: Option<&str>,
453    template: Option<&str>,
454) -> Result<()> {
455    info!("Creating database: {}", database_name);
456
457    let mut options = Vec::new();
458
459    if let Some(owner) = owner {
460        options.push(format!("OWNER {}", owner));
461    }
462
463    if let Some(encoding) = encoding {
464        options.push(format!("ENCODING '{}'", encoding));
465    }
466
467    if let Some(template) = template {
468        options.push(format!("TEMPLATE {}", template));
469    }
470
471    let options_str = if options.is_empty() {
472        String::new()
473    } else {
474        format!(" WITH {}", options.join(" "))
475    };
476
477    let create_cmd = format!(
478        r#"sudo -u postgres psql -c "CREATE DATABASE {}{};" || true"#,
479        database_name, options_str
480    );
481
482    ssh.execute(&create_cmd)
483        .map_err(|e| Error::Configuration(format!("Failed to create database: {}", e)))?;
484
485    info!("✓ Database {} created successfully", database_name);
486    Ok(())
487}
488
489/// Create a user with full options
490pub async fn create_user_with_options(
491    ssh: &mut SshClient,
492    username: &str,
493    password: &str,
494    is_superuser: bool,
495    can_create_db: bool,
496    can_create_role: bool,
497    connection_limit: Option<i32>,
498) -> Result<()> {
499    info!("Creating user: {}", username);
500
501    let mut options = Vec::new();
502
503    options.push(format!("PASSWORD '{}'", password));
504
505    if is_superuser {
506        options.push("SUPERUSER".to_string());
507    } else {
508        options.push("NOSUPERUSER".to_string());
509    }
510
511    if can_create_db {
512        options.push("CREATEDB".to_string());
513    } else {
514        options.push("NOCREATEDB".to_string());
515    }
516
517    if can_create_role {
518        options.push("CREATEROLE".to_string());
519    } else {
520        options.push("NOCREATEROLE".to_string());
521    }
522
523    if let Some(limit) = connection_limit {
524        options.push(format!("CONNECTION LIMIT {}", limit));
525    }
526
527    let options_str = options.join(" ");
528
529    let create_cmd = format!(
530        r#"sudo -u postgres psql -c "CREATE USER {} WITH {};" || true"#,
531        username, options_str
532    );
533
534    ssh.execute(&create_cmd)
535        .map_err(|e| Error::Configuration(format!("Failed to create user: {}", e)))?;
536
537    info!("✓ User {} created successfully", username);
538    Ok(())
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn test_privilege_as_str() {
547        assert_eq!(Privilege::Select.as_str(), "SELECT");
548        assert_eq!(Privilege::All.as_str(), "ALL PRIVILEGES");
549        assert_eq!(Privilege::Insert.as_str(), "INSERT");
550    }
551
552    #[test]
553    fn test_privilege_equality() {
554        assert_eq!(Privilege::Select, Privilege::Select);
555        assert_ne!(Privilege::Select, Privilege::Insert);
556    }
557}