bitwarden_server_communication_config/
client.rs

1use crate::{BootstrapConfig, ServerCommunicationConfig, ServerCommunicationConfigRepository};
2
3/// Server communication configuration client
4pub struct ServerCommunicationConfigClient<R>
5where
6    R: ServerCommunicationConfigRepository,
7{
8    repository: R,
9}
10
11impl<R> ServerCommunicationConfigClient<R>
12where
13    R: ServerCommunicationConfigRepository,
14{
15    /// Creates a new server communication configuration client
16    ///
17    /// # Arguments
18    ///
19    /// * `repository` - Repository implementation for storing configuration
20    pub fn new(repository: R) -> Self {
21        Self { repository }
22    }
23
24    /// Retrieves the server communication configuration for a hostname
25    pub async fn get_config(
26        &self,
27        hostname: String,
28    ) -> Result<ServerCommunicationConfig, R::GetError> {
29        Ok(self
30            .repository
31            .get(hostname)
32            .await?
33            .unwrap_or(ServerCommunicationConfig {
34                bootstrap: BootstrapConfig::Direct,
35            }))
36    }
37
38    /// Determines if cookie bootstrapping is needed for this hostname
39    pub async fn needs_bootstrap(&self, hostname: String) -> bool {
40        if let Ok(Some(config)) = self.repository.get(hostname).await {
41            if let BootstrapConfig::SsoCookieVendor(vendor_config) = config.bootstrap {
42                return vendor_config.cookie_value.is_none();
43            }
44        }
45        false
46    }
47
48    /// Returns cookies to include in HTTP requests
49    pub async fn cookies(&self, hostname: String) -> Vec<(String, String)> {
50        if let Ok(Some(config)) = self.repository.get(hostname).await {
51            if let BootstrapConfig::SsoCookieVendor(vendor_config) = config.bootstrap {
52                if let Some(cookie_value) = vendor_config.cookie_value {
53                    return vec![(vendor_config.cookie_name, cookie_value)];
54                }
55            }
56        }
57        Vec::new()
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use std::collections::HashMap;
64
65    use tokio::sync::RwLock;
66
67    use super::*;
68    use crate::SsoCookieVendorConfig;
69
70    /// Mock in-memory repository for testing
71    #[derive(Default, Clone)]
72    struct MockRepository {
73        storage: std::sync::Arc<RwLock<HashMap<String, ServerCommunicationConfig>>>,
74    }
75
76    impl ServerCommunicationConfigRepository for MockRepository {
77        type GetError = ();
78        type SaveError = ();
79
80        async fn get(&self, hostname: String) -> Result<Option<ServerCommunicationConfig>, ()> {
81            Ok(self.storage.read().await.get(&hostname).cloned())
82        }
83
84        async fn save(
85            &self,
86            hostname: String,
87            config: ServerCommunicationConfig,
88        ) -> Result<(), ()> {
89            self.storage.write().await.insert(hostname, config);
90            Ok(())
91        }
92    }
93
94    #[tokio::test]
95    async fn get_config_returns_direct_when_not_found() {
96        let repo = MockRepository::default();
97        let client = ServerCommunicationConfigClient::new(repo);
98
99        let config = client
100            .get_config("vault.example.com".to_string())
101            .await
102            .unwrap();
103
104        assert!(matches!(config.bootstrap, BootstrapConfig::Direct));
105    }
106
107    #[tokio::test]
108    async fn get_config_returns_saved_config() {
109        let repo = MockRepository::default();
110        let config = ServerCommunicationConfig {
111            bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
112                idp_login_url: "https://example.com".to_string(),
113                cookie_name: "TestCookie".to_string(),
114                cookie_domain: "example.com".to_string(),
115                cookie_value: Some("value123".to_string()),
116            }),
117        };
118
119        repo.save("vault.example.com".to_string(), config.clone())
120            .await
121            .unwrap();
122
123        let client = ServerCommunicationConfigClient::new(repo.clone());
124        let retrieved = client
125            .get_config("vault.example.com".to_string())
126            .await
127            .unwrap();
128
129        assert!(matches!(
130            retrieved.bootstrap,
131            BootstrapConfig::SsoCookieVendor(_)
132        ));
133    }
134
135    #[tokio::test]
136    async fn needs_bootstrap_true_when_cookie_missing() {
137        let repo = MockRepository::default();
138        let config = ServerCommunicationConfig {
139            bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
140                idp_login_url: "https://example.com".to_string(),
141                cookie_name: "TestCookie".to_string(),
142                cookie_domain: "example.com".to_string(),
143                cookie_value: None,
144            }),
145        };
146
147        repo.save("vault.example.com".to_string(), config)
148            .await
149            .unwrap();
150
151        let client = ServerCommunicationConfigClient::new(repo.clone());
152        assert!(
153            client
154                .needs_bootstrap("vault.example.com".to_string())
155                .await
156        );
157    }
158
159    #[tokio::test]
160    async fn needs_bootstrap_false_when_cookie_present() {
161        let repo = MockRepository::default();
162        let config = ServerCommunicationConfig {
163            bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
164                idp_login_url: "https://example.com".to_string(),
165                cookie_name: "TestCookie".to_string(),
166                cookie_domain: "example.com".to_string(),
167                cookie_value: Some("value123".to_string()),
168            }),
169        };
170
171        repo.save("vault.example.com".to_string(), config)
172            .await
173            .unwrap();
174
175        let client = ServerCommunicationConfigClient::new(repo.clone());
176        assert!(
177            !client
178                .needs_bootstrap("vault.example.com".to_string())
179                .await
180        );
181    }
182
183    #[tokio::test]
184    async fn needs_bootstrap_false_for_direct() {
185        let repo = MockRepository::default();
186        let config = ServerCommunicationConfig {
187            bootstrap: BootstrapConfig::Direct,
188        };
189
190        repo.save("vault.example.com".to_string(), config)
191            .await
192            .unwrap();
193
194        let client = ServerCommunicationConfigClient::new(repo.clone());
195        assert!(
196            !client
197                .needs_bootstrap("vault.example.com".to_string())
198                .await
199        );
200    }
201
202    #[tokio::test]
203    async fn cookies_returns_empty_for_direct() {
204        let repo = MockRepository::default();
205        let config = ServerCommunicationConfig {
206            bootstrap: BootstrapConfig::Direct,
207        };
208
209        repo.save("vault.example.com".to_string(), config)
210            .await
211            .unwrap();
212
213        let client = ServerCommunicationConfigClient::new(repo.clone());
214        let cookies = client.cookies("vault.example.com".to_string()).await;
215
216        assert!(cookies.is_empty());
217    }
218
219    #[tokio::test]
220    async fn cookies_returns_empty_when_value_none() {
221        let repo = MockRepository::default();
222        let config = ServerCommunicationConfig {
223            bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
224                idp_login_url: "https://example.com".to_string(),
225                cookie_name: "TestCookie".to_string(),
226                cookie_domain: "example.com".to_string(),
227                cookie_value: None,
228            }),
229        };
230
231        repo.save("vault.example.com".to_string(), config)
232            .await
233            .unwrap();
234
235        let client = ServerCommunicationConfigClient::new(repo.clone());
236        let cookies = client.cookies("vault.example.com".to_string()).await;
237
238        assert!(cookies.is_empty());
239    }
240
241    #[tokio::test]
242    async fn cookies_returns_cookie_when_present() {
243        let repo = MockRepository::default();
244        let config = ServerCommunicationConfig {
245            bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
246                idp_login_url: "https://example.com".to_string(),
247                cookie_name: "AELBAuthSessionCookie".to_string(),
248                cookie_domain: "example.com".to_string(),
249                cookie_value: Some("eyJhbGciOiJFUzI1NiIsImtpZCI6Im...".to_string()),
250            }),
251        };
252
253        repo.save("vault.example.com".to_string(), config)
254            .await
255            .unwrap();
256
257        let client = ServerCommunicationConfigClient::new(repo.clone());
258        let cookies = client.cookies("vault.example.com".to_string()).await;
259
260        assert_eq!(cookies.len(), 1);
261        assert_eq!(cookies[0].0, "AELBAuthSessionCookie");
262        assert_eq!(cookies[0].1, "eyJhbGciOiJFUzI1NiIsImtpZCI6Im...");
263    }
264
265    #[tokio::test]
266    async fn cookies_returns_empty_when_no_config() {
267        let repo = MockRepository::default();
268        let client = ServerCommunicationConfigClient::new(repo);
269        let cookies = client.cookies("vault.example.com".to_string()).await;
270
271        assert!(cookies.is_empty());
272    }
273}