use crate::{
AuthConfig, AuthenticationManager, ValidationConfig,
config::StorageConfig,
manager::AuthError,
vault::{VaultConfig, VaultError, VaultIntegration},
};
use std::collections::HashMap;
use tracing::{debug, info, warn};
pub struct VaultAuthenticationManager {
auth_manager: AuthenticationManager,
vault_integration: Option<VaultIntegration>,
fallback_to_env: bool,
}
impl VaultAuthenticationManager {
pub async fn new_with_vault(
mut auth_config: AuthConfig,
validation_config: Option<ValidationConfig>,
vault_config: Option<VaultConfig>,
fallback_to_env: bool,
) -> Result<Self, VaultAuthManagerError> {
let vault_integration = if let Some(vault_cfg) = vault_config {
match VaultIntegration::new(vault_cfg).await {
Ok(integration) => {
info!(
"Successfully connected to vault: {}",
integration.client_info().name
);
Some(integration)
}
Err(e) => {
if fallback_to_env {
warn!(
"Failed to connect to vault ({}), falling back to environment variables",
e
);
None
} else {
return Err(VaultAuthManagerError::VaultError(e));
}
}
}
} else {
None
};
let master_key = if let Some(vault) = &vault_integration {
match vault.get_master_key().await {
Ok(key) => {
debug!("Retrieved master key from vault");
key
}
Err(VaultError::SecretNotFound(_)) => {
if fallback_to_env {
debug!("Master key not found in vault, checking environment");
Self::get_master_key_from_env()?
} else {
return Err(VaultAuthManagerError::MasterKeyNotFound);
}
}
Err(e) => {
if fallback_to_env {
warn!(
"Failed to get master key from vault ({}), checking environment",
e
);
Self::get_master_key_from_env()?
} else {
return Err(VaultAuthManagerError::VaultError(e));
}
}
}
} else {
Self::get_master_key_from_env()?
};
unsafe {
std::env::set_var("PULSEENGINE_MCP_MASTER_KEY", &master_key);
}
if let Some(vault) = &vault_integration {
if let Ok(vault_config) = vault.get_api_config().await {
Self::apply_vault_config(&mut auth_config, &vault_config);
}
}
let validation_config = validation_config.unwrap_or_default();
let auth_manager =
AuthenticationManager::new_with_validation(auth_config, validation_config)
.await
.map_err(VaultAuthManagerError::AuthError)?;
Ok(Self {
auth_manager,
vault_integration,
fallback_to_env,
})
}
pub async fn new_with_default_vault(
auth_config: AuthConfig,
fallback_to_env: bool,
) -> Result<Self, VaultAuthManagerError> {
let vault_config = Some(VaultConfig::default());
Self::new_with_vault(auth_config, None, vault_config, fallback_to_env).await
}
fn get_master_key_from_env() -> Result<String, VaultAuthManagerError> {
std::env::var("PULSEENGINE_MCP_MASTER_KEY")
.map_err(|_| VaultAuthManagerError::MasterKeyNotFound)
}
fn apply_vault_config(auth_config: &mut AuthConfig, vault_config: &HashMap<String, String>) {
if let Some(timeout) = vault_config.get("PULSEENGINE_MCP_SESSION_TIMEOUT") {
if let Ok(timeout_secs) = timeout.parse::<u64>() {
auth_config.session_timeout_secs = timeout_secs;
debug!(
"Applied vault config: session_timeout_secs = {}",
timeout_secs
);
}
}
if let Some(max_attempts) = vault_config.get("PULSEENGINE_MCP_MAX_FAILED_ATTEMPTS") {
if let Ok(attempts) = max_attempts.parse::<u32>() {
auth_config.max_failed_attempts = attempts;
debug!("Applied vault config: max_failed_attempts = {}", attempts);
}
}
if let Some(rate_limit) = vault_config.get("PULSEENGINE_MCP_RATE_LIMIT_WINDOW") {
if let Ok(window_secs) = rate_limit.parse::<u64>() {
auth_config.rate_limit_window_secs = window_secs;
debug!(
"Applied vault config: rate_limit_window_secs = {}",
window_secs
);
}
}
if let Some(storage_path) = vault_config.get("PULSEENGINE_MCP_STORAGE_PATH") {
auth_config.storage = StorageConfig::File {
path: storage_path.into(),
file_permissions: 0o600,
dir_permissions: 0o700,
require_secure_filesystem: true,
enable_filesystem_monitoring: false,
};
debug!("Applied vault config: storage_path = {}", storage_path);
}
}
pub fn auth_manager(&self) -> &AuthenticationManager {
&self.auth_manager
}
pub fn vault_integration(&self) -> Option<&VaultIntegration> {
self.vault_integration.as_ref()
}
pub async fn test_vault_connection(&self) -> Result<(), VaultAuthManagerError> {
if let Some(vault) = &self.vault_integration {
vault
.test_connection()
.await
.map_err(VaultAuthManagerError::VaultError)
} else {
Err(VaultAuthManagerError::VaultNotConfigured)
}
}
pub async fn refresh_config_from_vault(&mut self) -> Result<(), VaultAuthManagerError> {
if let Some(vault) = &self.vault_integration {
vault.clear_cache().await;
let vault_config = vault
.get_api_config()
.await
.map_err(VaultAuthManagerError::VaultError)?;
info!(
"Refreshed {} configuration values from vault",
vault_config.len()
);
warn!("Configuration refresh requires recreating the authentication manager");
Ok(())
} else {
Err(VaultAuthManagerError::VaultNotConfigured)
}
}
pub async fn store_secret(&self, name: &str, value: &str) -> Result<(), VaultAuthManagerError> {
if let Some(vault) = &self.vault_integration {
if let Some(client) = vault.vault_integration() {
client
.set_secret(name, value)
.await
.map_err(VaultAuthManagerError::VaultError)
} else {
Err(VaultAuthManagerError::VaultNotConfigured)
}
} else {
Err(VaultAuthManagerError::VaultNotConfigured)
}
}
pub async fn get_secret(&self, name: &str) -> Result<String, VaultAuthManagerError> {
if let Some(vault) = &self.vault_integration {
vault
.get_secret_cached(name)
.await
.map_err(VaultAuthManagerError::VaultError)
} else {
Err(VaultAuthManagerError::VaultNotConfigured)
}
}
pub async fn list_vault_secrets(&self) -> Result<Vec<String>, VaultAuthManagerError> {
if let Some(vault) = &self.vault_integration {
if let Some(client) = vault.vault_integration() {
client
.list_secrets()
.await
.map_err(VaultAuthManagerError::VaultError)
} else {
Err(VaultAuthManagerError::VaultNotConfigured)
}
} else {
Err(VaultAuthManagerError::VaultNotConfigured)
}
}
pub fn vault_status(&self) -> VaultStatus {
if let Some(vault) = &self.vault_integration {
VaultStatus {
enabled: true,
connected: true, client_info: Some(vault.client_info()),
fallback_enabled: self.fallback_to_env,
}
} else {
VaultStatus {
enabled: false,
connected: false,
client_info: None,
fallback_enabled: self.fallback_to_env,
}
}
}
}
impl std::ops::Deref for VaultAuthenticationManager {
type Target = AuthenticationManager;
fn deref(&self) -> &Self::Target {
&self.auth_manager
}
}
#[derive(Debug, thiserror::Error)]
pub enum VaultAuthManagerError {
#[error("Vault error: {0}")]
VaultError(VaultError),
#[error("Authentication manager error: {0}")]
AuthError(AuthError),
#[error("Master key not found in vault or environment")]
MasterKeyNotFound,
#[error("Vault is not configured")]
VaultNotConfigured,
#[error("Configuration error: {0}")]
ConfigError(String),
}
#[derive(Debug, Clone)]
pub struct VaultStatus {
pub enabled: bool,
pub connected: bool,
pub client_info: Option<crate::vault::VaultClientInfo>,
pub fallback_enabled: bool,
}
impl std::fmt::Display for VaultStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Vault Status:")?;
writeln!(f, " Enabled: {}", self.enabled)?;
writeln!(f, " Connected: {}", self.connected)?;
writeln!(f, " Fallback Enabled: {}", self.fallback_enabled)?;
if let Some(info) = &self.client_info {
writeln!(f, " Client: {} v{}", info.name, info.version)?;
writeln!(f, " Type: {}", info.vault_type)?;
writeln!(f, " Read Only: {}", info.read_only)?;
}
Ok(())
}
}
impl VaultIntegration {
pub fn vault_integration(&self) -> Option<&dyn crate::vault::VaultClient> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::StorageConfig;
#[test]
fn test_vault_status_display() {
let status = VaultStatus {
enabled: true,
connected: true,
client_info: Some(crate::vault::VaultClientInfo {
name: "Test Vault".to_string(),
version: "1.0.0".to_string(),
vault_type: crate::vault::VaultType::Infisical,
read_only: false,
}),
fallback_enabled: true,
};
let output = status.to_string();
assert!(output.contains("Enabled: true"));
assert!(output.contains("Connected: true"));
assert!(output.contains("Test Vault"));
}
#[test]
fn test_apply_vault_config() {
let mut auth_config = AuthConfig {
enabled: true,
storage: StorageConfig::File {
path: std::env::temp_dir()
.join("mcp-auth-vault-test")
.join("test_vault"),
file_permissions: 0o600,
dir_permissions: 0o700,
require_secure_filesystem: false,
enable_filesystem_monitoring: false,
},
cache_size: 100,
session_timeout_secs: 3600,
max_failed_attempts: 5,
rate_limit_window_secs: 900,
};
let mut vault_config = HashMap::new();
vault_config.insert(
"PULSEENGINE_MCP_SESSION_TIMEOUT".to_string(),
"7200".to_string(),
);
vault_config.insert(
"PULSEENGINE_MCP_MAX_FAILED_ATTEMPTS".to_string(),
"3".to_string(),
);
VaultAuthenticationManager::apply_vault_config(&mut auth_config, &vault_config);
assert_eq!(auth_config.session_timeout_secs, 7200);
assert_eq!(auth_config.max_failed_attempts, 3);
}
}