Skip to main content

dataprof_db/security/
connection_security.rs

1//! Database connection security validation and configuration
2
3use super::{
4    credentials::DatabaseCredentials, environment::load_ssl_config_from_environment,
5    ssl_config::SslConfig,
6};
7use crate::DataProfilerError;
8use std::collections::HashMap;
9
10/// Validate database connection security
11pub fn validate_connection_security(
12    connection_string: &str,
13    ssl_config: &SslConfig,
14    database_type: &str,
15) -> Result<Vec<String>, DataProfilerError> {
16    let mut warnings = Vec::new();
17
18    if let Ok(conn_info) = crate::connection::ConnectionInfo::parse(connection_string) {
19        if conn_info.password.is_some() {
20            warnings.push(
21                "Password embedded in connection string. Consider using environment variables."
22                    .to_string(),
23            );
24        }
25
26        if database_type != "sqlite" {
27            let has_ssl_params = conn_info.query_params.contains_key("sslmode")
28                || conn_info.query_params.contains_key("tls")
29                || conn_info.query_params.contains_key("ssl");
30
31            if !has_ssl_params && !ssl_config.require_ssl {
32                warnings.push(
33                    "No SSL/TLS configuration detected. Database traffic may be unencrypted."
34                        .to_string(),
35                );
36            }
37        }
38
39        if let Some(host) = &conn_info.host
40            && (host == "localhost" || host == "127.0.0.1" || host == "::1")
41        {
42            warnings.push(
43                "Connecting to localhost. Ensure this is intentional in production.".to_string(),
44            );
45        }
46
47        let default_ports = HashMap::from([("postgresql", 5432), ("mysql", 3306)]);
48
49        if let Some(default_port) = default_ports.get(database_type)
50            && conn_info.port == Some(*default_port)
51        {
52            warnings.push(format!(
53                "Using default port for {}. Consider using a non-standard port for security.",
54                database_type
55            ));
56        }
57    }
58
59    if let Err(e) = ssl_config.validate() {
60        warnings.push(format!("SSL configuration issue: {}", e));
61    }
62
63    Ok(warnings)
64}
65
66/// Load database configuration from environment with security best practices
67pub fn load_secure_database_config(
68    database_type: &str,
69) -> Result<(String, SslConfig), DataProfilerError> {
70    let credentials = DatabaseCredentials::from_environment(database_type);
71
72    let base_connection_string = match database_type {
73        "postgresql" => {
74            let host = credentials.host.as_deref().unwrap_or("localhost");
75            let port = credentials.port.unwrap_or(5432);
76            let database = credentials.database.as_deref().unwrap_or("postgres");
77            format!("postgresql://{}:{}/{}", host, port, database)
78        }
79        "mysql" => {
80            let host = credentials.host.as_deref().unwrap_or("localhost");
81            let port = credentials.port.unwrap_or(3306);
82            let database = credentials.database.as_deref().unwrap_or("mysql");
83            format!("mysql://{}:{}/{}", host, port, database)
84        }
85        "sqlite" => credentials
86            .database
87            .clone()
88            .unwrap_or_else(|| ":memory:".to_string()),
89        _ => {
90            return Err(DataProfilerError::database_config(&format!(
91                "Unsupported database type: {}",
92                database_type
93            )));
94        }
95    };
96
97    let connection_string = credentials.apply_to_connection_string(&base_connection_string);
98    let ssl_config = load_ssl_config_from_environment(database_type);
99    let secure_connection_string =
100        ssl_config.apply_to_connection_string(connection_string, database_type);
101
102    credentials.validate(database_type)?;
103
104    let warnings =
105        validate_connection_security(&secure_connection_string, &ssl_config, database_type)?;
106    for warning in warnings {
107        log::warn!("SECURITY WARNING: {}", warning);
108    }
109
110    Ok((secure_connection_string, ssl_config))
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_connection_security_validation() {
119        let connection_string = "postgresql://user:pass@localhost:5432/db";
120        let ssl_config = SslConfig::default();
121
122        let warnings = validate_connection_security(connection_string, &ssl_config, "postgresql")
123            .expect("Failed to validate connection security");
124
125        assert!(!warnings.is_empty());
126        assert!(warnings.iter().any(|w| w.contains("Password embedded")));
127        assert!(warnings.iter().any(|w| w.contains("localhost")));
128    }
129}