use crate::credentials::Credentials;
use bon::bon;
use std::collections::HashMap;
use std::path::PathBuf;
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug)]
pub enum CredentialsLoaderError {
#[error(
"The user's home directory could not be found. Please refer to the `dirs` crate for more information."
)]
HomeDirNotFound,
#[error("The credentials file could not be loaded: {0}")]
CouldNotReadCredentials(#[from] std::io::Error),
#[error("The credentials file could not be parsed: {0}")]
CredentialsParse(#[from] config::ConfigError),
}
#[derive(
Debug, Clone, PartialOrd, PartialEq, Eq, Ord, Hash, serde::Deserialize, serde::Serialize,
)]
pub(crate) struct UnverifiedCredentials {
pub(crate) r3_access_key_id: String,
pub(crate) r3_secret_access_key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct CredentialProfiles {
#[serde(flatten)]
pub(crate) profiles: HashMap<String, UnverifiedCredentials>,
}
impl CredentialProfiles {
pub fn take_profile(
&mut self,
profile_name: &str,
) -> Result<Option<Credentials>, base64::DecodeError> {
let maybe_unverified_credentials = self.profiles.remove(profile_name);
let Some(unverified_credentials) = maybe_unverified_credentials else {
return Ok(None);
};
Credentials::builder()
.r3_access_key_id(unverified_credentials.r3_access_key_id)
.r3_secret_access_key(unverified_credentials.r3_secret_access_key)
.build()
.map(Some)
}
#[must_use]
pub fn len(&self) -> usize {
self.profiles.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.profiles.is_empty()
}
#[must_use]
pub fn available_profiles(&self) -> Vec<String> {
self.profiles.keys().cloned().collect()
}
}
#[bon]
impl Credentials {
#[builder]
pub fn load_from_disk(
#[builder(into)] custom_credentials_path: Option<PathBuf>,
) -> Result<CredentialProfiles, CredentialsLoaderError> {
let credentials_path = custom_credentials_path.unwrap_or(
dirs::home_dir()
.ok_or(CredentialsLoaderError::HomeDirNotFound)?
.join(".remoteit")
.join("credentials"),
);
let profiles: CredentialProfiles = config::Config::builder()
.add_source(config::File::new(
credentials_path
.to_str()
.expect("It is highly unlikely, that there would be a "),
config::FileFormat::Ini,
))
.build()?
.try_deserialize()?;
Ok(profiles)
}
}
#[cfg(test)]
mod tests {
use crate::CredentialsLoaderError;
use crate::credentials::Credentials;
use std::io::Write;
#[test]
fn test_load_from_disk_empty() {
let file = tempfile::NamedTempFile::new().unwrap();
let credentials = Credentials::load_from_disk()
.custom_credentials_path(file.path().to_path_buf())
.call()
.unwrap();
assert!(credentials.is_empty());
}
#[test]
fn test_load_from_disk_one() {
let credentials = r"
[default]
R3_ACCESS_KEY_ID=foo
R3_SECRET_ACCESS_KEY=YmFy
";
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(credentials.as_bytes()).unwrap();
let mut credentials = Credentials::load_from_disk()
.custom_credentials_path(file.path().to_path_buf())
.call()
.unwrap();
assert_eq!(credentials.len(), 1);
let credentials = credentials.take_profile("default").unwrap().unwrap();
assert_eq!(credentials.access_key_id(), "foo");
assert_eq!(credentials.secret_access_key(), "YmFy");
}
#[test]
fn test_load_from_disk_two() {
let credentials = r"
[default]
R3_ACCESS_KEY_ID=foo
R3_SECRET_ACCESS_KEY=YmFy
[other]
R3_ACCESS_KEY_ID=baz
R3_SECRET_ACCESS_KEY=YmFy
";
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(credentials.as_bytes()).unwrap();
let mut credentials = Credentials::load_from_disk()
.custom_credentials_path(file.path().to_path_buf())
.call()
.unwrap();
assert_eq!(credentials.len(), 2);
let profile = credentials.take_profile("default").unwrap().unwrap();
assert_eq!(profile.access_key_id(), "foo");
assert_eq!(profile.secret_access_key(), "YmFy");
let profile = credentials.take_profile("other").unwrap().unwrap();
assert_eq!(profile.access_key_id(), "baz");
assert_eq!(profile.secret_access_key(), "YmFy");
}
#[test]
fn test_load_from_disk_invalid_base64() {
let credentials = r"
[default]
R3_ACCESS_KEY_ID=foo
R3_SECRET_ACCESS_KEY=bar
";
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(credentials.as_bytes()).unwrap();
let mut credentials = Credentials::load_from_disk()
.custom_credentials_path(file.path().to_path_buf())
.call()
.unwrap();
let result = credentials.take_profile("default");
assert!(result.is_err());
}
#[test]
fn test_load_from_disk_invalid_file() {
let credentials = r"
foobar
";
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(credentials.as_bytes()).unwrap();
let result = Credentials::load_from_disk()
.custom_credentials_path(file.path().to_path_buf())
.call();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
CredentialsLoaderError::CredentialsParse(_)
));
}
#[test]
fn test_get_available_profiles() {
let credentials = r"
[default]
R3_ACCESS_KEY_ID=foo
R3_SECRET_ACCESS_KEY=YmFy
[other]
R3_ACCESS_KEY_ID=baz
R3_SECRET_ACCESS_KEY=YmFy
";
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(credentials.as_bytes()).unwrap();
let credentials = Credentials::load_from_disk()
.custom_credentials_path(file.path().to_path_buf())
.call()
.unwrap();
let profiles = credentials.available_profiles();
assert_eq!(profiles.len(), 2);
assert!(profiles.contains(&"default".to_string()));
assert!(profiles.contains(&"other".to_string()));
}
}