1use 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#[derive(Debug, Clone)]
74pub struct GoogleBuilder {
75 service_account_path: Option<String>,
77 service_account_key: Option<String>,
79 application_credentials_path: Option<String>,
81 retry_config: RetryConfig,
83 client_options: ClientOptions,
85 credentials: Option<GcpCredentialProvider>,
87}
88
89#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
100#[non_exhaustive]
101pub enum GoogleConfigKey {
102 ServiceAccount,
110
111 ServiceAccountKey,
117
118 ApplicationCredentials,
122
123 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 pub fn new() -> Self {
173 Default::default()
174 }
175
176 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 && key.starts_with("GOOGLE_")
201 && let Ok(config_key) = key.to_ascii_lowercase().parse()
202 {
203 builder = builder.with_config(config_key, value);
204 }
205 }
206
207 builder
208 }
209
210 pub fn with_config(mut self, key: GoogleConfigKey, value: impl Into<String>) -> Self {
212 match key {
213 GoogleConfigKey::ServiceAccount => self.service_account_path = Some(value.into()),
214 GoogleConfigKey::ServiceAccountKey => self.service_account_key = Some(value.into()),
215 GoogleConfigKey::ApplicationCredentials => {
216 self.application_credentials_path = Some(value.into())
217 }
218 GoogleConfigKey::Client(key) => {
219 self.client_options = self.client_options.with_config(key, value)
220 }
221 };
222 self
223 }
224
225 pub fn get_config_value(&self, key: &GoogleConfigKey) -> Option<String> {
237 match key {
238 GoogleConfigKey::ServiceAccount => self.service_account_path.clone(),
239 GoogleConfigKey::ServiceAccountKey => self.service_account_key.clone(),
240 GoogleConfigKey::ApplicationCredentials => self.application_credentials_path.clone(),
241 GoogleConfigKey::Client(key) => self.client_options.get_config_value(key),
242 }
243 }
244
245 pub fn with_service_account_path(mut self, service_account_path: impl Into<String>) -> Self {
263 self.service_account_path = Some(service_account_path.into());
264 self
265 }
266
267 pub fn with_service_account_key(mut self, service_account: impl Into<String>) -> Self {
273 self.service_account_key = Some(service_account.into());
274 self
275 }
276
277 pub fn with_application_credentials(
281 mut self,
282 application_credentials_path: impl Into<String>,
283 ) -> Self {
284 self.application_credentials_path = Some(application_credentials_path.into());
285 self
286 }
287
288 pub fn with_credentials(mut self, credentials: GcpCredentialProvider) -> Self {
290 self.credentials = Some(credentials);
291 self
292 }
293
294 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
296 self.retry_config = retry_config;
297 self
298 }
299
300 pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
302 self.client_options = self.client_options.with_proxy_url(proxy_url);
303 self
304 }
305
306 pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
308 self.client_options = self
309 .client_options
310 .with_proxy_ca_certificate(proxy_ca_certificate);
311 self
312 }
313
314 pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
316 self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
317 self
318 }
319
320 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
322 self.client_options = options;
323 self
324 }
325
326 pub fn build(self, runtime: Option<&Handle>) -> Result<GoogleConfig> {
332 let service_account_credentials =
334 match (self.service_account_path, self.service_account_key) {
335 (Some(path), None) => Some(
336 ServiceAccountCredentials::from_file(path)
337 .map_err(|source| Error::Credential { source })?,
338 ),
339 (None, Some(key)) => Some(
340 ServiceAccountCredentials::from_key(&key)
341 .map_err(|source| Error::Credential { source })?,
342 ),
343 (None, None) => None,
344 (Some(_), Some(_)) => return Err(Error::ServiceAccountPathAndKeyProvided.into()),
345 };
346
347 let application_default_credentials =
349 ApplicationDefaultCredentials::read(self.application_credentials_path.as_deref())?;
350
351 let disable_oauth = service_account_credentials
352 .as_ref()
353 .map(|c| c.disable_oauth)
354 .unwrap_or(false);
355
356 let credentials = if let Some(credentials) = self.credentials {
357 credentials
358 } else if disable_oauth {
359 Arc::new(StaticCredentialProvider::new(GcpCredential {
360 bearer: "".to_string(),
361 })) as _
362 } else if let Some(credentials) = service_account_credentials.clone() {
363 let client = self.client_options.client()?;
364 let service = make_service(client.clone(), runtime);
365 Arc::new(TokenCredentialProvider::new(
366 credentials.token_provider()?,
367 client,
368 service,
369 self.retry_config.clone(),
370 )) as _
371 } else if let Some(credentials) = application_default_credentials.clone() {
372 match credentials {
373 ApplicationDefaultCredentials::AuthorizedUser(token) => {
374 let client = self.client_options.client()?;
375 let service = make_service(client.clone(), runtime);
376 Arc::new(
377 TokenCredentialProvider::new(
378 token,
379 client,
380 service,
381 self.retry_config.clone(),
382 )
383 .with_min_ttl(TOKEN_MIN_TTL),
384 ) as _
385 }
386 ApplicationDefaultCredentials::ServiceAccount(token) => {
387 let client = self.client_options.client()?;
388 let service = make_service(client.clone(), runtime);
389 Arc::new(TokenCredentialProvider::new(
390 token.token_provider()?,
391 client,
392 service,
393 self.retry_config.clone(),
394 )) as _
395 }
396 ApplicationDefaultCredentials::ExternalAccount(ext) => {
397 let client = self.client_options.client()?;
398 let service = make_service(client.clone(), runtime);
399 let sts_endpoint = ext.token_url.clone();
400 Arc::new(
401 TokenCredentialProvider::new(
402 GcpWorkloadIdentityProvider {
403 credentials: ext,
404 sts_endpoint,
405 },
406 client,
407 service,
408 self.retry_config.clone(),
409 )
410 .with_min_ttl(TOKEN_MIN_TTL),
411 ) as _
412 }
413 }
414 } else {
415 let client = self.client_options.metadata_client()?;
416 let service = make_service(client.clone(), runtime);
417 Arc::new(
418 TokenCredentialProvider::new(
419 InstanceCredentialProvider::default(),
420 client,
421 service,
422 self.retry_config.clone(),
423 )
424 .with_min_ttl(TOKEN_MIN_TTL),
425 ) as _
426 };
427
428 Ok(GoogleConfig::new(
429 credentials,
430 self.retry_config,
431 self.client_options,
432 ))
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use std::collections::HashMap;
440 use std::io::Write;
441 use tempfile::NamedTempFile;
442
443 const FAKE_KEY: &str = r#"{"private_key": "private_key", "private_key_id": "private_key_id", "client_email":"client_email", "disable_oauth":true}"#;
444
445 #[test]
446 fn gcs_test_service_account_key_and_path() {
447 let mut tfile = NamedTempFile::new().unwrap();
448 write!(tfile, "{FAKE_KEY}").unwrap();
449 let _ = GoogleBuilder::new()
450 .with_service_account_key(FAKE_KEY)
451 .with_service_account_path(tfile.path().to_str().unwrap())
452 .build(None)
453 .unwrap_err();
454 }
455
456 #[test]
457 fn gcs_test_config_from_map() {
458 let google_service_account = "object_store:fake_service_account".to_string();
459 let options = HashMap::from([("google_service_account", google_service_account.clone())]);
460
461 let builder = options
462 .iter()
463 .fold(GoogleBuilder::new(), |builder, (key, value)| {
464 builder.with_config(key.parse().unwrap(), value)
465 });
466
467 assert_eq!(
468 builder.service_account_path.unwrap(),
469 google_service_account.as_str()
470 );
471 }
472
473 #[test]
474 fn gcs_test_config_aliases() {
475 for alias in [
477 "google_service_account",
478 "service_account",
479 "google_service_account_path",
480 "service_account_path",
481 ] {
482 let builder =
483 GoogleBuilder::new().with_config(alias.parse().unwrap(), "/fake/path.json");
484 assert_eq!("/fake/path.json", builder.service_account_path.unwrap());
485 }
486
487 for alias in ["google_service_account_key", "service_account_key"] {
489 let builder = GoogleBuilder::new().with_config(alias.parse().unwrap(), FAKE_KEY);
490 assert_eq!(FAKE_KEY, builder.service_account_key.unwrap());
491 }
492 }
493
494 #[tokio::test]
495 async fn gcs_test_proxy_url() {
496 let mut tfile = NamedTempFile::new().unwrap();
497 write!(tfile, "{FAKE_KEY}").unwrap();
498 let service_account_path = tfile.path();
499 let gcs = GoogleBuilder::new()
500 .with_service_account_path(service_account_path.to_str().unwrap())
501 .with_proxy_url("https://example.com")
502 .build(None);
503 assert!(gcs.is_ok());
504 }
505
506 #[test]
507 fn gcs_test_service_account_key_only() {
508 let _ = GoogleBuilder::new()
509 .with_service_account_key(FAKE_KEY)
510 .build(None)
511 .unwrap();
512 }
513
514 #[test]
515 fn gcs_test_config_get_value() {
516 let google_service_account = "object_store:fake_service_account".to_string();
517 let builder = GoogleBuilder::new()
518 .with_config(GoogleConfigKey::ServiceAccount, &google_service_account);
519
520 assert_eq!(
521 builder
522 .get_config_value(&GoogleConfigKey::ServiceAccount)
523 .unwrap(),
524 google_service_account
525 );
526 }
527
528 #[test]
529 fn gcp_test_client_opts() {
530 let key = "GOOGLE_PROXY_URL";
531 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
532 assert_eq!(
533 GoogleConfigKey::Client(ClientConfigKey::ProxyUrl),
534 config_key
535 );
536 } else {
537 panic!("{key} not propagated as ClientConfigKey");
538 }
539 }
540}