Skip to main content

olai_http/gcp/
builder.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 std::str::FromStr;
19use std::sync::Arc;
20use std::time::Duration;
21
22use serde::{Deserialize, Serialize};
23use tokio::runtime::Handle;
24
25use crate::gcp::GoogleConfig;
26use crate::gcp::credential::GcpCredential;
27use crate::gcp::credential::{
28    ApplicationDefaultCredentials, GcpWorkloadIdentityProvider, InstanceCredentialProvider,
29    ServiceAccountCredentials,
30};
31use crate::gcp::{GcpCredentialProvider, credential};
32use crate::service::make_service;
33use crate::{
34    ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider,
35    TokenCredentialProvider,
36};
37
38const TOKEN_MIN_TTL: Duration = Duration::from_secs(4 * 60);
39
40#[derive(Debug, thiserror::Error)]
41enum Error {
42    #[error("One of service account path or service account key may be provided.")]
43    ServiceAccountPathAndKeyProvided,
44
45    #[error("Configuration key: '{}' is not known.", key)]
46    UnknownConfigurationKey { key: String },
47
48    #[error("GCP credential error: {}", source)]
49    Credential { source: credential::Error },
50}
51
52impl From<Error> for crate::Error {
53    fn from(err: Error) -> Self {
54        match err {
55            Error::UnknownConfigurationKey { key } => Self::UnknownConfigurationKey { key },
56            _ => Self::Generic {
57                source: Box::new(err),
58            },
59        }
60    }
61}
62
63/// Configure a connection to Google Cloud Storage.
64///
65/// If no credentials are explicitly provided, they will be sourced
66/// from the environment as documented [here](https://cloud.google.com/docs/authentication/application-default-credentials).
67///
68/// # Example
69/// ```
70/// # use olai_http::gcp::GoogleBuilder;
71/// let gcs = GoogleBuilder::from_env().build(None);
72/// ```
73#[derive(Debug, Clone)]
74pub struct GoogleBuilder {
75    /// Path to the service account file
76    service_account_path: Option<String>,
77    /// The serialized service account key
78    service_account_key: Option<String>,
79    /// Path to the application credentials file.
80    application_credentials_path: Option<String>,
81    /// Retry config
82    retry_config: RetryConfig,
83    /// Client options
84    client_options: ClientOptions,
85    /// Credentials
86    credentials: Option<GcpCredentialProvider>,
87}
88
89/// Configuration keys for [`GoogleBuilder`]
90///
91/// Configuration via keys can be done via [`GoogleBuilder::with_config`]
92///
93/// # Example
94/// ```
95/// # use olai_http::gcp::{GoogleBuilder, GoogleConfigKey};
96/// let builder = GoogleBuilder::new()
97///     .with_config("google_service_account".parse().unwrap(), "my-service-account");
98/// ```
99#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
100#[non_exhaustive]
101pub enum GoogleConfigKey {
102    /// Path to the service account file
103    ///
104    /// Supported keys:
105    /// - `google_service_account`
106    /// - `service_account`
107    /// - `google_service_account_path`
108    /// - `service_account_path`
109    ServiceAccount,
110
111    /// The serialized service account key.
112    ///
113    /// Supported keys:
114    /// - `google_service_account_key`
115    /// - `service_account_key`
116    ServiceAccountKey,
117
118    /// Application credentials path
119    ///
120    /// See [`GoogleBuilder::with_application_credentials`].
121    ApplicationCredentials,
122
123    /// Client options
124    Client(ClientConfigKey),
125}
126
127impl AsRef<str> for GoogleConfigKey {
128    fn as_ref(&self) -> &str {
129        match self {
130            Self::ServiceAccount => "google_service_account",
131            Self::ServiceAccountKey => "google_service_account_key",
132            Self::ApplicationCredentials => "google_application_credentials",
133            Self::Client(key) => key.as_ref(),
134        }
135    }
136}
137
138impl FromStr for GoogleConfigKey {
139    type Err = crate::Error;
140
141    fn from_str(s: &str) -> Result<Self, Self::Err> {
142        match s {
143            "google_service_account"
144            | "service_account"
145            | "google_service_account_path"
146            | "service_account_path" => Ok(Self::ServiceAccount),
147            "google_service_account_key" | "service_account_key" => Ok(Self::ServiceAccountKey),
148            "google_application_credentials" => Ok(Self::ApplicationCredentials),
149            _ => match s.strip_prefix("google_").unwrap_or(s).parse() {
150                Ok(key) => Ok(Self::Client(key)),
151                Err(_) => Err(Error::UnknownConfigurationKey { key: s.into() }.into()),
152            },
153        }
154    }
155}
156
157impl Default for GoogleBuilder {
158    fn default() -> Self {
159        Self {
160            service_account_path: None,
161            service_account_key: None,
162            application_credentials_path: None,
163            retry_config: Default::default(),
164            client_options: ClientOptions::new().with_allow_http(true),
165            credentials: None,
166        }
167    }
168}
169
170impl GoogleBuilder {
171    /// Create a new [`GoogleBuilder`] with default values.
172    pub fn new() -> Self {
173        Default::default()
174    }
175
176    /// Create an instance of [`GoogleBuilder`] with values pre-populated from environment variables.
177    ///
178    /// Variables extracted from environment:
179    /// * GOOGLE_SERVICE_ACCOUNT: location of service account file
180    /// * GOOGLE_SERVICE_ACCOUNT_PATH: (alias) location of service account file
181    /// * SERVICE_ACCOUNT: (alias) location of service account file
182    /// * GOOGLE_SERVICE_ACCOUNT_KEY: JSON serialized service account key
183    ///
184    /// # Example
185    /// ```
186    /// use olai_http::gcp::GoogleBuilder;
187    ///
188    /// let gcs = GoogleBuilder::from_env()
189    ///     .build(None);
190    /// ```
191    pub fn from_env() -> Self {
192        let mut builder = Self::default();
193
194        if let Ok(service_account_path) = std::env::var("SERVICE_ACCOUNT") {
195            builder.service_account_path = Some(service_account_path);
196        }
197
198        for (os_key, os_value) in std::env::vars_os() {
199            if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str()) {
200                if key.starts_with("GOOGLE_") {
201                    if let Ok(config_key) = key.to_ascii_lowercase().parse() {
202                        builder = builder.with_config(config_key, value);
203                    }
204                }
205            }
206        }
207
208        builder
209    }
210
211    /// Set an option on the builder via a key - value pair.
212    pub fn with_config(mut self, key: GoogleConfigKey, value: impl Into<String>) -> Self {
213        match key {
214            GoogleConfigKey::ServiceAccount => self.service_account_path = Some(value.into()),
215            GoogleConfigKey::ServiceAccountKey => self.service_account_key = Some(value.into()),
216            GoogleConfigKey::ApplicationCredentials => {
217                self.application_credentials_path = Some(value.into())
218            }
219            GoogleConfigKey::Client(key) => {
220                self.client_options = self.client_options.with_config(key, value)
221            }
222        };
223        self
224    }
225
226    /// Get config value via a [`GoogleConfigKey`].
227    ///
228    /// # Example
229    /// ```
230    /// use olai_http::gcp::{GoogleBuilder, GoogleConfigKey};
231    ///
232    /// let builder = GoogleBuilder::from_env()
233    ///     .with_service_account_key("foo");
234    /// let service_account_key = builder.get_config_value(&GoogleConfigKey::ServiceAccountKey).unwrap_or_default();
235    /// assert_eq!("foo", &service_account_key);
236    /// ```
237    pub fn get_config_value(&self, key: &GoogleConfigKey) -> Option<String> {
238        match key {
239            GoogleConfigKey::ServiceAccount => self.service_account_path.clone(),
240            GoogleConfigKey::ServiceAccountKey => self.service_account_key.clone(),
241            GoogleConfigKey::ApplicationCredentials => self.application_credentials_path.clone(),
242            GoogleConfigKey::Client(key) => self.client_options.get_config_value(key),
243        }
244    }
245
246    /// Set the path to the service account file.
247    ///
248    /// This or [`GoogleBuilder::with_service_account_key`] must be
249    /// set.
250    ///
251    /// Example `"/tmp/gcs.json"`.
252    ///
253    /// Example contents of `gcs.json`:
254    ///
255    /// ```json
256    /// {
257    ///    "gcs_base_url": "https://localhost:4443",
258    ///    "disable_oauth": true,
259    ///    "client_email": "",
260    ///    "private_key": ""
261    /// }
262    /// ```
263    pub fn with_service_account_path(mut self, service_account_path: impl Into<String>) -> Self {
264        self.service_account_path = Some(service_account_path.into());
265        self
266    }
267
268    /// Set the service account key. The service account must be in the JSON
269    /// format.
270    ///
271    /// This or [`GoogleBuilder::with_service_account_path`] must be
272    /// set.
273    pub fn with_service_account_key(mut self, service_account: impl Into<String>) -> Self {
274        self.service_account_key = Some(service_account.into());
275        self
276    }
277
278    /// Set the path to the application credentials file.
279    ///
280    /// <https://cloud.google.com/docs/authentication/provide-credentials-adc>
281    pub fn with_application_credentials(
282        mut self,
283        application_credentials_path: impl Into<String>,
284    ) -> Self {
285        self.application_credentials_path = Some(application_credentials_path.into());
286        self
287    }
288
289    /// Set the credential provider overriding any other options
290    pub fn with_credentials(mut self, credentials: GcpCredentialProvider) -> Self {
291        self.credentials = Some(credentials);
292        self
293    }
294
295    /// Set the retry configuration
296    pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
297        self.retry_config = retry_config;
298        self
299    }
300
301    /// Set the proxy_url to be used by the underlying client
302    pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
303        self.client_options = self.client_options.with_proxy_url(proxy_url);
304        self
305    }
306
307    /// Set a trusted proxy CA certificate
308    pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
309        self.client_options = self
310            .client_options
311            .with_proxy_ca_certificate(proxy_ca_certificate);
312        self
313    }
314
315    /// Set a list of hosts to exclude from proxy connections
316    pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
317        self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
318        self
319    }
320
321    /// Sets the client options, overriding any already set
322    pub fn with_client_options(mut self, options: ClientOptions) -> Self {
323        self.client_options = options;
324        self
325    }
326
327    /// Configure a connection to Google Cloud Storage, returning a
328    /// new [`GoogleConfig`] and consuming `self`
329    ///
330    /// If `runtime` is provided, all HTTP I/O (including credential refresh)
331    /// will be spawned on the given runtime handle.
332    pub fn build(self, runtime: Option<&Handle>) -> Result<GoogleConfig> {
333        // First try to initialize from the service account information.
334        let service_account_credentials =
335            match (self.service_account_path, self.service_account_key) {
336                (Some(path), None) => Some(
337                    ServiceAccountCredentials::from_file(path)
338                        .map_err(|source| Error::Credential { source })?,
339                ),
340                (None, Some(key)) => Some(
341                    ServiceAccountCredentials::from_key(&key)
342                        .map_err(|source| Error::Credential { source })?,
343                ),
344                (None, None) => None,
345                (Some(_), Some(_)) => return Err(Error::ServiceAccountPathAndKeyProvided.into()),
346            };
347
348        // Then try to initialize from the application credentials file, or the environment.
349        let application_default_credentials =
350            ApplicationDefaultCredentials::read(self.application_credentials_path.as_deref())?;
351
352        let disable_oauth = service_account_credentials
353            .as_ref()
354            .map(|c| c.disable_oauth)
355            .unwrap_or(false);
356
357        let credentials = if let Some(credentials) = self.credentials {
358            credentials
359        } else if disable_oauth {
360            Arc::new(StaticCredentialProvider::new(GcpCredential {
361                bearer: "".to_string(),
362            })) as _
363        } else if let Some(credentials) = service_account_credentials.clone() {
364            let client = self.client_options.client()?;
365            let service = make_service(client.clone(), runtime);
366            Arc::new(TokenCredentialProvider::new(
367                credentials.token_provider()?,
368                client,
369                service,
370                self.retry_config.clone(),
371            )) as _
372        } else if let Some(credentials) = application_default_credentials.clone() {
373            match credentials {
374                ApplicationDefaultCredentials::AuthorizedUser(token) => {
375                    let client = self.client_options.client()?;
376                    let service = make_service(client.clone(), runtime);
377                    Arc::new(
378                        TokenCredentialProvider::new(
379                            token,
380                            client,
381                            service,
382                            self.retry_config.clone(),
383                        )
384                        .with_min_ttl(TOKEN_MIN_TTL),
385                    ) as _
386                }
387                ApplicationDefaultCredentials::ServiceAccount(token) => {
388                    let client = self.client_options.client()?;
389                    let service = make_service(client.clone(), runtime);
390                    Arc::new(TokenCredentialProvider::new(
391                        token.token_provider()?,
392                        client,
393                        service,
394                        self.retry_config.clone(),
395                    )) as _
396                }
397                ApplicationDefaultCredentials::ExternalAccount(ext) => {
398                    let client = self.client_options.client()?;
399                    let service = make_service(client.clone(), runtime);
400                    let sts_endpoint = ext.token_url.clone();
401                    Arc::new(
402                        TokenCredentialProvider::new(
403                            GcpWorkloadIdentityProvider {
404                                credentials: ext,
405                                sts_endpoint,
406                            },
407                            client,
408                            service,
409                            self.retry_config.clone(),
410                        )
411                        .with_min_ttl(TOKEN_MIN_TTL),
412                    ) as _
413                }
414            }
415        } else {
416            let client = self.client_options.metadata_client()?;
417            let service = make_service(client.clone(), runtime);
418            Arc::new(
419                TokenCredentialProvider::new(
420                    InstanceCredentialProvider::default(),
421                    client,
422                    service,
423                    self.retry_config.clone(),
424                )
425                .with_min_ttl(TOKEN_MIN_TTL),
426            ) as _
427        };
428
429        Ok(GoogleConfig::new(
430            credentials,
431            self.retry_config,
432            self.client_options,
433        ))
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440    use std::collections::HashMap;
441    use std::io::Write;
442    use tempfile::NamedTempFile;
443
444    const FAKE_KEY: &str = r#"{"private_key": "private_key", "private_key_id": "private_key_id", "client_email":"client_email", "disable_oauth":true}"#;
445
446    #[test]
447    fn gcs_test_service_account_key_and_path() {
448        let mut tfile = NamedTempFile::new().unwrap();
449        write!(tfile, "{FAKE_KEY}").unwrap();
450        let _ = GoogleBuilder::new()
451            .with_service_account_key(FAKE_KEY)
452            .with_service_account_path(tfile.path().to_str().unwrap())
453            .build(None)
454            .unwrap_err();
455    }
456
457    #[test]
458    fn gcs_test_config_from_map() {
459        let google_service_account = "object_store:fake_service_account".to_string();
460        let options = HashMap::from([("google_service_account", google_service_account.clone())]);
461
462        let builder = options
463            .iter()
464            .fold(GoogleBuilder::new(), |builder, (key, value)| {
465                builder.with_config(key.parse().unwrap(), value)
466            });
467
468        assert_eq!(
469            builder.service_account_path.unwrap(),
470            google_service_account.as_str()
471        );
472    }
473
474    #[test]
475    fn gcs_test_config_aliases() {
476        // Service account path
477        for alias in [
478            "google_service_account",
479            "service_account",
480            "google_service_account_path",
481            "service_account_path",
482        ] {
483            let builder =
484                GoogleBuilder::new().with_config(alias.parse().unwrap(), "/fake/path.json");
485            assert_eq!("/fake/path.json", builder.service_account_path.unwrap());
486        }
487
488        // Service account key
489        for alias in ["google_service_account_key", "service_account_key"] {
490            let builder = GoogleBuilder::new().with_config(alias.parse().unwrap(), FAKE_KEY);
491            assert_eq!(FAKE_KEY, builder.service_account_key.unwrap());
492        }
493    }
494
495    #[tokio::test]
496    async fn gcs_test_proxy_url() {
497        let mut tfile = NamedTempFile::new().unwrap();
498        write!(tfile, "{FAKE_KEY}").unwrap();
499        let service_account_path = tfile.path();
500        let gcs = GoogleBuilder::new()
501            .with_service_account_path(service_account_path.to_str().unwrap())
502            .with_proxy_url("https://example.com")
503            .build(None);
504        assert!(gcs.is_ok());
505    }
506
507    #[test]
508    fn gcs_test_service_account_key_only() {
509        let _ = GoogleBuilder::new()
510            .with_service_account_key(FAKE_KEY)
511            .build(None)
512            .unwrap();
513    }
514
515    #[test]
516    fn gcs_test_config_get_value() {
517        let google_service_account = "object_store:fake_service_account".to_string();
518        let builder = GoogleBuilder::new()
519            .with_config(GoogleConfigKey::ServiceAccount, &google_service_account);
520
521        assert_eq!(
522            builder
523                .get_config_value(&GoogleConfigKey::ServiceAccount)
524                .unwrap(),
525            google_service_account
526        );
527    }
528
529    #[test]
530    fn gcp_test_client_opts() {
531        let key = "GOOGLE_PROXY_URL";
532        if let Ok(config_key) = key.to_ascii_lowercase().parse() {
533            assert_eq!(
534                GoogleConfigKey::Client(ClientConfigKey::ProxyUrl),
535                config_key
536            );
537        } else {
538            panic!("{key} not propagated as ClientConfigKey");
539        }
540    }
541}