Skip to main content

modo_session/
config.rs

1use serde::Deserialize;
2
3fn deserialize_nonzero_usize<'de, D>(deserializer: D) -> Result<usize, D::Error>
4where
5    D: serde::Deserializer<'de>,
6{
7    let value = usize::deserialize(deserializer)?;
8    if value == 0 {
9        return Err(serde::de::Error::custom(
10            "max_sessions_per_user must be > 0; setting it to 0 would lock out all users",
11        ));
12    }
13    Ok(value)
14}
15
16/// Configuration for the session subsystem.
17///
18/// All fields have sane defaults (see [`Default`]). Deserialises from YAML/TOML
19/// with `#[serde(default)]`, so you only need to specify the fields you want to
20/// override.
21///
22/// # Example (YAML)
23///
24/// ```yaml
25/// session_ttl_secs: 86400
26/// cookie_name: "_sess"
27/// validate_fingerprint: true
28/// touch_interval_secs: 600
29/// max_sessions_per_user: 5
30/// trusted_proxies:
31///   - "10.0.0.0/8"
32/// ```
33#[derive(Debug, Clone, Deserialize)]
34#[serde(default)]
35pub struct SessionConfig {
36    /// Session lifetime in seconds (default: 2 592 000 = 30 days).
37    pub session_ttl_secs: u64,
38    /// Name of the HTTP cookie that carries the session token (default: `"_session"`).
39    pub cookie_name: String,
40    /// Whether to reject sessions whose request fingerprint changed since creation
41    /// (default: `true`).  Disabling this reduces hijack protection but allows users
42    /// behind rotating IPs or proxies to keep their session.
43    pub validate_fingerprint: bool,
44    /// Minimum number of seconds between consecutive `touch` (expiry renewal) DB
45    /// writes (default: 300 = 5 minutes).
46    pub touch_interval_secs: u64,
47    /// Maximum number of concurrent active sessions per user before the
48    /// least-recently-used session is evicted (default: 10).
49    ///
50    /// Returns a deserialization error if set to 0, which would lock out all
51    /// users.
52    #[serde(deserialize_with = "deserialize_nonzero_usize")]
53    pub max_sessions_per_user: usize,
54    /// CIDR ranges of trusted reverse-proxy addresses.
55    ///
56    /// When non-empty, the `X-Forwarded-For` / `X-Real-IP` headers are only
57    /// trusted when the TCP connection originates from one of these ranges.
58    ///
59    /// **Security:** When empty (the default), proxy headers are trusted
60    /// unconditionally — any client can spoof their IP. In production behind a
61    /// reverse proxy, always set this to your proxy's CIDR range. Without a
62    /// reverse proxy, set a dummy value like `["127.0.0.1/32"]` to disable
63    /// proxy header trust entirely.
64    pub trusted_proxies: Vec<String>,
65}
66
67impl Default for SessionConfig {
68    fn default() -> Self {
69        Self {
70            session_ttl_secs: 2_592_000, // 30 days
71            cookie_name: "_session".to_string(),
72            validate_fingerprint: true,
73            touch_interval_secs: 300, // 5 minutes
74            max_sessions_per_user: 10,
75            trusted_proxies: Vec::new(),
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn default_values() {
86        let config = SessionConfig::default();
87        assert_eq!(config.session_ttl_secs, 2_592_000);
88        assert_eq!(config.cookie_name, "_session");
89        assert!(config.validate_fingerprint);
90        assert_eq!(config.touch_interval_secs, 300);
91        assert_eq!(config.max_sessions_per_user, 10);
92        assert!(config.trusted_proxies.is_empty());
93    }
94
95    #[test]
96    fn partial_yaml_deserialization() {
97        let yaml = r#"
98session_ttl_secs: 3600
99cookie_name: "my_sess"
100"#;
101        let config: SessionConfig = serde_yaml_ng::from_str(yaml).unwrap();
102        assert_eq!(config.session_ttl_secs, 3600);
103        assert_eq!(config.cookie_name, "my_sess");
104        // defaults for omitted fields
105        assert!(config.validate_fingerprint);
106        assert_eq!(config.touch_interval_secs, 300);
107        assert_eq!(config.max_sessions_per_user, 10);
108    }
109
110    #[test]
111    fn zero_max_sessions_returns_error() {
112        let yaml = r#"
113max_sessions_per_user: 0
114"#;
115        let err = serde_yaml_ng::from_str::<SessionConfig>(yaml).unwrap_err();
116        assert!(
117            err.to_string()
118                .contains("max_sessions_per_user must be > 0"),
119            "unexpected error: {err}",
120        );
121    }
122
123    #[test]
124    fn nonzero_max_sessions_accepted() {
125        let yaml = r#"
126max_sessions_per_user: 1
127"#;
128        let config: SessionConfig = serde_yaml_ng::from_str(yaml).unwrap();
129        assert_eq!(config.max_sessions_per_user, 1);
130    }
131}