#![cfg_attr(doctest, allow(unused_imports))]
use crate::security::redact_cache_key;
pub fn log_cache_key(_level: &str, message: &str, key: &str) -> String {
let redacted = redact_cache_key(key);
format!("{}: {}", message, redacted)
}
pub fn sanitize_message(message: &str) -> String {
let mut result = String::with_capacity(message.len());
let mut remaining = message;
while let Some(rel_pos) = remaining.find("://") {
let protocol_start = remaining[..rel_pos]
.rfind(|c: char| c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
let protocol = &remaining[protocol_start..rel_pos];
let after_start = rel_pos + 3;
result.push_str(&remaining[..protocol_start]);
if let Some(at_pos) = remaining[after_start..].find('@') {
let abs_at_pos = after_start + at_pos;
let user_part = &remaining[after_start..abs_at_pos];
let host_end = remaining[abs_at_pos..]
.find(|c: char| c.is_whitespace())
.map(|i| abs_at_pos + i)
.unwrap_or(remaining.len());
let host_part = &remaining[abs_at_pos..host_end];
let sanitized_user: String = user_part
.chars()
.take_while(|c| *c != ':')
.chain(std::iter::once('*').chain(std::iter::once('*')).take(2))
.collect();
result.push_str(protocol);
result.push_str("://");
result.push_str(&sanitized_user);
result.push_str(host_part);
remaining = &remaining[host_end..];
} else {
result.push_str(protocol);
result.push_str("://");
remaining = &remaining[after_start..];
}
}
result.push_str(remaining);
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::security::{redact_cache_key, redact_connection_string};
#[test]
fn test_log_connection_string() {
let conn_str = "redis://user:password123@localhost:6379";
let redacted = redact_connection_string(conn_str);
assert!(!redacted.contains("password123"));
assert!(redacted.contains("user:****"));
}
#[test]
fn test_log_cache_key() {
let key = "user_token_abc123";
let redacted = redact_cache_key(key);
assert!(!redacted.contains("token"));
assert!(redacted.starts_with("****"));
}
#[test]
fn test_sanitize_message() {
let msg = "Connection: redis://user:secret123@localhost:6379";
let sanitized = sanitize_message(msg);
assert!(!sanitized.contains("secret123"));
assert!(sanitized.contains("**"));
}
#[test]
fn test_log_cache_key_info_level() {
log_cache_key("info", "Cache hit", "user_token_abc123");
}
#[test]
fn test_log_cache_key_debug_level() {
log_cache_key("debug", "Cache debug", "session_xyz");
}
#[test]
fn test_log_cache_key_warn_level() {
log_cache_key("warn", "Cache warning", "password_123");
}
#[test]
fn test_log_cache_key_error_level() {
log_cache_key("error", "Cache error", "api_key_test");
}
#[test]
fn test_log_cache_key_default_level() {
log_cache_key("trace", "Cache trace", "normal_key");
log_cache_key("unknown_level", "Cache unknown", "another_key");
}
#[test]
fn test_log_cache_key_non_sensitive_key() {
log_cache_key("info", "Cache access", "user_profile_123");
}
#[test]
fn test_log_cache_key_empty_key() {
log_cache_key("info", "Empty key", "");
}
#[test]
fn test_log_cache_key_empty_message() {
log_cache_key("info", "", "some_key");
}
#[test]
fn test_sanitize_message_no_connection_string() {
let msg = "This is a normal message without connection string";
let sanitized = sanitize_message(msg);
assert_eq!(sanitized, msg);
}
#[test]
fn test_sanitize_message_empty() {
let sanitized = sanitize_message("");
assert_eq!(sanitized, "");
}
#[test]
fn test_sanitize_message_no_at_symbol() {
let msg = "redis://localhost:6379";
let sanitized = sanitize_message(msg);
assert_eq!(sanitized, msg);
}
#[test]
fn test_sanitize_message_with_password() {
let msg = "redis://user:password123@host:6379";
let sanitized = sanitize_message(msg);
assert!(!sanitized.contains("password123"));
}
#[test]
fn test_sanitize_message_multiple_protocols() {
let msg = "redis://user:pass1@host1:6379 and redis://user:pass2@host2:6380";
let sanitized = sanitize_message(msg);
assert!(!sanitized.contains("pass1"));
assert!(!sanitized.contains("pass2"));
}
}