reqsign_google/provide_credential/
default.rs1use log::debug;
19
20use reqsign_core::{Context, ProvideCredential, ProvideCredentialChain, Result};
21
22use crate::constants::{DEFAULT_SCOPE, GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_SCOPE};
23use crate::credential::{Credential, CredentialFile};
24
25use super::{
26 authorized_user::AuthorizedUserCredentialProvider,
27 external_account::ExternalAccountCredentialProvider,
28 impersonated_service_account::ImpersonatedServiceAccountCredentialProvider,
29 vm_metadata::VmMetadataCredentialProvider,
30};
31
32#[derive(Debug)]
39pub struct DefaultCredentialProvider {
40 chain: ProvideCredentialChain<Credential>,
41}
42
43impl Default for DefaultCredentialProvider {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl DefaultCredentialProvider {
50 pub fn builder() -> DefaultCredentialProviderBuilder {
52 DefaultCredentialProviderBuilder::default()
53 }
54
55 pub fn new() -> Self {
58 Self::builder().build()
59 }
60
61 pub fn with_chain(chain: ProvideCredentialChain<Credential>) -> Self {
63 Self { chain }
64 }
65
66 pub fn push_front(
68 mut self,
69 provider: impl ProvideCredential<Credential = Credential> + 'static,
70 ) -> Self {
71 self.chain = self.chain.push_front(provider);
72 self
73 }
74
75 #[deprecated(
83 since = "1.0.0",
84 note = "Configure scope via specific providers or GOOGLE_SCOPE env var"
85 )]
86 pub fn with_scope(self, scope: impl Into<String>) -> Self {
87 let s = scope.into();
88 let chain = ProvideCredentialChain::new()
89 .push(EnvAdcCredentialProvider::new().with_scope(s.clone()))
90 .push(WellKnownAdcCredentialProvider::new().with_scope(s.clone()))
91 .push(VmMetadataCredentialProvider::new().with_scope(s));
92 Self { chain }
93 }
94
95 #[deprecated(
96 since = "1.0.0",
97 note = "Use DefaultCredentialProvider::builder().disable_env(skip).build() instead"
98 )]
99 pub fn skip_env_credentials(self, skip: bool) -> Self {
100 DefaultCredentialProvider::builder()
101 .disable_env(skip)
102 .build()
103 }
104
105 #[deprecated(
106 since = "1.0.0",
107 note = "Use DefaultCredentialProvider::builder().disable_well_known(skip).build() instead"
108 )]
109 pub fn skip_well_known_location(self, skip: bool) -> Self {
110 DefaultCredentialProvider::builder()
111 .disable_well_known(skip)
112 .build()
113 }
114}
115
116#[async_trait::async_trait]
117impl ProvideCredential for DefaultCredentialProvider {
118 type Credential = Credential;
119
120 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
121 self.chain.provide_credential(ctx).await
122 }
123}
124
125#[derive(Default, Clone, Debug)]
126struct EnvAdcCredentialProvider {
127 disabled: Option<bool>,
128 scope: Option<String>,
129}
130
131impl EnvAdcCredentialProvider {
132 fn new() -> Self {
133 Self::default()
134 }
135
136 fn with_scope(mut self, scope: impl Into<String>) -> Self {
138 self.scope = Some(scope.into());
139 self
140 }
141}
142
143#[async_trait::async_trait]
144impl ProvideCredential for EnvAdcCredentialProvider {
145 type Credential = Credential;
146
147 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
148 if self.disabled.unwrap_or(false) {
149 return Ok(None);
150 }
151
152 let path = match ctx.env_var(GOOGLE_APPLICATION_CREDENTIALS) {
153 Some(path) if !path.is_empty() => path,
154 _ => return Ok(None),
155 };
156
157 debug!("trying to load credential from env GOOGLE_APPLICATION_CREDENTIALS: {path}");
158
159 let content = ctx.file_read(&path).await?;
160 parse_credential_bytes(ctx, &content, self.scope.clone()).await
161 }
162}
163
164#[derive(Default, Clone, Debug)]
165struct WellKnownAdcCredentialProvider {
166 disabled: Option<bool>,
167 scope: Option<String>,
168}
169
170impl WellKnownAdcCredentialProvider {
171 fn new() -> Self {
172 Self::default()
173 }
174
175 fn with_scope(mut self, scope: impl Into<String>) -> Self {
177 self.scope = Some(scope.into());
178 self
179 }
180}
181
182#[async_trait::async_trait]
183impl ProvideCredential for WellKnownAdcCredentialProvider {
184 type Credential = Credential;
185
186 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
187 if self.disabled.unwrap_or(false) {
188 return Ok(None);
189 }
190
191 let config_dir = if let Some(v) = ctx.env_var("APPDATA") {
192 v
193 } else if let Some(v) = ctx.env_var("XDG_CONFIG_HOME") {
194 v
195 } else if let Some(v) = ctx.env_var("HOME") {
196 format!("{v}/.config")
197 } else {
198 return Ok(None);
199 };
200
201 let path = format!("{config_dir}/gcloud/application_default_credentials.json");
202 debug!("trying to load credential from well-known location: {path}");
203
204 let content = match ctx.file_read(&path).await {
205 Ok(v) => v,
206 Err(_) => return Ok(None),
207 };
208
209 match parse_credential_bytes(ctx, &content, self.scope.clone()).await {
210 Ok(v) => Ok(v),
211 Err(_) => Ok(None),
212 }
213 }
214}
215
216async fn parse_credential_bytes(
217 ctx: &Context,
218 content: &[u8],
219 scope_override: Option<String>,
220) -> Result<Option<Credential>> {
221 let cred_file = CredentialFile::from_slice(content)?;
222
223 let scope = scope_override
224 .or_else(|| ctx.env_var(GOOGLE_SCOPE))
225 .unwrap_or_else(|| DEFAULT_SCOPE.to_string());
226
227 match cred_file {
228 CredentialFile::ServiceAccount(sa) => {
229 debug!("loaded service account credential");
230 Ok(Some(Credential::with_service_account(sa)))
231 }
232 CredentialFile::ExternalAccount(ea) => {
233 debug!("loaded external account credential, exchanging for token");
234 let provider = ExternalAccountCredentialProvider::new(ea).with_scope(&scope);
235 provider.provide_credential(ctx).await
236 }
237 CredentialFile::ImpersonatedServiceAccount(isa) => {
238 debug!("loaded impersonated service account credential, exchanging for token");
239 let provider =
240 ImpersonatedServiceAccountCredentialProvider::new(isa).with_scope(&scope);
241 provider.provide_credential(ctx).await
242 }
243 CredentialFile::AuthorizedUser(au) => {
244 debug!("loaded authorized user credential, exchanging for token");
245 let provider = AuthorizedUserCredentialProvider::new(au);
246 provider.provide_credential(ctx).await
247 }
248 }
249}
250
251#[derive(Default)]
257pub struct DefaultCredentialProviderBuilder {
258 env_adc: Option<EnvAdcCredentialProvider>,
259 well_known_adc: Option<WellKnownAdcCredentialProvider>,
260 vm_metadata: Option<VmMetadataCredentialProvider>,
261}
262
263impl DefaultCredentialProviderBuilder {
264 pub fn new() -> Self {
266 Self::default()
267 }
268
269 pub fn configure_vm_metadata<F>(mut self, f: F) -> Self
276 where
277 F: FnOnce(VmMetadataCredentialProvider) -> VmMetadataCredentialProvider,
278 {
279 let p = self.vm_metadata.take().unwrap_or_default();
280 self.vm_metadata = Some(f(p));
281 self
282 }
283
284 pub fn disable_env(mut self, disable: bool) -> Self {
286 if disable {
287 self.env_adc = None;
288 } else if self.env_adc.is_none() {
289 self.env_adc = Some(EnvAdcCredentialProvider::new());
290 }
291 self
292 }
293
294 pub fn disable_well_known(mut self, disable: bool) -> Self {
296 if disable {
297 self.well_known_adc = None;
298 } else if self.well_known_adc.is_none() {
299 self.well_known_adc = Some(WellKnownAdcCredentialProvider::new());
300 }
301 self
302 }
303
304 pub fn disable_vm_metadata(mut self, disable: bool) -> Self {
306 if disable {
307 self.vm_metadata = None;
308 } else if self.vm_metadata.is_none() {
309 self.vm_metadata = Some(VmMetadataCredentialProvider::new());
310 }
311 self
312 }
313
314 pub fn build(self) -> DefaultCredentialProvider {
316 let mut chain = ProvideCredentialChain::new();
317
318 if let Some(p) = self.env_adc {
319 chain = chain.push(p);
320 } else {
321 chain = chain.push(EnvAdcCredentialProvider::new());
322 }
323
324 if let Some(p) = self.well_known_adc {
325 chain = chain.push(p);
326 } else {
327 chain = chain.push(WellKnownAdcCredentialProvider::new());
328 }
329
330 if let Some(p) = self.vm_metadata {
331 chain = chain.push(p);
332 } else {
333 chain = chain.push(VmMetadataCredentialProvider::new());
334 }
335
336 DefaultCredentialProvider::with_chain(chain)
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use reqsign_core::{Context, StaticEnv};
344 use std::collections::HashMap;
345 use std::env;
346
347 #[tokio::test]
348 async fn test_default_provider_env() {
349 let envs = HashMap::from([(
350 GOOGLE_APPLICATION_CREDENTIALS.to_string(),
351 format!(
352 "{}/testdata/test_credential.json",
353 env::current_dir()
354 .expect("current_dir must exist")
355 .to_string_lossy()
356 ),
357 )]);
358
359 let ctx = Context::new()
360 .with_file_read(reqsign_file_read_tokio::TokioFileRead)
361 .with_http_send(reqsign_http_send_reqwest::ReqwestHttpSend::default())
362 .with_env(StaticEnv {
363 home_dir: None,
364 envs,
365 });
366
367 let provider = DefaultCredentialProvider::new();
368 let cred = provider
369 .provide_credential(&ctx)
370 .await
371 .expect("load must succeed");
372 assert!(cred.is_some());
373
374 let cred = cred.unwrap();
375 assert!(cred.has_service_account());
376 let sa = cred.service_account.as_ref().unwrap();
377 assert_eq!("test-234@test.iam.gserviceaccount.com", &sa.client_email);
378 }
379
380 #[tokio::test]
381 async fn test_default_provider_with_scope() {
382 let provider = DefaultCredentialProvider::builder().build();
383
384 let ctx = Context::new()
386 .with_file_read(reqsign_file_read_tokio::TokioFileRead)
387 .with_http_send(reqsign_http_send_reqwest::ReqwestHttpSend::default());
388 let _ = provider.provide_credential(&ctx).await;
389 }
390}