Skip to main content

reqsign_oracle/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 crate::Credential;
19use crate::provide_credential::{ConfigFileCredentialProvider, EnvCredentialProvider};
20use reqsign_core::{Context, ProvideCredential, ProvideCredentialChain, Result};
21
22/// Default loader for Oracle Cloud Infrastructure.
23///
24/// This loader will try to load credentials in the following order:
25/// 1. From environment variables
26/// 2. From the default Oracle config file (~/.oci/config)
27#[derive(Debug)]
28pub struct DefaultCredentialProvider {
29    chain: ProvideCredentialChain<Credential>,
30}
31
32impl Default for DefaultCredentialProvider {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl DefaultCredentialProvider {
39    /// Create a builder to configure the default credential chain.
40    pub fn builder() -> DefaultCredentialProviderBuilder {
41        DefaultCredentialProviderBuilder::default()
42    }
43
44    /// Create a new DefaultCredentialProvider using the default chain.
45    pub fn new() -> Self {
46        Self::builder().build()
47    }
48
49    /// Create with a custom credential chain.
50    pub fn with_chain(chain: ProvideCredentialChain<Credential>) -> Self {
51        Self { chain }
52    }
53
54    /// Add a credential provider to the front of the default chain.
55    ///
56    /// This allows adding a high-priority credential source that will be tried
57    /// before all other providers in the default chain.
58    ///
59    /// # Example
60    ///
61    /// ```no_run
62    /// use reqsign_oracle::{DefaultCredentialProvider, StaticCredentialProvider};
63    ///
64    /// let provider = DefaultCredentialProvider::new()
65    ///     .push_front(StaticCredentialProvider::new("user", "tenancy", "key_file", "fingerprint"));
66    /// ```
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
76/// Builder for `DefaultCredentialProvider`.
77///
78/// Use `env` / `config_file` to customize providers, `no_env` /
79/// `no_config_file` to remove them from the chain, and `build()` to construct
80/// the provider.
81///
82/// # Example
83///
84/// ```no_run
85/// use reqsign_oracle::{
86///     ConfigFileCredentialProvider, DefaultCredentialProvider, EnvCredentialProvider,
87/// };
88///
89/// let provider = DefaultCredentialProvider::builder()
90///     .env(EnvCredentialProvider::new())
91///     .no_config_file()
92///     .config_file(ConfigFileCredentialProvider::new())
93///     .build();
94/// ```
95pub struct DefaultCredentialProviderBuilder {
96    env: Option<EnvCredentialProvider>,
97    config_file: Option<ConfigFileCredentialProvider>,
98}
99
100impl Default for DefaultCredentialProviderBuilder {
101    fn default() -> Self {
102        Self {
103            env: Some(EnvCredentialProvider::default()),
104            config_file: Some(ConfigFileCredentialProvider::default()),
105        }
106    }
107}
108
109impl DefaultCredentialProviderBuilder {
110    /// Create a new builder with default state.
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    /// Set the environment credential provider slot.
116    pub fn env(mut self, provider: EnvCredentialProvider) -> Self {
117        self.env = Some(provider);
118        self
119    }
120
121    /// Remove the environment credential provider slot.
122    pub fn no_env(mut self) -> Self {
123        self.env = None;
124        self
125    }
126
127    /// Set the config-file credential provider slot.
128    pub fn config_file(mut self, provider: ConfigFileCredentialProvider) -> Self {
129        self.config_file = Some(provider);
130        self
131    }
132
133    /// Remove the config-file credential provider slot.
134    pub fn no_config_file(mut self) -> Self {
135        self.config_file = None;
136        self
137    }
138
139    /// Build the `DefaultCredentialProvider` with the configured options.
140    pub fn build(self) -> DefaultCredentialProvider {
141        let mut chain = ProvideCredentialChain::new();
142        if let Some(p) = self.env {
143            chain = chain.push(p);
144        }
145        if let Some(p) = self.config_file {
146            chain = chain.push(p);
147        }
148        DefaultCredentialProvider::with_chain(chain)
149    }
150}
151impl ProvideCredential for DefaultCredentialProvider {
152    type Credential = Credential;
153
154    async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
155        self.chain.provide_credential(ctx).await
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::constants::{
163        ORACLE_CONFIG_FILE, ORACLE_FINGERPRINT, ORACLE_KEY_FILE, ORACLE_TENANCY, ORACLE_USER,
164    };
165    use reqsign_core::{Context, StaticEnv};
166    use reqsign_file_read_tokio::TokioFileRead;
167    use reqsign_http_send_reqwest::ReqwestHttpSend;
168    use std::collections::HashMap;
169    use std::fs;
170    use std::time::{SystemTime, UNIX_EPOCH};
171
172    #[tokio::test]
173    async fn test_default_matches_new() {
174        let ctx = Context::new().with_env(StaticEnv {
175            home_dir: None,
176            envs: HashMap::from([
177                (ORACLE_USER.to_string(), "test_user".to_string()),
178                (ORACLE_TENANCY.to_string(), "test_tenancy".to_string()),
179                (ORACLE_KEY_FILE.to_string(), "/tmp/key.pem".to_string()),
180                (
181                    ORACLE_FINGERPRINT.to_string(),
182                    "test_fingerprint".to_string(),
183                ),
184            ]),
185        });
186
187        let from_default = DefaultCredentialProvider::default()
188            .provide_credential(&ctx)
189            .await
190            .expect("load must succeed")
191            .expect("credential must exist");
192        let from_new = DefaultCredentialProvider::new()
193            .provide_credential(&ctx)
194            .await
195            .expect("load must succeed")
196            .expect("credential must exist");
197
198        assert_eq!(from_default.user, from_new.user);
199        assert_eq!(from_default.tenancy, from_new.tenancy);
200        assert_eq!(from_default.key_file, from_new.key_file);
201        assert_eq!(from_default.fingerprint, from_new.fingerprint);
202        assert!(from_default.expires_in.is_some());
203        assert!(from_new.expires_in.is_some());
204    }
205
206    #[tokio::test]
207    async fn test_builder_no_env_removes_env_provider() {
208        let ctx = Context::new()
209            .with_file_read(TokioFileRead)
210            .with_http_send(ReqwestHttpSend::default())
211            .with_env(StaticEnv {
212                home_dir: Some("/tmp".into()),
213                envs: HashMap::from([
214                    (ORACLE_USER.to_string(), "test_user".to_string()),
215                    (ORACLE_TENANCY.to_string(), "test_tenancy".to_string()),
216                    (ORACLE_KEY_FILE.to_string(), "/tmp/key.pem".to_string()),
217                    (
218                        ORACLE_FINGERPRINT.to_string(),
219                        "test_fingerprint".to_string(),
220                    ),
221                ]),
222            });
223
224        let credential = DefaultCredentialProvider::builder()
225            .no_env()
226            .build()
227            .provide_credential(&ctx)
228            .await
229            .expect("load must succeed");
230
231        assert!(credential.is_none());
232    }
233
234    #[tokio::test]
235    async fn test_builder_no_config_file_removes_config_file_provider() {
236        let unique = SystemTime::now()
237            .duration_since(UNIX_EPOCH)
238            .expect("system time must be after unix epoch")
239            .as_nanos();
240        let root = std::env::temp_dir().join(format!("reqsign-oracle-default-provider-{unique}"));
241        let config_dir = root.join(".oci");
242        let config_path = config_dir.join("config");
243
244        fs::create_dir_all(&config_dir).expect("create config dir must succeed");
245        fs::write(
246            &config_path,
247            "[DEFAULT]\ntenancy=test_tenancy\nuser=test_user\nkey_file=/tmp/key.pem\nfingerprint=test_fingerprint\n",
248        )
249        .expect("write config file must succeed");
250
251        let ctx = Context::new()
252            .with_file_read(TokioFileRead)
253            .with_http_send(ReqwestHttpSend::default())
254            .with_env(StaticEnv {
255                home_dir: Some(root.clone()),
256                envs: HashMap::from([(
257                    ORACLE_CONFIG_FILE.to_string(),
258                    "~/.oci/config".to_string(),
259                )]),
260            });
261
262        let from_default = DefaultCredentialProvider::new()
263            .provide_credential(&ctx)
264            .await
265            .expect("load must succeed");
266        assert!(from_default.is_some());
267
268        let without_config_file = DefaultCredentialProvider::builder()
269            .no_config_file()
270            .build()
271            .provide_credential(&ctx)
272            .await
273            .expect("load must succeed");
274
275        assert!(without_config_file.is_none());
276
277        fs::remove_dir_all(&root).expect("cleanup temp dir must succeed");
278    }
279}