remoteit_api/
credentials_loader.rs1use crate::credentials::Credentials;
6use bon::bon;
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10#[allow(missing_docs)]
12#[derive(thiserror::Error, Debug)]
13pub enum CredentialsLoaderError {
14 #[error("The user's home directory could not be found. Please refer to the `dirs` crate for more information.")]
15 HomeDirNotFound,
16 #[error("The credentials file could not be loaded: {0}")]
17 CouldNotReadCredentials(#[from] std::io::Error),
18 #[error("The credentials file could not be parsed: {0}")]
19 CredentialsParse(#[from] config::ConfigError),
20}
21
22#[derive(
25 Debug, Clone, PartialOrd, PartialEq, Eq, Ord, Hash, serde::Deserialize, serde::Serialize,
26)]
27pub(crate) struct UnverifiedCredentials {
28 pub(crate) r3_access_key_id: String,
29 pub(crate) r3_secret_access_key: String,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
42pub struct CredentialProfiles {
43 #[serde(flatten)]
44 pub(crate) profiles: HashMap<String, UnverifiedCredentials>,
45}
46
47impl CredentialProfiles {
48 pub fn take_profile(
58 &mut self,
59 profile_name: &str,
60 ) -> Result<Option<Credentials>, base64::DecodeError> {
61 let maybe_unverified_credentials = self.profiles.remove(profile_name);
62
63 let Some(unverified_credentials) = maybe_unverified_credentials else {
64 return Ok(None);
65 };
66
67 Credentials::builder()
68 .r3_access_key_id(&unverified_credentials.r3_access_key_id)
69 .r3_secret_access_key(&unverified_credentials.r3_secret_access_key)
70 .build()
71 .map(Some)
72 }
73
74 #[must_use]
77 pub fn len(&self) -> usize {
78 self.profiles.len()
79 }
80
81 #[must_use]
85 pub fn is_empty(&self) -> bool {
86 self.profiles.is_empty()
87 }
88
89 pub fn available_profiles(&self) -> Vec<String> {
96 self.profiles.keys().cloned().collect()
97 }
98}
99
100#[bon]
102impl Credentials {
103 #[builder]
127 pub fn load_from_disk(
128 custom_credentials_path: Option<PathBuf>,
129 ) -> Result<CredentialProfiles, CredentialsLoaderError> {
130 let credentials_path = custom_credentials_path.unwrap_or(
131 dirs::home_dir()
132 .ok_or(CredentialsLoaderError::HomeDirNotFound)?
133 .join(".remoteit")
134 .join("credentials"),
135 );
136
137 let profiles: CredentialProfiles = config::Config::builder()
138 .add_source(config::File::new(
139 credentials_path
140 .to_str()
141 .expect("It is highly unlikely, that there would be a "),
142 config::FileFormat::Ini,
143 ))
144 .build()?
145 .try_deserialize()?;
146
147 Ok(profiles)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use crate::credentials::Credentials;
154 use crate::CredentialsLoaderError;
155 use std::io::Write;
156
157 #[test]
158 fn test_load_from_disk_empty() {
159 let file = tempfile::NamedTempFile::new().unwrap();
160
161 let credentials = Credentials::load_from_disk()
162 .custom_credentials_path(file.path().to_path_buf())
163 .call()
164 .unwrap();
165
166 assert!(credentials.is_empty());
167 }
168
169 #[test]
170 fn test_load_from_disk_one() {
171 let credentials = r"
172 [default]
173 R3_ACCESS_KEY_ID=foo
174 R3_SECRET_ACCESS_KEY=YmFy
175 ";
176
177 let mut file = tempfile::NamedTempFile::new().unwrap();
178 file.write_all(credentials.as_bytes()).unwrap();
179
180 let mut credentials = Credentials::load_from_disk()
181 .custom_credentials_path(file.path().to_path_buf())
182 .call()
183 .unwrap();
184
185 assert_eq!(credentials.len(), 1);
186 let credentials = credentials.take_profile("default").unwrap().unwrap();
187 assert_eq!(credentials.r3_access_key_id, "foo");
188 assert_eq!(credentials.r3_secret_access_key, "YmFy");
189 }
190
191 #[test]
192 fn test_load_from_disk_two() {
193 let credentials = r"
194 [default]
195 R3_ACCESS_KEY_ID=foo
196 R3_SECRET_ACCESS_KEY=YmFy
197
198 [other]
199 R3_ACCESS_KEY_ID=baz
200 R3_SECRET_ACCESS_KEY=YmFy
201 ";
202
203 let mut file = tempfile::NamedTempFile::new().unwrap();
204 file.write_all(credentials.as_bytes()).unwrap();
205
206 let mut credentials = Credentials::load_from_disk()
207 .custom_credentials_path(file.path().to_path_buf())
208 .call()
209 .unwrap();
210
211 assert_eq!(credentials.len(), 2);
212 let profile = credentials.take_profile("default").unwrap().unwrap();
213 assert_eq!(profile.r3_access_key_id, "foo");
214 assert_eq!(profile.r3_secret_access_key, "YmFy");
215 let profile = credentials.take_profile("other").unwrap().unwrap();
216 assert_eq!(profile.r3_access_key_id, "baz");
217 assert_eq!(profile.r3_secret_access_key, "YmFy");
218 }
219
220 #[test]
221 fn test_load_from_disk_invalid_base64() {
222 let credentials = r"
223 [default]
224 R3_ACCESS_KEY_ID=foo
225 R3_SECRET_ACCESS_KEY=bar
226 ";
227 let mut file = tempfile::NamedTempFile::new().unwrap();
228 file.write_all(credentials.as_bytes()).unwrap();
229
230 let mut credentials = Credentials::load_from_disk()
231 .custom_credentials_path(file.path().to_path_buf())
232 .call()
233 .unwrap();
234
235 let result = credentials.take_profile("default");
236 assert!(result.is_err());
237 }
238
239 #[test]
240 fn test_load_from_disk_invalid_file() {
241 let credentials = r"
242 foobar
243 ";
244
245 let mut file = tempfile::NamedTempFile::new().unwrap();
246 file.write_all(credentials.as_bytes()).unwrap();
247
248 let result = Credentials::load_from_disk()
249 .custom_credentials_path(file.path().to_path_buf())
250 .call();
251
252 assert!(result.is_err());
253 assert!(matches!(
254 result.unwrap_err(),
255 CredentialsLoaderError::CredentialsParse(_)
256 ));
257 }
258
259 #[test]
260 fn test_get_available_profiles() {
261 let credentials = r"
262 [default]
263 R3_ACCESS_KEY_ID=foo
264 R3_SECRET_ACCESS_KEY=YmFy
265
266 [other]
267 R3_ACCESS_KEY_ID=baz
268 R3_SECRET_ACCESS_KEY=YmFy
269 ";
270
271 let mut file = tempfile::NamedTempFile::new().unwrap();
272 file.write_all(credentials.as_bytes()).unwrap();
273
274 let credentials = Credentials::load_from_disk()
275 .custom_credentials_path(file.path().to_path_buf())
276 .call()
277 .unwrap();
278
279 let profiles = credentials.available_profiles();
280 assert_eq!(profiles.len(), 2);
281 assert!(profiles.contains(&"default".to_string()));
282 assert!(profiles.contains(&"other".to_string()));
283 }
284}