1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

//! Config structs to programmatically customize the profile files that get loaded

use std::fmt;
use std::path::PathBuf;

/// Provides the ability to programmatically override the profile files that get loaded by the SDK.
///
/// The [`Default`] for `EnvConfigFiles` includes the default SDK config and credential files located in
/// `~/.aws/config` and `~/.aws/credentials` respectively.
///
/// Any number of config and credential files may be added to the `EnvConfigFiles` file set, with the
/// only requirement being that there is at least one of them. Custom file locations that are added
/// will produce errors if they don't exist, while the default config/credentials files paths are
/// allowed to not exist even if they're included.
///
/// # Example: Using a custom profile file path
///
/// ```no_run,ignore
/// use aws_runtime::env_config::file::{EnvConfigFiles, SharedConfigFileKind};
/// use std::sync::Arc;
///
/// # async fn example() {
/// let profile_files = EnvConfigFiles::builder()
///     .with_file(SharedConfigFileKind::Credentials, "some/path/to/credentials-file")
///     .build();
/// let sdk_config = aws_config::from_env()
///     .profile_files(profile_files)
///     .load()
///     .await;
/// # }
/// ```
#[derive(Clone, Debug)]
pub struct EnvConfigFiles {
    pub(crate) files: Vec<EnvConfigFile>,
}

impl EnvConfigFiles {
    /// Returns a builder to create `EnvConfigFiles`
    pub fn builder() -> Builder {
        Builder::new()
    }
}

impl Default for EnvConfigFiles {
    fn default() -> Self {
        Self {
            files: vec![
                EnvConfigFile::Default(EnvConfigFileKind::Config),
                EnvConfigFile::Default(EnvConfigFileKind::Credentials),
            ],
        }
    }
}

/// Profile file type (config or credentials)
#[derive(Copy, Clone, Debug)]
pub enum EnvConfigFileKind {
    /// The SDK config file that typically resides in `~/.aws/config`
    Config,
    /// The SDK credentials file that typically resides in `~/.aws/credentials`
    Credentials,
}

impl EnvConfigFileKind {
    pub(crate) fn default_path(&self) -> &'static str {
        match &self {
            EnvConfigFileKind::Credentials => "~/.aws/credentials",
            EnvConfigFileKind::Config => "~/.aws/config",
        }
    }

    pub(crate) fn override_environment_variable(&self) -> &'static str {
        match &self {
            EnvConfigFileKind::Config => "AWS_CONFIG_FILE",
            EnvConfigFileKind::Credentials => "AWS_SHARED_CREDENTIALS_FILE",
        }
    }
}

/// A single config file within a [`EnvConfigFiles`] file set.
#[derive(Clone)]
pub(crate) enum EnvConfigFile {
    /// One of the default profile files (config or credentials in their default locations)
    Default(EnvConfigFileKind),
    /// A profile file at a custom location
    FilePath {
        kind: EnvConfigFileKind,
        path: PathBuf,
    },
    /// The direct contents of a profile file
    FileContents {
        kind: EnvConfigFileKind,
        contents: String,
    },
}

impl fmt::Debug for EnvConfigFile {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Default(kind) => f.debug_tuple("Default").field(kind).finish(),
            Self::FilePath { kind, path } => f
                .debug_struct("FilePath")
                .field("kind", kind)
                .field("path", path)
                .finish(),
            // Security: Redact the file contents since they may have credentials in them
            Self::FileContents { kind, contents: _ } => f
                .debug_struct("FileContents")
                .field("kind", kind)
                .field("contents", &"** redacted **")
                .finish(),
        }
    }
}

/// Builder for [`EnvConfigFiles`].
#[derive(Clone, Default, Debug)]
pub struct Builder {
    with_config: bool,
    with_credentials: bool,
    custom_sources: Vec<EnvConfigFile>,
}

impl Builder {
    /// Creates a new builder instance.
    pub fn new() -> Self {
        Default::default()
    }

    /// Include the default SDK config file in the list of profile files to be loaded.
    ///
    /// The default SDK config typically resides in `~/.aws/config`. When this flag is enabled,
    /// this config file will be included in the profile files that get loaded in the built
    /// [`EnvConfigFiles`] file set.
    ///
    /// This flag defaults to `false` when using the builder to construct [`EnvConfigFiles`].
    pub fn include_default_config_file(mut self, include_default_config_file: bool) -> Self {
        self.with_config = include_default_config_file;
        self
    }

    /// Include the default SDK credentials file in the list of profile files to be loaded.
    ///
    /// The default SDK credentials typically reside in `~/.aws/credentials`. When this flag is enabled,
    /// this credentials file will be included in the profile files that get loaded in the built
    /// [`EnvConfigFiles`] file set.
    ///
    /// This flag defaults to `false` when using the builder to construct [`EnvConfigFiles`].
    pub fn include_default_credentials_file(
        mut self,
        include_default_credentials_file: bool,
    ) -> Self {
        self.with_credentials = include_default_credentials_file;
        self
    }

    /// Include a custom `file` in the list of profile files to be loaded.
    ///
    /// The `kind` informs the parser how to treat the file. If it's intended to be like
    /// the SDK credentials file typically in `~/.aws/config`, then use [`EnvConfigFileKind::Config`].
    /// Otherwise, use [`EnvConfigFileKind::Credentials`].
    pub fn with_file(mut self, kind: EnvConfigFileKind, file: impl Into<PathBuf>) -> Self {
        self.custom_sources.push(EnvConfigFile::FilePath {
            kind,
            path: file.into(),
        });
        self
    }

    /// Include custom file `contents` in the list of profile files to be loaded.
    ///
    /// The `kind` informs the parser how to treat the file. If it's intended to be like
    /// the SDK credentials file typically in `~/.aws/config`, then use [`EnvConfigFileKind::Config`].
    /// Otherwise, use [`EnvConfigFileKind::Credentials`].
    pub fn with_contents(mut self, kind: EnvConfigFileKind, contents: impl Into<String>) -> Self {
        self.custom_sources.push(EnvConfigFile::FileContents {
            kind,
            contents: contents.into(),
        });
        self
    }

    /// Build the [`EnvConfigFiles`] file set.
    pub fn build(self) -> EnvConfigFiles {
        let mut files = self.custom_sources;
        if self.with_credentials {
            files.insert(0, EnvConfigFile::Default(EnvConfigFileKind::Credentials));
        }
        if self.with_config {
            files.insert(0, EnvConfigFile::Default(EnvConfigFileKind::Config));
        }
        if files.is_empty() {
            panic!("At least one profile file must be included in the `EnvConfigFiles` file set.");
        }
        EnvConfigFiles { files }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn redact_file_contents_in_profile_file_debug() {
        let shared_config_file = EnvConfigFile::FileContents {
            kind: EnvConfigFileKind::Config,
            contents: "sensitive_contents".into(),
        };
        let debug = format!("{shared_config_file:?}");
        assert!(!debug.contains("sensitive_contents"));
        assert!(debug.contains("** redacted **"));
    }

    #[test]
    fn build_correctly_orders_default_config_credentials() {
        let shared_config_files = EnvConfigFiles::builder()
            .with_file(EnvConfigFileKind::Config, "foo")
            .include_default_credentials_file(true)
            .include_default_config_file(true)
            .build();
        assert_eq!(3, shared_config_files.files.len());
        assert!(matches!(
            shared_config_files.files[0],
            EnvConfigFile::Default(EnvConfigFileKind::Config)
        ));
        assert!(matches!(
            shared_config_files.files[1],
            EnvConfigFile::Default(EnvConfigFileKind::Credentials)
        ));
        assert!(matches!(
            shared_config_files.files[2],
            EnvConfigFile::FilePath {
                kind: EnvConfigFileKind::Config,
                path: _
            }
        ));
    }

    #[test]
    #[should_panic]
    fn empty_builder_panics() {
        EnvConfigFiles::builder().build();
    }
}