1use super::client::GoogleCloudStorageClient;
19use crate::client::builder::HttpRequestBuilder;
20use crate::client::retry::RetryExt;
21use crate::client::token::TemporaryToken;
22use crate::client::{HttpClient, HttpError, TokenProvider};
23use crate::gcp::{GcpSigningCredentialProvider, STORE};
24use crate::util::{hex_digest, hex_encode, STRICT_ENCODE_SET};
25use crate::{RetryConfig, StaticCredentialProvider};
26use async_trait::async_trait;
27use base64::prelude::BASE64_URL_SAFE_NO_PAD;
28use base64::Engine;
29use chrono::{DateTime, Utc};
30use futures::TryFutureExt;
31use http::{HeaderMap, Method};
32use itertools::Itertools;
33use percent_encoding::utf8_percent_encode;
34use ring::signature::RsaKeyPair;
35use serde::Deserialize;
36use std::collections::BTreeMap;
37use std::env;
38use std::fs::File;
39use std::io::BufReader;
40use std::path::{Path, PathBuf};
41use std::sync::Arc;
42use std::time::{Duration, Instant};
43use tracing::info;
44use url::Url;
45
46pub(crate) const DEFAULT_SCOPE: &str = "https://www.googleapis.com/auth/cloud-platform";
47
48pub(crate) const DEFAULT_GCS_BASE_URL: &str = "https://storage.googleapis.com";
49
50const DEFAULT_GCS_PLAYLOAD_STRING: &str = "UNSIGNED-PAYLOAD";
51const DEFAULT_GCS_SIGN_BLOB_HOST: &str = "storage.googleapis.com";
52
53const DEFAULT_METADATA_HOST: &str = "metadata.google.internal";
54const DEFAULT_METADATA_IP: &str = "169.254.169.254";
55
56#[derive(Debug, thiserror::Error)]
57pub enum Error {
58 #[error("Unable to open service account file from {}: {}", path.display(), source)]
59 OpenCredentials {
60 source: std::io::Error,
61 path: PathBuf,
62 },
63
64 #[error("Unable to decode service account file: {}", source)]
65 DecodeCredentials { source: serde_json::Error },
66
67 #[error("No RSA key found in pem file")]
68 MissingKey,
69
70 #[error("Invalid RSA key: {}", source)]
71 InvalidKey {
72 #[from]
73 source: ring::error::KeyRejected,
74 },
75
76 #[error("Error signing: {}", source)]
77 Sign { source: ring::error::Unspecified },
78
79 #[error("Error encoding jwt payload: {}", source)]
80 Encode { source: serde_json::Error },
81
82 #[error("Unsupported key encoding: {}", encoding)]
83 UnsupportedKey { encoding: String },
84
85 #[error("Error performing token request: {}", source)]
86 TokenRequest {
87 source: crate::client::retry::RetryError,
88 },
89
90 #[error("Error getting token response body: {}", source)]
91 TokenResponseBody { source: HttpError },
92
93 #[error("Error reading pem file: {}", source)]
94 ReadPem { source: std::io::Error },
95}
96
97impl From<Error> for crate::Error {
98 fn from(value: Error) -> Self {
99 Self::Generic {
100 store: STORE,
101 source: Box::new(value),
102 }
103 }
104}
105
106#[derive(Debug)]
108pub struct GcpSigningCredential {
109 pub email: String,
111
112 pub private_key: Option<ServiceAccountKey>,
121}
122
123#[derive(Debug)]
125pub struct ServiceAccountKey(RsaKeyPair);
126
127impl ServiceAccountKey {
128 pub fn from_pem(encoded: &[u8]) -> Result<Self> {
130 use rustls_pemfile::Item;
131 use std::io::Cursor;
132
133 let mut cursor = Cursor::new(encoded);
134 let mut reader = BufReader::new(&mut cursor);
135
136 match rustls_pemfile::read_one(&mut reader) {
137 Ok(item) => match item {
138 Some(Item::Pkcs8Key(key)) => Self::from_pkcs8(key.secret_pkcs8_der()),
139 Some(Item::Pkcs1Key(key)) => Self::from_der(key.secret_pkcs1_der()),
140 _ => Err(Error::MissingKey),
141 },
142 Err(e) => Err(Error::ReadPem { source: e }),
143 }
144 }
145
146 pub fn from_pkcs8(key: &[u8]) -> Result<Self> {
148 Ok(Self(RsaKeyPair::from_pkcs8(key)?))
149 }
150
151 pub fn from_der(key: &[u8]) -> Result<Self> {
153 Ok(Self(RsaKeyPair::from_der(key)?))
154 }
155
156 fn sign(&self, string_to_sign: &str) -> Result<String> {
157 let mut signature = vec![0; self.0.public().modulus_len()];
158 self.0
159 .sign(
160 &ring::signature::RSA_PKCS1_SHA256,
161 &ring::rand::SystemRandom::new(),
162 string_to_sign.as_bytes(),
163 &mut signature,
164 )
165 .map_err(|source| Error::Sign { source })?;
166
167 Ok(hex_encode(&signature))
168 }
169}
170
171#[derive(Debug, Eq, PartialEq)]
173pub struct GcpCredential {
174 pub bearer: String,
176}
177
178pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
179
180#[derive(Debug, Default, serde::Serialize)]
181pub(crate) struct JwtHeader<'a> {
182 #[serde(skip_serializing_if = "Option::is_none")]
186 pub typ: Option<&'a str>,
187 pub alg: &'a str,
191 #[serde(skip_serializing_if = "Option::is_none")]
195 pub cty: Option<&'a str>,
196 #[serde(skip_serializing_if = "Option::is_none")]
200 pub jku: Option<&'a str>,
201 #[serde(skip_serializing_if = "Option::is_none")]
205 pub kid: Option<&'a str>,
206 #[serde(skip_serializing_if = "Option::is_none")]
210 pub x5u: Option<&'a str>,
211 #[serde(skip_serializing_if = "Option::is_none")]
215 pub x5t: Option<&'a str>,
216}
217
218#[derive(serde::Serialize)]
219struct TokenClaims<'a> {
220 iss: &'a str,
221 sub: &'a str,
222 scope: &'a str,
223 exp: u64,
224 iat: u64,
225}
226
227#[derive(serde::Deserialize, Debug)]
228struct TokenResponse {
229 access_token: String,
230 expires_in: u64,
231 id_token: Option<String>,
232}
233
234#[derive(Debug)]
239pub(crate) struct SelfSignedJwt {
240 issuer: String,
241 scope: String,
242 private_key: ServiceAccountKey,
243 key_id: String,
244}
245
246impl SelfSignedJwt {
247 pub(crate) fn new(
249 key_id: String,
250 issuer: String,
251 private_key: ServiceAccountKey,
252 scope: String,
253 ) -> Result<Self> {
254 Ok(Self {
255 issuer,
256 scope,
257 private_key,
258 key_id,
259 })
260 }
261}
262
263#[async_trait]
264impl TokenProvider for SelfSignedJwt {
265 type Credential = GcpCredential;
266
267 async fn fetch_token(
269 &self,
270 _client: &HttpClient,
271 _retry: &RetryConfig,
272 ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
273 let now = seconds_since_epoch();
274 let exp = now + 3600;
275
276 let claims = TokenClaims {
277 iss: &self.issuer,
278 sub: &self.issuer,
279 scope: &self.scope,
280 iat: now,
281 exp,
282 };
283
284 let jwt_header = b64_encode_obj(&JwtHeader {
285 alg: "RS256",
286 typ: Some("JWT"),
287 kid: Some(&self.key_id),
288 ..Default::default()
289 })?;
290
291 let claim_str = b64_encode_obj(&claims)?;
292 let message = [jwt_header.as_ref(), claim_str.as_ref()].join(".");
293 let mut sig_bytes = vec![0; self.private_key.0.public().modulus_len()];
294 self.private_key
295 .0
296 .sign(
297 &ring::signature::RSA_PKCS1_SHA256,
298 &ring::rand::SystemRandom::new(),
299 message.as_bytes(),
300 &mut sig_bytes,
301 )
302 .map_err(|source| Error::Sign { source })?;
303
304 let signature = BASE64_URL_SAFE_NO_PAD.encode(sig_bytes);
305 let bearer = [message, signature].join(".");
306
307 Ok(TemporaryToken {
308 token: Arc::new(GcpCredential { bearer }),
309 expiry: Some(Instant::now() + Duration::from_secs(3600)),
310 })
311 }
312}
313
314fn read_credentials_file<T>(service_account_path: impl AsRef<std::path::Path>) -> Result<T>
315where
316 T: serde::de::DeserializeOwned,
317{
318 let file = File::open(&service_account_path).map_err(|source| {
319 let path = service_account_path.as_ref().to_owned();
320 Error::OpenCredentials { source, path }
321 })?;
322 let reader = BufReader::new(file);
323 serde_json::from_reader(reader).map_err(|source| Error::DecodeCredentials { source })
324}
325
326#[derive(serde::Deserialize, Debug, Clone)]
328pub(crate) struct ServiceAccountCredentials {
329 pub private_key: String,
331
332 pub private_key_id: String,
334
335 pub client_email: String,
337
338 #[serde(default)]
340 pub gcs_base_url: Option<String>,
341
342 #[serde(default)]
344 pub disable_oauth: bool,
345}
346
347impl ServiceAccountCredentials {
348 pub(crate) fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
350 read_credentials_file(path)
351 }
352
353 pub(crate) fn from_key(key: &str) -> Result<Self> {
355 serde_json::from_str(key).map_err(|source| Error::DecodeCredentials { source })
356 }
357
358 pub(crate) fn token_provider(self) -> crate::Result<SelfSignedJwt> {
367 Ok(SelfSignedJwt::new(
368 self.private_key_id,
369 self.client_email,
370 ServiceAccountKey::from_pem(self.private_key.as_bytes())?,
371 DEFAULT_SCOPE.to_string(),
372 )?)
373 }
374
375 pub(crate) fn signing_credentials(self) -> crate::Result<GcpSigningCredentialProvider> {
376 Ok(Arc::new(StaticCredentialProvider::new(
377 GcpSigningCredential {
378 email: self.client_email,
379 private_key: Some(ServiceAccountKey::from_pem(self.private_key.as_bytes())?),
380 },
381 )))
382 }
383}
384
385fn seconds_since_epoch() -> u64 {
387 std::time::SystemTime::now()
388 .duration_since(std::time::SystemTime::UNIX_EPOCH)
389 .unwrap()
390 .as_secs()
391}
392
393fn b64_encode_obj<T: serde::Serialize>(obj: &T) -> Result<String> {
394 let string = serde_json::to_string(obj).map_err(|source| Error::Encode { source })?;
395 Ok(BASE64_URL_SAFE_NO_PAD.encode(string))
396}
397
398#[derive(Debug, Default)]
402pub(crate) struct InstanceCredentialProvider {}
403
404async fn make_metadata_request(
406 client: &HttpClient,
407 hostname: &str,
408 retry: &RetryConfig,
409) -> crate::Result<TokenResponse> {
410 let url =
411 format!("http://{hostname}/computeMetadata/v1/instance/service-accounts/default/token");
412 let response: TokenResponse = client
413 .get(url)
414 .header("Metadata-Flavor", "Google")
415 .query(&[("audience", "https://www.googleapis.com/oauth2/v4/token")])
416 .send_retry(retry)
417 .await
418 .map_err(|source| Error::TokenRequest { source })?
419 .into_body()
420 .json()
421 .await
422 .map_err(|source| Error::TokenResponseBody { source })?;
423 Ok(response)
424}
425
426#[async_trait]
427impl TokenProvider for InstanceCredentialProvider {
428 type Credential = GcpCredential;
429
430 async fn fetch_token(
437 &self,
438 client: &HttpClient,
439 retry: &RetryConfig,
440 ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
441 let metadata_host = if let Ok(host) = env::var("GCE_METADATA_HOST") {
442 host
443 } else if let Ok(host) = env::var("GCE_METADATA_ROOT") {
444 host
445 } else {
446 DEFAULT_METADATA_HOST.to_string()
447 };
448 let metadata_ip = if let Ok(ip) = env::var("GCE_METADATA_IP") {
449 ip
450 } else {
451 DEFAULT_METADATA_IP.to_string()
452 };
453
454 info!("fetching token from metadata server");
455 let response = make_metadata_request(client, &metadata_host, retry)
456 .or_else(|_| make_metadata_request(client, &metadata_ip, retry))
457 .await?;
458
459 let token = TemporaryToken {
460 token: Arc::new(GcpCredential {
461 bearer: response.access_token,
462 }),
463 expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
464 };
465 Ok(token)
466 }
467}
468
469async fn make_metadata_request_for_email(
471 client: &HttpClient,
472 hostname: &str,
473 retry: &RetryConfig,
474) -> crate::Result<String> {
475 let url =
476 format!("http://{hostname}/computeMetadata/v1/instance/service-accounts/default/email",);
477 let response = client
478 .get(url)
479 .header("Metadata-Flavor", "Google")
480 .send_retry(retry)
481 .await
482 .map_err(|source| Error::TokenRequest { source })?
483 .into_body()
484 .text()
485 .await
486 .map_err(|source| Error::TokenResponseBody { source })?;
487 Ok(response)
488}
489
490#[derive(Debug, Default)]
494pub(crate) struct InstanceSigningCredentialProvider {}
495
496#[async_trait]
497impl TokenProvider for InstanceSigningCredentialProvider {
498 type Credential = GcpSigningCredential;
499
500 async fn fetch_token(
507 &self,
508 client: &HttpClient,
509 retry: &RetryConfig,
510 ) -> crate::Result<TemporaryToken<Arc<GcpSigningCredential>>> {
511 let metadata_host = if let Ok(host) = env::var("GCE_METADATA_HOST") {
512 host
513 } else if let Ok(host) = env::var("GCE_METADATA_ROOT") {
514 host
515 } else {
516 DEFAULT_METADATA_HOST.to_string()
517 };
518
519 let metadata_ip = if let Ok(ip) = env::var("GCE_METADATA_IP") {
520 ip
521 } else {
522 DEFAULT_METADATA_IP.to_string()
523 };
524
525 info!("fetching token from metadata server");
526
527 let email = make_metadata_request_for_email(client, &metadata_host, retry)
528 .or_else(|_| make_metadata_request_for_email(client, &metadata_ip, retry))
529 .await?;
530
531 let token = TemporaryToken {
532 token: Arc::new(GcpSigningCredential {
533 email,
534 private_key: None,
535 }),
536 expiry: None,
537 };
538 Ok(token)
539 }
540}
541
542#[derive(serde::Deserialize, Clone)]
548#[serde(tag = "type")]
549pub(crate) enum ApplicationDefaultCredentials {
550 #[serde(rename = "service_account")]
555 ServiceAccount(ServiceAccountCredentials),
556 #[serde(rename = "authorized_user")]
561 AuthorizedUser(AuthorizedUserCredentials),
562}
563
564impl ApplicationDefaultCredentials {
565 const CREDENTIALS_PATH: &'static str = if cfg!(windows) {
566 "gcloud/application_default_credentials.json"
567 } else {
568 ".config/gcloud/application_default_credentials.json"
569 };
570
571 pub(crate) fn read(path: Option<&str>) -> Result<Option<Self>, Error> {
575 if let Some(path) = path {
576 return read_credentials_file::<Self>(path).map(Some);
577 }
578
579 let home_var = if cfg!(windows) { "APPDATA" } else { "HOME" };
580 if let Some(home) = env::var_os(home_var) {
581 let path = Path::new(&home).join(Self::CREDENTIALS_PATH);
582
583 if path.exists() {
585 return read_credentials_file::<Self>(path).map(Some);
586 }
587 }
588 Ok(None)
589 }
590}
591
592const DEFAULT_TOKEN_GCP_URI: &str = "https://accounts.google.com/o/oauth2/token";
593
594#[derive(Debug, Deserialize, Clone)]
596pub(crate) struct AuthorizedUserCredentials {
597 client_id: String,
598 client_secret: String,
599 refresh_token: String,
600}
601
602#[derive(Debug, Deserialize)]
603pub(crate) struct AuthorizedUserSigningCredentials {
604 credential: AuthorizedUserCredentials,
605}
606
607#[derive(Debug, Deserialize)]
609struct EmailResponse {
610 email: String,
611}
612
613#[derive(Debug, Deserialize)]
614struct IdTokenClaims {
615 email: String,
616}
617
618async fn get_token_response(
619 client_id: &str,
620 client_secret: &str,
621 refresh_token: &str,
622 client: &HttpClient,
623 retry: &RetryConfig,
624) -> Result<TokenResponse> {
625 client
626 .post(DEFAULT_TOKEN_GCP_URI)
627 .form([
628 ("grant_type", "refresh_token"),
629 ("client_id", client_id),
630 ("client_secret", client_secret),
631 ("refresh_token", refresh_token),
632 ])
633 .retryable(retry)
634 .idempotent(true)
635 .send()
636 .await
637 .map_err(|source| Error::TokenRequest { source })?
638 .into_body()
639 .json::<TokenResponse>()
640 .await
641 .map_err(|source| Error::TokenResponseBody { source })
642}
643
644impl AuthorizedUserSigningCredentials {
645 pub(crate) fn from(credential: AuthorizedUserCredentials) -> crate::Result<Self> {
646 Ok(Self { credential })
647 }
648
649 async fn client_email(
650 &self,
651 client: &HttpClient,
652 retry: &RetryConfig,
653 ) -> crate::Result<String> {
654 let response = get_token_response(
655 &self.credential.client_id,
656 &self.credential.client_secret,
657 &self.credential.refresh_token,
658 client,
659 retry,
660 )
661 .await?;
662
663 if let Some(id_token) = response.id_token {
665 let parts: Vec<&str> = id_token.split('.').collect();
667 if parts.len() == 3 {
668 if let Ok(payload) = BASE64_URL_SAFE_NO_PAD.decode(parts[1]) {
670 if let Ok(claims) = serde_json::from_slice::<IdTokenClaims>(&payload) {
672 return Ok(claims.email);
673 }
674 }
675 }
677 }
678
679 let response = client
681 .get("https://oauth2.googleapis.com/tokeninfo")
682 .query(&[("access_token", response.access_token)])
683 .send_retry(retry)
684 .await
685 .map_err(|source| Error::TokenRequest { source })?
686 .into_body()
687 .json::<EmailResponse>()
688 .await
689 .map_err(|source: HttpError| Error::TokenResponseBody { source })?;
690
691 Ok(response.email)
692 }
693}
694
695#[async_trait]
696impl TokenProvider for AuthorizedUserSigningCredentials {
697 type Credential = GcpSigningCredential;
698
699 async fn fetch_token(
700 &self,
701 client: &HttpClient,
702 retry: &RetryConfig,
703 ) -> crate::Result<TemporaryToken<Arc<GcpSigningCredential>>> {
704 let email = self.client_email(client, retry).await?;
705 Ok(TemporaryToken {
706 token: Arc::new(GcpSigningCredential {
707 email,
708 private_key: None,
709 }),
710 expiry: None,
711 })
712 }
713}
714
715#[async_trait]
716impl TokenProvider for AuthorizedUserCredentials {
717 type Credential = GcpCredential;
718
719 async fn fetch_token(
720 &self,
721 client: &HttpClient,
722 retry: &RetryConfig,
723 ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
724 let response = get_token_response(
725 &self.client_id,
726 &self.client_secret,
727 &self.refresh_token,
728 client,
729 retry,
730 )
731 .await?;
732
733 Ok(TemporaryToken {
734 token: Arc::new(GcpCredential {
735 bearer: response.access_token,
736 }),
737 expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
738 })
739 }
740}
741
742fn trim_header_value(value: &str) -> String {
744 let mut ret = value.to_string();
745 ret.retain(|c| !c.is_whitespace());
746 ret
747}
748
749#[derive(Debug)]
753pub(crate) struct GCSAuthorizer {
754 date: Option<DateTime<Utc>>,
755 credential: Arc<GcpSigningCredential>,
756}
757
758impl GCSAuthorizer {
759 pub(crate) fn new(credential: Arc<GcpSigningCredential>) -> Self {
761 Self {
762 date: None,
763 credential,
764 }
765 }
766
767 pub(crate) async fn sign(
768 &self,
769 method: Method,
770 url: &mut Url,
771 expires_in: Duration,
772 client: &GoogleCloudStorageClient,
773 ) -> crate::Result<()> {
774 let email = &self.credential.email;
775 let date = self.date.unwrap_or_else(Utc::now);
776 let scope = self.scope(date);
777 let credential_with_scope = format!("{email}/{scope}");
778
779 let mut headers = HeaderMap::new();
780 headers.insert("host", DEFAULT_GCS_SIGN_BLOB_HOST.parse().unwrap());
781
782 let (_, signed_headers) = Self::canonicalize_headers(&headers);
783
784 url.query_pairs_mut()
785 .append_pair("X-Goog-Algorithm", "GOOG4-RSA-SHA256")
786 .append_pair("X-Goog-Credential", &credential_with_scope)
787 .append_pair("X-Goog-Date", &date.format("%Y%m%dT%H%M%SZ").to_string())
788 .append_pair("X-Goog-Expires", &expires_in.as_secs().to_string())
789 .append_pair("X-Goog-SignedHeaders", &signed_headers);
790
791 let string_to_sign = self.string_to_sign(date, &method, url, &headers);
792 let signature = match &self.credential.private_key {
793 Some(key) => key.sign(&string_to_sign)?,
794 None => client.sign_blob(&string_to_sign, email).await?,
795 };
796
797 url.query_pairs_mut()
798 .append_pair("X-Goog-Signature", &signature);
799 Ok(())
800 }
801
802 fn scope(&self, date: DateTime<Utc>) -> String {
806 format!("{}/auto/storage/goog4_request", date.format("%Y%m%d"),)
807 }
808
809 fn canonicalize_request(url: &Url, method: &Method, headers: &HeaderMap) -> String {
823 let verb = method.as_str();
824 let path = url.path();
825 let query = Self::canonicalize_query(url);
826 let (canonical_headers, signed_headers) = Self::canonicalize_headers(headers);
827
828 format!(
829 "{verb}\n{path}\n{query}\n{canonical_headers}\n\n{signed_headers}\n{DEFAULT_GCS_PLAYLOAD_STRING}"
830 )
831 }
832
833 fn canonicalize_query(url: &Url) -> String {
838 url.query_pairs()
839 .sorted_unstable_by(|a, b| a.0.cmp(&b.0))
840 .map(|(k, v)| {
841 format!(
842 "{}={}",
843 utf8_percent_encode(k.as_ref(), &STRICT_ENCODE_SET),
844 utf8_percent_encode(v.as_ref(), &STRICT_ENCODE_SET)
845 )
846 })
847 .join("&")
848 }
849
850 fn canonicalize_headers(header_map: &HeaderMap) -> (String, String) {
854 let mut headers = BTreeMap::<String, Vec<&str>>::new();
856 for (k, v) in header_map {
857 headers
858 .entry(k.as_str().to_lowercase())
859 .or_default()
860 .push(std::str::from_utf8(v.as_bytes()).unwrap());
861 }
862
863 let canonicalize_headers = headers
864 .iter()
865 .map(|(k, v)| {
866 format!(
867 "{}:{}",
868 k.trim(),
869 v.iter().map(|v| trim_header_value(v)).join(",")
870 )
871 })
872 .join("\n");
873
874 let signed_headers = headers.keys().join(";");
875
876 (canonicalize_headers, signed_headers)
877 }
878
879 pub(crate) fn string_to_sign(
890 &self,
891 date: DateTime<Utc>,
892 request_method: &Method,
893 url: &Url,
894 headers: &HeaderMap,
895 ) -> String {
896 let canonical_request = Self::canonicalize_request(url, request_method, headers);
897 let hashed_canonical_req = hex_digest(canonical_request.as_bytes());
898 let scope = self.scope(date);
899
900 format!(
901 "{}\n{}\n{}\n{}",
902 "GOOG4-RSA-SHA256",
903 date.format("%Y%m%dT%H%M%SZ"),
904 scope,
905 hashed_canonical_req
906 )
907 }
908}
909
910pub(crate) trait CredentialExt {
911 fn with_bearer_auth(self, credential: Option<&GcpCredential>) -> Self;
913}
914
915impl CredentialExt for HttpRequestBuilder {
916 fn with_bearer_auth(self, credential: Option<&GcpCredential>) -> Self {
917 match credential {
918 Some(credential) => {
919 if credential.bearer.is_empty() {
920 self
921 } else {
922 self.bearer_auth(&credential.bearer)
923 }
924 }
925 None => self,
926 }
927 }
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933
934 #[test]
935 fn test_canonicalize_headers() {
936 let mut input_header = HeaderMap::new();
937 input_header.insert("content-type", "text/plain".parse().unwrap());
938 input_header.insert("host", "storage.googleapis.com".parse().unwrap());
939 input_header.insert("x-goog-meta-reviewer", "jane".parse().unwrap());
940 input_header.append("x-goog-meta-reviewer", "john".parse().unwrap());
941 assert_eq!(
942 GCSAuthorizer::canonicalize_headers(&input_header),
943 (
944 "content-type:text/plain
945host:storage.googleapis.com
946x-goog-meta-reviewer:jane,john"
947 .into(),
948 "content-type;host;x-goog-meta-reviewer".to_string()
949 )
950 );
951 }
952
953 #[test]
954 fn test_canonicalize_query() {
955 let mut url = Url::parse("https://storage.googleapis.com/bucket/object").unwrap();
956 url.query_pairs_mut()
957 .append_pair("max-keys", "2")
958 .append_pair("prefix", "object");
959 assert_eq!(
960 GCSAuthorizer::canonicalize_query(&url),
961 "max-keys=2&prefix=object".to_string()
962 );
963 }
964}