bitwarden_server_communication_config/
config.rs

1use serde::{Deserialize, Serialize};
2
3/// Server communication configuration
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[cfg_attr(
6    feature = "wasm",
7    derive(tsify::Tsify),
8    tsify(into_wasm_abi, from_wasm_abi)
9)]
10pub struct ServerCommunicationConfig {
11    /// Bootstrap configuration determining how to establish server communication
12    pub bootstrap: BootstrapConfig,
13}
14
15/// Bootstrap configuration for server communication
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[cfg_attr(
18    feature = "wasm",
19    derive(tsify::Tsify),
20    tsify(into_wasm_abi, from_wasm_abi)
21)]
22#[serde(tag = "type", rename_all = "camelCase")]
23pub enum BootstrapConfig {
24    /// Direct connection with no special authentication requirements
25    Direct,
26    /// SSO cookie vendor configuration for load balancer authentication
27    SsoCookieVendor(SsoCookieVendorConfig),
28}
29
30/// SSO cookie vendor configuration
31///
32/// This configuration is provided by the server.
33#[derive(Clone, Serialize, Deserialize)]
34#[cfg_attr(
35    feature = "wasm",
36    derive(tsify::Tsify),
37    tsify(into_wasm_abi, from_wasm_abi)
38)]
39pub struct SsoCookieVendorConfig {
40    /// Identity provider login URL for browser redirect during bootstrap
41    pub idp_login_url: String,
42    /// Cookie name
43    pub cookie_name: String,
44    /// Cookie domain for validation
45    pub cookie_domain: String,
46    /// Cookie value
47    pub cookie_value: Option<String>,
48}
49
50// We manually implement Debug to make sure we don't print sensitive cookie values
51impl std::fmt::Debug for SsoCookieVendorConfig {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("SsoCookieVendorConfig")
54            .field("idp_login_url", &self.idp_login_url)
55            .field("cookie_name", &self.cookie_name)
56            .field("cookie_domain", &self.cookie_domain)
57            .field("cookie_value", &"[REDACTED]")
58            .finish()
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn direct_bootstrap_serialization() {
68        let config = ServerCommunicationConfig {
69            bootstrap: BootstrapConfig::Direct,
70        };
71
72        let json = serde_json::to_string(&config).unwrap();
73        assert!(json.contains("\"type\":\"direct\""));
74
75        let deserialized: ServerCommunicationConfig = serde_json::from_str(&json).unwrap();
76        assert!(matches!(deserialized.bootstrap, BootstrapConfig::Direct));
77    }
78
79    #[test]
80    fn sso_cookie_vendor_serialization() {
81        let config = ServerCommunicationConfig {
82            bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
83                idp_login_url: "https://timeloop-auth.acme.com/login".to_string(),
84                cookie_name: "ALBAuthSessionCookie".to_string(),
85                cookie_domain: "vault.example.com".to_string(),
86                cookie_value: None,
87            }),
88        };
89
90        let json = serde_json::to_string(&config).unwrap();
91        assert!(json.contains("\"type\":\"ssoCookieVendor\""));
92        assert!(json.contains("timeloop-auth.acme.com"));
93        assert!(json.contains("ALBAuthSessionCookie"));
94
95        let deserialized: ServerCommunicationConfig = serde_json::from_str(&json).unwrap();
96        if let BootstrapConfig::SsoCookieVendor(vendor_config) = deserialized.bootstrap {
97            assert_eq!(
98                vendor_config.idp_login_url,
99                "https://timeloop-auth.acme.com/login"
100            );
101            assert_eq!(vendor_config.cookie_name, "ALBAuthSessionCookie");
102            assert_eq!(vendor_config.cookie_domain, "vault.example.com");
103            assert!(vendor_config.cookie_value.is_none());
104        } else {
105            panic!("Expected SsoCookieVendor variant");
106        }
107    }
108
109    #[test]
110    fn cookie_value_some_and_none() {
111        // Test with None
112        let config_none = SsoCookieVendorConfig {
113            idp_login_url: "https://example.com".to_string(),
114            cookie_name: "TestCookie".to_string(),
115            cookie_domain: "example.com".to_string(),
116            cookie_value: None,
117        };
118
119        let json_none = serde_json::to_string(&config_none).unwrap();
120        let deserialized_none: SsoCookieVendorConfig = serde_json::from_str(&json_none).unwrap();
121        assert!(deserialized_none.cookie_value.is_none());
122
123        // Test with Some
124        let config_some = SsoCookieVendorConfig {
125            idp_login_url: "https://example.com".to_string(),
126            cookie_name: "TestCookie".to_string(),
127            cookie_domain: "example.com".to_string(),
128            cookie_value: Some("eyJhbGciOiJFUzI1NiIsImtpZCI6Im...".to_string()),
129        };
130
131        let json_some = serde_json::to_string(&config_some).unwrap();
132        let deserialized_some: SsoCookieVendorConfig = serde_json::from_str(&json_some).unwrap();
133        assert_eq!(
134            deserialized_some.cookie_value,
135            Some("eyJhbGciOiJFUzI1NiIsImtpZCI6Im...".to_string())
136        );
137    }
138
139    #[test]
140    fn enum_variants() {
141        let direct = BootstrapConfig::Direct;
142        assert!(matches!(direct, BootstrapConfig::Direct));
143
144        let vendor = BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
145            idp_login_url: "https://example.com".to_string(),
146            cookie_name: "Cookie".to_string(),
147            cookie_domain: "example.com".to_string(),
148            cookie_value: None,
149        });
150        assert!(matches!(vendor, BootstrapConfig::SsoCookieVendor(_)));
151    }
152
153    #[test]
154    fn debug_output_redacts_cookie_value() {
155        // Test that cookie values are not exposed in Debug output
156        let config_with_cookie = SsoCookieVendorConfig {
157            idp_login_url: "https://example.com/login".to_string(),
158            cookie_name: "SessionCookie".to_string(),
159            cookie_domain: "example.com".to_string(),
160            cookie_value: Some("super-secret-cookie-value-abc123".to_string()),
161        };
162
163        let debug_output = format!("{:?}", config_with_cookie);
164
165        // Should contain non-sensitive fields
166        assert!(debug_output.contains("SsoCookieVendorConfig"));
167        assert!(debug_output.contains("example.com/login"));
168        assert!(debug_output.contains("SessionCookie"));
169        assert!(debug_output.contains("example.com"));
170
171        // Should NOT contain the actual cookie value
172        assert!(!debug_output.contains("super-secret-cookie-value-abc123"));
173        // Should show redaction marker
174        assert!(debug_output.contains("[REDACTED]"));
175    }
176}