Skip to main content

dataprof_db/security/
ssl_config.rs

1//! SSL/TLS configuration for secure database connections
2
3use super::utils::add_query_param;
4use crate::DataProfilerError;
5use serde::{Deserialize, Serialize};
6use std::path::Path;
7
8/// SSL/TLS configuration for database connections
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct SslConfig {
11    /// Whether to require SSL/TLS
12    pub require_ssl: bool,
13    /// Path to CA certificate file
14    pub ca_cert_path: Option<String>,
15    /// Path to client certificate file
16    pub client_cert_path: Option<String>,
17    /// Path to client private key file
18    pub client_key_path: Option<String>,
19    /// Whether to verify the server certificate
20    pub verify_server_cert: bool,
21    /// SSL mode (for PostgreSQL: disable, allow, prefer, require, verify-ca, verify-full)
22    pub ssl_mode: Option<String>,
23}
24
25impl Default for SslConfig {
26    fn default() -> Self {
27        Self {
28            require_ssl: false,
29            ca_cert_path: None,
30            client_cert_path: None,
31            client_key_path: None,
32            verify_server_cert: true,
33            ssl_mode: Some("prefer".to_string()),
34        }
35    }
36}
37
38impl SslConfig {
39    /// Create SSL config for production environments (strict security)
40    pub fn production() -> Self {
41        Self {
42            require_ssl: true,
43            ca_cert_path: None,
44            client_cert_path: None,
45            client_key_path: None,
46            verify_server_cert: true,
47            ssl_mode: Some("require".to_string()),
48        }
49    }
50
51    /// Create SSL config for development environments (relaxed security)
52    pub fn development() -> Self {
53        Self {
54            require_ssl: false,
55            ca_cert_path: None,
56            client_cert_path: None,
57            client_key_path: None,
58            verify_server_cert: false,
59            ssl_mode: Some("prefer".to_string()),
60        }
61    }
62
63    /// Validate SSL configuration
64    pub fn validate(&self) -> Result<(), DataProfilerError> {
65        if let Some(ca_cert_path) = &self.ca_cert_path
66            && !Path::new(ca_cert_path).exists()
67        {
68            return Err(DataProfilerError::database_ssl(&format!(
69                "CA certificate file not found: {}",
70                ca_cert_path
71            )));
72        }
73
74        if let Some(client_cert_path) = &self.client_cert_path
75            && !Path::new(client_cert_path).exists()
76        {
77            return Err(DataProfilerError::database_ssl(&format!(
78                "Client certificate file not found: {}",
79                client_cert_path
80            )));
81        }
82
83        if let Some(client_key_path) = &self.client_key_path
84            && !Path::new(client_key_path).exists()
85        {
86            return Err(DataProfilerError::database_ssl(&format!(
87                "Client private key file not found: {}",
88                client_key_path
89            )));
90        }
91
92        if let Some(ssl_mode) = &self.ssl_mode {
93            let valid_modes = [
94                "disable",
95                "allow",
96                "prefer",
97                "require",
98                "verify-ca",
99                "verify-full",
100            ];
101            if !valid_modes.contains(&ssl_mode.as_str()) {
102                return Err(DataProfilerError::database_ssl(&format!(
103                    "Invalid SSL mode '{}'. Valid modes: {}",
104                    ssl_mode,
105                    valid_modes.join(", ")
106                )));
107            }
108        }
109
110        if self.require_ssl && !self.verify_server_cert {
111            log::warn!(
112                "SSL required but server certificate verification is disabled. This may be insecure."
113            );
114        }
115
116        if !self.require_ssl {
117            log::warn!("SSL not required. Database connections may be unencrypted.");
118        }
119
120        Ok(())
121    }
122
123    /// Apply SSL configuration to connection string
124    pub fn apply_to_connection_string(
125        &self,
126        mut connection_string: String,
127        database_type: &str,
128    ) -> String {
129        match database_type {
130            "postgresql" => {
131                if let Some(ssl_mode) = &self.ssl_mode {
132                    connection_string = add_query_param(connection_string, "sslmode", ssl_mode);
133                }
134
135                if let Some(ca_cert) = &self.ca_cert_path {
136                    connection_string = add_query_param(connection_string, "sslrootcert", ca_cert);
137                }
138
139                if let Some(client_cert) = &self.client_cert_path {
140                    connection_string = add_query_param(connection_string, "sslcert", client_cert);
141                }
142
143                if let Some(client_key) = &self.client_key_path {
144                    connection_string = add_query_param(connection_string, "sslkey", client_key);
145                }
146            }
147            "mysql" => {
148                if self.require_ssl {
149                    connection_string = add_query_param(connection_string, "tls", "true");
150                }
151
152                if !self.verify_server_cert {
153                    connection_string =
154                        add_query_param(connection_string, "tls-skip-verify", "true");
155                }
156
157                if let Some(ca_cert) = &self.ca_cert_path {
158                    connection_string = add_query_param(connection_string, "tls-ca", ca_cert);
159                }
160            }
161            "sqlite" => {
162                if self.require_ssl {
163                    log::warn!("SSL configuration ignored for SQLite (embedded database)");
164                }
165            }
166            _ => {
167                log::warn!(
168                    "SSL configuration validation for '{}' not fully implemented",
169                    database_type
170                );
171            }
172        }
173
174        connection_string
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_ssl_config_validation() {
184        let mut config = SslConfig::default();
185        assert!(config.validate().is_ok());
186
187        config.ca_cert_path = Some("/nonexistent/path".to_string());
188        assert!(config.validate().is_err());
189    }
190}