1use 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#[derive(Debug, Clone, Default, serde::Deserialize)]
15#[serde(rename_all = "snake_case")]
16#[non_exhaustive]
17pub enum ServiceAccountAuth {
18 #[default]
21 EnvVar,
22
23 Path(PathBuf),
25
26 ApplicationDefault,
29}
30
31#[derive(Debug, Clone, serde::Deserialize)]
33#[serde(rename_all = "snake_case")]
34#[non_exhaustive]
35pub enum AuthFlow {
36 ServiceAccount(ServiceAccountAuth),
38
39 UserAccount(PathBuf),
42
43 ServiceAccountImpersonation {
45 user: PathBuf,
47 email: String,
49 },
50
51 NoAuth,
55 }
57
58impl Default for AuthFlow {
59 fn default() -> Self {
60 AuthFlow::ServiceAccount(ServiceAccountAuth::default())
61 }
62}
63
64config_default! {
65 #[derive(Debug, Clone, serde::Deserialize)]
67 pub struct ClientBuilderConfig {
68 @default(AuthFlow::ServiceAccount(ServiceAccountAuth::EnvVar), "ClientBuilderConfig::default_auth_flow")
70 pub auth_flow: AuthFlow,
71 }
72}
73
74#[derive(Debug, thiserror::Error)]
76#[non_exhaustive]
77pub enum CreateBuilderError {
78 #[error("failed to read external account credentials from {}", _1.display())]
80 ReadExternalAccountCredential(#[source] std::io::Error, PathBuf),
81
82 #[error("failed to read service account key {}", _1.display())]
84 ReadServiceAccountKey(#[source] std::io::Error, PathBuf),
85
86 #[error("failed to read user account secrets {}", _1.display())]
88 ReadUserSecrets(#[source] std::io::Error, PathBuf),
89
90 #[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 #[error("failed to initialize authenticator")]
99 Authenticator(#[source] std::io::Error),
100
101 #[error("failed to initialize HTTP connector")]
103 Connector(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
104}
105
106#[allow(unused)] pub(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
137pub struct ClientBuilder {
142 #[allow(unused)]
143 pub(crate) auth: Option<Auth>,
144}
145
146impl ClientBuilder {
147 #[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#[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#[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}