Skip to main content

dataprof_db/security/
credentials.rs

1//! Database credentials management with environment variable support
2
3use crate::DataProfilerError;
4use std::collections::HashMap;
5use std::env;
6
7/// Database credentials management with environment variable support
8#[derive(Clone, Default)]
9pub struct DatabaseCredentials {
10    /// Database username
11    pub username: Option<String>,
12    /// Database password
13    pub password: Option<String>,
14    /// Database host
15    pub host: Option<String>,
16    /// Database port
17    pub port: Option<u16>,
18    /// Database name
19    pub database: Option<String>,
20    /// Additional connection parameters
21    pub extra_params: HashMap<String, String>,
22}
23
24impl std::fmt::Debug for DatabaseCredentials {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        let redacted_password: Option<&'static str> = match self.password {
27            Some(_) => Some("<REDACTED>"),
28            None => None,
29        };
30        f.debug_struct("DatabaseCredentials")
31            .field("username", &self.username)
32            .field("password", &redacted_password)
33            .field("host", &self.host)
34            .field("port", &self.port)
35            .field("database", &self.database)
36            .field("extra_params", &self.extra_params)
37            .finish()
38    }
39}
40
41impl DatabaseCredentials {
42    /// Create empty credentials
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Load credentials from environment variables
48    pub fn from_environment(database_type: &str) -> Self {
49        let prefix = match database_type {
50            "postgresql" => "POSTGRES",
51            "mysql" => "MYSQL",
52            "sqlite" => "SQLITE",
53            _ => "DATABASE",
54        };
55
56        let mut creds = Self::new();
57
58        creds.username = env::var(format!("{}_USER", prefix))
59            .ok()
60            .or_else(|| env::var(format!("{}_USERNAME", prefix)).ok())
61            .or_else(|| env::var("DATABASE_USER").ok())
62            .or_else(|| env::var("DB_USER").ok());
63
64        creds.password = env::var(format!("{}_PASSWORD", prefix))
65            .ok()
66            .or_else(|| env::var("DATABASE_PASSWORD").ok())
67            .or_else(|| env::var("DB_PASSWORD").ok());
68
69        creds.host = env::var(format!("{}_HOST", prefix))
70            .ok()
71            .or_else(|| env::var("DATABASE_HOST").ok())
72            .or_else(|| env::var("DB_HOST").ok());
73
74        if let Ok(port_str) = env::var(format!("{}_PORT", prefix))
75            .or_else(|_| env::var("DATABASE_PORT"))
76            .or_else(|_| env::var("DB_PORT"))
77        {
78            creds.port = port_str.parse().ok();
79        }
80
81        creds.database = env::var(format!("{}_DATABASE", prefix))
82            .ok()
83            .or_else(|| env::var(format!("{}_DB", prefix)).ok())
84            .or_else(|| env::var("DATABASE_NAME").ok())
85            .or_else(|| env::var("DB_NAME").ok());
86
87        if let Ok(database_url) = env::var("DATABASE_URL")
88            && let Ok(parsed) = crate::connection::ConnectionInfo::parse(&database_url)
89        {
90            creds.username = creds.username.or(parsed.username);
91            creds.password = creds.password.or(parsed.password);
92            creds.host = creds.host.or(parsed.host);
93            creds.port = creds.port.or(parsed.port);
94            creds.database = creds.database.or(parsed.database);
95        }
96
97        let url_var = format!("{}_URL", prefix);
98        if let Ok(url) = env::var(&url_var)
99            && let Ok(parsed) = crate::connection::ConnectionInfo::parse(&url)
100        {
101            creds.username = creds.username.or(parsed.username);
102            creds.password = creds.password.or(parsed.password);
103            creds.host = creds.host.or(parsed.host);
104            creds.port = creds.port.or(parsed.port);
105            creds.database = creds.database.or(parsed.database);
106        }
107
108        creds
109    }
110
111    /// Apply credentials to connection string
112    pub fn apply_to_connection_string(&self, connection_string: &str) -> String {
113        if let Ok(mut conn_info) = crate::connection::ConnectionInfo::parse(connection_string) {
114            if let Some(username) = &self.username {
115                conn_info.username = Some(username.clone());
116            }
117            if let Some(password) = &self.password {
118                conn_info.password = Some(password.clone());
119            }
120            if let Some(host) = &self.host {
121                conn_info.host = Some(host.clone());
122            }
123            if let Some(port) = self.port {
124                conn_info.port = Some(port);
125            }
126            if let Some(database) = &self.database {
127                conn_info.database = Some(database.clone());
128            }
129
130            for (key, value) in &self.extra_params {
131                conn_info.query_params.insert(key.clone(), value.clone());
132            }
133
134            conn_info.to_original_string()
135        } else {
136            connection_string.to_string()
137        }
138    }
139
140    /// Validate that required credentials are present
141    pub fn validate(&self, database_type: &str) -> Result<(), DataProfilerError> {
142        match database_type {
143            "postgresql" | "mysql" => {
144                if self.host.is_none() {
145                    return Err(DataProfilerError::database_config(&format!(
146                        "Database host is required for {}",
147                        database_type
148                    )));
149                }
150                if self.username.is_none() {
151                    return Err(DataProfilerError::database_config(&format!(
152                        "Database username is required for {}",
153                        database_type
154                    )));
155                }
156            }
157            "sqlite" => {
158                if self.database.is_none() {
159                    log::warn!("No database file path specified for SQLite");
160                }
161            }
162            _ => {
163                log::warn!(
164                    "Credential validation for database type '{}' not implemented",
165                    database_type
166                );
167            }
168        }
169
170        Ok(())
171    }
172
173    /// Get a masked version for logging (passwords hidden)
174    pub fn to_masked_string(&self) -> String {
175        format!(
176            "DatabaseCredentials {{ username: {:?}, password: {}, host: {:?}, port: {:?}, database: {:?} }}",
177            self.username,
178            if self.password.is_some() {
179                "***"
180            } else {
181                "None"
182            },
183            self.host,
184            self.port,
185            self.database
186        )
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_credentials_masking() {
196        let mut creds = DatabaseCredentials::new();
197        creds.username = Some("testuser".to_string());
198
199        let masked = creds.to_masked_string();
200        assert!(masked.contains("testuser"));
201
202        let test_creds_with_pass = DatabaseCredentials {
203            username: Some("user".to_string()),
204            password: Some(format!("{}123", "testpass")),
205            host: None,
206            port: None,
207            database: Some("testdb".to_string()),
208            extra_params: HashMap::new(),
209        };
210        let masked_with_pass = test_creds_with_pass.to_masked_string();
211        assert!(masked_with_pass.contains("***"));
212        assert!(!masked_with_pass.contains("testpass123"));
213    }
214}