Skip to main content

ya_gcp/
builder.rs

1//! Items and functions used to create new service clients.
2//!
3//! See [`ClientBuilder`], which is used to instantiate the various GCP service clients.
4
5use crate::auth::Auth;
6use std::path::PathBuf;
7
8#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
9use std::path::Path;
10
11const SERVICE_ACCOUNT_ENV_VAR: &str = "GOOGLE_APPLICATION_CREDENTIALS";
12
13/// Configuration for loading service account credentials from file
14#[derive(Debug, Clone, Default, serde::Deserialize)]
15#[serde(rename_all = "snake_case")]
16#[non_exhaustive]
17pub enum ServiceAccountAuth {
18    /// Specifies that the service account credentials should be read from the path stored in the
19    /// environment variable `GOOGLE_APPLICATION_CREDENTIALS`
20    #[default]
21    EnvVar,
22
23    /// Specifies that the service account credentials should be read from the given path
24    Path(PathBuf),
25
26    /// Use the Application Default Service Account, which is often attached to a specific
27    /// compute instance via metadata
28    ApplicationDefault,
29}
30
31/// A marker to choose the mechanism by which authentication credentials should be loaded
32#[derive(Debug, Clone, serde::Deserialize)]
33#[serde(rename_all = "snake_case")]
34#[non_exhaustive]
35pub enum AuthFlow {
36    /// Load credentials for a service account
37    ServiceAccount(ServiceAccountAuth),
38
39    /// Load credentials from an authorized user secret, such as the one created when
40    /// running `gcloud auth application-default login`
41    UserAccount(PathBuf),
42
43    /// Impersonate a service account from a user account
44    ServiceAccountImpersonation {
45        /// Path to the user credentials
46        user: PathBuf,
47        /// Email address of the service account
48        email: String,
49    },
50
51    /// Skip authentication.
52    ///
53    /// Requests will not include any authorization header. This can be useful for tests
54    NoAuth,
55    // TODO consider support for InstalledFlow, DeviceFlow, etc
56}
57
58impl Default for AuthFlow {
59    fn default() -> Self {
60        AuthFlow::ServiceAccount(ServiceAccountAuth::default())
61    }
62}
63
64config_default! {
65    /// Configuration for creating a [`ClientBuilder`]
66    #[derive(Debug, Clone, serde::Deserialize)]
67    pub struct ClientBuilderConfig {
68        /// How authentication credentials should be loaded
69        @default(AuthFlow::ServiceAccount(ServiceAccountAuth::EnvVar), "ClientBuilderConfig::default_auth_flow")
70        pub auth_flow: AuthFlow,
71    }
72}
73
74/// The possible errors encountered when creating a [`ClientBuilder`]
75#[derive(Debug, thiserror::Error)]
76#[non_exhaustive]
77pub enum CreateBuilderError {
78    /// An error in loading workload identity federation / external-account credentials from file
79    #[error("failed to read external account credentials from {}", _1.display())]
80    ReadExternalAccountCredential(#[source] std::io::Error, PathBuf),
81
82    /// An error in loading service account credentials from file
83    #[error("failed to read service account key {}", _1.display())]
84    ReadServiceAccountKey(#[source] std::io::Error, PathBuf),
85
86    /// An error in loading user account credentials from file
87    #[error("failed to read user account secrets {}", _1.display())]
88    ReadUserSecrets(#[source] std::io::Error, PathBuf),
89
90    /// An error in reading an environment variable
91    #[error(
92        "environment variable {SERVICE_ACCOUNT_ENV_VAR} isn't set. Consider either setting the \
93         variable, or specifying the path with ServiceAccountAuth::Path(...)"
94    )]
95    CredentialsVarMissing,
96
97    /// An error in initializing the authenticator
98    #[error("failed to initialize authenticator")]
99    Authenticator(#[source] std::io::Error),
100
101    /// An error in initializing the HTTP connector
102    #[error("failed to initialize HTTP connector")]
103    Connector(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
104}
105
106#[allow(unused)] // only used by some feature combinations
107pub(crate) fn https_connector() -> hyper_rustls::HttpsConnector<hyper::client::HttpConnector> {
108    #[allow(unused_mut)]
109    let mut roots = rustls::RootCertStore::empty();
110
111    #[cfg(feature = "rustls-native-certs")]
112    roots.add_parsable_certificates(
113        &rustls_native_certs::load_native_certs().expect("could not load native certs"),
114    );
115
116    #[cfg(feature = "webpki-roots")]
117    roots.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| {
118        rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
119            ta.subject,
120            ta.spki,
121            ta.name_constraints,
122        )
123    }));
124
125    let tls_config = rustls::client::ClientConfig::builder()
126        .with_safe_defaults()
127        .with_root_certificates(roots)
128        .with_no_client_auth();
129
130    hyper_rustls::HttpsConnectorBuilder::new()
131        .with_tls_config(tls_config)
132        .https_or_http()
133        .enable_all_versions()
134        .build()
135}
136
137/// A builder used to create all the clients for interacting with GCP services.
138///
139/// Note that the builder is not consumed when creating clients, and many clients can be built
140/// using the same builder. This may allow some resource re-use across the clients
141pub struct ClientBuilder {
142    #[allow(unused)]
143    pub(crate) auth: Option<Auth>,
144}
145
146impl ClientBuilder {
147    /// Create a new client builder with credentials loaded per `config`.
148    ///
149    /// Service clients built from this builder (for example GCS or Pub/Sub) use a rustls-backed
150    /// HTTPS connector configured from this crate's TLS feature flags. OAuth token exchange is
151    /// handled separately by yup-oauth2 and does not use that connector.
152    #[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
153    pub async fn new(config: ClientBuilderConfig) -> Result<Self, CreateBuilderError> {
154        use AuthFlow::{NoAuth, ServiceAccount, ServiceAccountImpersonation, UserAccount};
155
156        let auth = match config.auth_flow {
157            NoAuth => None,
158            ServiceAccount(service_config) => Some(
159                create_service_auth(match service_config {
160                    ServiceAccountAuth::Path(path) => Some(path.into_os_string()),
161                    ServiceAccountAuth::EnvVar => Some(
162                        std::env::var_os(SERVICE_ACCOUNT_ENV_VAR)
163                            .ok_or(CreateBuilderError::CredentialsVarMissing)?,
164                    ),
165                    ServiceAccountAuth::ApplicationDefault => None,
166                })
167                .await?,
168            ),
169            ServiceAccountImpersonation { user, email } => {
170                Some(create_service_impersonation_auth(user.into_os_string(), email).await?)
171            }
172            UserAccount(path) => Some(create_user_auth(path.into_os_string()).await?),
173        };
174
175        Ok(Self { auth })
176    }
177}
178
179#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
180fn is_external_account_json(contents: &str) -> bool {
181    serde_json::from_str::<serde_json::Value>(contents)
182        .ok()
183        .and_then(|v| v.get("type")?.as_str().map(|t| t == "external_account"))
184        .unwrap_or(false)
185}
186
187#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
188fn log_external_account_credentials(path: &Path) {
189    tracing::info!(
190        auth_build = crate::AUTH_BUILD_ID,
191        path = %path.display(),
192        "ya-gcp: using external_account credentials"
193    );
194}
195
196#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
197async fn build_external_account_auth(path: &Path) -> Result<Auth, CreateBuilderError> {
198    log_external_account_credentials(path);
199
200    let secret = yup_oauth2::read_external_account_secret(path)
201        .await
202        .map_err(|e| CreateBuilderError::ReadExternalAccountCredential(e, path.to_owned()))?;
203
204    yup_oauth2::ExternalAccountAuthenticator::builder(secret)
205        .build()
206        .await
207        .map_err(CreateBuilderError::Authenticator)
208}
209
210/// Load WIF / external-account credentials from `GOOGLE_APPLICATION_CREDENTIALS` when set.
211#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
212async fn external_account_auth_from_gac_env() -> Result<Option<Auth>, CreateBuilderError> {
213    let Ok(path_buf) = std::env::var(SERVICE_ACCOUNT_ENV_VAR).map(PathBuf::from) else {
214        return Ok(None);
215    };
216
217    if !tokio::fs::try_exists(&path_buf).await.unwrap_or(false) {
218        return Ok(None);
219    }
220
221    let Ok(contents) = tokio::fs::read_to_string(&path_buf).await else {
222        return Ok(None);
223    };
224
225    if !is_external_account_json(&contents) {
226        return Ok(None);
227    }
228
229    build_external_account_auth(&path_buf).await.map(Some)
230}
231
232/// Convenience method to create an Authorization for the oauth ServiceFlow.
233#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
234async fn create_service_auth(
235    service_account_key_path: Option<impl AsRef<std::path::Path>>,
236) -> Result<Auth, CreateBuilderError> {
237    match service_account_key_path.as_ref().map(|p| p.as_ref()) {
238        Some(path) => {
239            let contents = tokio::fs::read_to_string(path)
240                .await
241                .map_err(|e| CreateBuilderError::ReadServiceAccountKey(e, path.to_owned()))?;
242
243            if is_external_account_json(&contents) {
244                return build_external_account_auth(path).await;
245            }
246
247            let service_account_key = yup_oauth2::read_service_account_key(path)
248                .await
249                .map_err(|e| CreateBuilderError::ReadServiceAccountKey(e, path.to_owned()))?;
250
251            yup_oauth2::ServiceAccountAuthenticator::builder(service_account_key)
252                .build()
253                .await
254                .map_err(CreateBuilderError::Authenticator)
255        }
256        None => {
257            if let Some(auth) = external_account_auth_from_gac_env().await? {
258                return Ok(auth);
259            }
260
261            match yup_oauth2::ApplicationDefaultCredentialsAuthenticator::builder(
262                yup_oauth2::ApplicationDefaultCredentialsFlowOpts::default(),
263            )
264            .await
265            {
266                yup_oauth2::authenticator::ApplicationDefaultCredentialsTypes::ServiceAccount(
267                    auth,
268                ) => auth
269                    .build()
270                    .await
271                    .map_err(CreateBuilderError::Authenticator),
272                yup_oauth2::authenticator::ApplicationDefaultCredentialsTypes::InstanceMetadata(
273                    auth,
274                ) => auth
275                    .build()
276                    .await
277                    .map_err(CreateBuilderError::Authenticator),
278            }
279        }
280    }
281}
282
283#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
284async fn create_user_auth(
285    user_secrets_path: impl AsRef<std::path::Path>,
286) -> Result<Auth, CreateBuilderError> {
287    let user_secret = yup_oauth2::read_authorized_user_secret(user_secrets_path.as_ref())
288        .await
289        .map_err(|e| {
290            CreateBuilderError::ReadUserSecrets(e, user_secrets_path.as_ref().to_owned())
291        })?;
292
293    yup_oauth2::AuthorizedUserAuthenticator::builder(user_secret)
294        .build()
295        .await
296        .map_err(CreateBuilderError::Authenticator)
297}
298
299#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
300async fn create_service_impersonation_auth(
301    user_secrets_path: impl AsRef<std::path::Path>,
302    email: String,
303) -> Result<Auth, CreateBuilderError> {
304    let user_secret = yup_oauth2::read_authorized_user_secret(user_secrets_path.as_ref())
305        .await
306        .map_err(|e| {
307            CreateBuilderError::ReadUserSecrets(e, user_secrets_path.as_ref().to_owned())
308        })?;
309
310    yup_oauth2::ServiceAccountImpersonationAuthenticator::builder(user_secret, &email)
311        .build()
312        .await
313        .map_err(CreateBuilderError::Authenticator)
314}