Skip to main content

reinhardt_conf/settings/
security.rs

1//! Security settings fragment
2//!
3//! Controls HTTPS enforcement, HSTS policy, cookie security, and URL handling.
4
5use super::fragment::SettingsValidation;
6use super::profile::Profile;
7use super::validation::{ValidationError, ValidationResult};
8use reinhardt_core::macros::settings;
9use serde::{Deserialize, Serialize};
10
11/// Security-related configuration settings.
12///
13/// Controls HTTPS enforcement, HSTS policy, cookie security, and URL handling.
14/// Nested inside [`CoreSettings`] by default, but can also be used as
15/// a top-level fragment.
16///
17/// [`CoreSettings`]: super::core_settings::CoreSettings
18#[settings(fragment = true, section = "security", validate = false)]
19#[derive(Clone, Debug, Serialize, Deserialize)]
20pub struct SecuritySettings {
21	/// Header name and value for identifying secure requests behind a proxy.
22	#[serde(default)]
23	pub secure_proxy_ssl_header: Option<(String, String)>,
24	/// Redirect all HTTP requests to HTTPS.
25	#[serde(default)]
26	pub secure_ssl_redirect: bool,
27	/// Seconds to set HSTS max-age header.
28	#[serde(default)]
29	pub secure_hsts_seconds: Option<u64>,
30	/// Include subdomains in HSTS policy.
31	#[serde(default)]
32	pub secure_hsts_include_subdomains: bool,
33	/// Include preload directive in HSTS header.
34	#[serde(default)]
35	pub secure_hsts_preload: bool,
36	/// Only send session cookies over HTTPS.
37	#[serde(default)]
38	pub session_cookie_secure: bool,
39	/// Only send CSRF cookie over HTTPS.
40	#[serde(default)]
41	pub csrf_cookie_secure: bool,
42	/// Automatically append trailing slashes to URLs.
43	#[serde(default = "default_append_slash")]
44	pub append_slash: bool,
45}
46
47fn default_append_slash() -> bool {
48	true
49}
50
51impl Default for SecuritySettings {
52	fn default() -> Self {
53		Self {
54			secure_proxy_ssl_header: None,
55			secure_ssl_redirect: false,
56			secure_hsts_seconds: None,
57			secure_hsts_include_subdomains: false,
58			secure_hsts_preload: false,
59			session_cookie_secure: false,
60			csrf_cookie_secure: false,
61			append_slash: true,
62		}
63	}
64}
65
66impl SettingsValidation for SecuritySettings {
67	fn validate(&self, profile: &Profile) -> ValidationResult {
68		if profile.is_production() {
69			if !self.secure_ssl_redirect {
70				return Err(ValidationError::Security(
71					"secure_ssl_redirect should be true in production".to_string(),
72				));
73			}
74			if !self.session_cookie_secure {
75				return Err(ValidationError::Security(
76					"session_cookie_secure should be true in production".to_string(),
77				));
78			}
79			if !self.csrf_cookie_secure {
80				return Err(ValidationError::Security(
81					"csrf_cookie_secure should be true in production".to_string(),
82				));
83			}
84		}
85		Ok(())
86	}
87}
88
89#[cfg(test)]
90mod tests {
91	use super::SecuritySettings;
92	use crate::settings::fragment::SettingsFragment;
93	use crate::settings::profile::Profile;
94	use rstest::rstest;
95
96	#[rstest]
97	fn test_security_settings_section() {
98		// Arrange / Act
99		let section = SecuritySettings::section();
100
101		// Assert
102		assert_eq!(section, "security");
103	}
104
105	#[rstest]
106	fn test_security_settings_default() {
107		// Arrange / Act
108		let settings = SecuritySettings::default();
109
110		// Assert
111		assert!(!settings.secure_ssl_redirect);
112		assert!(!settings.session_cookie_secure);
113		assert!(!settings.csrf_cookie_secure);
114		assert!(settings.append_slash);
115		assert!(settings.secure_proxy_ssl_header.is_none());
116	}
117
118	#[rstest]
119	fn test_security_settings_development_validation_ok() {
120		// Arrange
121		let settings = SecuritySettings::default();
122
123		// Act
124		let result = settings.validate(&Profile::Development);
125
126		// Assert
127		assert!(result.is_ok());
128	}
129
130	#[rstest]
131	fn test_security_settings_production_validation_fails_without_ssl() {
132		// Arrange
133		let settings = SecuritySettings::default();
134
135		// Act
136		let result = settings.validate(&Profile::Production);
137
138		// Assert
139		assert!(result.is_err());
140	}
141
142	#[rstest]
143	fn test_security_settings_production_validation_ok() {
144		// Arrange
145		let settings = SecuritySettings {
146			secure_ssl_redirect: true,
147			session_cookie_secure: true,
148			csrf_cookie_secure: true,
149			..Default::default()
150		};
151
152		// Act
153		let result = settings.validate(&Profile::Production);
154
155		// Assert
156		assert!(result.is_ok());
157	}
158}