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 #[must_use]
96 pub fn available_profiles(&self) -> Vec<String> {
97 self.profiles.keys().cloned().collect()
98 }
99}
100
101#[bon]
103impl Credentials {
104 #[builder]
121 pub fn load_from_disk(
122 #[builder(into)]
123 custom_credentials_path: Option<PathBuf>,
124 ) -> Result<CredentialProfiles, CredentialsLoaderError> {
125 let credentials_path = custom_credentials_path.unwrap_or(
126 dirs::home_dir()
127 .ok_or(CredentialsLoaderError::HomeDirNotFound)?
128 .join(".remoteit")
129 .join("credentials"),
130 );
131
132 let profiles: CredentialProfiles = config::Config::builder()
133 .add_source(config::File::new(
134 credentials_path
135 .to_str()
136 .expect("It is highly unlikely, that there would be a "),
137 config::FileFormat::Ini,
138 ))
139 .build()?
140 .try_deserialize()?;
141
142 Ok(profiles)
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use crate::credentials::Credentials;
149 use crate::CredentialsLoaderError;
150 use std::io::Write;
151
152 #[test]
153 fn test_load_from_disk_empty() {
154 let file = tempfile::NamedTempFile::new().unwrap();
155
156 let credentials = Credentials::load_from_disk()
157 .custom_credentials_path(file.path().to_path_buf())
158 .call()
159 .unwrap();
160
161 assert!(credentials.is_empty());
162 }
163
164 #[test]
165 fn test_load_from_disk_one() {
166 let credentials = r"
167 [default]
168 R3_ACCESS_KEY_ID=foo
169 R3_SECRET_ACCESS_KEY=YmFy
170 ";
171
172 let mut file = tempfile::NamedTempFile::new().unwrap();
173 file.write_all(credentials.as_bytes()).unwrap();
174
175 let mut credentials = Credentials::load_from_disk()
176 .custom_credentials_path(file.path().to_path_buf())
177 .call()
178 .unwrap();
179
180 assert_eq!(credentials.len(), 1);
181 let credentials = credentials.take_profile("default").unwrap().unwrap();
182 assert_eq!(credentials.access_key_id(), "foo");
183 assert_eq!(credentials.secret_access_key(), "YmFy");
184 }
185
186 #[test]
187 fn test_load_from_disk_two() {
188 let credentials = r"
189 [default]
190 R3_ACCESS_KEY_ID=foo
191 R3_SECRET_ACCESS_KEY=YmFy
192
193 [other]
194 R3_ACCESS_KEY_ID=baz
195 R3_SECRET_ACCESS_KEY=YmFy
196 ";
197
198 let mut file = tempfile::NamedTempFile::new().unwrap();
199 file.write_all(credentials.as_bytes()).unwrap();
200
201 let mut credentials = Credentials::load_from_disk()
202 .custom_credentials_path(file.path().to_path_buf())
203 .call()
204 .unwrap();
205
206 assert_eq!(credentials.len(), 2);
207 let profile = credentials.take_profile("default").unwrap().unwrap();
208 assert_eq!(profile.access_key_id(), "foo");
209 assert_eq!(profile.secret_access_key(), "YmFy");
210 let profile = credentials.take_profile("other").unwrap().unwrap();
211 assert_eq!(profile.access_key_id(), "baz");
212 assert_eq!(profile.secret_access_key(), "YmFy");
213 }
214
215 #[test]
216 fn test_load_from_disk_invalid_base64() {
217 let credentials = r"
218 [default]
219 R3_ACCESS_KEY_ID=foo
220 R3_SECRET_ACCESS_KEY=bar
221 ";
222 let mut file = tempfile::NamedTempFile::new().unwrap();
223 file.write_all(credentials.as_bytes()).unwrap();
224
225 let mut credentials = Credentials::load_from_disk()
226 .custom_credentials_path(file.path().to_path_buf())
227 .call()
228 .unwrap();
229
230 let result = credentials.take_profile("default");
231 assert!(result.is_err());
232 }
233
234 #[test]
235 fn test_load_from_disk_invalid_file() {
236 let credentials = r"
237 foobar
238 ";
239
240 let mut file = tempfile::NamedTempFile::new().unwrap();
241 file.write_all(credentials.as_bytes()).unwrap();
242
243 let result = Credentials::load_from_disk()
244 .custom_credentials_path(file.path().to_path_buf())
245 .call();
246
247 assert!(result.is_err());
248 assert!(matches!(
249 result.unwrap_err(),
250 CredentialsLoaderError::CredentialsParse(_)
251 ));
252 }
253
254 #[test]
255 fn test_get_available_profiles() {
256 let credentials = r"
257 [default]
258 R3_ACCESS_KEY_ID=foo
259 R3_SECRET_ACCESS_KEY=YmFy
260
261 [other]
262 R3_ACCESS_KEY_ID=baz
263 R3_SECRET_ACCESS_KEY=YmFy
264 ";
265
266 let mut file = tempfile::NamedTempFile::new().unwrap();
267 file.write_all(credentials.as_bytes()).unwrap();
268
269 let credentials = Credentials::load_from_disk()
270 .custom_credentials_path(file.path().to_path_buf())
271 .call()
272 .unwrap();
273
274 let profiles = credentials.available_profiles();
275 assert_eq!(profiles.len(), 2);
276 assert!(profiles.contains(&"default".to_string()));
277 assert!(profiles.contains(&"other".to_string()));
278 }
279}