object_store/gcp/
credential.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// A Google Cloud Storage Credential for signing
107#[derive(Debug)]
108pub struct GcpSigningCredential {
109    /// The email of the service account
110    pub email: String,
111
112    /// An optional RSA private key
113    ///
114    /// If provided this will be used to sign the URL, otherwise a call will be made to
115    /// [`iam.serviceAccounts.signBlob`]. This allows supporting credential sources
116    /// that don't expose the service account private key, e.g. [IMDS].
117    ///
118    /// [IMDS]: https://cloud.google.com/docs/authentication/get-id-token#metadata-server
119    /// [`iam.serviceAccounts.signBlob`]: https://cloud.google.com/storage/docs/authentication/creating-signatures
120    pub private_key: Option<ServiceAccountKey>,
121}
122
123/// A private RSA key for a service account
124#[derive(Debug)]
125pub struct ServiceAccountKey(RsaKeyPair);
126
127impl ServiceAccountKey {
128    /// Parses a pem-encoded RSA key
129    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    /// Parses an unencrypted PKCS#8-encoded RSA private key.
147    pub fn from_pkcs8(key: &[u8]) -> Result<Self> {
148        Ok(Self(RsaKeyPair::from_pkcs8(key)?))
149    }
150
151    /// Parses an unencrypted PKCS#8-encoded RSA private key.
152    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/// A Google Cloud Storage Credential
172#[derive(Debug, Eq, PartialEq)]
173pub struct GcpCredential {
174    /// An HTTP bearer token
175    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    /// The type of JWS: it can only be "JWT" here
183    ///
184    /// Defined in [RFC7515#4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9).
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub typ: Option<&'a str>,
187    /// The algorithm used
188    ///
189    /// Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1).
190    pub alg: &'a str,
191    /// Content type
192    ///
193    /// Defined in [RFC7519#5.2](https://tools.ietf.org/html/rfc7519#section-5.2).
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub cty: Option<&'a str>,
196    /// JSON Key URL
197    ///
198    /// Defined in [RFC7515#4.1.2](https://tools.ietf.org/html/rfc7515#section-4.1.2).
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub jku: Option<&'a str>,
201    /// Key ID
202    ///
203    /// Defined in [RFC7515#4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4).
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub kid: Option<&'a str>,
206    /// X.509 URL
207    ///
208    /// Defined in [RFC7515#4.1.5](https://tools.ietf.org/html/rfc7515#section-4.1.5).
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub x5u: Option<&'a str>,
211    /// X.509 certificate thumbprint
212    ///
213    /// Defined in [RFC7515#4.1.7](https://tools.ietf.org/html/rfc7515#section-4.1.7).
214    #[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/// Self-signed JWT (JSON Web Token).
235///
236/// # References
237/// - <https://google.aip.dev/auth/4111>
238#[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    /// Create a new [`SelfSignedJwt`]
248    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    /// Fetch a fresh token
268    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/// A deserialized `service-account-********.json`-file.
327#[derive(serde::Deserialize, Debug, Clone)]
328pub(crate) struct ServiceAccountCredentials {
329    /// The private key in RSA format.
330    pub private_key: String,
331
332    /// The private key ID
333    pub private_key_id: String,
334
335    /// The email address associated with the service account.
336    pub client_email: String,
337
338    /// Base URL for GCS
339    #[serde(default)]
340    pub gcs_base_url: Option<String>,
341
342    /// Disable oauth and use empty tokens.
343    #[serde(default)]
344    pub disable_oauth: bool,
345}
346
347impl ServiceAccountCredentials {
348    /// Create a new [`ServiceAccountCredentials`] from a file.
349    pub(crate) fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
350        read_credentials_file(path)
351    }
352
353    /// Create a new [`ServiceAccountCredentials`] from a string.
354    pub(crate) fn from_key(key: &str) -> Result<Self> {
355        serde_json::from_str(key).map_err(|source| Error::DecodeCredentials { source })
356    }
357
358    /// Create a [`SelfSignedJwt`] from this credentials struct.
359    ///
360    /// We use a scope of [`DEFAULT_SCOPE`] as opposed to an audience
361    /// as GCS appears to not support audience
362    ///
363    /// # References
364    /// - <https://stackoverflow.com/questions/63222450/service-account-authorization-without-oauth-can-we-get-file-from-google-cloud/71834557#71834557>
365    /// - <https://www.codejam.info/2022/05/google-cloud-service-account-authorization-without-oauth.html>
366    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
385/// Returns the number of seconds since unix epoch
386fn 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/// A provider that uses the Google Cloud Platform metadata server to fetch a token.
399///
400/// <https://cloud.google.com/docs/authentication/get-id-token#metadata-server>
401#[derive(Debug, Default)]
402pub(crate) struct InstanceCredentialProvider {}
403
404/// Make a request to the metadata server to fetch a token, using a a given hostname.
405async 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    /// Fetch a token from the metadata server.
431    /// Since the connection is local we need to enable http access and don't actually use the client object passed in.
432    /// Respects the `GCE_METADATA_HOST`, `GCE_METADATA_ROOT`, and `GCE_METADATA_IP`
433    /// environment variables.
434    ///
435    /// References: <https://googleapis.dev/python/google-auth/latest/reference/google.auth.environment_vars.html>
436    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
469/// Make a request to the metadata server to fetch the client email, using a given hostname.
470async 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/// A provider that uses the Google Cloud Platform metadata server to fetch a email for signing.
491///
492/// <https://cloud.google.com/appengine/docs/legacy/standard/java/accessing-instance-metadata>
493#[derive(Debug, Default)]
494pub(crate) struct InstanceSigningCredentialProvider {}
495
496#[async_trait]
497impl TokenProvider for InstanceSigningCredentialProvider {
498    type Credential = GcpSigningCredential;
499
500    /// Fetch a token from the metadata server.
501    /// Since the connection is local we need to enable http access and don't actually use the client object passed in.
502    /// Respects the `GCE_METADATA_HOST`, `GCE_METADATA_ROOT`, and `GCE_METADATA_IP`
503    /// environment variables.
504    ///
505    /// References: <https://googleapis.dev/python/google-auth/latest/reference/google.auth.environment_vars.html>
506    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/// A deserialized `application_default_credentials.json`-file.
543///
544/// # References
545/// - <https://cloud.google.com/docs/authentication/application-default-credentials#personal>
546/// - <https://google.aip.dev/auth/4110>
547#[derive(serde::Deserialize, Clone)]
548#[serde(tag = "type")]
549pub(crate) enum ApplicationDefaultCredentials {
550    /// Service Account.
551    ///
552    /// # References
553    /// - <https://google.aip.dev/auth/4112>
554    #[serde(rename = "service_account")]
555    ServiceAccount(ServiceAccountCredentials),
556    /// Authorized user via "gcloud CLI Integration".
557    ///
558    /// # References
559    /// - <https://google.aip.dev/auth/4113>
560    #[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    // Create a new application default credential in the following situations:
572    //  1. a file is passed in and the type matches.
573    //  2. without argument if the well-known configuration file is present.
574    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            // It's expected for this file to not exist unless it has been explicitly configured by the user.
584            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/// <https://google.aip.dev/auth/4113>
595#[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///<https://oauth2.googleapis.com/tokeninfo?access_token=ACCESS_TOKEN>
608#[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        // Extract email from id_token if available
664        if let Some(id_token) = response.id_token {
665            // Split the JWT string by dots to get the payload section
666            let parts: Vec<&str> = id_token.split('.').collect();
667            if parts.len() == 3 {
668                // Decode the base64-encoded payload (middle part)
669                if let Ok(payload) = BASE64_URL_SAFE_NO_PAD.decode(parts[1]) {
670                    // Parse the payload as JSON and extract the email
671                    if let Ok(claims) = serde_json::from_slice::<IdTokenClaims>(&payload) {
672                        return Ok(claims.email);
673                    }
674                }
675                // If any of the parsing steps fail, fallback to other method
676            }
677        }
678
679        // Fallback to the original method if id_token is not available or invalid
680        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
742/// Trim whitespace from header values
743fn 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/// A Google Cloud Storage Authorizer for generating signed URL using [Google SigV4]
750///
751/// [Google SigV4]: https://cloud.google.com/storage/docs/access-control/signed-urls
752#[derive(Debug)]
753pub(crate) struct GCSAuthorizer {
754    date: Option<DateTime<Utc>>,
755    credential: Arc<GcpSigningCredential>,
756}
757
758impl GCSAuthorizer {
759    /// Create a new [`GCSAuthorizer`]
760    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    /// Get scope for the request
803    ///
804    /// <https://cloud.google.com/storage/docs/authentication/signatures#credential-scope>
805    fn scope(&self, date: DateTime<Utc>) -> String {
806        format!("{}/auto/storage/goog4_request", date.format("%Y%m%d"),)
807    }
808
809    /// Canonicalizes query parameters into the GCP canonical form
810    /// form like:
811    ///```plaintext
812    ///HTTP_VERB
813    ///PATH_TO_RESOURCE
814    ///CANONICAL_QUERY_STRING
815    ///CANONICAL_HEADERS
816    ///
817    ///SIGNED_HEADERS
818    ///PAYLOAD
819    ///```
820    ///
821    /// <https://cloud.google.com/storage/docs/authentication/canonical-requests>
822    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    /// Canonicalizes query parameters into the GCP canonical form
834    /// form like `max-keys=2&prefix=object`
835    ///
836    /// <https://cloud.google.com/storage/docs/authentication/canonical-requests#about-query-strings>
837    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    /// Canonicalizes header into the GCP canonical form
851    ///
852    /// <https://cloud.google.com/storage/docs/authentication/canonical-requests#about-headers>
853    fn canonicalize_headers(header_map: &HeaderMap) -> (String, String) {
854        //FIXME add error handling for invalid header values
855        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    ///construct the string to sign
880    ///form like:
881    ///```plaintext
882    ///SIGNING_ALGORITHM
883    ///ACTIVE_DATETIME
884    ///CREDENTIAL_SCOPE
885    ///HASHED_CANONICAL_REQUEST
886    ///```
887    ///`ACTIVE_DATETIME` format:`YYYYMMDD'T'HHMMSS'Z'`
888    /// <https://cloud.google.com/storage/docs/authentication/signatures#string-to-sign>
889    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    /// Apply bearer authentication to the request if the credential is not None
912    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}