use serde::{Deserialize, Serialize};
use crate::config::defaults;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SecurityConfig {
pub enable_headers: bool,
pub content_type_options: String,
pub frame_options: String,
pub xss_protection: String,
pub cache_control: String,
pub content_security_policy: String,
pub strict_transport_security: String,
pub referrer_policy: String,
pub permissions_policy: String,
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
enable_headers: true,
content_type_options: defaults::security_headers::CONTENT_TYPE_OPTIONS.to_string(),
frame_options: defaults::security_headers::FRAME_OPTIONS.to_string(),
xss_protection: defaults::security_headers::XSS_PROTECTION.to_string(),
cache_control: defaults::security_headers::CACHE_CONTROL.to_string(),
content_security_policy: defaults::security_headers::CONTENT_SECURITY_POLICY
.to_string(),
strict_transport_security: defaults::security_headers::STRICT_TRANSPORT_SECURITY
.to_string(),
referrer_policy: defaults::security_headers::REFERRER_POLICY.to_string(),
permissions_policy: defaults::security_headers::PERMISSIONS_POLICY.to_string(),
}
}
}
impl SecurityConfig {
pub fn minimal() -> Self {
Self {
enable_headers: true,
content_type_options: defaults::security_headers::CONTENT_TYPE_OPTIONS.to_string(),
frame_options: "SAMEORIGIN".to_string(),
xss_protection: defaults::security_headers::XSS_PROTECTION.to_string(),
cache_control: defaults::security_headers::CACHE_CONTROL.to_string(),
content_security_policy: "default-src 'self'".to_string(),
strict_transport_security: defaults::security_headers::STRICT_TRANSPORT_SECURITY
.to_string(),
referrer_policy: defaults::security_headers::REFERRER_POLICY.to_string(),
permissions_policy: defaults::security_headers::PERMISSIONS_POLICY.to_string(),
}
}
pub fn disabled() -> Self {
Self {
enable_headers: false,
..Default::default()
}
}
pub fn headers_enabled(&self) -> bool {
self.enable_headers
}
}
impl crate::config::ValidateConfig for SecurityConfig {
fn validate(&self) -> Result<(), crate::config::ConfigError> {
Ok(())
}
}
impl SecurityConfig {
pub fn get_headers(&self) -> Vec<(&str, &str)> {
if !self.enable_headers {
return vec![];
}
vec![
("Content-Type-Options", &self.content_type_options),
("X-Frame-Options", &self.frame_options),
("X-XSS-Protection", &self.xss_protection),
("Cache-Control", &self.cache_control),
("Content-Security-Policy", &self.content_security_policy),
("Strict-Transport-Security", &self.strict_transport_security),
("Referrer-Policy", &self.referrer_policy),
("Permissions-Policy", &self.permissions_policy),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_security_config_default() {
let config = SecurityConfig::default();
assert!(config.enable_headers);
assert_eq!(config.content_type_options, "nosniff");
assert_eq!(config.frame_options, "DENY");
}
#[test]
fn test_security_config_minimal() {
let config = SecurityConfig::minimal();
assert!(config.enable_headers);
assert_eq!(config.frame_options, "SAMEORIGIN");
}
#[test]
fn test_security_config_disabled() {
let config = SecurityConfig::disabled();
assert!(!config.enable_headers);
}
#[test]
fn test_get_headers_enabled() {
let config = SecurityConfig::default();
let headers = config.get_headers();
assert!(!headers.is_empty());
assert!(headers.iter().any(|(k, _)| *k == "Content-Type-Options"));
}
#[test]
fn test_get_headers_disabled() {
let config = SecurityConfig::disabled();
let headers = config.get_headers();
assert!(headers.is_empty());
}
#[test]
fn test_headers_enabled_reflects_flag() {
let enabled = SecurityConfig::default();
assert!(enabled.headers_enabled());
let disabled = SecurityConfig::disabled();
assert!(!disabled.headers_enabled());
let minimal = SecurityConfig::minimal();
assert!(minimal.headers_enabled());
}
#[test]
fn test_get_headers_contains_all_eight_headers() {
let config = SecurityConfig::default();
let headers = config.get_headers();
assert_eq!(headers.len(), 8);
let names: Vec<&str> = headers.iter().map(|(n, _)| *n).collect();
assert_eq!(
names,
vec![
"Content-Type-Options",
"X-Frame-Options",
"X-XSS-Protection",
"Cache-Control",
"Content-Security-Policy",
"Strict-Transport-Security",
"Referrer-Policy",
"Permissions-Policy",
]
);
}
#[test]
fn test_minimal_config_differs_from_default() {
let default = SecurityConfig::default();
let minimal = SecurityConfig::minimal();
assert_ne!(default.frame_options, minimal.frame_options);
assert_eq!(minimal.frame_options, "SAMEORIGIN");
assert_ne!(
default.content_security_policy,
minimal.content_security_policy
);
assert_eq!(minimal.content_security_policy, "default-src 'self'");
}
#[test]
fn test_security_config_serialization_roundtrip() {
let config = SecurityConfig::default();
let json = serde_json::to_string(&config).unwrap();
let restored: SecurityConfig = serde_json::from_str(&json).unwrap();
assert_eq!(restored.enable_headers, config.enable_headers);
assert_eq!(restored.content_type_options, config.content_type_options);
assert_eq!(restored.frame_options, config.frame_options);
}
#[test]
fn test_security_config_deserialization_with_empty_object() {
let json = r#"{}"#;
let config: SecurityConfig = serde_json::from_str(json).unwrap();
assert!(config.enable_headers);
assert_eq!(config.content_type_options, "nosniff");
}
#[test]
fn test_security_config_validate_always_ok() {
use crate::config::ValidateConfig;
let config = SecurityConfig::default();
assert!(config.validate().is_ok());
let disabled = SecurityConfig::disabled();
assert!(disabled.validate().is_ok());
}
}