1use crate::{
2 AcquireCookieError, AcquiredCookie, BootstrapConfig, BootstrapConfigRequest,
3 ServerCommunicationConfig, ServerCommunicationConfigPlatformApi,
4 ServerCommunicationConfigRepository, SetCommunicationTypeRequest, SsoCookieVendorConfig,
5};
6
7pub struct ServerCommunicationConfigClient<R, P>
9where
10 R: ServerCommunicationConfigRepository,
11 P: ServerCommunicationConfigPlatformApi,
12{
13 repository: R,
14 platform_api: P,
15}
16
17impl<R, P> ServerCommunicationConfigClient<R, P>
18where
19 R: ServerCommunicationConfigRepository,
20 P: ServerCommunicationConfigPlatformApi,
21{
22 pub fn new(repository: R, platform_api: P) -> Self {
29 Self {
30 repository,
31 platform_api,
32 }
33 }
34
35 pub async fn get_config(
37 &self,
38 domain: String,
39 ) -> Result<ServerCommunicationConfig, R::GetError> {
40 Ok(self
41 .repository
42 .get(domain)
43 .await?
44 .unwrap_or(ServerCommunicationConfig {
45 bootstrap: BootstrapConfig::Direct,
46 }))
47 }
48
49 pub async fn needs_bootstrap(&self, domain: String) -> bool {
56 if let Ok(Some(config)) = self.repository.get(domain).await {
57 return matches!(config.bootstrap, BootstrapConfig::SsoCookieVendor(_));
58 }
59 false
60 }
61
62 #[deprecated(
67 note = "Use get_cookies() instead, which will acquire cookies if not present in the config"
68 )]
69 pub async fn cookies(&self, domain: String) -> Vec<(String, String)> {
70 if let Ok(Some(config)) = self.repository.get(domain).await
71 && let BootstrapConfig::SsoCookieVendor(vendor_config) = config.bootstrap
72 && let Some(acquired_cookies) = vendor_config.cookie_value
73 {
74 return acquired_cookies
75 .into_iter()
76 .map(|cookie| (cookie.name, cookie.value))
77 .collect();
78 }
79 Vec::new()
80 }
81
82 pub async fn get_cookies(
89 &self,
90 domain: String,
91 ) -> Result<Vec<AcquiredCookie>, AcquireCookieError> {
92 let config = self
93 .repository
94 .get(domain.clone())
95 .await
96 .map_err(|e| AcquireCookieError::RepositoryGetError(format!("{:?}", e)))?;
97
98 let Some(config) = config else {
99 return Ok(vec![]);
100 };
101
102 let BootstrapConfig::SsoCookieVendor(vendor_config) = config.bootstrap else {
103 return Ok(vec![]);
104 };
105
106 match vendor_config.cookie_value {
107 Some(ref cookies) => Ok(cookies.clone()),
108 None => Ok(self.acquire_cookie(&domain).await?),
109 }
110 }
111
112 #[deprecated(
130 note = "Use set_communication_type_v2() instead, which extracts the domain from the config"
131 )]
132 pub async fn set_communication_type(
133 &self,
134 domain: String,
135 request: SetCommunicationTypeRequest,
136 ) -> Result<(), R::SaveError> {
137 let existing_cookie_value = match &request.bootstrap {
138 BootstrapConfigRequest::SsoCookieVendor(_) => self
139 .repository
140 .get(domain.clone())
141 .await
142 .ok()
143 .flatten()
144 .and_then(|existing| match existing.bootstrap {
145 BootstrapConfig::SsoCookieVendor(v) => v.cookie_value,
146 _ => None,
147 }),
148 BootstrapConfigRequest::Direct => None,
149 };
150
151 let config = ServerCommunicationConfig {
152 bootstrap: match request.bootstrap {
153 BootstrapConfigRequest::Direct => BootstrapConfig::Direct,
154 BootstrapConfigRequest::SsoCookieVendor(v) => {
155 BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
156 idp_login_url: v.idp_login_url,
157 cookie_name: v.cookie_name,
158 cookie_domain: v.cookie_domain,
159 vault_url: v.vault_url,
160 cookie_value: existing_cookie_value,
161 })
162 }
163 },
164 };
165
166 self.repository.save(domain, config).await
167 }
168
169 pub async fn set_communication_type_v2(
184 &self,
185 request: SetCommunicationTypeRequest,
186 ) -> Result<(), R::SaveError> {
187 let BootstrapConfigRequest::SsoCookieVendor(v) = request.bootstrap else {
188 return Ok(());
189 };
190
191 let domain = v.cookie_domain.clone();
192
193 let existing_cookie_value = self
194 .repository
195 .get(domain.clone())
196 .await
197 .ok()
198 .flatten()
199 .and_then(|existing| match existing.bootstrap {
200 BootstrapConfig::SsoCookieVendor(v) => v.cookie_value,
201 _ => None,
202 });
203
204 let config = ServerCommunicationConfig {
205 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
206 idp_login_url: v.idp_login_url,
207 cookie_name: v.cookie_name,
208 cookie_domain: v.cookie_domain,
209 vault_url: v.vault_url,
210 cookie_value: existing_cookie_value,
211 }),
212 };
213
214 self.repository.save(domain, config).await
215 }
216
217 pub async fn acquire_cookie(
237 &self,
238 domain: &str,
239 ) -> Result<Vec<AcquiredCookie>, AcquireCookieError> {
240 let mut config = self
242 .repository
243 .get(domain.to_string())
244 .await
245 .map_err(|e| AcquireCookieError::RepositoryGetError(format!("{:?}", e)))?
246 .ok_or(AcquireCookieError::UnsupportedConfiguration)?;
247
248 let BootstrapConfig::SsoCookieVendor(ref mut vendor_config) = config.bootstrap else {
250 return Err(AcquireCookieError::UnsupportedConfiguration);
251 };
252
253 let expected_cookie_name = &vendor_config.cookie_name;
254
255 if vendor_config.vault_url.is_empty() {
257 return Err(AcquireCookieError::UnsupportedConfiguration);
258 }
259 let vault_url = vendor_config.vault_url.clone();
260
261 let cookies = self
263 .platform_api
264 .acquire_cookies(vault_url)
265 .await
266 .ok_or(AcquireCookieError::Cancelled)?;
267
268 let all_cookies_match = cookies.iter().all(|cookie| {
276 cookie.name == *expected_cookie_name
277 || cookie
278 .name
279 .strip_prefix(&format!("{}-", expected_cookie_name))
280 .is_some_and(|suffix| suffix.chars().all(|c| c.is_ascii_digit()))
281 });
282
283 if !all_cookies_match {
284 let mismatched = cookies
286 .iter()
287 .find(|cookie| {
288 cookie.name != *expected_cookie_name
289 && !cookie
290 .name
291 .strip_prefix(&format!("{}-", expected_cookie_name))
292 .is_some_and(|suffix| suffix.chars().all(|c| c.is_ascii_digit()))
293 })
294 .expect("all_cookies_match is false, so at least one cookie must not match");
295
296 return Err(AcquireCookieError::CookieNameMismatch {
297 expected: expected_cookie_name.clone(),
298 actual: mismatched.name.clone(),
299 });
300 }
301
302 vendor_config.cookie_value = Some(cookies.clone());
304
305 self.repository
307 .save(domain.to_string(), config)
308 .await
309 .map_err(|e| AcquireCookieError::RepositorySaveError(format!("{:?}", e)))?;
310
311 Ok(cookies)
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use std::collections::HashMap;
318
319 use tokio::sync::RwLock;
320
321 use super::*;
322 use crate::{SsoCookieVendorConfig, SsoCookieVendorConfigRequest};
323
324 #[derive(Default, Clone)]
326 struct MockRepository {
327 storage: std::sync::Arc<RwLock<HashMap<String, ServerCommunicationConfig>>>,
328 }
329
330 impl ServerCommunicationConfigRepository for MockRepository {
331 type GetError = ();
332 type SaveError = ();
333
334 async fn get(&self, domain: String) -> Result<Option<ServerCommunicationConfig>, ()> {
335 Ok(self.storage.read().await.get(&domain).cloned())
336 }
337
338 async fn save(&self, domain: String, config: ServerCommunicationConfig) -> Result<(), ()> {
339 self.storage.write().await.insert(domain, config);
340 Ok(())
341 }
342 }
343
344 #[derive(Clone)]
346 struct MockPlatformApi {
347 cookies_to_return: std::sync::Arc<RwLock<Option<Vec<AcquiredCookie>>>>,
348 }
349
350 impl MockPlatformApi {
351 fn new() -> Self {
352 Self {
353 cookies_to_return: std::sync::Arc::new(RwLock::new(None)),
354 }
355 }
356
357 async fn set_cookies(&self, cookies: Option<Vec<AcquiredCookie>>) {
358 *self.cookies_to_return.write().await = cookies;
359 }
360 }
361
362 #[async_trait::async_trait]
363 impl ServerCommunicationConfigPlatformApi for MockPlatformApi {
364 async fn acquire_cookies(&self, _vault_url: String) -> Option<Vec<AcquiredCookie>> {
365 self.cookies_to_return.read().await.clone()
366 }
367 }
368
369 #[tokio::test]
370 async fn get_config_returns_direct_when_not_found() {
371 let repo = MockRepository::default();
372 let platform_api = MockPlatformApi::new();
373 let client = ServerCommunicationConfigClient::new(repo, platform_api);
374
375 let config = client
376 .get_config("vault.example.com".to_string())
377 .await
378 .unwrap();
379
380 assert!(matches!(config.bootstrap, BootstrapConfig::Direct));
381 }
382
383 #[tokio::test]
384 async fn get_config_returns_saved_config() {
385 let repo = MockRepository::default();
386 let config = ServerCommunicationConfig {
387 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
388 idp_login_url: Some("https://example.com".to_string()),
389 cookie_name: "TestCookie".to_string(),
390 cookie_domain: "example.com".to_string(),
391 vault_url: "https://vault.example.com".to_string(),
392 cookie_value: Some(vec![AcquiredCookie {
393 name: "TestCookie".to_string(),
394 value: "value123".to_string(),
395 }]),
396 }),
397 };
398
399 repo.save("vault.example.com".to_string(), config.clone())
400 .await
401 .unwrap();
402
403 let platform_api = MockPlatformApi::new();
404 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
405 let retrieved = client
406 .get_config("vault.example.com".to_string())
407 .await
408 .unwrap();
409
410 assert!(matches!(
411 retrieved.bootstrap,
412 BootstrapConfig::SsoCookieVendor(_)
413 ));
414 }
415
416 #[tokio::test]
417 async fn needs_bootstrap_true_for_sso_cookie_vendor() {
418 let repo = MockRepository::default();
419 let config = ServerCommunicationConfig {
420 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
421 idp_login_url: Some("https://example.com".to_string()),
422 cookie_name: "TestCookie".to_string(),
423 cookie_domain: "example.com".to_string(),
424 vault_url: "https://vault.example.com".to_string(),
425 cookie_value: None,
426 }),
427 };
428
429 repo.save("vault.example.com".to_string(), config)
430 .await
431 .unwrap();
432
433 let platform_api = MockPlatformApi::new();
434 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
435 assert!(
436 client
437 .needs_bootstrap("vault.example.com".to_string())
438 .await
439 );
440 }
441
442 #[tokio::test]
443 async fn needs_bootstrap_true_when_cookie_already_present() {
444 let repo = MockRepository::default();
445 let config = ServerCommunicationConfig {
446 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
447 idp_login_url: Some("https://example.com".to_string()),
448 cookie_name: "TestCookie".to_string(),
449 cookie_domain: "example.com".to_string(),
450 vault_url: "https://vault.example.com".to_string(),
451 cookie_value: Some(vec![AcquiredCookie {
452 name: "TestCookie".to_string(),
453 value: "value123".to_string(),
454 }]),
455 }),
456 };
457
458 repo.save("vault.example.com".to_string(), config)
459 .await
460 .unwrap();
461
462 let platform_api = MockPlatformApi::new();
463 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
464 assert!(
465 client
466 .needs_bootstrap("vault.example.com".to_string())
467 .await
468 );
469 }
470
471 #[tokio::test]
472 async fn needs_bootstrap_false_for_direct() {
473 let repo = MockRepository::default();
474 let config = ServerCommunicationConfig {
475 bootstrap: BootstrapConfig::Direct,
476 };
477
478 repo.save("vault.example.com".to_string(), config)
479 .await
480 .unwrap();
481
482 let platform_api = MockPlatformApi::new();
483 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
484 assert!(
485 !client
486 .needs_bootstrap("vault.example.com".to_string())
487 .await
488 );
489 }
490
491 #[tokio::test]
492 #[allow(deprecated)]
493 async fn cookies_returns_empty_for_direct() {
494 let repo = MockRepository::default();
495 let config = ServerCommunicationConfig {
496 bootstrap: BootstrapConfig::Direct,
497 };
498
499 repo.save("vault.example.com".to_string(), config)
500 .await
501 .unwrap();
502
503 let platform_api = MockPlatformApi::new();
504 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
505 let cookies = client.cookies("vault.example.com".to_string()).await;
506
507 assert!(cookies.is_empty());
508 }
509
510 #[tokio::test]
511 #[allow(deprecated)]
512 async fn cookies_returns_empty_when_value_none() {
513 let repo = MockRepository::default();
514 let config = ServerCommunicationConfig {
515 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
516 idp_login_url: Some("https://example.com".to_string()),
517 cookie_name: "TestCookie".to_string(),
518 cookie_domain: "example.com".to_string(),
519 vault_url: "https://vault.example.com".to_string(),
520 cookie_value: None,
521 }),
522 };
523
524 repo.save("vault.example.com".to_string(), config)
525 .await
526 .unwrap();
527
528 let platform_api = MockPlatformApi::new();
529 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
530 let cookies = client.cookies("vault.example.com".to_string()).await;
531
532 assert!(cookies.is_empty());
533 }
534
535 #[tokio::test]
536 #[allow(deprecated)]
537 async fn cookies_returns_unsharded_cookie_without_suffix() {
538 let repo = MockRepository::default();
539 let config = ServerCommunicationConfig {
540 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
541 idp_login_url: Some("https://example.com".to_string()),
542 cookie_name: "AWSELBAuthSessionCookie".to_string(),
543 cookie_domain: "example.com".to_string(),
544 vault_url: "https://vault.example.com".to_string(),
545 cookie_value: Some(vec![AcquiredCookie {
546 name: "AWSELBAuthSessionCookie".to_string(),
547 value: "eyJhbGciOiJFUzI1NiIsImtpZCI6Im...".to_string(),
548 }]),
549 }),
550 };
551
552 repo.save("vault.example.com".to_string(), config)
553 .await
554 .unwrap();
555
556 let platform_api = MockPlatformApi::new();
557 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
558 let cookies = client.cookies("vault.example.com".to_string()).await;
559
560 assert_eq!(cookies.len(), 1);
562 assert_eq!(cookies[0].0, "AWSELBAuthSessionCookie");
563 assert_eq!(cookies[0].1, "eyJhbGciOiJFUzI1NiIsImtpZCI6Im...");
564 }
565
566 #[tokio::test]
567 #[allow(deprecated)]
568 async fn cookies_returns_empty_when_no_config() {
569 let repo = MockRepository::default();
570 let platform_api = MockPlatformApi::new();
571 let client = ServerCommunicationConfigClient::new(repo, platform_api);
572 let cookies = client.cookies("vault.example.com".to_string()).await;
573
574 assert!(cookies.is_empty());
575 }
576
577 #[tokio::test]
578 #[allow(deprecated)]
579 async fn cookies_returns_shards_with_numbered_suffixes() {
580 let repo = MockRepository::default();
581 let config = ServerCommunicationConfig {
582 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
583 idp_login_url: Some("https://example.com".to_string()),
584 cookie_name: "AWSELBAuthSessionCookie".to_string(),
585 cookie_domain: "example.com".to_string(),
586 vault_url: "https://vault.example.com".to_string(),
587 cookie_value: Some(vec![
588 AcquiredCookie {
589 name: "AWSELBAuthSessionCookie-0".to_string(),
590 value: "shard0value".to_string(),
591 },
592 AcquiredCookie {
593 name: "AWSELBAuthSessionCookie-1".to_string(),
594 value: "shard1value".to_string(),
595 },
596 AcquiredCookie {
597 name: "AWSELBAuthSessionCookie-2".to_string(),
598 value: "shard2value".to_string(),
599 },
600 ]),
601 }),
602 };
603
604 repo.save("vault.example.com".to_string(), config)
605 .await
606 .unwrap();
607
608 let platform_api = MockPlatformApi::new();
609 let client = ServerCommunicationConfigClient::new(repo, platform_api);
610 let cookies = client.cookies("vault.example.com".to_string()).await;
611
612 assert_eq!(cookies.len(), 3);
614 assert_eq!(
615 cookies[0],
616 (
617 "AWSELBAuthSessionCookie-0".to_string(),
618 "shard0value".to_string()
619 )
620 );
621 assert_eq!(
622 cookies[1],
623 (
624 "AWSELBAuthSessionCookie-1".to_string(),
625 "shard1value".to_string()
626 )
627 );
628 assert_eq!(
629 cookies[2],
630 (
631 "AWSELBAuthSessionCookie-2".to_string(),
632 "shard2value".to_string()
633 )
634 );
635 }
636
637 #[tokio::test]
638 async fn acquire_cookie_saves_when_cookie_returned() {
639 let repo = MockRepository::default();
640 let platform_api = MockPlatformApi::new();
641
642 let config = ServerCommunicationConfig {
644 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
645 idp_login_url: Some("https://example.com".to_string()),
646 cookie_name: "TestCookie".to_string(),
647 cookie_domain: "example.com".to_string(),
648 vault_url: "https://vault.example.com".to_string(),
649 cookie_value: None,
650 }),
651 };
652 repo.save("vault.example.com".to_string(), config)
653 .await
654 .unwrap();
655
656 platform_api
658 .set_cookies(Some(vec![AcquiredCookie {
659 name: "TestCookie".to_string(),
660 value: "acquired-cookie-value".to_string(),
661 }]))
662 .await;
663
664 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
665
666 client.acquire_cookie("vault.example.com").await.unwrap();
668
669 let saved_config = repo
671 .get("vault.example.com".to_string())
672 .await
673 .unwrap()
674 .unwrap();
675
676 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
677 assert_eq!(vendor_config.cookie_value.as_ref().unwrap().len(), 1);
678 assert_eq!(
679 vendor_config.cookie_value.as_ref().unwrap()[0].name,
680 "TestCookie"
681 );
682 assert_eq!(
683 vendor_config.cookie_value.as_ref().unwrap()[0].value,
684 "acquired-cookie-value"
685 );
686 } else {
687 panic!("Expected SsoCookieVendor config");
688 }
689 }
690
691 #[tokio::test]
692 async fn acquire_cookie_returns_cancelled_when_none() {
693 let repo = MockRepository::default();
694 let platform_api = MockPlatformApi::new();
695
696 let config = ServerCommunicationConfig {
698 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
699 idp_login_url: Some("https://example.com".to_string()),
700 cookie_name: "TestCookie".to_string(),
701 cookie_domain: "example.com".to_string(),
702 vault_url: "https://vault.example.com".to_string(),
703 cookie_value: None,
704 }),
705 };
706 repo.save("vault.example.com".to_string(), config)
707 .await
708 .unwrap();
709
710 platform_api.set_cookies(None).await;
712
713 let client = ServerCommunicationConfigClient::new(repo, platform_api);
714
715 let result = client.acquire_cookie("vault.example.com").await;
716
717 assert!(matches!(result, Err(AcquireCookieError::Cancelled)));
718 }
719
720 #[tokio::test]
721 async fn acquire_cookie_returns_unsupported_for_direct_config() {
722 let repo = MockRepository::default();
723 let platform_api = MockPlatformApi::new();
724
725 let config = ServerCommunicationConfig {
727 bootstrap: BootstrapConfig::Direct,
728 };
729 repo.save("vault.example.com".to_string(), config)
730 .await
731 .unwrap();
732
733 platform_api
735 .set_cookies(Some(vec![AcquiredCookie {
736 name: "TestCookie".to_string(),
737 value: "cookie-value".to_string(),
738 }]))
739 .await;
740
741 let client = ServerCommunicationConfigClient::new(repo, platform_api);
742
743 let result = client.acquire_cookie("vault.example.com").await;
744
745 assert!(matches!(
747 result,
748 Err(AcquireCookieError::UnsupportedConfiguration)
749 ));
750 }
751
752 #[tokio::test]
753 async fn acquire_cookie_validates_cookie_name() {
754 let repo = MockRepository::default();
755 let platform_api = MockPlatformApi::new();
756
757 let config = ServerCommunicationConfig {
759 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
760 idp_login_url: Some("https://example.com".to_string()),
761 cookie_name: "ExpectedCookie".to_string(),
762 cookie_domain: "example.com".to_string(),
763 vault_url: "https://vault.example.com".to_string(),
764 cookie_value: None,
765 }),
766 };
767 repo.save("vault.example.com".to_string(), config)
768 .await
769 .unwrap();
770
771 platform_api
773 .set_cookies(Some(vec![AcquiredCookie {
774 name: "WrongCookie".to_string(),
775 value: "some-value".to_string(),
776 }]))
777 .await;
778
779 let client = ServerCommunicationConfigClient::new(repo, platform_api);
780
781 let result = client.acquire_cookie("vault.example.com").await;
782
783 match result {
785 Err(AcquireCookieError::CookieNameMismatch { expected, actual }) => {
786 assert_eq!(expected, "ExpectedCookie");
787 assert_eq!(actual, "WrongCookie");
788 }
789 _ => panic!("Expected CookieNameMismatch error"),
790 }
791 }
792
793 #[tokio::test]
794 async fn acquire_cookie_returns_unsupported_when_no_config() {
795 let repo = MockRepository::default();
796 let platform_api = MockPlatformApi::new();
797
798 let client = ServerCommunicationConfigClient::new(repo, platform_api);
801
802 let result = client.acquire_cookie("vault.example.com").await;
803
804 assert!(matches!(
806 result,
807 Err(AcquireCookieError::UnsupportedConfiguration)
808 ));
809 }
810
811 #[tokio::test]
812 async fn acquire_cookie_accepts_sharded_cookies_with_numbered_suffixes() {
813 let repo = MockRepository::default();
814 let platform_api = MockPlatformApi::new();
815
816 let config = ServerCommunicationConfig {
818 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
819 idp_login_url: Some("https://example.com".to_string()),
820 cookie_name: "AWSELBAuthSessionCookie".to_string(),
821 cookie_domain: "example.com".to_string(),
822 vault_url: "https://vault.example.com".to_string(),
823 cookie_value: None,
824 }),
825 };
826 repo.save("vault.example.com".to_string(), config)
827 .await
828 .unwrap();
829
830 platform_api
832 .set_cookies(Some(vec![
833 AcquiredCookie {
834 name: "AWSELBAuthSessionCookie-0".to_string(),
835 value: "shard0value".to_string(),
836 },
837 AcquiredCookie {
838 name: "AWSELBAuthSessionCookie-1".to_string(),
839 value: "shard1value".to_string(),
840 },
841 AcquiredCookie {
842 name: "AWSELBAuthSessionCookie-2".to_string(),
843 value: "shard2value".to_string(),
844 },
845 ]))
846 .await;
847
848 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
849
850 client.acquire_cookie("vault.example.com").await.unwrap();
852
853 let saved_config = repo
855 .get("vault.example.com".to_string())
856 .await
857 .unwrap()
858 .unwrap();
859
860 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
861 assert_eq!(vendor_config.cookie_value.as_ref().unwrap().len(), 3);
862 assert_eq!(
863 vendor_config.cookie_value.as_ref().unwrap()[0].name,
864 "AWSELBAuthSessionCookie-0"
865 );
866 assert_eq!(
867 vendor_config.cookie_value.as_ref().unwrap()[1].name,
868 "AWSELBAuthSessionCookie-1"
869 );
870 assert_eq!(
871 vendor_config.cookie_value.as_ref().unwrap()[2].name,
872 "AWSELBAuthSessionCookie-2"
873 );
874 } else {
875 panic!("Expected SsoCookieVendor config");
876 }
877 }
878
879 #[tokio::test]
880 async fn acquire_cookie_accepts_unsharded_cookie_without_suffix() {
881 let repo = MockRepository::default();
882 let platform_api = MockPlatformApi::new();
883
884 let config = ServerCommunicationConfig {
886 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
887 idp_login_url: Some("https://example.com".to_string()),
888 cookie_name: "SessionCookie".to_string(),
889 cookie_domain: "example.com".to_string(),
890 vault_url: "https://vault.example.com".to_string(),
891 cookie_value: None,
892 }),
893 };
894 repo.save("vault.example.com".to_string(), config)
895 .await
896 .unwrap();
897
898 platform_api
900 .set_cookies(Some(vec![AcquiredCookie {
901 name: "SessionCookie".to_string(),
902 value: "single-cookie-value".to_string(),
903 }]))
904 .await;
905
906 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
907
908 client.acquire_cookie("vault.example.com").await.unwrap();
910
911 let saved_config = repo
913 .get("vault.example.com".to_string())
914 .await
915 .unwrap()
916 .unwrap();
917
918 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
919 assert_eq!(vendor_config.cookie_value.as_ref().unwrap().len(), 1);
920 assert_eq!(
921 vendor_config.cookie_value.as_ref().unwrap()[0].name,
922 "SessionCookie"
923 );
924 assert_eq!(
925 vendor_config.cookie_value.as_ref().unwrap()[0].value,
926 "single-cookie-value"
927 );
928 } else {
929 panic!("Expected SsoCookieVendor config");
930 }
931 }
932
933 #[tokio::test]
934 async fn get_cookies_returns_empty_for_direct() {
935 let repo = MockRepository::default();
936 let config = ServerCommunicationConfig {
937 bootstrap: BootstrapConfig::Direct,
938 };
939
940 repo.save("vault.example.com".to_string(), config)
941 .await
942 .unwrap();
943
944 let platform_api = MockPlatformApi::new();
945 let client = ServerCommunicationConfigClient::new(repo, platform_api);
946 let cookies = client
947 .get_cookies("vault.example.com".to_string())
948 .await
949 .unwrap();
950
951 assert!(cookies.is_empty());
952 }
953
954 #[tokio::test]
955 async fn get_cookies_returns_empty_when_no_config() {
956 let repo = MockRepository::default();
957 let platform_api = MockPlatformApi::new();
958 let client = ServerCommunicationConfigClient::new(repo, platform_api);
959 let cookies = client
960 .get_cookies("vault.example.com".to_string())
961 .await
962 .unwrap();
963
964 assert!(cookies.is_empty());
965 }
966
967 #[tokio::test]
968 async fn get_cookies_returns_existing_cookies() {
969 let repo = MockRepository::default();
970 let config = ServerCommunicationConfig {
971 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
972 idp_login_url: Some("https://example.com".to_string()),
973 cookie_name: "TestCookie".to_string(),
974 cookie_domain: "example.com".to_string(),
975 vault_url: "https://vault.example.com".to_string(),
976 cookie_value: Some(vec![AcquiredCookie {
977 name: "TestCookie".to_string(),
978 value: "existing-value".to_string(),
979 }]),
980 }),
981 };
982
983 repo.save("vault.example.com".to_string(), config)
984 .await
985 .unwrap();
986
987 let platform_api = MockPlatformApi::new();
988 let client = ServerCommunicationConfigClient::new(repo, platform_api);
989 let cookies = client
990 .get_cookies("vault.example.com".to_string())
991 .await
992 .unwrap();
993
994 assert_eq!(cookies.len(), 1);
995 assert_eq!(cookies[0].name, "TestCookie");
996 assert_eq!(cookies[0].value, "existing-value");
997 }
998
999 #[tokio::test]
1000 async fn get_cookies_acquires_when_none_present() {
1001 let repo = MockRepository::default();
1002 let config = ServerCommunicationConfig {
1003 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
1004 idp_login_url: Some("https://example.com".to_string()),
1005 cookie_name: "TestCookie".to_string(),
1006 cookie_domain: "example.com".to_string(),
1007 vault_url: "https://vault.example.com".to_string(),
1008 cookie_value: None,
1009 }),
1010 };
1011
1012 repo.save("vault.example.com".to_string(), config)
1013 .await
1014 .unwrap();
1015
1016 let platform_api = MockPlatformApi::new();
1017 platform_api
1018 .set_cookies(Some(vec![AcquiredCookie {
1019 name: "TestCookie".to_string(),
1020 value: "acquired-value".to_string(),
1021 }]))
1022 .await;
1023
1024 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
1025 let cookies = client
1026 .get_cookies("vault.example.com".to_string())
1027 .await
1028 .unwrap();
1029
1030 assert_eq!(cookies.len(), 1);
1031 assert_eq!(cookies[0].name, "TestCookie");
1032 assert_eq!(cookies[0].value, "acquired-value");
1033
1034 let saved_config = repo
1036 .get("vault.example.com".to_string())
1037 .await
1038 .unwrap()
1039 .unwrap();
1040
1041 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
1042 assert_eq!(vendor_config.cookie_value.as_ref().unwrap().len(), 1);
1043 assert_eq!(
1044 vendor_config.cookie_value.as_ref().unwrap()[0].value,
1045 "acquired-value"
1046 );
1047 } else {
1048 panic!("Expected SsoCookieVendor config");
1049 }
1050 }
1051
1052 #[tokio::test]
1053 async fn get_cookies_returns_cancelled_when_acquisition_fails() {
1054 let repo = MockRepository::default();
1055 let config = ServerCommunicationConfig {
1056 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
1057 idp_login_url: Some("https://example.com".to_string()),
1058 cookie_name: "TestCookie".to_string(),
1059 cookie_domain: "example.com".to_string(),
1060 vault_url: "https://vault.example.com".to_string(),
1061 cookie_value: None,
1062 }),
1063 };
1064
1065 repo.save("vault.example.com".to_string(), config)
1066 .await
1067 .unwrap();
1068
1069 let platform_api = MockPlatformApi::new();
1071 platform_api.set_cookies(None).await;
1072
1073 let client = ServerCommunicationConfigClient::new(repo, platform_api);
1074 let result = client.get_cookies("vault.example.com".to_string()).await;
1075
1076 assert!(matches!(result, Err(AcquireCookieError::Cancelled)));
1077 }
1078
1079 #[tokio::test]
1080 async fn get_cookies_returns_existing_sharded_cookies() {
1081 let repo = MockRepository::default();
1082 let config = ServerCommunicationConfig {
1083 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
1084 idp_login_url: Some("https://example.com".to_string()),
1085 cookie_name: "AWSELBAuthSessionCookie".to_string(),
1086 cookie_domain: "example.com".to_string(),
1087 vault_url: "https://vault.example.com".to_string(),
1088 cookie_value: Some(vec![
1089 AcquiredCookie {
1090 name: "AWSELBAuthSessionCookie-0".to_string(),
1091 value: "shard0value".to_string(),
1092 },
1093 AcquiredCookie {
1094 name: "AWSELBAuthSessionCookie-1".to_string(),
1095 value: "shard1value".to_string(),
1096 },
1097 ]),
1098 }),
1099 };
1100
1101 repo.save("vault.example.com".to_string(), config)
1102 .await
1103 .unwrap();
1104
1105 let platform_api = MockPlatformApi::new();
1106 let client = ServerCommunicationConfigClient::new(repo, platform_api);
1107 let cookies = client
1108 .get_cookies("vault.example.com".to_string())
1109 .await
1110 .unwrap();
1111
1112 assert_eq!(cookies.len(), 2);
1113 assert_eq!(cookies[0].name, "AWSELBAuthSessionCookie-0");
1114 assert_eq!(cookies[1].name, "AWSELBAuthSessionCookie-1");
1115 }
1116
1117 #[tokio::test]
1118 #[allow(deprecated)]
1119 async fn set_communication_type_saves_direct_config() {
1120 let repo = MockRepository::default();
1121 let platform_api = MockPlatformApi::new();
1122 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
1123
1124 let request = SetCommunicationTypeRequest {
1125 bootstrap: BootstrapConfigRequest::Direct,
1126 };
1127
1128 client
1129 .set_communication_type("vault.example.com".to_string(), request)
1130 .await
1131 .unwrap();
1132
1133 let saved_config = repo
1134 .get("vault.example.com".to_string())
1135 .await
1136 .unwrap()
1137 .unwrap();
1138
1139 assert!(matches!(saved_config.bootstrap, BootstrapConfig::Direct));
1140 }
1141
1142 #[tokio::test]
1143 #[allow(deprecated)]
1144 async fn set_communication_type_saves_sso_cookie_vendor_config() {
1145 let repo = MockRepository::default();
1146 let platform_api = MockPlatformApi::new();
1147 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
1148
1149 let request = SetCommunicationTypeRequest {
1150 bootstrap: BootstrapConfigRequest::SsoCookieVendor(SsoCookieVendorConfigRequest {
1151 idp_login_url: Some("https://idp.example.com/login".to_string()),
1152 cookie_name: "SessionCookie".to_string(),
1153 cookie_domain: "vault.example.com".to_string(),
1154 vault_url: "https://vault.example.com".to_string(),
1155 }),
1156 };
1157
1158 client
1159 .set_communication_type("vault.example.com".to_string(), request)
1160 .await
1161 .unwrap();
1162
1163 let saved_config = repo
1164 .get("vault.example.com".to_string())
1165 .await
1166 .unwrap()
1167 .unwrap();
1168
1169 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
1170 assert_eq!(
1171 vendor_config.idp_login_url,
1172 Some("https://idp.example.com/login".to_string())
1173 );
1174 assert_eq!(vendor_config.cookie_name, "SessionCookie");
1175 assert_eq!(vendor_config.cookie_domain, "vault.example.com");
1176 assert!(vendor_config.cookie_value.is_none());
1177 } else {
1178 panic!("Expected SsoCookieVendor config");
1179 }
1180 }
1181
1182 #[tokio::test]
1183 #[allow(deprecated)]
1184 async fn set_communication_type_overwrites_existing_config() {
1185 let repo = MockRepository::default();
1186 let platform_api = MockPlatformApi::new();
1187
1188 let old_config = ServerCommunicationConfig {
1190 bootstrap: BootstrapConfig::Direct,
1191 };
1192 repo.save("vault.example.com".to_string(), old_config)
1193 .await
1194 .unwrap();
1195
1196 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
1197
1198 let request = SetCommunicationTypeRequest {
1200 bootstrap: BootstrapConfigRequest::SsoCookieVendor(SsoCookieVendorConfigRequest {
1201 idp_login_url: Some("https://new-idp.example.com/login".to_string()),
1202 cookie_name: "NewCookie".to_string(),
1203 cookie_domain: "vault.example.com".to_string(),
1204 vault_url: "https://vault.example.com".to_string(),
1205 }),
1206 };
1207
1208 client
1209 .set_communication_type("vault.example.com".to_string(), request)
1210 .await
1211 .unwrap();
1212
1213 let saved_config = repo
1214 .get("vault.example.com".to_string())
1215 .await
1216 .unwrap()
1217 .unwrap();
1218
1219 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
1220 assert_eq!(
1221 vendor_config.idp_login_url,
1222 Some("https://new-idp.example.com/login".to_string())
1223 );
1224 assert_eq!(vendor_config.cookie_name, "NewCookie");
1225 } else {
1226 panic!("Expected SsoCookieVendor config");
1227 }
1228 }
1229
1230 #[tokio::test]
1231 #[allow(deprecated)]
1232 async fn set_communication_type_preserves_per_domain_isolation() {
1233 let repo = MockRepository::default();
1234 let platform_api = MockPlatformApi::new();
1235 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
1236
1237 let request1 = SetCommunicationTypeRequest {
1238 bootstrap: BootstrapConfigRequest::Direct,
1239 };
1240 client
1241 .set_communication_type("vault1.example.com".to_string(), request1)
1242 .await
1243 .unwrap();
1244
1245 let request2 = SetCommunicationTypeRequest {
1246 bootstrap: BootstrapConfigRequest::SsoCookieVendor(SsoCookieVendorConfigRequest {
1247 idp_login_url: Some("https://idp.example.com/login".to_string()),
1248 cookie_name: "TestCookie".to_string(),
1249 cookie_domain: "vault2.example.com".to_string(),
1250 vault_url: "https://vault2.example.com".to_string(),
1251 }),
1252 };
1253 client
1254 .set_communication_type("vault2.example.com".to_string(), request2)
1255 .await
1256 .unwrap();
1257
1258 let saved_config1 = repo
1259 .get("vault1.example.com".to_string())
1260 .await
1261 .unwrap()
1262 .unwrap();
1263 assert!(matches!(saved_config1.bootstrap, BootstrapConfig::Direct));
1264
1265 let saved_config2 = repo
1266 .get("vault2.example.com".to_string())
1267 .await
1268 .unwrap()
1269 .unwrap();
1270 assert!(matches!(
1271 saved_config2.bootstrap,
1272 BootstrapConfig::SsoCookieVendor(_)
1273 ));
1274 }
1275
1276 #[tokio::test]
1277 #[allow(deprecated)]
1278 async fn set_communication_type_preserves_existing_cookies() {
1279 let repo = MockRepository::default();
1280 let platform_api = MockPlatformApi::new();
1281
1282 let existing_config = ServerCommunicationConfig {
1284 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
1285 idp_login_url: Some("https://idp.example.com/login".to_string()),
1286 cookie_name: "SessionCookie".to_string(),
1287 cookie_domain: "example.com".to_string(),
1288 vault_url: "https://vault.example.com".to_string(),
1289 cookie_value: Some(vec![AcquiredCookie {
1290 name: "SessionCookie".to_string(),
1291 value: "previously-acquired-value".to_string(),
1292 }]),
1293 }),
1294 };
1295 repo.save("vault.example.com".to_string(), existing_config)
1296 .await
1297 .unwrap();
1298
1299 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
1300
1301 let request = SetCommunicationTypeRequest {
1303 bootstrap: BootstrapConfigRequest::SsoCookieVendor(SsoCookieVendorConfigRequest {
1304 idp_login_url: Some("https://idp.example.com/login".to_string()),
1305 cookie_name: "SessionCookie".to_string(),
1306 cookie_domain: "example.com".to_string(),
1307 vault_url: "https://vault.example.com".to_string(),
1308 }),
1309 };
1310
1311 client
1312 .set_communication_type("vault.example.com".to_string(), request)
1313 .await
1314 .unwrap();
1315
1316 let saved_config = repo
1318 .get("vault.example.com".to_string())
1319 .await
1320 .unwrap()
1321 .unwrap();
1322
1323 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
1324 let cookies = vendor_config
1325 .cookie_value
1326 .expect("cookies should be preserved");
1327 assert_eq!(cookies.len(), 1);
1328 assert_eq!(cookies[0].name, "SessionCookie");
1329 assert_eq!(cookies[0].value, "previously-acquired-value");
1330 } else {
1331 panic!("Expected SsoCookieVendor config");
1332 }
1333 }
1334
1335 #[tokio::test]
1336 async fn set_communication_type_v2_saves_using_cookie_domain() {
1337 let repo = MockRepository::default();
1338 let platform_api = MockPlatformApi::new();
1339 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
1340
1341 let request = SetCommunicationTypeRequest {
1342 bootstrap: BootstrapConfigRequest::SsoCookieVendor(SsoCookieVendorConfigRequest {
1343 idp_login_url: Some("https://idp.example.com/login".to_string()),
1344 cookie_name: "SessionCookie".to_string(),
1345 cookie_domain: "vault.example.com".to_string(),
1346 vault_url: "https://vault.example.com".to_string(),
1347 }),
1348 };
1349
1350 client.set_communication_type_v2(request).await.unwrap();
1351
1352 let saved_config = repo
1354 .get("vault.example.com".to_string())
1355 .await
1356 .unwrap()
1357 .unwrap();
1358
1359 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
1360 assert_eq!(
1361 vendor_config.idp_login_url,
1362 Some("https://idp.example.com/login".to_string())
1363 );
1364 assert_eq!(vendor_config.cookie_name, "SessionCookie");
1365 } else {
1366 panic!("Expected SsoCookieVendor config");
1367 }
1368 }
1369
1370 #[tokio::test]
1371 async fn acquire_cookie_returns_unsupported_when_vault_url_empty() {
1372 let repo = MockRepository::default();
1373 let platform_api = MockPlatformApi::new();
1374
1375 let config = ServerCommunicationConfig {
1377 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
1378 idp_login_url: Some("https://example.com".to_string()),
1379 cookie_name: "TestCookie".to_string(),
1380 cookie_domain: "example.com".to_string(),
1381 vault_url: "".to_string(), cookie_value: None,
1383 }),
1384 };
1385 repo.save("vault.example.com".to_string(), config)
1386 .await
1387 .unwrap();
1388
1389 platform_api
1391 .set_cookies(Some(vec![AcquiredCookie {
1392 name: "TestCookie".to_string(),
1393 value: "value".to_string(),
1394 }]))
1395 .await;
1396
1397 let client = ServerCommunicationConfigClient::new(repo, platform_api);
1398
1399 let result = client.acquire_cookie("vault.example.com").await;
1400
1401 assert!(matches!(
1403 result,
1404 Err(AcquireCookieError::UnsupportedConfiguration)
1405 ));
1406 }
1407
1408 #[tokio::test]
1409 async fn set_communication_type_v2_ignores_direct_config() {
1410 let repo = MockRepository::default();
1411 let platform_api = MockPlatformApi::new();
1412 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
1413
1414 let request = SetCommunicationTypeRequest {
1415 bootstrap: BootstrapConfigRequest::Direct,
1416 };
1417
1418 client.set_communication_type_v2(request).await.unwrap();
1419
1420 assert!(repo.storage.read().await.is_empty());
1422 }
1423
1424 #[tokio::test]
1425 async fn set_communication_type_v2_preserves_existing_cookies() {
1426 let repo = MockRepository::default();
1427 let platform_api = MockPlatformApi::new();
1428
1429 let existing_config = ServerCommunicationConfig {
1431 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
1432 idp_login_url: Some("https://idp.example.com/login".to_string()),
1433 cookie_name: "SessionCookie".to_string(),
1434 cookie_domain: "vault.example.com".to_string(),
1435 vault_url: "https://vault.example.com".to_string(),
1436 cookie_value: Some(vec![AcquiredCookie {
1437 name: "SessionCookie".to_string(),
1438 value: "previously-acquired-value".to_string(),
1439 }]),
1440 }),
1441 };
1442 repo.save("vault.example.com".to_string(), existing_config)
1443 .await
1444 .unwrap();
1445
1446 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
1447
1448 let new_request = SetCommunicationTypeRequest {
1450 bootstrap: BootstrapConfigRequest::SsoCookieVendor(SsoCookieVendorConfigRequest {
1451 idp_login_url: Some("https://idp.example.com/login".to_string()),
1452 cookie_name: "SessionCookie".to_string(),
1453 cookie_domain: "vault.example.com".to_string(),
1454 vault_url: "https://vault.example.com".to_string(),
1455 }),
1456 };
1457 client.set_communication_type_v2(new_request).await.unwrap();
1458
1459 let saved = repo
1461 .get("vault.example.com".to_string())
1462 .await
1463 .unwrap()
1464 .unwrap();
1465 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved.bootstrap {
1466 let cookies = vendor_config
1467 .cookie_value
1468 .expect("cookies should be preserved");
1469 assert_eq!(cookies.len(), 1);
1470 assert_eq!(cookies[0].value, "previously-acquired-value");
1471 } else {
1472 panic!("Expected SsoCookieVendor config");
1473 }
1474 }
1475}