dataprof_db/security/
credentials.rs1use crate::DataProfilerError;
4use std::collections::HashMap;
5use std::env;
6
7#[derive(Clone, Default)]
9pub struct DatabaseCredentials {
10 pub username: Option<String>,
12 pub password: Option<String>,
14 pub host: Option<String>,
16 pub port: Option<u16>,
18 pub database: Option<String>,
20 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 pub fn new() -> Self {
44 Self::default()
45 }
46
47 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 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 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 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}