reqsign_google/provide_credential/
default.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// Default credential provider for Google Cloud Storage (GCS).
33///
34/// Resolution order follows ADC (Application Default Credentials):
35/// 1. Env var `GOOGLE_APPLICATION_CREDENTIALS`
36/// 2. Well-known location (`~/.config/gcloud/application_default_credentials.json`)
37/// 3. VM metadata service (GCE / Cloud Functions / App Engine)
38#[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    /// Create a builder to configure the default ADC chain for GCS.
51    pub fn builder() -> DefaultCredentialProviderBuilder {
52        DefaultCredentialProviderBuilder::default()
53    }
54
55    /// Create a new DefaultCredentialProvider with the default chain:
56    /// env ADC -> well-known ADC -> VM metadata
57    pub fn new() -> Self {
58        Self::builder().build()
59    }
60
61    /// Create with a custom credential chain.
62    pub fn with_chain(chain: ProvideCredentialChain<Credential>) -> Self {
63        Self { chain }
64    }
65
66    /// Add a credential provider to the front of the default chain.
67    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    /// Set the OAuth2 scope for ADC providers (deprecated).
76    ///
77    /// This helper configures the scope used by the environment and
78    /// well-known ADC providers, as well as the VM metadata provider, by
79    /// constructing a new chain with the provided scope. Prefer configuring
80    /// scope via specific providers (e.g., `VmMetadataCredentialProvider::with_scope`)
81    /// or using the `GOOGLE_SCOPE` environment variable.
82    #[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    /// Set the OAuth2 scope to request when exchanging ADC credentials.
137    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    /// Set the OAuth2 scope to request when exchanging ADC credentials.
176    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/// Builder for `DefaultCredentialProvider`.
252///
253/// Use `configure_vm_metadata` to customize VM metadata behavior and
254/// `disable_env` / `disable_well_known` / `disable_vm_metadata` to control
255/// participation. Call `build()` to construct the provider.
256#[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    /// Create a new builder with default state.
265    pub fn new() -> Self {
266        Self::default()
267    }
268
269    // No global scope configurator; configure scope on specific providers if needed.
270
271    /// Configure the VM metadata provider.
272    ///
273    /// This allows setting a custom endpoint or other options for retrieving
274    /// tokens when running on Google Compute Engine or compatible environments.
275    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    /// Disable (true) or ensure enabled (false) the env-based ADC provider.
285    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    /// Disable (true) or ensure enabled (false) the well-known ADC provider.
295    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    /// Disable (true) or ensure enabled (false) the VM metadata provider.
305    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    /// Build the `DefaultCredentialProvider` with the configured options.
315    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        // Even without valid credentials, this should not panic
385        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}