reqsign/aws/
config.rs

1use std::collections::HashMap;
2use std::env;
3#[cfg(not(target_arch = "wasm32"))]
4use std::fs;
5
6#[cfg(not(target_arch = "wasm32"))]
7use anyhow::anyhow;
8#[cfg(not(target_arch = "wasm32"))]
9use anyhow::Result;
10#[cfg(not(target_arch = "wasm32"))]
11use ini::Ini;
12#[cfg(not(target_arch = "wasm32"))]
13use log::debug;
14
15use super::constants::*;
16#[cfg(not(target_arch = "wasm32"))]
17use crate::dirs::expand_homedir;
18
19/// Config for aws services.
20#[derive(Clone)]
21#[cfg_attr(test, derive(Debug))]
22pub struct Config {
23    /// `config_file` will be load from:
24    ///
25    /// - env value: [`AWS_CONFIG_FILE`]
26    /// - default to: `~/.aws/config`
27    pub config_file: String,
28    /// `shared_credentials_file` will be loaded from:
29    ///
30    /// - env value: [`AWS_SHARED_CREDENTIALS_FILE`]
31    /// - default to: `~/.aws/credentials`
32    pub shared_credentials_file: String,
33    /// `profile` will be loaded from:
34    ///
35    /// - this field if it's `is_some`
36    /// - env value: [`AWS_PROFILE`]
37    /// - default to: `default`
38    pub profile: String,
39
40    /// `region` will be loaded from:
41    ///
42    /// - this field if it's `is_some`
43    /// - env value: [`AWS_REGION`]
44    /// - profile config: `region`
45    pub region: Option<String>,
46    /// `sts_regional_endpoints` will be loaded from:
47    ///
48    /// - env value: [`AWS_STS_REGIONAL_ENDPOINTS`]
49    /// - profile config: `sts_regional_endpoints`
50    /// - default to `legacy`
51    pub sts_regional_endpoints: String,
52    /// `access_key_id` will be loaded from
53    ///
54    /// - this field if it's `is_some`
55    /// - env value: [`AWS_ACCESS_KEY_ID`]
56    /// - profile config: `aws_access_key_id`
57    pub access_key_id: Option<String>,
58    /// `secret_access_key` will be loaded from
59    ///
60    /// - this field if it's `is_some`
61    /// - env value: [`AWS_SECRET_ACCESS_KEY`]
62    /// - profile config: `aws_secret_access_key`
63    pub secret_access_key: Option<String>,
64    /// `session_token` will be loaded from
65    ///
66    /// - this field if it's `is_some`
67    /// - env value: [`AWS_SESSION_TOKEN`]
68    /// - profile config: `aws_session_token`
69    pub session_token: Option<String>,
70    /// `role_arn` value will be load from:
71    ///
72    /// - this field if it's `is_some`.
73    /// - env value: [`AWS_ROLE_ARN`]
74    /// - profile config: `role_arn`
75    pub role_arn: Option<String>,
76    /// `role_session_name` value will be load from:
77    ///
78    /// - env value: [`AWS_ROLE_SESSION_NAME`]
79    /// - profile config: `role_session_name`
80    /// - default to `reqsign`.
81    pub role_session_name: String,
82    /// `duration_seconds` value will be load from:
83    ///
84    /// - this field if it's `is_some`.
85    /// - profile config: `duration_seconds`
86    /// - default to `3600`.
87    pub duration_seconds: Option<usize>,
88    /// `external_id` value will be load from:
89    ///
90    /// - this field if it's `is_some`.
91    /// - profile config: `external_id`
92    pub external_id: Option<String>,
93    /// `tags` value will be loaded from:
94    ///
95    /// - this field if it's `is_some`
96    pub tags: Option<Vec<(String, String)>>,
97    /// `web_identity_token_file` value will be loaded from:
98    ///
99    /// - this field if it's `is_some`
100    /// - env value: [`AWS_WEB_IDENTITY_TOKEN_FILE`]
101    /// - profile config: `web_identity_token_file`
102    pub web_identity_token_file: Option<String>,
103    /// `ec2_metadata_disabled` value will be loaded from:
104    ///
105    /// - this field
106    /// - env value: [`AWS_EC2_METADATA_DISABLED`]
107    pub ec2_metadata_disabled: bool,
108    /// `endpoint_url` value will be loaded from:
109    ///
110    /// - this field
111    /// - env value: [`AWS_ENDPOINT_URL`]
112    pub endpoint_url: Option<String>,
113}
114
115impl Default for Config {
116    fn default() -> Self {
117        Self {
118            config_file: "~/.aws/config".to_string(),
119            shared_credentials_file: "~/.aws/credentials".to_string(),
120            profile: "default".to_string(),
121            region: None,
122            sts_regional_endpoints: "legacy".to_string(),
123            access_key_id: None,
124            secret_access_key: None,
125            session_token: None,
126            role_arn: None,
127            role_session_name: "reqsign".to_string(),
128            duration_seconds: Some(3600),
129            external_id: None,
130            tags: None,
131            web_identity_token_file: None,
132            ec2_metadata_disabled: false,
133            endpoint_url: None,
134        }
135    }
136}
137
138impl Config {
139    /// Load config from env.
140    pub fn from_env(mut self) -> Self {
141        let envs = env::vars().collect::<HashMap<_, _>>();
142
143        if let Some(v) = envs.get(AWS_CONFIG_FILE) {
144            self.config_file = v.to_string();
145        }
146        if let Some(v) = envs.get(AWS_SHARED_CREDENTIALS_FILE) {
147            self.shared_credentials_file = v.to_string();
148        }
149        if let Some(v) = envs.get(AWS_PROFILE) {
150            self.profile = v.to_string();
151        }
152        if let Some(v) = envs.get(AWS_REGION) {
153            self.region = Some(v.to_string())
154        }
155        if let Some(v) = envs.get(AWS_STS_REGIONAL_ENDPOINTS) {
156            self.sts_regional_endpoints = v.to_string();
157        }
158        if let Some(v) = envs.get(AWS_ACCESS_KEY_ID) {
159            self.access_key_id = Some(v.to_string())
160        }
161        if let Some(v) = envs.get(AWS_SECRET_ACCESS_KEY) {
162            self.secret_access_key = Some(v.to_string())
163        }
164        if let Some(v) = envs.get(AWS_SESSION_TOKEN) {
165            self.session_token = Some(v.to_string())
166        }
167        if let Some(v) = envs.get(AWS_ROLE_ARN) {
168            self.role_arn = Some(v.to_string())
169        }
170        if let Some(v) = envs.get(AWS_ROLE_SESSION_NAME) {
171            self.role_session_name = v.to_string();
172        }
173        if let Some(v) = envs.get(AWS_WEB_IDENTITY_TOKEN_FILE) {
174            self.web_identity_token_file = Some(v.to_string());
175        }
176        if let Some(v) = envs.get(AWS_EC2_METADATA_DISABLED) {
177            self.ec2_metadata_disabled = v == "true";
178        }
179        if let Some(v) = envs.get(AWS_ENDPOINT_URL) {
180            self.endpoint_url = Some(v.to_string());
181        }
182        self
183    }
184
185    /// Load config from profile (and shared profile).
186    ///
187    /// If the env var AWS_PROFILE is set, this profile will be used,
188    /// otherwise the contents of `self.profile` will be used.
189    #[cfg(not(target_arch = "wasm32"))]
190    pub fn from_profile(mut self) -> Self {
191        // self.profile is checked by the two load methods.
192        if let Ok(profile) = env::var(AWS_PROFILE) {
193            self.profile = profile;
194        }
195
196        // make sure we're getting profile info from the correct place.
197        // Respecting these env vars also makes it possible to unit test
198        // this method.
199        if let Ok(config_file) = env::var(AWS_CONFIG_FILE) {
200            self.config_file = config_file;
201        }
202
203        if let Ok(shared_credentials_file) = env::var(AWS_SHARED_CREDENTIALS_FILE) {
204            self.shared_credentials_file = shared_credentials_file;
205        }
206
207        // Ignore all errors happened internally.
208        let _ = self.load_via_profile_config_file().map_err(|err| {
209            debug!("load_via_profile_config_file failed: {err:?}");
210        });
211
212        let _ = self
213            .load_via_profile_shared_credentials_file()
214            .map_err(|err| debug!("load_via_profile_shared_credentials_file failed: {err:?}"));
215
216        self
217    }
218
219    /// Only the following fields will exist in shared_credentials_file:
220    ///
221    /// - `aws_access_key_id`
222    /// - `aws_secret_access_key`
223    /// - `aws_session_token`
224    #[cfg(not(target_arch = "wasm32"))]
225    fn load_via_profile_shared_credentials_file(&mut self) -> Result<()> {
226        let path = expand_homedir(&self.shared_credentials_file)
227            .ok_or_else(|| anyhow!("expand homedir failed"))?;
228
229        let _ = fs::metadata(&path)?;
230
231        let conf = Ini::load_from_file(path)?;
232
233        let props = conf
234            .section(Some(&self.profile))
235            .ok_or_else(|| anyhow!("section {} is not found", self.profile))?;
236
237        if let Some(v) = props.get("aws_access_key_id") {
238            self.access_key_id = Some(v.to_string())
239        }
240        if let Some(v) = props.get("aws_secret_access_key") {
241            self.secret_access_key = Some(v.to_string())
242        }
243        if let Some(v) = props.get("aws_session_token") {
244            self.session_token = Some(v.to_string())
245        }
246
247        Ok(())
248    }
249
250    #[cfg(not(target_arch = "wasm32"))]
251    fn load_via_profile_config_file(&mut self) -> Result<()> {
252        let path =
253            expand_homedir(&self.config_file).ok_or_else(|| anyhow!("expand homedir failed"))?;
254
255        let _ = fs::metadata(&path)?;
256
257        let conf = Ini::load_from_file(path)?;
258
259        let section = match self.profile.as_str() {
260            "default" => "default".to_string(),
261            x => format!("profile {x}"),
262        };
263        let props = conf
264            .section(Some(section))
265            .ok_or_else(|| anyhow!("section {} is not found", self.profile))?;
266
267        if let Some(v) = props.get("region") {
268            self.region = Some(v.to_string())
269        }
270        if let Some(v) = props.get("sts_regional_endpoints") {
271            self.sts_regional_endpoints = v.to_string();
272        }
273        if let Some(v) = props.get("aws_access_key_id") {
274            self.access_key_id = Some(v.to_string())
275        }
276        if let Some(v) = props.get("aws_secret_access_key") {
277            self.secret_access_key = Some(v.to_string())
278        }
279        if let Some(v) = props.get("aws_session_token") {
280            self.session_token = Some(v.to_string())
281        }
282        if let Some(v) = props.get("role_arn") {
283            self.role_arn = Some(v.to_string())
284        }
285        if let Some(v) = props.get("role_session_name") {
286            self.role_session_name = v.to_string()
287        }
288        if let Some(v) = props.get("duration_seconds") {
289            self.duration_seconds = Some(v.to_string().parse::<usize>().unwrap())
290        }
291        if let Some(v) = props.get("web_identity_token_file") {
292            self.web_identity_token_file = Some(v.to_string())
293        }
294        if let Some(v) = props.get("endpoint_url") {
295            self.endpoint_url = Some(v.to_string())
296        }
297
298        Ok(())
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use pretty_assertions::assert_eq;
306    use std::fs::File;
307    use std::io::Write;
308    use tempfile::tempdir;
309
310    #[test]
311    #[cfg(not(target_arch = "wasm32"))]
312    fn test_config_from_profile_shared_credentials() -> Result<()> {
313        let _ = env_logger::builder().is_test(true).try_init();
314
315        // Create a dummy credentials file to test against
316        let tmp_dir = tempdir()?;
317        let file_path = tmp_dir.path().join("credentials");
318        let mut tmp_file = File::create(&file_path)?;
319        writeln!(tmp_file, "[default]")?;
320        writeln!(tmp_file, "aws_access_key_id = DEFAULTACCESSKEYID")?;
321        writeln!(tmp_file, "aws_secret_access_key = DEFAULTSECRETACCESSKEY")?;
322        writeln!(tmp_file, "aws_session_token = DEFAULTSESSIONTOKEN")?;
323        writeln!(tmp_file)?;
324        writeln!(tmp_file, "[profile1]")?;
325        writeln!(tmp_file, "aws_access_key_id = PROFILE1ACCESSKEYID")?;
326        writeln!(tmp_file, "aws_secret_access_key = PROFILE1SECRETACCESSKEY")?;
327        writeln!(tmp_file, "aws_session_token = PROFILE1SESSIONTOKEN")?;
328
329        temp_env::with_vars(
330            [
331                (AWS_PROFILE, Some("profile1".to_owned())),
332                (AWS_CONFIG_FILE, None::<String>),
333                (
334                    AWS_SHARED_CREDENTIALS_FILE,
335                    Some(file_path.to_str().unwrap().to_owned()),
336                ),
337            ],
338            || {
339                let config = Config::default().from_profile();
340
341                assert_eq!(config.profile, "profile1".to_owned());
342                assert_eq!(config.access_key_id, Some("PROFILE1ACCESSKEYID".to_owned()));
343                assert_eq!(
344                    config.secret_access_key,
345                    Some("PROFILE1SECRETACCESSKEY".to_owned())
346                );
347                assert_eq!(
348                    config.session_token,
349                    Some("PROFILE1SESSIONTOKEN".to_owned())
350                );
351            },
352        );
353
354        Ok(())
355    }
356
357    #[test]
358    #[cfg(not(target_arch = "wasm32"))]
359    fn test_config_from_profile_config() -> Result<()> {
360        let _ = env_logger::builder().is_test(true).try_init();
361
362        // Create a dummy credentials file to test against
363        let tmp_dir = tempdir()?;
364        let file_path = tmp_dir.path().join("config");
365        let mut tmp_file = File::create(&file_path)?;
366        writeln!(tmp_file, "[default]")?;
367        writeln!(tmp_file, "aws_access_key_id = DEFAULTACCESSKEYID")?;
368        writeln!(tmp_file, "aws_secret_access_key = DEFAULTSECRETACCESSKEY")?;
369        writeln!(tmp_file, "aws_session_token = DEFAULTSESSIONTOKEN")?;
370        writeln!(tmp_file)?;
371        writeln!(tmp_file, "[profile profile1]")?;
372        writeln!(tmp_file, "aws_access_key_id = PROFILE1ACCESSKEYID")?;
373        writeln!(tmp_file, "aws_secret_access_key = PROFILE1SECRETACCESSKEY")?;
374        writeln!(tmp_file, "aws_session_token = PROFILE1SESSIONTOKEN")?;
375        writeln!(tmp_file, "endpoint_url = http://localhost:8080")?;
376
377        temp_env::with_vars(
378            [
379                (AWS_PROFILE, Some("profile1".to_owned())),
380                (
381                    AWS_CONFIG_FILE,
382                    Some(file_path.to_str().unwrap().to_owned()),
383                ),
384                (AWS_SHARED_CREDENTIALS_FILE, None::<String>),
385            ],
386            || {
387                let config = Config::default().from_profile();
388
389                assert_eq!(config.profile, "profile1".to_owned());
390                assert_eq!(config.access_key_id, Some("PROFILE1ACCESSKEYID".to_owned()));
391                assert_eq!(
392                    config.secret_access_key,
393                    Some("PROFILE1SECRETACCESSKEY".to_owned())
394                );
395                assert_eq!(
396                    config.session_token,
397                    Some("PROFILE1SESSIONTOKEN".to_owned())
398                );
399                assert_eq!(
400                    config.endpoint_url,
401                    Some("http://localhost:8080".to_owned())
402                );
403            },
404        );
405
406        Ok(())
407    }
408}