Initial Commit
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
use docx_mcp::security::{SecurityConfig, SecurityMiddleware, SecurityError};
|
||||
use serde_json::json;
|
||||
use std::collections::HashSet;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_security_config() {
|
||||
let config = SecurityConfig::default();
|
||||
|
||||
assert!(!config.readonly_mode);
|
||||
assert!(config.command_whitelist.is_none());
|
||||
assert!(config.command_blacklist.is_none());
|
||||
assert_eq!(config.max_document_size, 100 * 1024 * 1024);
|
||||
assert_eq!(config.max_open_documents, 50);
|
||||
assert!(config.allow_external_tools);
|
||||
assert!(config.allow_network);
|
||||
assert!(!config.sandbox_mode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readonly_mode_allows_only_safe_commands() {
|
||||
let config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Should allow readonly commands
|
||||
assert!(config.is_command_allowed("open_document"));
|
||||
assert!(config.is_command_allowed("extract_text"));
|
||||
assert!(config.is_command_allowed("get_metadata"));
|
||||
assert!(config.is_command_allowed("search_text"));
|
||||
assert!(config.is_command_allowed("export_to_markdown"));
|
||||
|
||||
// Should block write commands
|
||||
assert!(!config.is_command_allowed("create_document"));
|
||||
assert!(!config.is_command_allowed("add_paragraph"));
|
||||
assert!(!config.is_command_allowed("save_document"));
|
||||
assert!(!config.is_command_allowed("find_and_replace"));
|
||||
assert!(!config.is_command_allowed("convert_to_pdf"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_whitelist() {
|
||||
let mut whitelist = HashSet::new();
|
||||
whitelist.insert("open_document".to_string());
|
||||
whitelist.insert("extract_text".to_string());
|
||||
|
||||
let config = SecurityConfig {
|
||||
command_whitelist: Some(whitelist),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Should allow whitelisted commands
|
||||
assert!(config.is_command_allowed("open_document"));
|
||||
assert!(config.is_command_allowed("extract_text"));
|
||||
|
||||
// Should block non-whitelisted commands
|
||||
assert!(!config.is_command_allowed("create_document"));
|
||||
assert!(!config.is_command_allowed("add_paragraph"));
|
||||
assert!(!config.is_command_allowed("get_metadata"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_blacklist() {
|
||||
let mut blacklist = HashSet::new();
|
||||
blacklist.insert("save_document".to_string());
|
||||
blacklist.insert("convert_to_pdf".to_string());
|
||||
|
||||
let config = SecurityConfig {
|
||||
command_blacklist: Some(blacklist),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Should allow non-blacklisted commands
|
||||
assert!(config.is_command_allowed("open_document"));
|
||||
assert!(config.is_command_allowed("extract_text"));
|
||||
assert!(config.is_command_allowed("add_paragraph"));
|
||||
|
||||
// Should block blacklisted commands
|
||||
assert!(!config.is_command_allowed("save_document"));
|
||||
assert!(!config.is_command_allowed("convert_to_pdf"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitelist_overrides_blacklist() {
|
||||
let mut whitelist = HashSet::new();
|
||||
whitelist.insert("save_document".to_string());
|
||||
|
||||
let mut blacklist = HashSet::new();
|
||||
blacklist.insert("save_document".to_string());
|
||||
|
||||
let config = SecurityConfig {
|
||||
command_whitelist: Some(whitelist),
|
||||
command_blacklist: Some(blacklist),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Whitelist should take precedence
|
||||
assert!(config.is_command_allowed("save_document"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_external_tools_restriction() {
|
||||
let config = SecurityConfig {
|
||||
allow_external_tools: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Should block conversion commands that might use external tools
|
||||
assert!(!config.is_command_allowed("convert_to_pdf"));
|
||||
assert!(!config.is_command_allowed("convert_to_images"));
|
||||
|
||||
// Should allow other commands
|
||||
assert!(config.is_command_allowed("open_document"));
|
||||
assert!(config.is_command_allowed("add_paragraph"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_middleware_command_check() {
|
||||
let config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
..Default::default()
|
||||
};
|
||||
let middleware = SecurityMiddleware::new(config);
|
||||
|
||||
let safe_args = json!({"document_id": "test"});
|
||||
|
||||
// Should pass readonly commands
|
||||
let result = middleware.check_command("extract_text", &safe_args);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Should fail write commands
|
||||
let result = middleware.check_command("add_paragraph", &safe_args);
|
||||
assert!(matches!(result, Err(SecurityError::CommandNotAllowed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sandbox_mode_path_restrictions() {
|
||||
let config = SecurityConfig {
|
||||
sandbox_mode: true,
|
||||
..Default::default()
|
||||
};
|
||||
let middleware = SecurityMiddleware::new(config);
|
||||
|
||||
// Should allow temp directory paths
|
||||
let temp_args = json!({"path": "/tmp/docx-mcp/test.docx"});
|
||||
let result = middleware.check_command("open_document", &temp_args);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Should block paths outside temp directory
|
||||
let home_args = json!({"path": "/home/user/documents/test.docx"});
|
||||
let result = middleware.check_command("open_document", &home_args);
|
||||
assert!(matches!(result, Err(SecurityError::PathNotAllowed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_size_limits() {
|
||||
use tempfile::NamedTempFile;
|
||||
use std::io::Write;
|
||||
|
||||
let config = SecurityConfig {
|
||||
max_document_size: 100, // 100 bytes limit
|
||||
..Default::default()
|
||||
};
|
||||
let middleware = SecurityMiddleware::new(config);
|
||||
|
||||
// Create a test file larger than limit
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let large_content = vec![0u8; 200]; // 200 bytes
|
||||
temp_file.write_all(&large_content).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let args = json!({"path": temp_file.path().to_str().unwrap()});
|
||||
let result = middleware.check_command("open_document", &args);
|
||||
|
||||
assert!(matches!(result, Err(SecurityError::FileTooLarge { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readonly_commands_list() {
|
||||
let readonly_commands = SecurityConfig::get_readonly_commands();
|
||||
|
||||
// Should include expected readonly commands
|
||||
assert!(readonly_commands.contains("open_document"));
|
||||
assert!(readonly_commands.contains("extract_text"));
|
||||
assert!(readonly_commands.contains("get_metadata"));
|
||||
assert!(readonly_commands.contains("search_text"));
|
||||
assert!(readonly_commands.contains("analyze_formatting"));
|
||||
|
||||
// Should not include write commands
|
||||
assert!(!readonly_commands.contains("create_document"));
|
||||
assert!(!readonly_commands.contains("add_paragraph"));
|
||||
assert!(!readonly_commands.contains("save_document"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_commands_list() {
|
||||
let write_commands = SecurityConfig::get_write_commands();
|
||||
|
||||
// Should include expected write commands
|
||||
assert!(write_commands.contains("create_document"));
|
||||
assert!(write_commands.contains("add_paragraph"));
|
||||
assert!(write_commands.contains("save_document"));
|
||||
assert!(write_commands.contains("find_and_replace"));
|
||||
|
||||
// Should not include readonly commands
|
||||
assert!(!write_commands.contains("open_document"));
|
||||
assert!(!write_commands.contains("extract_text"));
|
||||
assert!(!write_commands.contains("get_metadata"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_summary() {
|
||||
let config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
sandbox_mode: true,
|
||||
allow_external_tools: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let summary = config.get_summary();
|
||||
assert!(summary.contains("READONLY MODE"));
|
||||
assert!(summary.contains("SANDBOX MODE"));
|
||||
assert!(summary.contains("No external tools"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_combined_security_modes() {
|
||||
let mut whitelist = HashSet::new();
|
||||
whitelist.insert("open_document".to_string());
|
||||
whitelist.insert("extract_text".to_string());
|
||||
|
||||
let config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
sandbox_mode: true,
|
||||
command_whitelist: Some(whitelist),
|
||||
allow_external_tools: false,
|
||||
allow_network: false,
|
||||
max_document_size: 1024,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Should only allow whitelisted readonly commands
|
||||
assert!(config.is_command_allowed("open_document"));
|
||||
assert!(config.is_command_allowed("extract_text"));
|
||||
|
||||
// Should block everything else
|
||||
assert!(!config.is_command_allowed("get_metadata")); // Not in whitelist
|
||||
assert!(!config.is_command_allowed("add_paragraph")); // Not readonly
|
||||
assert!(!config.is_command_allowed("convert_to_pdf")); // External tools disabled
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recursive_path_argument_checking() {
|
||||
let config = SecurityConfig {
|
||||
sandbox_mode: true,
|
||||
..Default::default()
|
||||
};
|
||||
let middleware = SecurityMiddleware::new(config);
|
||||
|
||||
// Complex nested arguments with paths
|
||||
let nested_args = json!({
|
||||
"document_id": "test",
|
||||
"options": {
|
||||
"output_path": "/home/user/bad/path.docx",
|
||||
"settings": {
|
||||
"temp_file": "/tmp/safe/path.tmp"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"/home/user/another/bad/path.docx",
|
||||
"/tmp/docx-mcp/safe/path.docx"
|
||||
]
|
||||
});
|
||||
|
||||
let result = middleware.check_command("some_command", &nested_args);
|
||||
assert!(matches!(result, Err(SecurityError::PathNotAllowed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_error_messages() {
|
||||
let error = SecurityError::CommandNotAllowed("dangerous_command".to_string());
|
||||
assert!(error.to_string().contains("dangerous_command"));
|
||||
|
||||
let error = SecurityError::PathNotAllowed("/bad/path".to_string());
|
||||
assert!(error.to_string().contains("/bad/path"));
|
||||
|
||||
let error = SecurityError::FileTooLarge { size: 2000, max_size: 1000 };
|
||||
assert!(error.to_string().contains("2000"));
|
||||
assert!(error.to_string().contains("1000"));
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn readonly_config() -> SecurityConfig {
|
||||
SecurityConfig {
|
||||
readonly_mode: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn sandbox_config() -> SecurityConfig {
|
||||
SecurityConfig {
|
||||
sandbox_mode: true,
|
||||
allow_external_tools: false,
|
||||
allow_network: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn restrictive_config() -> SecurityConfig {
|
||||
let mut whitelist = HashSet::new();
|
||||
whitelist.insert("open_document".to_string());
|
||||
whitelist.insert("extract_text".to_string());
|
||||
|
||||
SecurityConfig {
|
||||
readonly_mode: true,
|
||||
sandbox_mode: true,
|
||||
command_whitelist: Some(whitelist),
|
||||
max_document_size: 1024 * 1024, // 1MB
|
||||
max_open_documents: 5,
|
||||
allow_external_tools: false,
|
||||
allow_network: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("open_document", true)]
|
||||
#[case("extract_text", true)]
|
||||
#[case("get_metadata", true)]
|
||||
#[case("create_document", false)]
|
||||
#[case("add_paragraph", false)]
|
||||
#[case("save_document", false)]
|
||||
fn test_readonly_mode_commands(readonly_config: SecurityConfig, #[case] command: &str, #[case] expected: bool) {
|
||||
assert_eq!(readonly_config.is_command_allowed(command), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("open_document", true)]
|
||||
#[case("extract_text", true)]
|
||||
#[case("add_paragraph", false)] // Not in whitelist
|
||||
#[case("get_metadata", false)] // Not in whitelist
|
||||
fn test_restrictive_mode_commands(restrictive_config: SecurityConfig, #[case] command: &str, #[case] expected: bool) {
|
||||
assert_eq!(restrictive_config.is_command_allowed(command), expected);
|
||||
}
|
||||
Reference in New Issue
Block a user