bitwarden_server_communication_config/
client.rs1use crate::{BootstrapConfig, ServerCommunicationConfig, ServerCommunicationConfigRepository};
2
3pub struct ServerCommunicationConfigClient<R>
5where
6 R: ServerCommunicationConfigRepository,
7{
8 repository: R,
9}
10
11impl<R> ServerCommunicationConfigClient<R>
12where
13 R: ServerCommunicationConfigRepository,
14{
15 pub fn new(repository: R) -> Self {
21 Self { repository }
22 }
23
24 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 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 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 #[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}