Skip to main content

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)]
10#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
11pub struct ServerCommunicationConfig {
12    /// Bootstrap configuration determining how to establish server communication
13    pub bootstrap: BootstrapConfig,
14}
15
16/// Bootstrap configuration for server communication
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[cfg_attr(
19    feature = "wasm",
20    derive(tsify::Tsify),
21    tsify(into_wasm_abi, from_wasm_abi)
22)]
23#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
24#[serde(tag = "type", rename_all = "camelCase")]
25pub enum BootstrapConfig {
26    /// Direct connection with no special authentication requirements
27    Direct,
28    /// SSO cookie vendor configuration for load balancer authentication
29    SsoCookieVendor(SsoCookieVendorConfig),
30}
31
32/// SSO cookie vendor configuration
33///
34/// This configuration is provided by the server.
35#[derive(Clone, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37#[cfg_attr(
38    feature = "wasm",
39    derive(tsify::Tsify),
40    tsify(into_wasm_abi, from_wasm_abi)
41)]
42#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
43pub struct SsoCookieVendorConfig {
44    /// Identity provider login URL for browser redirect during bootstrap
45    pub idp_login_url: Option<String>,
46    /// Cookie name (base name, without shard suffix)
47    pub cookie_name: String,
48    /// Cookie domain for validation
49    pub cookie_domain: String,
50    /// Vault URL for cookie acquisition redirect
51    ///
52    /// This is the full vault URL (scheme + host + port) where the browser
53    /// should be redirected for SSO cookie acquisition.
54    pub vault_url: String,
55    /// Acquired cookies
56    ///
57    /// For sharded cookies, this contains multiple entries with names like
58    /// `AWSELBAuthSessionCookie-0`, `AWSELBAuthSessionCookie-1`, etc.
59    /// For unsharded cookies, this contains a single entry with the base name.
60    pub cookie_value: Option<Vec<crate::AcquiredCookie>>,
61}
62
63/// Request to set server communication configuration for a hostname
64///
65/// This is the input type for
66/// [`ServerCommunicationConfigClient::set_communication_type`](crate::ServerCommunicationConfigClient::set_communication_type).
67/// Unlike [`ServerCommunicationConfig`], this type does not include acquired cookies,
68/// since cookies are managed separately via
69/// [`ServerCommunicationConfigClient::acquire_cookie`](crate::ServerCommunicationConfigClient::acquire_cookie).
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[cfg_attr(
72    feature = "wasm",
73    derive(tsify::Tsify),
74    tsify(into_wasm_abi, from_wasm_abi)
75)]
76#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
77pub struct SetCommunicationTypeRequest {
78    /// Bootstrap configuration determining how to establish server communication
79    pub bootstrap: BootstrapConfigRequest,
80}
81
82/// Bootstrap configuration variant for [`SetCommunicationTypeRequest`]
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[cfg_attr(
85    feature = "wasm",
86    derive(tsify::Tsify),
87    tsify(into_wasm_abi, from_wasm_abi)
88)]
89#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
90#[serde(tag = "type", rename_all = "camelCase")]
91pub enum BootstrapConfigRequest {
92    /// Direct connection with no special authentication requirements
93    Direct,
94    /// SSO cookie vendor configuration for load balancer authentication
95    SsoCookieVendor(SsoCookieVendorConfigRequest),
96}
97
98/// SSO cookie vendor configuration for [`SetCommunicationTypeRequest`]
99///
100/// Contains the server-provided configuration fields without acquired cookies.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103#[cfg_attr(
104    feature = "wasm",
105    derive(tsify::Tsify),
106    tsify(into_wasm_abi, from_wasm_abi)
107)]
108#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
109pub struct SsoCookieVendorConfigRequest {
110    /// Identity provider login URL for browser redirect during bootstrap
111    pub idp_login_url: Option<String>,
112    /// Cookie name (base name, without shard suffix)
113    pub cookie_name: String,
114    /// Cookie domain for validation
115    pub cookie_domain: String,
116    /// Vault URL for cookie acquisition redirect
117    ///
118    /// This is the full vault URL (scheme + host + port) where the browser
119    /// should be redirected for SSO cookie acquisition.
120    pub vault_url: String,
121}
122
123// We manually implement Debug to make sure we don't print sensitive cookie values
124impl std::fmt::Debug for SsoCookieVendorConfig {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        f.debug_struct("SsoCookieVendorConfig")
127            .field("idp_login_url", &self.idp_login_url)
128            .field("cookie_name", &self.cookie_name)
129            .field("cookie_domain", &self.cookie_domain)
130            .field("vault_url", &self.vault_url)
131            .field(
132                "cookie_value",
133                &self.cookie_value.as_ref().map(|_| "[REDACTED]"),
134            )
135            .finish()
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn direct_bootstrap_serialization() {
145        let config = ServerCommunicationConfig {
146            bootstrap: BootstrapConfig::Direct,
147        };
148
149        let json = serde_json::to_string(&config).unwrap();
150        assert!(json.contains("\"type\":\"direct\""));
151
152        let deserialized: ServerCommunicationConfig = serde_json::from_str(&json).unwrap();
153        assert!(matches!(deserialized.bootstrap, BootstrapConfig::Direct));
154    }
155
156    #[test]
157    fn sso_cookie_vendor_serialization() {
158        let config = ServerCommunicationConfig {
159            bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
160                idp_login_url: Some("https://timeloop-auth.acme.com/login".to_string()),
161                cookie_name: "ALBAuthSessionCookie".to_string(),
162                cookie_domain: "vault.example.com".to_string(),
163                vault_url: "https://vault.example.com".to_string(),
164                cookie_value: None,
165            }),
166        };
167
168        let json = serde_json::to_string(&config).unwrap();
169        assert!(json.contains("\"type\":\"ssoCookieVendor\""));
170        assert!(json.contains("timeloop-auth.acme.com"));
171        assert!(json.contains("ALBAuthSessionCookie"));
172
173        // Verify SDK can parse server JSON with camelCase fields
174        let server_json = r#"{"bootstrap":{"type":"ssoCookieVendor","idpLoginUrl":"https://idp.example.com/login","cookieName":"TestCookie","cookieDomain":"example.com","vaultUrl":"https://vault.example.com"}}"#;
175        let parsed = serde_json::from_str::<ServerCommunicationConfig>(server_json).unwrap();
176        if let BootstrapConfig::SsoCookieVendor(vendor) = parsed.bootstrap {
177            assert_eq!(
178                vendor.idp_login_url,
179                Some("https://idp.example.com/login".to_string())
180            );
181            assert_eq!(vendor.cookie_name, "TestCookie");
182            assert_eq!(vendor.cookie_domain, "example.com");
183        } else {
184            panic!("Expected SsoCookieVendor variant");
185        }
186
187        let deserialized: ServerCommunicationConfig = serde_json::from_str(&json).unwrap();
188        if let BootstrapConfig::SsoCookieVendor(vendor_config) = deserialized.bootstrap {
189            assert_eq!(
190                vendor_config.idp_login_url,
191                Some("https://timeloop-auth.acme.com/login".to_string())
192            );
193            assert_eq!(vendor_config.cookie_name, "ALBAuthSessionCookie");
194            assert_eq!(vendor_config.cookie_domain, "vault.example.com");
195            assert!(vendor_config.cookie_value.is_none());
196        } else {
197            panic!("Expected SsoCookieVendor variant");
198        }
199    }
200
201    #[test]
202    fn cookie_value_some_and_none() {
203        use crate::AcquiredCookie;
204
205        // Test with None
206        let config_none = SsoCookieVendorConfig {
207            idp_login_url: Some("https://example.com".to_string()),
208            cookie_name: "TestCookie".to_string(),
209            cookie_domain: "example.com".to_string(),
210            vault_url: "https://vault.example.com".to_string(),
211            cookie_value: None,
212        };
213
214        let json_none = serde_json::to_string(&config_none).unwrap();
215        let deserialized_none: SsoCookieVendorConfig = serde_json::from_str(&json_none).unwrap();
216        assert!(deserialized_none.cookie_value.is_none());
217
218        // Test with Some - single cookie
219        let config_some = SsoCookieVendorConfig {
220            idp_login_url: Some("https://example.com".to_string()),
221            cookie_name: "TestCookie".to_string(),
222            cookie_domain: "example.com".to_string(),
223            vault_url: "https://vault.example.com".to_string(),
224            cookie_value: Some(vec![AcquiredCookie {
225                name: "TestCookie".to_string(),
226                value: "eyJhbGciOiJFUzI1NiIsImtpZCI6Im...".to_string(),
227            }]),
228        };
229
230        let json_some = serde_json::to_string(&config_some).unwrap();
231        let deserialized_some: SsoCookieVendorConfig = serde_json::from_str(&json_some).unwrap();
232        assert_eq!(deserialized_some.cookie_value.as_ref().unwrap().len(), 1);
233        assert_eq!(
234            deserialized_some.cookie_value.as_ref().unwrap()[0].name,
235            "TestCookie"
236        );
237
238        // Test with multiple shards
239        let config_sharded = SsoCookieVendorConfig {
240            idp_login_url: Some("https://example.com".to_string()),
241            cookie_name: "TestCookie".to_string(),
242            cookie_domain: "example.com".to_string(),
243            vault_url: "https://vault.example.com".to_string(),
244            cookie_value: Some(vec![
245                AcquiredCookie {
246                    name: "TestCookie-0".to_string(),
247                    value: "shard1".to_string(),
248                },
249                AcquiredCookie {
250                    name: "TestCookie-1".to_string(),
251                    value: "shard2".to_string(),
252                },
253                AcquiredCookie {
254                    name: "TestCookie-2".to_string(),
255                    value: "shard3".to_string(),
256                },
257            ]),
258        };
259
260        let json_sharded = serde_json::to_string(&config_sharded).unwrap();
261        let deserialized_sharded: SsoCookieVendorConfig =
262            serde_json::from_str(&json_sharded).unwrap();
263        assert_eq!(deserialized_sharded.cookie_value.as_ref().unwrap().len(), 3);
264        assert_eq!(
265            deserialized_sharded.cookie_value.as_ref().unwrap()[0].name,
266            "TestCookie-0"
267        );
268    }
269
270    #[test]
271    fn enum_variants() {
272        let direct = BootstrapConfig::Direct;
273        assert!(matches!(direct, BootstrapConfig::Direct));
274
275        let vendor = BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
276            idp_login_url: Some("https://example.com".to_string()),
277            cookie_name: "Cookie".to_string(),
278            cookie_domain: "example.com".to_string(),
279            vault_url: "https://vault.example.com".to_string(),
280            cookie_value: None,
281        });
282        assert!(matches!(vendor, BootstrapConfig::SsoCookieVendor(_)));
283    }
284
285    #[test]
286    fn debug_output_redacts_cookie_value() {
287        use crate::AcquiredCookie;
288
289        // Test that cookie values are not exposed in Debug output
290        let config_with_cookie = SsoCookieVendorConfig {
291            idp_login_url: Some("https://example.com/login".to_string()),
292            cookie_name: "SessionCookie".to_string(),
293            cookie_domain: "example.com".to_string(),
294            vault_url: "https://vault.example.com".to_string(),
295            cookie_value: Some(vec![AcquiredCookie {
296                name: "SessionCookie".to_string(),
297                value: "super-secret-cookie-value-abc123".to_string(),
298            }]),
299        };
300
301        let debug_output = format!("{:?}", config_with_cookie);
302
303        // Should contain non-sensitive fields
304        assert!(debug_output.contains("SsoCookieVendorConfig"));
305        assert!(debug_output.contains("example.com/login"));
306        assert!(debug_output.contains("SessionCookie"));
307        assert!(debug_output.contains("example.com"));
308
309        // Should NOT contain the actual cookie value
310        assert!(!debug_output.contains("super-secret-cookie-value-abc123"));
311        // Should show redaction marker
312        assert!(debug_output.contains("[REDACTED]"));
313    }
314}