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