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: Option<String>,
48    /// Cookie domain for validation
49    pub cookie_domain: Option<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: Option<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: Option<String>,
114    /// Cookie domain for validation
115    pub cookie_domain: Option<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: Option<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: Some("ALBAuthSessionCookie".to_string()),
162                cookie_domain: Some("vault.example.com".to_string()),
163                vault_url: Some("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, Some("TestCookie".to_string()));
182            assert_eq!(vendor.cookie_domain, Some("example.com".to_string()));
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!(
194                vendor_config.cookie_name,
195                Some("ALBAuthSessionCookie".to_string())
196            );
197            assert_eq!(
198                vendor_config.cookie_domain,
199                Some("vault.example.com".to_string())
200            );
201            assert!(vendor_config.cookie_value.is_none());
202        } else {
203            panic!("Expected SsoCookieVendor variant");
204        }
205    }
206
207    #[test]
208    fn cookie_value_some_and_none() {
209        use crate::AcquiredCookie;
210
211        // Test with None
212        let config_none = SsoCookieVendorConfig {
213            idp_login_url: Some("https://example.com".to_string()),
214            cookie_name: Some("TestCookie".to_string()),
215            cookie_domain: Some("example.com".to_string()),
216            vault_url: Some("https://vault.example.com".to_string()),
217            cookie_value: None,
218        };
219
220        let json_none = serde_json::to_string(&config_none).unwrap();
221        let deserialized_none: SsoCookieVendorConfig = serde_json::from_str(&json_none).unwrap();
222        assert!(deserialized_none.cookie_value.is_none());
223
224        // Test with Some - single cookie
225        let config_some = SsoCookieVendorConfig {
226            idp_login_url: Some("https://example.com".to_string()),
227            cookie_name: Some("TestCookie".to_string()),
228            cookie_domain: Some("example.com".to_string()),
229            vault_url: Some("https://vault.example.com".to_string()),
230            cookie_value: Some(vec![AcquiredCookie {
231                name: "TestCookie".to_string(),
232                value: "eyJhbGciOiJFUzI1NiIsImtpZCI6Im...".to_string(),
233            }]),
234        };
235
236        let json_some = serde_json::to_string(&config_some).unwrap();
237        let deserialized_some: SsoCookieVendorConfig = serde_json::from_str(&json_some).unwrap();
238        assert_eq!(deserialized_some.cookie_value.as_ref().unwrap().len(), 1);
239        assert_eq!(
240            deserialized_some.cookie_value.as_ref().unwrap()[0].name,
241            "TestCookie"
242        );
243
244        // Test with multiple shards
245        let config_sharded = SsoCookieVendorConfig {
246            idp_login_url: Some("https://example.com".to_string()),
247            cookie_name: Some("TestCookie".to_string()),
248            cookie_domain: Some("example.com".to_string()),
249            vault_url: Some("https://vault.example.com".to_string()),
250            cookie_value: Some(vec![
251                AcquiredCookie {
252                    name: "TestCookie-0".to_string(),
253                    value: "shard1".to_string(),
254                },
255                AcquiredCookie {
256                    name: "TestCookie-1".to_string(),
257                    value: "shard2".to_string(),
258                },
259                AcquiredCookie {
260                    name: "TestCookie-2".to_string(),
261                    value: "shard3".to_string(),
262                },
263            ]),
264        };
265
266        let json_sharded = serde_json::to_string(&config_sharded).unwrap();
267        let deserialized_sharded: SsoCookieVendorConfig =
268            serde_json::from_str(&json_sharded).unwrap();
269        assert_eq!(deserialized_sharded.cookie_value.as_ref().unwrap().len(), 3);
270        assert_eq!(
271            deserialized_sharded.cookie_value.as_ref().unwrap()[0].name,
272            "TestCookie-0"
273        );
274    }
275
276    #[test]
277    fn enum_variants() {
278        let direct = BootstrapConfig::Direct;
279        assert!(matches!(direct, BootstrapConfig::Direct));
280
281        let vendor = BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
282            idp_login_url: Some("https://example.com".to_string()),
283            cookie_name: Some("Cookie".to_string()),
284            cookie_domain: Some("example.com".to_string()),
285            vault_url: Some("https://vault.example.com".to_string()),
286            cookie_value: None,
287        });
288        assert!(matches!(vendor, BootstrapConfig::SsoCookieVendor(_)));
289    }
290
291    #[test]
292    fn debug_output_redacts_cookie_value() {
293        use crate::AcquiredCookie;
294
295        // Test that cookie values are not exposed in Debug output
296        let config_with_cookie = SsoCookieVendorConfig {
297            idp_login_url: Some("https://example.com/login".to_string()),
298            cookie_name: Some("SessionCookie".to_string()),
299            cookie_domain: Some("example.com".to_string()),
300            vault_url: Some("https://vault.example.com".to_string()),
301            cookie_value: Some(vec![AcquiredCookie {
302                name: "SessionCookie".to_string(),
303                value: "super-secret-cookie-value-abc123".to_string(),
304            }]),
305        };
306
307        let debug_output = format!("{:?}", config_with_cookie);
308
309        // Should contain non-sensitive fields
310        assert!(debug_output.contains("SsoCookieVendorConfig"));
311        assert!(debug_output.contains("example.com/login"));
312        assert!(debug_output.contains("SessionCookie"));
313        assert!(debug_output.contains("example.com"));
314
315        // Should NOT contain the actual cookie value
316        assert!(!debug_output.contains("super-secret-cookie-value-abc123"));
317        // Should show redaction marker
318        assert!(debug_output.contains("[REDACTED]"));
319    }
320}