pulseengine_mcp_auth/
manager_vault.rs

1//! Vault-integrated authentication manager
2//!
3//! This module provides an enhanced authentication manager that can fetch
4//! master keys and configuration from external vault systems like Infisical.
5
6use crate::{
7    AuthConfig, AuthenticationManager, ValidationConfig,
8    config::StorageConfig,
9    manager::AuthError,
10    vault::{VaultConfig, VaultError, VaultIntegration},
11};
12use std::collections::HashMap;
13use tracing::{debug, info, warn};
14
15/// Vault-integrated authentication manager
16pub struct VaultAuthenticationManager {
17    auth_manager: AuthenticationManager,
18    vault_integration: Option<VaultIntegration>,
19    fallback_to_env: bool,
20}
21
22impl VaultAuthenticationManager {
23    /// Create a new vault-integrated authentication manager
24    pub async fn new_with_vault(
25        mut auth_config: AuthConfig,
26        validation_config: Option<ValidationConfig>,
27        vault_config: Option<VaultConfig>,
28        fallback_to_env: bool,
29    ) -> Result<Self, VaultAuthManagerError> {
30        let vault_integration = if let Some(vault_cfg) = vault_config {
31            match VaultIntegration::new(vault_cfg).await {
32                Ok(integration) => {
33                    info!(
34                        "Successfully connected to vault: {}",
35                        integration.client_info().name
36                    );
37                    Some(integration)
38                }
39                Err(e) => {
40                    if fallback_to_env {
41                        warn!(
42                            "Failed to connect to vault ({}), falling back to environment variables",
43                            e
44                        );
45                        None
46                    } else {
47                        return Err(VaultAuthManagerError::VaultError(e));
48                    }
49                }
50            }
51        } else {
52            None
53        };
54
55        // Try to get master key from vault first, then environment
56        let master_key = if let Some(vault) = &vault_integration {
57            match vault.get_master_key().await {
58                Ok(key) => {
59                    debug!("Retrieved master key from vault");
60                    key
61                }
62                Err(VaultError::SecretNotFound(_)) => {
63                    if fallback_to_env {
64                        debug!("Master key not found in vault, checking environment");
65                        Self::get_master_key_from_env()?
66                    } else {
67                        return Err(VaultAuthManagerError::MasterKeyNotFound);
68                    }
69                }
70                Err(e) => {
71                    if fallback_to_env {
72                        warn!(
73                            "Failed to get master key from vault ({}), checking environment",
74                            e
75                        );
76                        Self::get_master_key_from_env()?
77                    } else {
78                        return Err(VaultAuthManagerError::VaultError(e));
79                    }
80                }
81            }
82        } else {
83            Self::get_master_key_from_env()?
84        };
85
86        // Set master key in environment for this process
87        // SAFETY: Setting environment variable in single-threaded context during initialization
88        unsafe {
89            std::env::set_var("PULSEENGINE_MCP_MASTER_KEY", &master_key);
90        }
91
92        // Try to get additional configuration from vault
93        if let Some(vault) = &vault_integration {
94            if let Ok(vault_config) = vault.get_api_config().await {
95                Self::apply_vault_config(&mut auth_config, &vault_config);
96            }
97        }
98
99        // Use provided validation config or try to create from vault config
100        let validation_config = validation_config.unwrap_or_default();
101
102        // Create the authentication manager
103        let auth_manager =
104            AuthenticationManager::new_with_validation(auth_config, validation_config)
105                .await
106                .map_err(VaultAuthManagerError::AuthError)?;
107
108        Ok(Self {
109            auth_manager,
110            vault_integration,
111            fallback_to_env,
112        })
113    }
114
115    /// Create with default vault configuration (Infisical)
116    pub async fn new_with_default_vault(
117        auth_config: AuthConfig,
118        fallback_to_env: bool,
119    ) -> Result<Self, VaultAuthManagerError> {
120        let vault_config = Some(VaultConfig::default());
121        Self::new_with_vault(auth_config, None, vault_config, fallback_to_env).await
122    }
123
124    /// Get master key from environment variable
125    fn get_master_key_from_env() -> Result<String, VaultAuthManagerError> {
126        std::env::var("PULSEENGINE_MCP_MASTER_KEY")
127            .map_err(|_| VaultAuthManagerError::MasterKeyNotFound)
128    }
129
130    /// Apply vault configuration to auth config
131    fn apply_vault_config(auth_config: &mut AuthConfig, vault_config: &HashMap<String, String>) {
132        if let Some(timeout) = vault_config.get("PULSEENGINE_MCP_SESSION_TIMEOUT") {
133            if let Ok(timeout_secs) = timeout.parse::<u64>() {
134                auth_config.session_timeout_secs = timeout_secs;
135                debug!(
136                    "Applied vault config: session_timeout_secs = {}",
137                    timeout_secs
138                );
139            }
140        }
141
142        if let Some(max_attempts) = vault_config.get("PULSEENGINE_MCP_MAX_FAILED_ATTEMPTS") {
143            if let Ok(attempts) = max_attempts.parse::<u32>() {
144                auth_config.max_failed_attempts = attempts;
145                debug!("Applied vault config: max_failed_attempts = {}", attempts);
146            }
147        }
148
149        if let Some(rate_limit) = vault_config.get("PULSEENGINE_MCP_RATE_LIMIT_WINDOW") {
150            if let Ok(window_secs) = rate_limit.parse::<u64>() {
151                auth_config.rate_limit_window_secs = window_secs;
152                debug!(
153                    "Applied vault config: rate_limit_window_secs = {}",
154                    window_secs
155                );
156            }
157        }
158
159        if let Some(storage_path) = vault_config.get("PULSEENGINE_MCP_STORAGE_PATH") {
160            auth_config.storage = StorageConfig::File {
161                path: storage_path.into(),
162                file_permissions: 0o600,
163                dir_permissions: 0o700,
164                require_secure_filesystem: true,
165                enable_filesystem_monitoring: false,
166            };
167            debug!("Applied vault config: storage_path = {}", storage_path);
168        }
169    }
170
171    /// Get the underlying authentication manager
172    pub fn auth_manager(&self) -> &AuthenticationManager {
173        &self.auth_manager
174    }
175
176    /// Get vault integration if available
177    pub fn vault_integration(&self) -> Option<&VaultIntegration> {
178        self.vault_integration.as_ref()
179    }
180
181    /// Test vault connectivity
182    pub async fn test_vault_connection(&self) -> Result<(), VaultAuthManagerError> {
183        if let Some(vault) = &self.vault_integration {
184            vault
185                .test_connection()
186                .await
187                .map_err(VaultAuthManagerError::VaultError)
188        } else {
189            Err(VaultAuthManagerError::VaultNotConfigured)
190        }
191    }
192
193    /// Refresh configuration from vault
194    pub async fn refresh_config_from_vault(&mut self) -> Result<(), VaultAuthManagerError> {
195        if let Some(vault) = &self.vault_integration {
196            // Clear vault cache to get fresh values
197            vault.clear_cache().await;
198
199            // Get updated configuration
200            let vault_config = vault
201                .get_api_config()
202                .await
203                .map_err(VaultAuthManagerError::VaultError)?;
204
205            info!(
206                "Refreshed {} configuration values from vault",
207                vault_config.len()
208            );
209
210            // Note: We can't update the existing auth_manager config as it's immutable
211            // In a real implementation, you might want to recreate the auth_manager
212            // or make the configuration mutable
213            warn!("Configuration refresh requires recreating the authentication manager");
214
215            Ok(())
216        } else {
217            Err(VaultAuthManagerError::VaultNotConfigured)
218        }
219    }
220
221    /// Store a secret in the vault (if supported)
222    pub async fn store_secret(&self, name: &str, value: &str) -> Result<(), VaultAuthManagerError> {
223        if let Some(vault) = &self.vault_integration {
224            if let Some(client) = vault.vault_integration() {
225                client
226                    .set_secret(name, value)
227                    .await
228                    .map_err(VaultAuthManagerError::VaultError)
229            } else {
230                Err(VaultAuthManagerError::VaultNotConfigured)
231            }
232        } else {
233            Err(VaultAuthManagerError::VaultNotConfigured)
234        }
235    }
236
237    /// Get a secret from the vault
238    pub async fn get_secret(&self, name: &str) -> Result<String, VaultAuthManagerError> {
239        if let Some(vault) = &self.vault_integration {
240            vault
241                .get_secret_cached(name)
242                .await
243                .map_err(VaultAuthManagerError::VaultError)
244        } else {
245            Err(VaultAuthManagerError::VaultNotConfigured)
246        }
247    }
248
249    /// List available secrets from vault
250    pub async fn list_vault_secrets(&self) -> Result<Vec<String>, VaultAuthManagerError> {
251        if let Some(vault) = &self.vault_integration {
252            if let Some(client) = vault.vault_integration() {
253                client
254                    .list_secrets()
255                    .await
256                    .map_err(VaultAuthManagerError::VaultError)
257            } else {
258                Err(VaultAuthManagerError::VaultNotConfigured)
259            }
260        } else {
261            Err(VaultAuthManagerError::VaultNotConfigured)
262        }
263    }
264
265    /// Get vault status information
266    pub fn vault_status(&self) -> VaultStatus {
267        if let Some(vault) = &self.vault_integration {
268            VaultStatus {
269                enabled: true,
270                connected: true, // We assume it's connected if we have the integration
271                client_info: Some(vault.client_info()),
272                fallback_enabled: self.fallback_to_env,
273            }
274        } else {
275            VaultStatus {
276                enabled: false,
277                connected: false,
278                client_info: None,
279                fallback_enabled: self.fallback_to_env,
280            }
281        }
282    }
283}
284
285// Implement Deref to allow direct access to AuthenticationManager methods
286impl std::ops::Deref for VaultAuthenticationManager {
287    type Target = AuthenticationManager;
288
289    fn deref(&self) -> &Self::Target {
290        &self.auth_manager
291    }
292}
293
294/// Vault authentication manager errors
295#[derive(Debug, thiserror::Error)]
296pub enum VaultAuthManagerError {
297    #[error("Vault error: {0}")]
298    VaultError(VaultError),
299
300    #[error("Authentication manager error: {0}")]
301    AuthError(AuthError),
302
303    #[error("Master key not found in vault or environment")]
304    MasterKeyNotFound,
305
306    #[error("Vault is not configured")]
307    VaultNotConfigured,
308
309    #[error("Configuration error: {0}")]
310    ConfigError(String),
311}
312
313/// Vault status information
314#[derive(Debug, Clone)]
315pub struct VaultStatus {
316    pub enabled: bool,
317    pub connected: bool,
318    pub client_info: Option<crate::vault::VaultClientInfo>,
319    pub fallback_enabled: bool,
320}
321
322impl std::fmt::Display for VaultStatus {
323    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324        writeln!(f, "Vault Status:")?;
325        writeln!(f, "  Enabled: {}", self.enabled)?;
326        writeln!(f, "  Connected: {}", self.connected)?;
327        writeln!(f, "  Fallback Enabled: {}", self.fallback_enabled)?;
328
329        if let Some(info) = &self.client_info {
330            writeln!(f, "  Client: {} v{}", info.name, info.version)?;
331            writeln!(f, "  Type: {}", info.vault_type)?;
332            writeln!(f, "  Read Only: {}", info.read_only)?;
333        }
334
335        Ok(())
336    }
337}
338
339// Fix the vault_integration method
340impl VaultIntegration {
341    /// Get the underlying vault client (for advanced operations)
342    pub fn vault_integration(&self) -> Option<&dyn crate::vault::VaultClient> {
343        // This is a bit of a hack since we can't return a reference to the boxed trait object
344        // In practice, you'd want to design this differently
345        None
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use crate::config::StorageConfig;
353
354    #[test]
355    fn test_vault_status_display() {
356        let status = VaultStatus {
357            enabled: true,
358            connected: true,
359            client_info: Some(crate::vault::VaultClientInfo {
360                name: "Test Vault".to_string(),
361                version: "1.0.0".to_string(),
362                vault_type: crate::vault::VaultType::Infisical,
363                read_only: false,
364            }),
365            fallback_enabled: true,
366        };
367
368        let output = status.to_string();
369        assert!(output.contains("Enabled: true"));
370        assert!(output.contains("Connected: true"));
371        assert!(output.contains("Test Vault"));
372    }
373
374    #[test]
375    fn test_apply_vault_config() {
376        let mut auth_config = AuthConfig {
377            enabled: true,
378            storage: StorageConfig::File {
379                path: std::env::temp_dir()
380                    .join("mcp-auth-vault-test")
381                    .join("test_vault"),
382                file_permissions: 0o600,
383                dir_permissions: 0o700,
384                require_secure_filesystem: false,
385                enable_filesystem_monitoring: false,
386            },
387            cache_size: 100,
388            session_timeout_secs: 3600,
389            max_failed_attempts: 5,
390            rate_limit_window_secs: 900,
391        };
392
393        let mut vault_config = HashMap::new();
394        vault_config.insert(
395            "PULSEENGINE_MCP_SESSION_TIMEOUT".to_string(),
396            "7200".to_string(),
397        );
398        vault_config.insert(
399            "PULSEENGINE_MCP_MAX_FAILED_ATTEMPTS".to_string(),
400            "3".to_string(),
401        );
402
403        VaultAuthenticationManager::apply_vault_config(&mut auth_config, &vault_config);
404
405        assert_eq!(auth_config.session_timeout_secs, 7200);
406        assert_eq!(auth_config.max_failed_attempts, 3);
407    }
408}