1use crate::build_errors::Error as BuilderError;
16use crate::constants::GOOGLE_CLOUD_QUOTA_PROJECT_VAR;
17use crate::errors::{self, CredentialsError};
18use crate::{BuildResult, Result};
19use http::{Extensions, HeaderMap};
20use serde_json::Value;
21use std::future::Future;
22use std::sync::Arc;
23use std::sync::atomic::{AtomicU64, Ordering};
24
25pub mod api_key_credentials;
26pub mod external_account;
27pub(crate) mod external_account_sources;
28pub mod impersonated;
29pub(crate) mod internal;
30pub mod mds;
31pub mod service_account;
32pub mod subject_token;
33pub mod user_account;
34pub(crate) const QUOTA_PROJECT_KEY: &str = "x-goog-user-project";
35pub(crate) const DEFAULT_UNIVERSE_DOMAIN: &str = "googleapis.com";
36
37#[derive(Clone, Debug, PartialEq, Default)]
47pub struct EntityTag(u64);
48
49static ENTITY_TAG_GENERATOR: AtomicU64 = AtomicU64::new(0);
50impl EntityTag {
51 pub fn new() -> Self {
52 let value = ENTITY_TAG_GENERATOR.fetch_add(1, Ordering::SeqCst);
53 Self(value)
54 }
55}
56
57#[derive(Clone, PartialEq, Debug)]
64pub enum CacheableResource<T> {
65 NotModified,
66 New { entity_tag: EntityTag, data: T },
67}
68
69#[derive(Clone, Debug)]
102pub struct Credentials {
103 inner: Arc<dyn dynamic::CredentialsProvider>,
112}
113
114impl<T> std::convert::From<T> for Credentials
115where
116 T: crate::credentials::CredentialsProvider + Send + Sync + 'static,
117{
118 fn from(value: T) -> Self {
119 Self {
120 inner: Arc::new(value),
121 }
122 }
123}
124
125impl Credentials {
126 pub async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
127 self.inner.headers(extensions).await
128 }
129
130 pub async fn universe_domain(&self) -> Option<String> {
131 self.inner.universe_domain().await
132 }
133}
134
135pub trait CredentialsProvider: std::fmt::Debug {
175 fn headers(
197 &self,
198 extensions: Extensions,
199 ) -> impl Future<Output = Result<CacheableResource<HeaderMap>>> + Send;
200
201 fn universe_domain(&self) -> impl Future<Output = Option<String>> + Send;
203}
204
205pub(crate) mod dynamic {
206 use super::Result;
207 use super::{CacheableResource, Extensions, HeaderMap};
208
209 #[async_trait::async_trait]
211 pub trait CredentialsProvider: Send + Sync + std::fmt::Debug {
212 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>>;
234
235 async fn universe_domain(&self) -> Option<String> {
237 Some("googleapis.com".to_string())
238 }
239 }
240
241 #[async_trait::async_trait]
243 impl<T> CredentialsProvider for T
244 where
245 T: super::CredentialsProvider + Send + Sync,
246 {
247 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
248 T::headers(self, extensions).await
249 }
250 async fn universe_domain(&self) -> Option<String> {
251 T::universe_domain(self).await
252 }
253 }
254}
255
256#[derive(Debug)]
311pub struct Builder {
312 quota_project_id: Option<String>,
313 scopes: Option<Vec<String>>,
314}
315
316impl Default for Builder {
317 fn default() -> Self {
329 Self {
330 quota_project_id: None,
331 scopes: None,
332 }
333 }
334}
335
336impl Builder {
337 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
359 self.quota_project_id = Some(quota_project_id.into());
360 self
361 }
362
363 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
383 where
384 I: IntoIterator<Item = S>,
385 S: Into<String>,
386 {
387 self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
388 self
389 }
390
391 pub fn build(self) -> BuildResult<Credentials> {
402 let json_data = match load_adc()? {
403 AdcContents::Contents(contents) => {
404 Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
405 }
406 AdcContents::FallbackToMds => None,
407 };
408 let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
409 .ok()
410 .or(self.quota_project_id);
411 build_credentials(json_data, quota_project_id, self.scopes)
412 }
413}
414
415#[derive(Debug, PartialEq)]
416enum AdcPath {
417 FromEnv(String),
418 WellKnown(String),
419}
420
421#[derive(Debug, PartialEq)]
422enum AdcContents {
423 Contents(String),
424 FallbackToMds,
425}
426
427fn extract_credential_type(json: &Value) -> BuildResult<&str> {
428 json.get("type")
429 .ok_or_else(|| BuilderError::parsing("no `type` field found."))?
430 .as_str()
431 .ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
432}
433
434macro_rules! config_builder {
442 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
443 let builder = $builder_instance;
444 let builder = $quota_project_id_option
445 .into_iter()
446 .fold(builder, |b, qp| b.with_quota_project_id(qp));
447
448 let builder = $scopes_option
449 .into_iter()
450 .fold(builder, |b, s| $apply_scopes_closure(b, s));
451
452 builder.build()
453 }};
454}
455
456fn build_credentials(
457 json: Option<Value>,
458 quota_project_id: Option<String>,
459 scopes: Option<Vec<String>>,
460) -> BuildResult<Credentials> {
461 match json {
462 None => config_builder!(
463 mds::Builder::from_adc(),
464 quota_project_id,
465 scopes,
466 |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
467 ),
468 Some(json) => {
469 let cred_type = extract_credential_type(&json)?;
470 match cred_type {
471 "authorized_user" => {
472 config_builder!(
473 user_account::Builder::new(json),
474 quota_project_id,
475 scopes,
476 |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
477 )
478 }
479 "service_account" => config_builder!(
480 service_account::Builder::new(json),
481 quota_project_id,
482 scopes,
483 |b: service_account::Builder, s: Vec<String>| b
484 .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
485 ),
486 "impersonated_service_account" => {
487 config_builder!(
488 impersonated::Builder::new(json),
489 quota_project_id,
490 scopes,
491 |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
492 )
493 }
494 "external_account" => config_builder!(
495 external_account::Builder::new(json),
496 quota_project_id,
497 scopes,
498 |b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
499 ),
500 _ => Err(BuilderError::unknown_type(cred_type)),
501 }
502 }
503 }
504}
505
506fn path_not_found(path: String) -> BuilderError {
507 BuilderError::loading(format!(
508 "{path}. {}",
509 concat!(
510 "This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
511 "environment variable. Verify this environment variable points to ",
512 "a valid file."
513 )
514 ))
515}
516
517fn load_adc() -> BuildResult<AdcContents> {
518 match adc_path() {
519 None => Ok(AdcContents::FallbackToMds),
520 Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
521 Ok(contents) => Ok(AdcContents::Contents(contents)),
522 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
523 Err(e) => Err(BuilderError::loading(e)),
524 },
525 Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
526 Ok(contents) => Ok(AdcContents::Contents(contents)),
527 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
528 Err(e) => Err(BuilderError::loading(e)),
529 },
530 }
531}
532
533fn adc_path() -> Option<AdcPath> {
537 if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
538 return Some(AdcPath::FromEnv(path));
539 }
540 Some(AdcPath::WellKnown(adc_well_known_path()?))
541}
542
543#[cfg(target_os = "windows")]
547fn adc_well_known_path() -> Option<String> {
548 std::env::var("APPDATA")
549 .ok()
550 .map(|root| root + "/gcloud/application_default_credentials.json")
551}
552
553#[cfg(not(target_os = "windows"))]
557fn adc_well_known_path() -> Option<String> {
558 std::env::var("HOME")
559 .ok()
560 .map(|root| root + "/.config/gcloud/application_default_credentials.json")
561}
562
563#[cfg_attr(test, mutants::skip)]
574#[doc(hidden)]
575pub mod testing {
576 use super::{CacheableResource, EntityTag};
577 use crate::Result;
578 use crate::credentials::Credentials;
579 use crate::credentials::dynamic::CredentialsProvider;
580 use http::{Extensions, HeaderMap};
581 use std::sync::Arc;
582
583 pub fn test_credentials() -> Credentials {
587 Credentials {
588 inner: Arc::from(TestCredentials {}),
589 }
590 }
591
592 #[derive(Debug)]
593 struct TestCredentials;
594
595 #[async_trait::async_trait]
596 impl CredentialsProvider for TestCredentials {
597 async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
598 Ok(CacheableResource::New {
599 entity_tag: EntityTag::default(),
600 data: HeaderMap::new(),
601 })
602 }
603
604 async fn universe_domain(&self) -> Option<String> {
605 None
606 }
607 }
608
609 pub fn error_credentials(retryable: bool) -> Credentials {
613 Credentials {
614 inner: Arc::from(ErrorCredentials(retryable)),
615 }
616 }
617
618 #[derive(Debug, Default)]
619 struct ErrorCredentials(bool);
620
621 #[async_trait::async_trait]
622 impl CredentialsProvider for ErrorCredentials {
623 async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
624 Err(super::CredentialsError::from_msg(self.0, "test-only"))
625 }
626
627 async fn universe_domain(&self) -> Option<String> {
628 None
629 }
630 }
631}
632
633#[cfg(test)]
634pub(crate) mod tests {
635 use super::*;
636 use base64::Engine;
637 use gax::backoff_policy::BackoffPolicy;
638 use gax::retry_policy::RetryPolicy;
639 use gax::retry_result::RetryResult;
640 use gax::retry_throttler::RetryThrottler;
641 use mockall::mock;
642 use num_bigint_dig::BigUint;
643 use reqwest::header::AUTHORIZATION;
644 use rsa::RsaPrivateKey;
645 use rsa::pkcs8::{EncodePrivateKey, LineEnding};
646 use scoped_env::ScopedEnv;
647 use std::error::Error;
648 use std::sync::LazyLock;
649 use test_case::test_case;
650 use tokio::time::Duration;
651
652 pub(crate) fn find_source_error<'a, T: Error + 'static>(
653 error: &'a (dyn Error + 'static),
654 ) -> Option<&'a T> {
655 let mut source = error.source();
656 while let Some(err) = source {
657 if let Some(target_err) = err.downcast_ref::<T>() {
658 return Some(target_err);
659 }
660 source = err.source();
661 }
662 None
663 }
664
665 mock! {
666 #[derive(Debug)]
667 pub RetryPolicy {}
668 impl RetryPolicy for RetryPolicy {
669 fn on_error(
670 &self,
671 loop_start: std::time::Instant,
672 attempt_count: u32,
673 idempotent: bool,
674 error: gax::error::Error,
675 ) -> RetryResult;
676 }
677 }
678
679 mock! {
680 #[derive(Debug)]
681 pub BackoffPolicy {}
682 impl BackoffPolicy for BackoffPolicy {
683 fn on_failure(
684 &self,
685 loop_start: std::time::Instant,
686 attempt_count: u32,
687 ) -> std::time::Duration;
688 }
689 }
690
691 mockall::mock! {
692 #[derive(Debug)]
693 pub RetryThrottler {}
694 impl RetryThrottler for RetryThrottler {
695 fn throttle_retry_attempt(&self) -> bool;
696 fn on_retry_failure(&mut self, error: &RetryResult);
697 fn on_success(&mut self);
698 }
699 }
700
701 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
702
703 pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
704 let mut retry_policy = MockRetryPolicy::new();
705 retry_policy
706 .expect_on_error()
707 .returning(move |_, attempt_count, _, error| {
708 if attempt_count >= attempts as u32 {
709 return RetryResult::Exhausted(error);
710 }
711 let is_transient = error
712 .source()
713 .and_then(|e| e.downcast_ref::<CredentialsError>())
714 .is_some_and(|ce| ce.is_transient());
715 if is_transient {
716 RetryResult::Continue(error)
717 } else {
718 RetryResult::Permanent(error)
719 }
720 });
721 retry_policy
722 }
723
724 pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
725 let mut backoff_policy = MockBackoffPolicy::new();
726 backoff_policy
727 .expect_on_failure()
728 .return_const(Duration::from_secs(0));
729 backoff_policy
730 }
731
732 pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
733 let mut throttler = MockRetryThrottler::new();
734 throttler.expect_on_retry_failure().return_const(());
735 throttler
736 .expect_throttle_retry_attempt()
737 .return_const(false);
738 throttler.expect_on_success().return_const(());
739 throttler
740 }
741
742 pub(crate) fn get_headers_from_cache(
743 headers: CacheableResource<HeaderMap>,
744 ) -> Result<HeaderMap> {
745 match headers {
746 CacheableResource::New { data, .. } => Ok(data),
747 CacheableResource::NotModified => Err(CredentialsError::from_msg(
748 false,
749 "Expecting headers to be present",
750 )),
751 }
752 }
753
754 pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
755 match headers {
756 CacheableResource::New { data, .. } => data
757 .get(AUTHORIZATION)
758 .and_then(|token_value| token_value.to_str().ok())
759 .and_then(|s| s.split_whitespace().nth(1))
760 .map(|s| s.to_string()),
761 CacheableResource::NotModified => None,
762 }
763 }
764
765 pub(crate) fn get_token_type_from_headers(
766 headers: CacheableResource<HeaderMap>,
767 ) -> Option<String> {
768 match headers {
769 CacheableResource::New { data, .. } => data
770 .get(AUTHORIZATION)
771 .and_then(|token_value| token_value.to_str().ok())
772 .and_then(|s| s.split_whitespace().next())
773 .map(|s| s.to_string()),
774 CacheableResource::NotModified => None,
775 }
776 }
777
778 pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
779 let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
780 let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
781 let e_str: &str = "65537";
782
783 let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
784 let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
785 let public_exponent =
786 BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
787
788 RsaPrivateKey::from_primes(vec![p, q], public_exponent)
789 .expect("Failed to create RsaPrivateKey from primes")
790 });
791
792 pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
793 RSA_PRIVATE_KEY
794 .to_pkcs8_pem(LineEnding::LF)
795 .expect("Failed to encode key to PKCS#8 PEM")
796 .to_string()
797 });
798
799 pub fn b64_decode_to_json(s: String) -> serde_json::Value {
800 let decoded = String::from_utf8(
801 base64::engine::general_purpose::URL_SAFE_NO_PAD
802 .decode(s)
803 .unwrap(),
804 )
805 .unwrap();
806 serde_json::from_str(&decoded).unwrap()
807 }
808
809 #[cfg(target_os = "windows")]
810 #[test]
811 #[serial_test::serial]
812 fn adc_well_known_path_windows() {
813 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
814 let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
815 assert_eq!(
816 adc_well_known_path(),
817 Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
818 );
819 assert_eq!(
820 adc_path(),
821 Some(AdcPath::WellKnown(
822 "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
823 ))
824 );
825 }
826
827 #[cfg(target_os = "windows")]
828 #[test]
829 #[serial_test::serial]
830 fn adc_well_known_path_windows_no_appdata() {
831 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
832 let _appdata = ScopedEnv::remove("APPDATA");
833 assert_eq!(adc_well_known_path(), None);
834 assert_eq!(adc_path(), None);
835 }
836
837 #[cfg(not(target_os = "windows"))]
838 #[test]
839 #[serial_test::serial]
840 fn adc_well_known_path_posix() {
841 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
842 let _home = ScopedEnv::set("HOME", "/home/foo");
843 assert_eq!(
844 adc_well_known_path(),
845 Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
846 );
847 assert_eq!(
848 adc_path(),
849 Some(AdcPath::WellKnown(
850 "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
851 ))
852 );
853 }
854
855 #[cfg(not(target_os = "windows"))]
856 #[test]
857 #[serial_test::serial]
858 fn adc_well_known_path_posix_no_home() {
859 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
860 let _appdata = ScopedEnv::remove("HOME");
861 assert_eq!(adc_well_known_path(), None);
862 assert_eq!(adc_path(), None);
863 }
864
865 #[test]
866 #[serial_test::serial]
867 fn adc_path_from_env() {
868 let _creds = ScopedEnv::set(
869 "GOOGLE_APPLICATION_CREDENTIALS",
870 "/usr/bar/application_default_credentials.json",
871 );
872 assert_eq!(
873 adc_path(),
874 Some(AdcPath::FromEnv(
875 "/usr/bar/application_default_credentials.json".to_string()
876 ))
877 );
878 }
879
880 #[test]
881 #[serial_test::serial]
882 fn load_adc_no_well_known_path_fallback_to_mds() {
883 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
884 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
887 }
888
889 #[test]
890 #[serial_test::serial]
891 fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
892 let dir = tempfile::TempDir::new().unwrap();
894 let path = dir.path().to_str().unwrap();
895 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
896 let _e2 = ScopedEnv::set("HOME", path); let _e3 = ScopedEnv::set("APPDATA", path); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
899 }
900
901 #[test]
902 #[serial_test::serial]
903 fn load_adc_no_file_at_env_is_error() {
904 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
905 let err = load_adc().unwrap_err();
906 assert!(err.is_loading(), "{err:?}");
907 let msg = format!("{err:?}");
908 assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
909 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
910 }
911
912 #[test]
913 #[serial_test::serial]
914 fn load_adc_success() {
915 let file = tempfile::NamedTempFile::new().unwrap();
916 let path = file.into_temp_path();
917 std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
918 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
919
920 assert_eq!(
921 load_adc().unwrap(),
922 AdcContents::Contents("contents".to_string())
923 );
924 }
925
926 #[test_case(true; "retryable")]
927 #[test_case(false; "non-retryable")]
928 #[tokio::test]
929 async fn error_credentials(retryable: bool) {
930 let credentials = super::testing::error_credentials(retryable);
931 assert!(
932 credentials.universe_domain().await.is_none(),
933 "{credentials:?}"
934 );
935 let err = credentials.headers(Extensions::new()).await.err().unwrap();
936 assert_eq!(err.is_transient(), retryable, "{err:?}");
937 let err = credentials.headers(Extensions::new()).await.err().unwrap();
938 assert_eq!(err.is_transient(), retryable, "{err:?}");
939 }
940
941 #[tokio::test]
942 #[serial_test::serial]
943 async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
944 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
945 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
948
949 let mds = Builder::default()
950 .with_quota_project_id("test-quota-project")
951 .build()
952 .unwrap();
953 let fmt = format!("{mds:?}");
954 assert!(fmt.contains("MDSCredentials"));
955 assert!(
956 fmt.contains("env-quota-project"),
957 "Expected 'env-quota-project', got: {fmt}"
958 );
959 }
960
961 #[tokio::test]
962 #[serial_test::serial]
963 async fn create_access_token_credentials_with_quota_project_from_builder() {
964 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
965 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
968
969 let creds = Builder::default()
970 .with_quota_project_id("test-quota-project")
971 .build()
972 .unwrap();
973 let fmt = format!("{creds:?}");
974 assert!(
975 fmt.contains("test-quota-project"),
976 "Expected 'test-quota-project', got: {fmt}"
977 );
978 }
979
980 #[tokio::test]
981 #[serial_test::serial]
982 async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
983 let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
984 let mut service_account_key = serde_json::json!({
985 "type": "service_account",
986 "project_id": "test-project-id",
987 "private_key_id": "test-private-key-id",
988 "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
989 "client_email": "test-client-email",
990 "universe_domain": "test-universe-domain"
991 });
992
993 let scopes =
994 ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
995
996 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
997
998 let file = tempfile::NamedTempFile::new().unwrap();
999 let path = file.into_temp_path();
1000 std::fs::write(&path, service_account_key.to_string())
1001 .expect("Unable to write to temporary file.");
1002 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1003
1004 let sac = Builder::default()
1005 .with_quota_project_id("test-quota-project")
1006 .with_scopes(scopes)
1007 .build()
1008 .unwrap();
1009
1010 let headers = sac.headers(Extensions::new()).await?;
1011 let token = get_token_from_headers(headers).unwrap();
1012 let parts: Vec<_> = token.split('.').collect();
1013 assert_eq!(parts.len(), 3);
1014 let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1015
1016 let fmt = format!("{sac:?}");
1017 assert!(fmt.contains("ServiceAccountCredentials"));
1018 assert!(fmt.contains("test-quota-project"));
1019 assert_eq!(claims["scope"], scopes.join(" "));
1020
1021 Ok(())
1022 }
1023}