pulseengine_mcp_security_middleware/
lib.rs

1//! # PulseEngine MCP Security Middleware
2//!
3//! Zero-configuration security middleware for MCP servers with Axum integration.
4//!
5//! This crate provides a simple, secure-by-default authentication and authorization
6//! middleware system that can be integrated into MCP servers with minimal configuration.
7//!
8//! ## Features
9//!
10//! - **Zero Configuration**: Works out of the box with sensible secure defaults
11//! - **Security Profiles**: Dev, staging, and production profiles with appropriate security levels
12//! - **Environment-Based Config**: Configure via environment variables without CLI tools
13//! - **Auto-Generation**: Automatically generates API keys and JWT secrets securely
14//! - **Axum Integration**: Built on `middleware::from_fn` for seamless integration
15//! - **MCP Compliance**: Follows 2025 MCP security best practices
16//!
17//! ## Quick Start
18//!
19//! ```rust,no_run
20//! use pulseengine_mcp_security_middleware::*;
21//! use axum::{Router, routing::get};
22//! use axum::middleware::from_fn;
23//!
24//! #[tokio::main]
25//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
26//!     // Zero-config development setup
27//!     let security = SecurityConfig::development();
28//!     let middleware = security.create_middleware().await?;
29//!     
30//!     let app: Router = Router::new()
31//!         .route("/", get(|| async { "Hello, secure world!" }))
32//!         .layer(from_fn(move |req, next| {
33//!             let middleware = middleware.clone();
34//!             async move { middleware.process(req, next).await }
35//!         }));
36//!         
37//!     // Server setup...
38//!     Ok(())
39//! }
40//! ```
41//!
42//! ## Security Profiles
43//!
44//! ### Development Profile
45//! ```rust
46//! use pulseengine_mcp_security_middleware::SecurityConfig;
47//!
48//! let config = SecurityConfig::development();
49//! // - Permissive settings for local development
50//! // - Simple API key authentication
51//! // - Detailed logging for debugging
52//! // - CORS enabled for localhost
53//! ```
54//!
55//! ### Production Profile
56//! ```rust
57//! use pulseengine_mcp_security_middleware::SecurityConfig;
58//! let config = SecurityConfig::production();
59//! // - Strict security settings
60//! // - JWT authentication with secure secrets
61//! // - Rate limiting enabled
62//! // - Audit logging
63//! // - HTTPS enforcement
64//! ```
65//!
66//! ## Environment Configuration
67//!
68//! ```bash
69//! # Security profile
70//! MCP_SECURITY_PROFILE=production
71//!
72//! # Auto-generated if not provided
73//! MCP_API_KEY=auto-generate
74//! MCP_JWT_SECRET=auto-generate
75//!
76//! # CORS and networking
77//! MCP_CORS_ORIGIN=localhost
78//! MCP_RATE_LIMIT=100/min
79//!
80//! # Security features
81//! MCP_ENABLE_AUDIT_LOG=true
82//! MCP_REQUIRE_HTTPS=true
83//! ```
84
85pub mod auth;
86pub mod config;
87pub mod error;
88pub mod middleware;
89pub mod profiles;
90pub mod utils;
91
92// Re-export main types for convenience
93pub use auth::{ApiKeyValidator, AuthContext, TokenValidator};
94pub use config::SecurityConfig;
95pub use error::{SecurityError, SecurityResult};
96pub use middleware::{SecurityMiddleware, mcp_auth_middleware, mcp_rate_limit_middleware};
97pub use profiles::SecurityProfile;
98pub use profiles::{DevelopmentProfile, ProductionProfile, StagingProfile};
99pub use utils::{SecureRandom, generate_api_key, generate_jwt_secret};
100
101/// Version information for the security middleware
102pub const VERSION: &str = env!("CARGO_PKG_VERSION");
103
104/// Creates a development security configuration with sensible defaults
105///
106/// This is the quickest way to get started with MCP security in development.
107///
108/// # Example
109/// ```rust
110/// use pulseengine_mcp_security_middleware::dev_security;
111///
112/// let config = dev_security();
113/// // Ready to use with permissive development settings
114/// ```
115pub fn dev_security() -> SecurityConfig {
116    SecurityConfig::development()
117}
118
119/// Creates a production security configuration with strict defaults
120///
121/// This should be used for production deployments where security is critical.
122///
123/// # Example
124/// ```rust
125/// use pulseengine_mcp_security_middleware::prod_security;
126///
127/// let config = prod_security();
128/// // Ready to use with strict production security
129/// ```
130pub fn prod_security() -> SecurityConfig {
131    SecurityConfig::production()
132}
133
134/// Creates a security configuration from environment variables
135///
136/// This automatically detects the security profile from `MCP_SECURITY_PROFILE`
137/// environment variable and configures accordingly.
138///
139/// # Example
140/// ```rust
141/// use pulseengine_mcp_security_middleware::env_security;
142///
143/// // Reads MCP_SECURITY_PROFILE=production from environment
144/// let config = env_security().unwrap();
145/// ```
146pub fn env_security() -> SecurityResult<SecurityConfig> {
147    SecurityConfig::from_env()
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_dev_security_creation() {
156        let config = dev_security();
157        assert_eq!(config.profile, SecurityProfile::Development);
158    }
159
160    #[test]
161    fn test_prod_security_creation() {
162        let config = prod_security();
163        assert_eq!(config.profile, SecurityProfile::Production);
164    }
165
166    #[test]
167    fn test_version_format() {
168        // VERSION is a compile-time constant from CARGO_PKG_VERSION, check it follows semver format
169        assert!(
170            VERSION.chars().any(|c| c.is_ascii_digit()),
171            "VERSION should contain digits: {VERSION}"
172        );
173    }
174
175    #[test]
176    fn test_env_security_with_invalid_profile() {
177        use std::env;
178
179        // Set invalid profile
180        unsafe {
181            env::set_var("MCP_SECURITY_PROFILE", "invalid");
182        }
183
184        let result = env_security();
185        assert!(result.is_err(), "Should fail with invalid profile");
186
187        // Clean up
188        unsafe {
189            env::remove_var("MCP_SECURITY_PROFILE");
190        }
191    }
192
193    #[test]
194    fn test_env_security_with_valid_profiles() {
195        use std::env;
196
197        for profile in &["development", "staging", "production"] {
198            unsafe {
199                env::set_var("MCP_SECURITY_PROFILE", profile);
200            }
201            let result = env_security();
202            assert!(result.is_ok(), "Should succeed with profile {profile}");
203            unsafe {
204                env::remove_var("MCP_SECURITY_PROFILE");
205            }
206        }
207    }
208
209    #[test]
210    fn test_version_constant() {
211        // VERSION is a compile-time constant, test that it has expected format
212        assert!(VERSION.contains('.'), "Version should contain dots");
213        assert!(
214            VERSION.chars().any(char::is_numeric),
215            "Version should contain numbers"
216        );
217    }
218
219    #[test]
220    fn test_module_exports() {
221        // Test that all main exports are accessible
222        let _config = dev_security();
223        let _prod_config = prod_security();
224
225        // Test that we can create configs from different profiles
226        use crate::profiles::SecurityProfile;
227        let _dev_profile = SecurityProfile::Development;
228        let _staging_profile = SecurityProfile::Staging;
229        let _prod_profile = SecurityProfile::Production;
230    }
231
232    #[test]
233    fn test_error_constructors() {
234        use crate::error::SecurityError;
235
236        let config_err = SecurityError::config("test config error");
237        assert!(config_err.to_string().contains("test config error"));
238
239        let auth_err = SecurityError::auth("test auth error");
240        assert!(auth_err.to_string().contains("test auth error"));
241
242        let authz_err = SecurityError::authz("test authz error");
243        assert!(authz_err.to_string().contains("test authz error"));
244
245        let token_err = SecurityError::invalid_token("test token error");
246        assert!(token_err.to_string().contains("test token error"));
247
248        let jwt_err = SecurityError::jwt("test jwt error");
249        assert!(jwt_err.to_string().contains("test jwt error"));
250
251        let random_err = SecurityError::random("test random error");
252        assert!(random_err.to_string().contains("test random error"));
253
254        let crypto_err = SecurityError::crypto("test crypto error");
255        assert!(crypto_err.to_string().contains("test crypto error"));
256
257        let http_err = SecurityError::http("test http error");
258        assert!(http_err.to_string().contains("test http error"));
259
260        let internal_err = SecurityError::internal("test internal error");
261        assert!(internal_err.to_string().contains("test internal error"));
262    }
263
264    #[test]
265    fn test_security_config_methods() {
266        use crate::config::SecurityConfig;
267        use crate::profiles::SecurityProfile;
268
269        // Test all config creation methods
270        let dev_config = SecurityConfig::development();
271        assert_eq!(dev_config.profile, SecurityProfile::Development);
272
273        let staging_config = SecurityConfig::staging();
274        assert_eq!(staging_config.profile, SecurityProfile::Staging);
275
276        let prod_config = SecurityConfig::production();
277        assert_eq!(prod_config.profile, SecurityProfile::Production);
278
279        // Test builder methods
280        let config = SecurityConfig::development()
281            .with_api_key("test_key")
282            .with_jwt_secret("test_secret")
283            .with_jwt_issuer("test_issuer")
284            .with_jwt_audience("test_audience");
285
286        assert_eq!(config.api_key.as_ref().unwrap(), "test_key");
287        assert_eq!(config.jwt_secret.as_ref().unwrap(), "test_secret");
288        assert_eq!(config.jwt_issuer, "test_issuer");
289        assert_eq!(config.jwt_audience, "test_audience");
290    }
291
292    #[test]
293    fn test_additional_utility_functions() {
294        use crate::utils::{SecureRandom, generate_request_id, generate_session_id};
295
296        // Test session ID generation
297        let session1 = generate_session_id();
298        let session2 = generate_session_id();
299        assert_ne!(session1, session2);
300        assert!(session1.len() > 10);
301
302        // Test request ID generation
303        let request1 = generate_request_id();
304        let request2 = generate_request_id();
305        assert_ne!(request1, request2);
306        assert!(request1.len() > 10);
307
308        // Test base64 string generation
309        let b64_str1 = SecureRandom::base64_string(32);
310        let b64_str2 = SecureRandom::base64_string(32);
311        assert_ne!(b64_str1, b64_str2);
312        assert!(b64_str1.len() > 40); // Base64 encoded 32 bytes
313
314        // Test base64 URL-safe string generation
315        let b64_url_str1 = SecureRandom::base64_url_string(32);
316        let b64_url_str2 = SecureRandom::base64_url_string(32);
317        assert_ne!(b64_url_str1, b64_url_str2);
318        assert!(b64_url_str1.len() > 40);
319        assert!(!b64_url_str1.contains('+'));
320        assert!(!b64_url_str1.contains('/'));
321    }
322}