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 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 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 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 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 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 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 pub fn with_credentials(mut self, credentials: GcpCredentialProvider) -> Self {
291 self.credentials = Some(credentials);
292 self
293 }
294
295 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
297 self.retry_config = retry_config;
298 self
299 }
300
301 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 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 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 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
323 self.client_options = options;
324 self
325 }
326
327 pub fn build(self, runtime: Option<&Handle>) -> Result<GoogleConfig> {
333 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 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 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 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}