use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthConfig {
pub storage: StorageConfig,
pub enabled: bool,
pub cache_size: usize,
pub session_timeout_secs: u64,
pub max_failed_attempts: u32,
pub rate_limit_window_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StorageConfig {
File {
path: PathBuf,
#[serde(default = "default_file_permissions")]
file_permissions: u32,
#[serde(default = "default_dir_permissions")]
dir_permissions: u32,
#[serde(default)]
require_secure_filesystem: bool,
#[serde(default)]
enable_filesystem_monitoring: bool,
},
Environment {
prefix: String,
},
Memory,
}
fn default_file_permissions() -> u32 {
0o600 }
fn default_dir_permissions() -> u32 {
0o700 }
impl Default for AuthConfig {
fn default() -> Self {
Self {
storage: StorageConfig::File {
path: dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".pulseengine")
.join("mcp-auth")
.join("keys.enc"),
file_permissions: 0o600,
dir_permissions: 0o700,
require_secure_filesystem: true,
enable_filesystem_monitoring: false,
},
enabled: true,
cache_size: 1000,
session_timeout_secs: 3600, max_failed_attempts: 5,
rate_limit_window_secs: 900, }
}
}
impl AuthConfig {
pub fn disabled() -> Self {
Self {
enabled: false,
..Default::default()
}
}
pub fn memory() -> Self {
Self {
storage: StorageConfig::Memory,
..Default::default()
}
}
pub fn for_application(app_name: &str) -> Self {
Self {
storage: StorageConfig::File {
path: Self::get_app_storage_path(app_name),
file_permissions: 0o600,
dir_permissions: 0o700,
require_secure_filesystem: true,
enable_filesystem_monitoring: false,
},
enabled: true,
cache_size: 1000,
session_timeout_secs: 3600, max_failed_attempts: 5,
rate_limit_window_secs: 900, }
}
pub fn with_custom_path(app_name: &str, base_path: PathBuf) -> Self {
Self {
storage: StorageConfig::File {
path: base_path.join(app_name).join("mcp-auth").join("keys.enc"),
file_permissions: 0o600,
dir_permissions: 0o700,
require_secure_filesystem: true,
enable_filesystem_monitoring: false,
},
enabled: true,
cache_size: 1000,
session_timeout_secs: 3600, max_failed_attempts: 5,
rate_limit_window_secs: 900, }
}
fn get_app_storage_path(app_name: &str) -> PathBuf {
if let Ok(app_name_override) = std::env::var("PULSEENGINE_MCP_APP_NAME") {
if !app_name_override.trim().is_empty() {
return Self::build_storage_path(&app_name_override);
}
}
Self::build_storage_path(app_name)
}
fn build_storage_path(app_name: &str) -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".pulseengine")
.join(app_name)
.join("mcp-auth")
.join("keys.enc")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_default_file_permissions() {
assert_eq!(default_file_permissions(), 0o600);
}
#[test]
fn test_default_dir_permissions() {
assert_eq!(default_dir_permissions(), 0o700);
}
#[test]
fn test_auth_config_default() {
let config = AuthConfig::default();
assert!(config.enabled);
assert_eq!(config.cache_size, 1000);
assert_eq!(config.session_timeout_secs, 3600);
assert_eq!(config.max_failed_attempts, 5);
assert_eq!(config.rate_limit_window_secs, 900);
match config.storage {
StorageConfig::File {
path,
file_permissions,
dir_permissions,
require_secure_filesystem,
enable_filesystem_monitoring,
} => {
assert!(path.to_string_lossy().contains(".pulseengine"));
assert!(path.to_string_lossy().contains("mcp-auth"));
assert!(path.to_string_lossy().contains("keys.enc"));
assert_eq!(file_permissions, 0o600);
assert_eq!(dir_permissions, 0o700);
assert!(require_secure_filesystem);
assert!(!enable_filesystem_monitoring);
}
_ => panic!("Expected File storage config"),
}
}
#[test]
fn test_auth_config_disabled() {
let config = AuthConfig::disabled();
assert!(!config.enabled);
assert_eq!(config.cache_size, 1000); assert_eq!(config.session_timeout_secs, 3600);
assert_eq!(config.max_failed_attempts, 5);
assert_eq!(config.rate_limit_window_secs, 900);
}
#[test]
fn test_auth_config_memory() {
let config = AuthConfig::memory();
assert!(config.enabled);
assert!(matches!(config.storage, StorageConfig::Memory));
assert_eq!(config.cache_size, 1000);
assert_eq!(config.session_timeout_secs, 3600);
assert_eq!(config.max_failed_attempts, 5);
assert_eq!(config.rate_limit_window_secs, 900);
}
#[test]
fn test_storage_config_file() {
let expected_path = std::env::temp_dir()
.join("mcp-auth-config-test")
.join("test_storage");
let storage = StorageConfig::File {
path: expected_path.clone(),
file_permissions: 0o644,
dir_permissions: 0o755,
require_secure_filesystem: false,
enable_filesystem_monitoring: true,
};
match storage {
StorageConfig::File {
path,
file_permissions,
dir_permissions,
require_secure_filesystem,
enable_filesystem_monitoring,
} => {
assert_eq!(path, expected_path);
assert_eq!(file_permissions, 0o644);
assert_eq!(dir_permissions, 0o755);
assert!(!require_secure_filesystem);
assert!(enable_filesystem_monitoring);
}
_ => panic!("Expected File storage config"),
}
}
#[test]
fn test_storage_config_environment() {
let storage = StorageConfig::Environment {
prefix: "MCP_AUTH".to_string(),
};
match storage {
StorageConfig::Environment { prefix } => {
assert_eq!(prefix, "MCP_AUTH");
}
_ => panic!("Expected Environment storage config"),
}
}
#[test]
fn test_storage_config_memory() {
let storage = StorageConfig::Memory;
assert!(matches!(storage, StorageConfig::Memory));
}
#[test]
fn test_auth_config_serialization() {
let config = AuthConfig {
storage: StorageConfig::File {
path: PathBuf::from("/test/path"),
file_permissions: 0o600,
dir_permissions: 0o700,
require_secure_filesystem: true,
enable_filesystem_monitoring: false,
},
enabled: true,
cache_size: 500,
session_timeout_secs: 7200,
max_failed_attempts: 3,
rate_limit_window_secs: 1800,
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: AuthConfig = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.enabled, config.enabled);
assert_eq!(deserialized.cache_size, config.cache_size);
assert_eq!(
deserialized.session_timeout_secs,
config.session_timeout_secs
);
assert_eq!(deserialized.max_failed_attempts, config.max_failed_attempts);
assert_eq!(
deserialized.rate_limit_window_secs,
config.rate_limit_window_secs
);
match (config.storage, deserialized.storage) {
(
StorageConfig::File {
path: p1,
file_permissions: fp1,
dir_permissions: dp1,
..
},
StorageConfig::File {
path: p2,
file_permissions: fp2,
dir_permissions: dp2,
..
},
) => {
assert_eq!(p1, p2);
assert_eq!(fp1, fp2);
assert_eq!(dp1, dp2);
}
_ => panic!("Storage configs don't match"),
}
}
#[test]
fn test_storage_config_file_with_defaults() {
let json = r#"{
"File": {
"path": "/test/path"
}
}"#;
let storage: StorageConfig = serde_json::from_str(json).unwrap();
match storage {
StorageConfig::File {
path,
file_permissions,
dir_permissions,
require_secure_filesystem,
enable_filesystem_monitoring,
} => {
assert_eq!(path, PathBuf::from("/test/path"));
assert_eq!(file_permissions, 0o600); assert_eq!(dir_permissions, 0o700); assert!(!require_secure_filesystem); assert!(!enable_filesystem_monitoring); }
_ => panic!("Expected File storage config"),
}
}
#[test]
fn test_storage_config_environment_serialization() {
let storage = StorageConfig::Environment {
prefix: "TEST_PREFIX".to_string(),
};
let json = serde_json::to_string(&storage).unwrap();
let deserialized: StorageConfig = serde_json::from_str(&json).unwrap();
match deserialized {
StorageConfig::Environment { prefix } => {
assert_eq!(prefix, "TEST_PREFIX");
}
_ => panic!("Expected Environment storage config"),
}
}
#[test]
fn test_storage_config_memory_serialization() {
let storage = StorageConfig::Memory;
let json = serde_json::to_string(&storage).unwrap();
let deserialized: StorageConfig = serde_json::from_str(&json).unwrap();
assert!(matches!(deserialized, StorageConfig::Memory));
}
#[test]
fn test_auth_config_custom_values() {
let config = AuthConfig {
storage: StorageConfig::Environment {
prefix: "CUSTOM".to_string(),
},
enabled: false,
cache_size: 2000,
session_timeout_secs: 1800,
max_failed_attempts: 10,
rate_limit_window_secs: 300,
};
assert!(!config.enabled);
assert_eq!(config.cache_size, 2000);
assert_eq!(config.session_timeout_secs, 1800);
assert_eq!(config.max_failed_attempts, 10);
assert_eq!(config.rate_limit_window_secs, 300);
match config.storage {
StorageConfig::Environment { prefix } => {
assert_eq!(prefix, "CUSTOM");
}
_ => panic!("Expected Environment storage"),
}
}
#[test]
fn test_auth_config_clone() {
let original = AuthConfig::default();
let cloned = original.clone();
assert_eq!(cloned.enabled, original.enabled);
assert_eq!(cloned.cache_size, original.cache_size);
assert_eq!(cloned.session_timeout_secs, original.session_timeout_secs);
assert_eq!(cloned.max_failed_attempts, original.max_failed_attempts);
assert_eq!(
cloned.rate_limit_window_secs,
original.rate_limit_window_secs
);
}
#[test]
fn test_storage_config_debug() {
let file_storage = StorageConfig::File {
path: PathBuf::from("/test"),
file_permissions: 0o600,
dir_permissions: 0o700,
require_secure_filesystem: true,
enable_filesystem_monitoring: false,
};
let debug_str = format!("{:?}", file_storage);
assert!(debug_str.contains("File"));
assert!(debug_str.contains("/test"));
assert!(debug_str.contains("384"));
let env_storage = StorageConfig::Environment {
prefix: "TEST".to_string(),
};
let debug_str = format!("{:?}", env_storage);
assert!(debug_str.contains("Environment"));
assert!(debug_str.contains("TEST"));
let memory_storage = StorageConfig::Memory;
let debug_str = format!("{:?}", memory_storage);
assert!(debug_str.contains("Memory"));
}
}