1use crate::error::{Error, Result};
11use lmrc_ssh::SshClient;
12use tracing::{debug, info};
13
14#[derive(Debug, Clone)]
16pub struct UserInfo {
17 pub name: String,
19 pub is_superuser: bool,
21 pub can_create_db: bool,
23 pub can_create_role: bool,
25 pub connection_limit: i32,
27}
28
29#[derive(Debug, Clone)]
31pub struct DatabaseInfo {
32 pub name: String,
34 pub owner: String,
36 pub encoding: String,
38 pub size: Option<String>,
40}
41
42#[derive(Debug, Clone, PartialEq)]
44pub enum Privilege {
45 Select,
47 Insert,
49 Update,
51 Delete,
53 Truncate,
55 References,
57 Trigger,
59 Create,
61 Connect,
63 Temporary,
65 Execute,
67 Usage,
69 All,
71}
72
73impl Privilege {
74 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
94pub 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
142pub 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
190pub async fn drop_database(ssh: &mut SshClient, database_name: &str) -> Result<()> {
192 info!("Dropping database: {}", database_name);
193
194 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 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
216pub async fn drop_user(ssh: &mut SshClient, username: &str) -> Result<()> {
218 info!("Dropping user: {}", username);
219
220 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(); 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(); 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
249pub 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
269pub 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 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 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 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 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 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
329pub 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
356pub 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
391pub 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
407pub 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
423pub 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
435pub 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
447pub 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
489pub 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}